@tldraw/tlschema 4.2.2 → 4.2.3

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 +71 -242
  4. package/dist-cjs/index.js +1 -4
  5. package/dist-cjs/index.js.map +2 -2
  6. package/dist-cjs/misc/TLOpacity.js +5 -1
  7. package/dist-cjs/misc/TLOpacity.js.map +2 -2
  8. package/dist-cjs/misc/TLRichText.js +1 -5
  9. package/dist-cjs/misc/TLRichText.js.map +2 -2
  10. package/dist-cjs/records/TLAsset.js.map +1 -1
  11. package/dist-cjs/records/TLBinding.js.map +2 -2
  12. package/dist-cjs/records/TLShape.js.map +2 -2
  13. package/dist-cjs/shapes/ShapeWithCrop.js.map +1 -1
  14. package/dist-cjs/shapes/TLArrowShape.js +13 -26
  15. package/dist-cjs/shapes/TLArrowShape.js.map +2 -2
  16. package/dist-cjs/shapes/TLBaseShape.js.map +2 -2
  17. package/dist-cjs/shapes/TLDrawShape.js +4 -37
  18. package/dist-cjs/shapes/TLDrawShape.js.map +2 -2
  19. package/dist-cjs/shapes/TLEmbedShape.js +0 -17
  20. package/dist-cjs/shapes/TLEmbedShape.js.map +2 -2
  21. package/dist-cjs/shapes/TLGeoShape.js +1 -12
  22. package/dist-cjs/shapes/TLGeoShape.js.map +2 -2
  23. package/dist-cjs/shapes/TLHighlightShape.js +2 -29
  24. package/dist-cjs/shapes/TLHighlightShape.js.map +2 -2
  25. package/dist-cjs/shapes/TLNoteShape.js +1 -12
  26. package/dist-cjs/shapes/TLNoteShape.js.map +2 -2
  27. package/dist-cjs/shapes/TLTextShape.js +1 -12
  28. package/dist-cjs/shapes/TLTextShape.js.map +2 -2
  29. package/dist-cjs/store-migrations.js +15 -15
  30. package/dist-cjs/store-migrations.js.map +2 -2
  31. package/dist-esm/bindings/TLBaseBinding.mjs.map +2 -2
  32. package/dist-esm/createTLSchema.mjs.map +2 -2
  33. package/dist-esm/index.d.mts +71 -242
  34. package/dist-esm/index.mjs +1 -5
  35. package/dist-esm/index.mjs.map +2 -2
  36. package/dist-esm/misc/TLOpacity.mjs +5 -1
  37. package/dist-esm/misc/TLOpacity.mjs.map +2 -2
  38. package/dist-esm/misc/TLRichText.mjs +1 -5
  39. package/dist-esm/misc/TLRichText.mjs.map +2 -2
  40. package/dist-esm/records/TLAsset.mjs.map +1 -1
  41. package/dist-esm/records/TLBinding.mjs.map +2 -2
  42. package/dist-esm/records/TLShape.mjs.map +2 -2
  43. package/dist-esm/shapes/TLArrowShape.mjs +13 -26
  44. package/dist-esm/shapes/TLArrowShape.mjs.map +2 -2
  45. package/dist-esm/shapes/TLBaseShape.mjs.map +2 -2
  46. package/dist-esm/shapes/TLDrawShape.mjs +4 -37
  47. package/dist-esm/shapes/TLDrawShape.mjs.map +2 -2
  48. package/dist-esm/shapes/TLEmbedShape.mjs +0 -17
  49. package/dist-esm/shapes/TLEmbedShape.mjs.map +2 -2
  50. package/dist-esm/shapes/TLGeoShape.mjs +1 -12
  51. package/dist-esm/shapes/TLGeoShape.mjs.map +2 -2
  52. package/dist-esm/shapes/TLHighlightShape.mjs +2 -29
  53. package/dist-esm/shapes/TLHighlightShape.mjs.map +2 -2
  54. package/dist-esm/shapes/TLNoteShape.mjs +1 -12
  55. package/dist-esm/shapes/TLNoteShape.mjs.map +2 -2
  56. package/dist-esm/shapes/TLTextShape.mjs +1 -12
  57. package/dist-esm/shapes/TLTextShape.mjs.map +2 -2
  58. package/dist-esm/store-migrations.mjs +15 -15
  59. package/dist-esm/store-migrations.mjs.map +2 -2
  60. package/package.json +8 -8
  61. package/src/__tests__/migrationTestUtils.ts +3 -9
  62. package/src/assets/TLBookmarkAsset.test.ts +96 -0
  63. package/src/assets/TLImageAsset.test.ts +213 -0
  64. package/src/assets/TLVideoAsset.test.ts +105 -0
  65. package/src/bindings/TLArrowBinding.test.ts +55 -0
  66. package/src/bindings/TLBaseBinding.ts +14 -25
  67. package/src/createTLSchema.ts +2 -8
  68. package/src/index.ts +0 -9
  69. package/src/migrations.test.ts +1 -149
  70. package/src/misc/TLOpacity.ts +5 -1
  71. package/src/misc/TLRichText.ts +1 -6
  72. package/src/misc/id-validator.test.ts +50 -0
  73. package/src/records/TLAsset.test.ts +234 -0
  74. package/src/records/TLAsset.ts +2 -2
  75. package/src/records/TLBinding.test.ts +22 -0
  76. package/src/records/TLBinding.ts +23 -65
  77. package/src/records/TLCamera.test.ts +19 -0
  78. package/src/records/TLDocument.test.ts +35 -0
  79. package/src/records/TLInstance.test.ts +201 -0
  80. package/src/records/TLPage.test.ts +110 -0
  81. package/src/records/TLPageState.test.ts +228 -0
  82. package/src/records/TLPointer.test.ts +63 -0
  83. package/src/records/TLPresence.test.ts +190 -0
  84. package/src/records/TLRecord.test.ts +70 -0
  85. package/src/records/TLShape.test.ts +232 -0
  86. package/src/records/TLShape.ts +5 -100
  87. package/src/shapes/ShapeWithCrop.test.ts +18 -0
  88. package/src/shapes/ShapeWithCrop.ts +2 -2
  89. package/src/shapes/TLArrowShape.test.ts +505 -0
  90. package/src/shapes/TLArrowShape.ts +14 -28
  91. package/src/shapes/TLBaseShape.test.ts +142 -0
  92. package/src/shapes/TLBaseShape.ts +10 -34
  93. package/src/shapes/TLBookmarkShape.test.ts +122 -0
  94. package/src/shapes/TLDrawShape.test.ts +177 -0
  95. package/src/shapes/TLDrawShape.ts +12 -59
  96. package/src/shapes/TLEmbedShape.test.ts +286 -0
  97. package/src/shapes/TLEmbedShape.ts +0 -17
  98. package/src/shapes/TLFrameShape.test.ts +71 -0
  99. package/src/shapes/TLGeoShape.test.ts +247 -0
  100. package/src/shapes/TLGeoShape.ts +1 -14
  101. package/src/shapes/TLGroupShape.test.ts +59 -0
  102. package/src/shapes/TLHighlightShape.test.ts +325 -0
  103. package/src/shapes/TLHighlightShape.ts +0 -37
  104. package/src/shapes/TLImageShape.test.ts +534 -0
  105. package/src/shapes/TLLineShape.test.ts +269 -0
  106. package/src/shapes/TLNoteShape.test.ts +1568 -0
  107. package/src/shapes/TLNoteShape.ts +1 -15
  108. package/src/shapes/TLTextShape.test.ts +407 -0
  109. package/src/shapes/TLTextShape.ts +2 -16
  110. package/src/shapes/TLVideoShape.test.ts +112 -0
  111. package/src/store-migrations.ts +16 -17
  112. package/src/styles/TLColorStyle.test.ts +439 -0
  113. package/dist-cjs/misc/b64Vecs.js +0 -224
  114. package/dist-cjs/misc/b64Vecs.js.map +0 -7
  115. package/dist-esm/misc/b64Vecs.mjs +0 -204
  116. package/dist-esm/misc/b64Vecs.mjs.map +0 -7
  117. package/src/misc/b64Vecs.ts +0 -308
@@ -0,0 +1,142 @@
1
+ import { T } from '@tldraw/validate'
2
+ import { describe, expect, it } from 'vitest'
3
+ import { createShapeValidator, parentIdValidator, shapeIdValidator } from './TLBaseShape'
4
+
5
+ describe('TLBaseShape', () => {
6
+ describe('parentIdValidator', () => {
7
+ it('should accept valid page and shape parent IDs', () => {
8
+ expect(() => parentIdValidator.validate('page:main')).not.toThrow()
9
+ expect(() => parentIdValidator.validate('shape:frame1')).not.toThrow()
10
+ expect(parentIdValidator.validate('page:main')).toBe('page:main')
11
+ })
12
+
13
+ it('should reject invalid parent ID prefixes', () => {
14
+ expect(() => parentIdValidator.validate('invalid:123')).toThrow(
15
+ 'Parent ID must start with "page:" or "shape:"'
16
+ )
17
+ expect(() => parentIdValidator.validate('asset:123')).toThrow(
18
+ 'Parent ID must start with "page:" or "shape:"'
19
+ )
20
+ expect(() => parentIdValidator.validate('page-main')).toThrow(
21
+ 'Parent ID must start with "page:" or "shape:"'
22
+ )
23
+ })
24
+ })
25
+
26
+ describe('shapeIdValidator', () => {
27
+ it('should accept valid shape IDs', () => {
28
+ expect(() => shapeIdValidator.validate('shape:abc123')).not.toThrow()
29
+ expect(shapeIdValidator.validate('shape:test')).toBe('shape:test')
30
+ })
31
+
32
+ it('should reject non-shape IDs', () => {
33
+ expect(() => shapeIdValidator.validate('page:123')).toThrow(
34
+ 'shape ID must start with "shape:"'
35
+ )
36
+ expect(() => shapeIdValidator.validate('asset:123')).toThrow(
37
+ 'shape ID must start with "shape:"'
38
+ )
39
+ expect(() => shapeIdValidator.validate('shape-abc123')).toThrow(
40
+ 'shape ID must start with "shape:"'
41
+ )
42
+ })
43
+ })
44
+
45
+ describe('createShapeValidator', () => {
46
+ it('should create validator for shape with no custom props', () => {
47
+ const validator = createShapeValidator('simple')
48
+
49
+ const validShape = {
50
+ id: 'shape:simple123',
51
+ typeName: 'shape',
52
+ type: 'simple',
53
+ x: 100,
54
+ y: 200,
55
+ rotation: 0.5,
56
+ index: 'a1',
57
+ parentId: 'page:main',
58
+ isLocked: false,
59
+ opacity: 0.8,
60
+ props: {},
61
+ meta: {},
62
+ }
63
+
64
+ expect(() => validator.validate(validShape)).not.toThrow()
65
+ })
66
+
67
+ it('should create validator for shape with custom props', () => {
68
+ const validator = createShapeValidator('custom', {
69
+ width: T.number,
70
+ height: T.number,
71
+ color: T.string,
72
+ })
73
+
74
+ const validShape = {
75
+ id: 'shape:custom456',
76
+ typeName: 'shape',
77
+ type: 'custom',
78
+ x: 50,
79
+ y: 75,
80
+ rotation: 1.0,
81
+ index: 'a2',
82
+ parentId: 'shape:frame1',
83
+ isLocked: true,
84
+ opacity: 0.5,
85
+ props: {
86
+ width: 150,
87
+ height: 100,
88
+ color: 'blue',
89
+ },
90
+ meta: {},
91
+ }
92
+
93
+ expect(() => validator.validate(validShape)).not.toThrow()
94
+ })
95
+
96
+ it('should reject shapes with wrong type', () => {
97
+ const validator = createShapeValidator('test')
98
+
99
+ const invalidShape = {
100
+ id: 'shape:test',
101
+ typeName: 'shape',
102
+ type: 'wrong',
103
+ x: 0,
104
+ y: 0,
105
+ rotation: 0,
106
+ index: 'a1',
107
+ parentId: 'page:main',
108
+ isLocked: false,
109
+ opacity: 1,
110
+ props: {},
111
+ meta: {},
112
+ }
113
+
114
+ expect(() => validator.validate(invalidShape)).toThrow()
115
+ })
116
+
117
+ it('should reject shapes with invalid custom props', () => {
118
+ const validator = createShapeValidator('custom', {
119
+ width: T.number,
120
+ height: T.number,
121
+ color: T.string,
122
+ })
123
+
124
+ const invalidShape = {
125
+ id: 'shape:custom123',
126
+ typeName: 'shape',
127
+ type: 'custom',
128
+ x: 0,
129
+ y: 0,
130
+ rotation: 0,
131
+ index: 'a1',
132
+ parentId: 'page:main',
133
+ isLocked: false,
134
+ opacity: 1,
135
+ meta: {},
136
+ props: { width: '100' }, // Wrong type - should be number
137
+ }
138
+
139
+ expect(() => validator.validate(invalidShape)).toThrow()
140
+ })
141
+ })
142
+ })
@@ -1,3 +1,4 @@
1
+ import { BaseRecord } from '@tldraw/store'
1
2
  import { IndexKey, JsonObject } from '@tldraw/utils'
2
3
  import { T } from '@tldraw/validate'
3
4
  import { TLOpacityType, opacityValidator } from '../misc/TLOpacity'
@@ -8,37 +9,18 @@ import { TLParentId, TLShapeId } from '../records/TLShape'
8
9
  * Base interface for all shapes in tldraw.
9
10
  *
10
11
  * This interface defines the common properties that all shapes share, regardless of their
11
- * specific type. Every default shape extends this base with additional type-specific properties.
12
- *
13
- * Custom shapes should be defined by augmenting the TLGlobalShapePropsMap type and getting the shape type from the TLShape type.
12
+ * specific type. Every shape extends this base with additional type-specific properties.
14
13
  *
15
14
  * @example
16
15
  * ```ts
17
- * // Define a default shape type
18
- * interface TLArrowShape extends TLBaseShape<'arrow', {
19
- * kind: TLArrowShapeKind
20
- * labelColor: TLDefaultColorStyle
21
- * color: TLDefaultColorStyle
22
- * fill: TLDefaultFillStyle
23
- * dash: TLDefaultDashStyle
24
- * size: TLDefaultSizeStyle
25
- * arrowheadStart: TLArrowShapeArrowheadStyle
26
- * arrowheadEnd: TLArrowShapeArrowheadStyle
27
- * font: TLDefaultFontStyle
28
- * start: VecModel
29
- * end: VecModel
30
- * bend: number
31
- * richText: TLRichText
32
- * labelPosition: number
33
- * scale: number
34
- * elbowMidPoint: number
35
- * }> {}
16
+ * // Define a custom shape type
17
+ * interface MyCustomShape extends TLBaseShape<'custom', { size: number; color: string }> {}
36
18
  *
37
19
  * // Create a shape instance
38
- * const arrowShape: TLArrowShape = {
20
+ * const myShape: MyCustomShape = {
39
21
  * id: 'shape:abc123',
40
22
  * typeName: 'shape',
41
- * type: 'arrow',
23
+ * type: 'custom',
42
24
  * x: 100,
43
25
  * y: 200,
44
26
  * rotation: 0,
@@ -47,10 +29,8 @@ import { TLParentId, TLShapeId } from '../records/TLShape'
47
29
  * isLocked: false,
48
30
  * opacity: 1,
49
31
  * props: {
50
- * kind: 'arc',
51
- * start: { x: 0, y: 0 },
52
- * end: { x: 100, y: 100 },
53
- * // ... other props
32
+ * size: 50,
33
+ * color: 'blue'
54
34
  * },
55
35
  * meta: {}
56
36
  * }
@@ -58,12 +38,8 @@ import { TLParentId, TLShapeId } from '../records/TLShape'
58
38
  *
59
39
  * @public
60
40
  */
61
- export interface TLBaseShape<Type extends string, Props extends object> {
62
- // using real `extends BaseRecord<'shape', TLShapeId>` introduces a circularity in the types
63
- // and for that reason those "base members" have to be declared manually here
64
- readonly id: TLShapeId
65
- readonly typeName: 'shape'
66
-
41
+ export interface TLBaseShape<Type extends string, Props extends object>
42
+ extends BaseRecord<'shape', TLShapeId> {
67
43
  type: Type
68
44
  x: number
69
45
  y: number
@@ -0,0 +1,122 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { getTestMigration } from '../__tests__/migrationTestUtils'
3
+ import { bookmarkShapeProps, bookmarkShapeVersions } from './TLBookmarkShape'
4
+
5
+ describe('TLBookmarkShape', () => {
6
+ describe('bookmarkShapeProps validation', () => {
7
+ it('should validate width as nonZeroNumber', () => {
8
+ // Valid non-zero positive numbers
9
+ expect(() => bookmarkShapeProps.w.validate(0.1)).not.toThrow()
10
+ expect(() => bookmarkShapeProps.w.validate(100)).not.toThrow()
11
+
12
+ // Invalid: zero and negative
13
+ expect(() => bookmarkShapeProps.w.validate(0)).toThrow()
14
+ expect(() => bookmarkShapeProps.w.validate(-1)).toThrow()
15
+ })
16
+
17
+ it('should validate height as nonZeroNumber', () => {
18
+ // Valid non-zero positive numbers
19
+ expect(() => bookmarkShapeProps.h.validate(0.1)).not.toThrow()
20
+ expect(() => bookmarkShapeProps.h.validate(100)).not.toThrow()
21
+
22
+ // Invalid: zero and negative
23
+ expect(() => bookmarkShapeProps.h.validate(0)).toThrow()
24
+ expect(() => bookmarkShapeProps.h.validate(-1)).toThrow()
25
+ })
26
+
27
+ it('should validate assetId as nullable asset ID', () => {
28
+ // Valid asset IDs
29
+ expect(() => bookmarkShapeProps.assetId.validate(null)).not.toThrow()
30
+ expect(() => bookmarkShapeProps.assetId.validate('asset:bookmark123')).not.toThrow()
31
+
32
+ // Invalid asset IDs
33
+ expect(() => bookmarkShapeProps.assetId.validate('shape:notasset')).toThrow()
34
+ expect(() => bookmarkShapeProps.assetId.validate('bookmark123')).toThrow()
35
+ expect(() => bookmarkShapeProps.assetId.validate(undefined)).toThrow()
36
+ })
37
+
38
+ it('should validate url as linkUrl', () => {
39
+ // Valid URLs
40
+ expect(() => bookmarkShapeProps.url.validate('')).not.toThrow()
41
+ expect(() => bookmarkShapeProps.url.validate('https://example.com')).not.toThrow()
42
+
43
+ // Invalid URLs
44
+ expect(() => bookmarkShapeProps.url.validate('not-a-url')).toThrow()
45
+ expect(() => bookmarkShapeProps.url.validate('javascript:alert("xss")')).toThrow()
46
+ })
47
+ })
48
+
49
+ describe('NullAssetId migration', () => {
50
+ const { up, down } = getTestMigration(bookmarkShapeVersions.NullAssetId)
51
+
52
+ it('should add assetId as null when undefined', () => {
53
+ const oldRecord = {
54
+ props: {
55
+ w: 300,
56
+ h: 320,
57
+ url: 'https://example.com',
58
+ // assetId undefined
59
+ },
60
+ }
61
+
62
+ const result = up(oldRecord)
63
+ expect(result.props.assetId).toBeNull()
64
+ })
65
+
66
+ it('should preserve existing assetId when present', () => {
67
+ const oldRecord = {
68
+ props: {
69
+ w: 300,
70
+ h: 320,
71
+ url: 'https://example.com',
72
+ assetId: 'asset:existing123',
73
+ },
74
+ }
75
+
76
+ const result = up(oldRecord)
77
+ expect(result.props.assetId).toBe('asset:existing123')
78
+ })
79
+
80
+ it('should throw on retired down migration', () => {
81
+ expect(() => down({})).toThrow()
82
+ })
83
+ })
84
+
85
+ describe('MakeUrlsValid migration', () => {
86
+ const { up, down } = getTestMigration(bookmarkShapeVersions.MakeUrlsValid)
87
+
88
+ it('should set invalid URLs to empty string', () => {
89
+ const oldRecord = {
90
+ props: {
91
+ w: 300,
92
+ h: 320,
93
+ assetId: null,
94
+ url: 'not-a-valid-url',
95
+ },
96
+ }
97
+
98
+ const result = up(oldRecord)
99
+ expect(result.props.url).toBe('')
100
+ })
101
+
102
+ it('should preserve valid URLs', () => {
103
+ const oldRecord = {
104
+ props: {
105
+ w: 300,
106
+ h: 320,
107
+ assetId: null,
108
+ url: 'https://example.com',
109
+ },
110
+ }
111
+
112
+ const result = up(oldRecord)
113
+ expect(result.props.url).toBe('https://example.com')
114
+ })
115
+
116
+ it('should be noop for down migration', () => {
117
+ const newRecord = { props: { url: 'https://example.com' } }
118
+ const result = down(newRecord)
119
+ expect(result).toEqual(newRecord)
120
+ })
121
+ })
122
+ })
@@ -0,0 +1,177 @@
1
+ import { T } from '@tldraw/validate'
2
+ import { describe, expect, it } from 'vitest'
3
+ import { getTestMigration } from '../__tests__/migrationTestUtils'
4
+ import { VecModel } from '../misc/geometry-types'
5
+ import { DefaultColorStyle } from '../styles/TLColorStyle'
6
+ import { DefaultDashStyle } from '../styles/TLDashStyle'
7
+ import { DefaultFillStyle } from '../styles/TLFillStyle'
8
+ import { DefaultSizeStyle } from '../styles/TLSizeStyle'
9
+ import { DrawShapeSegment, drawShapeProps, drawShapeVersions } from './TLDrawShape'
10
+
11
+ describe('TLDrawShape', () => {
12
+ describe('DrawShapeSegment validator', () => {
13
+ it('should validate valid segment structures', () => {
14
+ const validSegments = [
15
+ { type: 'free', points: [{ x: 0, y: 0 }] },
16
+ { type: 'straight', points: [{ x: 0, y: 0, z: 0.5 }] },
17
+ { type: 'free', points: [] },
18
+ ]
19
+
20
+ validSegments.forEach((segment) => {
21
+ expect(() => DrawShapeSegment.validate(segment)).not.toThrow()
22
+ })
23
+ })
24
+
25
+ it('should reject invalid segment types and points', () => {
26
+ const invalidSegments = [
27
+ { type: 'invalid', points: [{ x: 0, y: 0 }] },
28
+ { type: 'free', points: [{ x: 'invalid', y: 0 }] },
29
+ { type: 'free', points: 'not-array' },
30
+ {}, // Missing required fields
31
+ ]
32
+
33
+ invalidSegments.forEach((segment) => {
34
+ expect(() => DrawShapeSegment.validate(segment)).toThrow()
35
+ })
36
+ })
37
+ })
38
+
39
+ describe('drawShapeProps validation schema', () => {
40
+ it('should validate complete valid props object', () => {
41
+ const fullValidator = T.object(drawShapeProps)
42
+
43
+ const validProps = {
44
+ color: 'red' as const,
45
+ fill: 'solid' as const,
46
+ dash: 'dashed' as const,
47
+ size: 'l' as const,
48
+ segments: [{ type: 'free' as const, points: [{ x: 0, y: 0, z: 0.5 }] as VecModel[] }],
49
+ isComplete: true,
50
+ isClosed: true,
51
+ isPen: true,
52
+ scale: 1.5,
53
+ }
54
+
55
+ expect(() => fullValidator.validate(validProps)).not.toThrow()
56
+ })
57
+
58
+ it('should reject invalid property values', () => {
59
+ // Test key invalid cases that matter for business logic
60
+ expect(() => drawShapeProps.scale.validate(0)).toThrow() // zero scale invalid
61
+ expect(() => drawShapeProps.scale.validate(-1)).toThrow() // negative scale invalid
62
+ expect(() => drawShapeProps.segments.validate('not-array')).toThrow()
63
+ expect(() => drawShapeProps.segments.validate([{ type: 'invalid' }])).toThrow()
64
+ })
65
+
66
+ it('should use correct default style validators', () => {
67
+ expect(drawShapeProps.color).toBe(DefaultColorStyle)
68
+ expect(drawShapeProps.fill).toBe(DefaultFillStyle)
69
+ expect(drawShapeProps.dash).toBe(DefaultDashStyle)
70
+ expect(drawShapeProps.size).toBe(DefaultSizeStyle)
71
+ })
72
+ })
73
+
74
+ describe('AddInPen migration', () => {
75
+ const { up } = getTestMigration(drawShapeVersions.AddInPen)
76
+
77
+ it('should detect pen from non-standard pressure values', () => {
78
+ const recordWithPen = {
79
+ props: {
80
+ segments: [
81
+ {
82
+ type: 'free',
83
+ points: [
84
+ { x: 0, y: 0, z: 0.3 }, // Non-standard pressure
85
+ { x: 10, y: 10, z: 0.7 }, // Non-standard pressure
86
+ ],
87
+ },
88
+ ],
89
+ },
90
+ }
91
+
92
+ const result = up(recordWithPen)
93
+ expect(result.props.isPen).toBe(true)
94
+ })
95
+
96
+ it('should not detect pen from standard pressure values', () => {
97
+ const recordWithoutPen = {
98
+ props: {
99
+ segments: [
100
+ {
101
+ type: 'free',
102
+ points: [
103
+ { x: 0, y: 0, z: 0 }, // Standard mouse pressure
104
+ { x: 10, y: 10, z: 0.5 }, // Standard touch pressure
105
+ ],
106
+ },
107
+ ],
108
+ },
109
+ }
110
+
111
+ const result = up(recordWithoutPen)
112
+ expect(result.props.isPen).toBe(false)
113
+ })
114
+
115
+ it('should handle empty segments', () => {
116
+ const recordEmpty = {
117
+ props: {
118
+ segments: [{ type: 'free', points: [] }],
119
+ },
120
+ }
121
+
122
+ const result = up(recordEmpty)
123
+ expect(result.props.isPen).toBe(false)
124
+ })
125
+
126
+ it('should require both points to have non-standard pressure', () => {
127
+ const recordMixed = {
128
+ props: {
129
+ segments: [
130
+ {
131
+ type: 'free',
132
+ points: [
133
+ { x: 0, y: 0, z: 0.3 }, // Non-standard
134
+ { x: 10, y: 10, z: 0.5 }, // Standard
135
+ ],
136
+ },
137
+ ],
138
+ },
139
+ }
140
+
141
+ const result = up(recordMixed)
142
+ expect(result.props.isPen).toBe(false)
143
+ })
144
+ })
145
+
146
+ describe('AddScale migration', () => {
147
+ const { up, down } = getTestMigration(drawShapeVersions.AddScale)
148
+
149
+ it('should add scale property with default value 1', () => {
150
+ const oldRecord = {
151
+ props: {
152
+ color: 'blue',
153
+ segments: [{ type: 'free', points: [{ x: 0, y: 0 }] }],
154
+ isPen: false,
155
+ },
156
+ }
157
+
158
+ const result = up(oldRecord)
159
+ expect(result.props.scale).toBe(1)
160
+ })
161
+
162
+ it('should remove scale property on down migration', () => {
163
+ const newRecord = {
164
+ props: {
165
+ color: 'blue',
166
+ segments: [{ type: 'free', points: [{ x: 0, y: 0 }] }],
167
+ isPen: false,
168
+ scale: 1.5,
169
+ },
170
+ }
171
+
172
+ const result = down(newRecord)
173
+ expect(result.props.scale).toBeUndefined()
174
+ expect(result.props.color).toBe('blue') // Other props preserved
175
+ })
176
+ })
177
+ })
@@ -1,6 +1,5 @@
1
1
  import { T } from '@tldraw/validate'
2
- import { b64Vecs } from '../misc/b64Vecs'
3
- import { VecModel } from '../misc/geometry-types'
2
+ import { VecModel, vecModelValidator } from '../misc/geometry-types'
4
3
  import { createShapePropsMigrationIds, createShapePropsMigrationSequence } from '../records/TLShape'
5
4
  import { RecordProps } from '../recordsWithProps'
6
5
  import { DefaultColorStyle, TLDefaultColorStyle } from '../styles/TLColorStyle'
@@ -17,18 +16,26 @@ import { TLBaseShape } from './TLBaseShape'
17
16
  export interface TLDrawShapeSegment {
18
17
  /** Type of drawing segment - 'free' for freehand curves, 'straight' for line segments */
19
18
  type: 'free' | 'straight'
20
- /** Base64-encoded points (x, y, z triplets stored as Float16) */
21
- points: string
19
+ /** Array of points defining the segment path with x, y coordinates and pressure (z) */
20
+ points: VecModel[]
22
21
  }
23
22
 
24
23
  /**
25
24
  * Validator for draw shape segments ensuring proper structure and data types.
26
25
  *
27
26
  * @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
+ * ```
28
35
  */
29
36
  export const DrawShapeSegment: T.ObjectValidator<TLDrawShapeSegment> = T.object({
30
37
  type: T.literalEnum('free', 'straight'),
31
- points: T.string,
38
+ points: T.arrayOf(vecModelValidator),
32
39
  })
33
40
 
34
41
  /**
@@ -55,10 +62,6 @@ export interface TLDrawShapeProps {
55
62
  isPen: boolean
56
63
  /** Scale factor applied to the drawing */
57
64
  scale: number
58
- /** Horizontal scale factor for lazy resize */
59
- scaleX: number
60
- /** Vertical scale factor for lazy resize */
61
- scaleY: number
62
65
  }
63
66
 
64
67
  /**
@@ -115,7 +118,6 @@ export type TLDrawShape = TLBaseShape<'draw', TLDrawShapeProps>
115
118
  * const isValid = drawShapeProps.color.isValid(props.color)
116
119
  * ```
117
120
  */
118
- /** @public */
119
121
  export const drawShapeProps: RecordProps<TLDrawShape> = {
120
122
  color: DefaultColorStyle,
121
123
  fill: DefaultFillStyle,
@@ -126,14 +128,11 @@ export const drawShapeProps: RecordProps<TLDrawShape> = {
126
128
  isClosed: T.boolean,
127
129
  isPen: T.boolean,
128
130
  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,
137
136
  })
138
137
 
139
138
  /**
@@ -185,51 +184,5 @@ export const drawShapeMigrations = createShapePropsMigrationSequence({
185
184
  delete props.scale
186
185
  },
187
186
  },
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
- },
216
187
  ],
217
188
  })
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
- }