@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,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/store-migrations.ts"],
4
- "sourcesContent": ["import { createMigrationIds, createMigrationSequence } from '@tldraw/store'\nimport { IndexKey, objectMapEntries } from '@tldraw/utils'\nimport { TLPage } from './records/TLPage'\nimport { TLShape } from './records/TLShape'\nimport { TLLineShape } from './shapes/TLLineShape'\n\n/**\n * Migration version constants for store-level schema changes.\n * Each version represents a breaking change that requires data transformation.\n *\n * @internal\n */\nconst Versions = createMigrationIds('com.tldraw.store', {\n\tRemoveCodeAndIconShapeTypes: 1,\n\tAddInstancePresenceType: 2,\n\tRemoveTLUserAndPresenceAndAddPointer: 3,\n\tRemoveUserDocument: 4,\n\tFixIndexKeys: 5,\n} as const)\n\n/**\n * Migration version identifiers for store-level migrations.\n * These versions track changes to the overall store structure and data model.\n *\n * @example\n * ```ts\n * import { storeVersions } from '@tldraw/tlschema'\n *\n * // Check if a specific migration version exists\n * const hasRemoveCodeShapes = storeVersions.RemoveCodeAndIconShapeTypes\n * ```\n *\n * @public\n */\nexport { Versions as storeVersions }\n\n/**\n * Store-level migration sequence that handles evolution of the tldraw data model.\n * These migrations run when the store schema version changes and ensure backward\n * compatibility by transforming old data structures to new formats.\n *\n * The migrations handle:\n * - Removal of deprecated shape types (code, icon)\n * - Addition of new record types (instance presence)\n * - Cleanup of obsolete user and presence data\n * - Removal of deprecated user document records\n *\n * @example\n * ```ts\n * import { storeMigrations } from '@tldraw/tlschema'\n * import { migrate } from '@tldraw/store'\n *\n * // Apply store migrations to old data\n * const migratedStore = migrate({\n * store: oldStoreData,\n * migrations: storeMigrations,\n * fromVersion: 0,\n * toVersion: storeMigrations.currentVersion\n * })\n * ```\n *\n * @public\n */\nexport const storeMigrations = createMigrationSequence({\n\tsequenceId: 'com.tldraw.store',\n\tretroactive: false,\n\tsequence: [\n\t\t{\n\t\t\tid: Versions.RemoveCodeAndIconShapeTypes,\n\t\t\tscope: 'store',\n\t\t\tup: (store) => {\n\t\t\t\tfor (const [id, record] of objectMapEntries(store)) {\n\t\t\t\t\tif (\n\t\t\t\t\t\trecord.typeName === 'shape' &&\n\t\t\t\t\t\t'type' in record &&\n\t\t\t\t\t\t(record.type === 'icon' || record.type === 'code')\n\t\t\t\t\t) {\n\t\t\t\t\t\tdelete store[id]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: Versions.AddInstancePresenceType,\n\t\t\tscope: 'store',\n\t\t\tup(_store) {\n\t\t\t\t// noop\n\t\t\t\t// there used to be a down migration for this but we made down migrations optional\n\t\t\t\t// and we don't use them on store-level migrations so we can just remove it\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t// remove user and presence records and add pointer records\n\t\t\tid: Versions.RemoveTLUserAndPresenceAndAddPointer,\n\t\t\tscope: 'store',\n\t\t\tup: (store) => {\n\t\t\t\tfor (const [id, record] of objectMapEntries(store)) {\n\t\t\t\t\tif (record.typeName.match(/^(user|user_presence)$/)) {\n\t\t\t\t\t\tdelete store[id]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t// remove user document records\n\t\t\tid: Versions.RemoveUserDocument,\n\t\t\tscope: 'store',\n\t\t\tup: (store) => {\n\t\t\t\tfor (const [id, record] of objectMapEntries(store)) {\n\t\t\t\t\tif (record.typeName.match('user_document')) {\n\t\t\t\t\t\tdelete store[id]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: Versions.FixIndexKeys,\n\t\t\tscope: 'record',\n\t\t\tup: (record) => {\n\t\t\t\tif (['shape', 'page'].includes(record.typeName) && 'index' in record) {\n\t\t\t\t\tconst recordWithIndex = record as TLShape | TLPage\n\t\t\t\t\t// Our newer fractional indexed library (more correctly) validates that indices\n\t\t\t\t\t// do not end with 0. ('a0' being an exception)\n\t\t\t\t\tif (recordWithIndex.index.endsWith('0') && recordWithIndex.index !== 'a0') {\n\t\t\t\t\t\trecordWithIndex.index = (recordWithIndex.index.slice(0, -1) +\n\t\t\t\t\t\t\tgetNRandomBase62Digits(3)) as IndexKey\n\t\t\t\t\t}\n\t\t\t\t\t// Line shapes have 'points' that have indices as well.\n\t\t\t\t\tif (record.typeName === 'shape' && (recordWithIndex as TLShape).type === 'line') {\n\t\t\t\t\t\tconst lineShape = recordWithIndex as TLLineShape\n\t\t\t\t\t\tfor (const [_, point] of objectMapEntries(lineShape.props.points)) {\n\t\t\t\t\t\t\tif (point.index.endsWith('0') && point.index !== 'a0') {\n\t\t\t\t\t\t\t\tpoint.index = (point.index.slice(0, -1) + getNRandomBase62Digits(3)) as IndexKey\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\t\t\tdown: () => {\n\t\t\t\t// noop\n\t\t\t\t// Enables tlsync to support older clients so as to not force people to refresh immediately after deploying.\n\t\t\t},\n\t\t},\n\t],\n})\n\nconst BASE_62_DIGITS_WITHOUT_ZERO = '123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'\nconst getRandomBase62Digit = () => {\n\treturn BASE_62_DIGITS_WITHOUT_ZERO.charAt(\n\t\tMath.floor(Math.random() * BASE_62_DIGITS_WITHOUT_ZERO.length)\n\t)\n}\n\nconst getNRandomBase62Digits = (n: number) => {\n\treturn Array.from({ length: n }, getRandomBase62Digit).join('')\n}\n"],
5
- "mappings": "AAAA,SAAS,oBAAoB,+BAA+B;AAC5D,SAAmB,wBAAwB;AAW3C,MAAM,WAAW,mBAAmB,oBAAoB;AAAA,EACvD,6BAA6B;AAAA,EAC7B,yBAAyB;AAAA,EACzB,sCAAsC;AAAA,EACtC,oBAAoB;AAAA,EACpB,cAAc;AACf,CAAU;AA6CH,MAAM,kBAAkB,wBAAwB;AAAA,EACtD,YAAY;AAAA,EACZ,aAAa;AAAA,EACb,UAAU;AAAA,IACT;AAAA,MACC,IAAI,SAAS;AAAA,MACb,OAAO;AAAA,MACP,IAAI,CAAC,UAAU;AACd,mBAAW,CAAC,IAAI,MAAM,KAAK,iBAAiB,KAAK,GAAG;AACnD,cACC,OAAO,aAAa,WACpB,UAAU,WACT,OAAO,SAAS,UAAU,OAAO,SAAS,SAC1C;AACD,mBAAO,MAAM,EAAE;AAAA,UAChB;AAAA,QACD;AAAA,MACD;AAAA,IACD;AAAA,IACA;AAAA,MACC,IAAI,SAAS;AAAA,MACb,OAAO;AAAA,MACP,GAAG,QAAQ;AAAA,MAIX;AAAA,IACD;AAAA,IACA;AAAA;AAAA,MAEC,IAAI,SAAS;AAAA,MACb,OAAO;AAAA,MACP,IAAI,CAAC,UAAU;AACd,mBAAW,CAAC,IAAI,MAAM,KAAK,iBAAiB,KAAK,GAAG;AACnD,cAAI,OAAO,SAAS,MAAM,wBAAwB,GAAG;AACpD,mBAAO,MAAM,EAAE;AAAA,UAChB;AAAA,QACD;AAAA,MACD;AAAA,IACD;AAAA,IACA;AAAA;AAAA,MAEC,IAAI,SAAS;AAAA,MACb,OAAO;AAAA,MACP,IAAI,CAAC,UAAU;AACd,mBAAW,CAAC,IAAI,MAAM,KAAK,iBAAiB,KAAK,GAAG;AACnD,cAAI,OAAO,SAAS,MAAM,eAAe,GAAG;AAC3C,mBAAO,MAAM,EAAE;AAAA,UAChB;AAAA,QACD;AAAA,MACD;AAAA,IACD;AAAA,IACA;AAAA,MACC,IAAI,SAAS;AAAA,MACb,OAAO;AAAA,MACP,IAAI,CAAC,WAAW;AACf,YAAI,CAAC,SAAS,MAAM,EAAE,SAAS,OAAO,QAAQ,KAAK,WAAW,QAAQ;AACrE,gBAAM,kBAAkB;AAGxB,cAAI,gBAAgB,MAAM,SAAS,GAAG,KAAK,gBAAgB,UAAU,MAAM;AAC1E,4BAAgB,QAAS,gBAAgB,MAAM,MAAM,GAAG,EAAE,IACzD,uBAAuB,CAAC;AAAA,UAC1B;AAEA,cAAI,OAAO,aAAa,WAAY,gBAA4B,SAAS,QAAQ;AAChF,kBAAM,YAAY;AAClB,uBAAW,CAAC,GAAG,KAAK,KAAK,iBAAiB,UAAU,MAAM,MAAM,GAAG;AAClE,kBAAI,MAAM,MAAM,SAAS,GAAG,KAAK,MAAM,UAAU,MAAM;AACtD,sBAAM,QAAS,MAAM,MAAM,MAAM,GAAG,EAAE,IAAI,uBAAuB,CAAC;AAAA,cACnE;AAAA,YACD;AAAA,UACD;AAAA,QACD;AAAA,MACD;AAAA,MACA,MAAM,MAAM;AAAA,MAGZ;AAAA,IACD;AAAA,EACD;AACD,CAAC;AAED,MAAM,8BAA8B;AACpC,MAAM,uBAAuB,MAAM;AAClC,SAAO,4BAA4B;AAAA,IAClC,KAAK,MAAM,KAAK,OAAO,IAAI,4BAA4B,MAAM;AAAA,EAC9D;AACD;AAEA,MAAM,yBAAyB,CAAC,MAAc;AAC7C,SAAO,MAAM,KAAK,EAAE,QAAQ,EAAE,GAAG,oBAAoB,EAAE,KAAK,EAAE;AAC/D;",
4
+ "sourcesContent": ["import { createMigrationIds, createMigrationSequence } from '@tldraw/store'\nimport { IndexKey, objectMapEntries } from '@tldraw/utils'\nimport { TLPage } from './records/TLPage'\nimport { TLShape } from './records/TLShape'\nimport { TLLineShape } from './shapes/TLLineShape'\n\n/**\n * Migration version constants for store-level schema changes.\n * Each version represents a breaking change that requires data transformation.\n *\n * @internal\n */\nconst Versions = createMigrationIds('com.tldraw.store', {\n\tRemoveCodeAndIconShapeTypes: 1,\n\tAddInstancePresenceType: 2,\n\tRemoveTLUserAndPresenceAndAddPointer: 3,\n\tRemoveUserDocument: 4,\n\tFixIndexKeys: 5,\n} as const)\n\n/**\n * Migration version identifiers for store-level migrations.\n * These versions track changes to the overall store structure and data model.\n *\n * @example\n * ```ts\n * import { storeVersions } from '@tldraw/tlschema'\n *\n * // Check if a specific migration version exists\n * const hasRemoveCodeShapes = storeVersions.RemoveCodeAndIconShapeTypes\n * ```\n *\n * @public\n */\nexport { Versions as storeVersions }\n\n/**\n * Store-level migration sequence that handles evolution of the tldraw data model.\n * These migrations run when the store schema version changes and ensure backward\n * compatibility by transforming old data structures to new formats.\n *\n * The migrations handle:\n * - Removal of deprecated shape types (code, icon)\n * - Addition of new record types (instance presence)\n * - Cleanup of obsolete user and presence data\n * - Removal of deprecated user document records\n *\n * @example\n * ```ts\n * import { storeMigrations } from '@tldraw/tlschema'\n * import { migrate } from '@tldraw/store'\n *\n * // Apply store migrations to old data\n * const migratedStore = migrate({\n * store: oldStoreData,\n * migrations: storeMigrations,\n * fromVersion: 0,\n * toVersion: storeMigrations.currentVersion\n * })\n * ```\n *\n * @public\n */\nexport const storeMigrations = createMigrationSequence({\n\tsequenceId: 'com.tldraw.store',\n\tretroactive: false,\n\tsequence: [\n\t\t{\n\t\t\tid: Versions.RemoveCodeAndIconShapeTypes,\n\t\t\tscope: 'storage',\n\t\t\tup: (storage) => {\n\t\t\t\tfor (const [id, record] of storage.entries()) {\n\t\t\t\t\tif (\n\t\t\t\t\t\trecord.typeName === 'shape' &&\n\t\t\t\t\t\t'type' in record &&\n\t\t\t\t\t\t(record.type === 'icon' || record.type === 'code')\n\t\t\t\t\t) {\n\t\t\t\t\t\tstorage.delete(id)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: Versions.AddInstancePresenceType,\n\t\t\tscope: 'storage',\n\t\t\tup(_storage) {\n\t\t\t\t// noop\n\t\t\t\t// there used to be a down migration for this but we made down migrations optional\n\t\t\t\t// and we don't use them on storage-level migrations so we can just remove it\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t// remove user and presence records and add pointer records\n\t\t\tid: Versions.RemoveTLUserAndPresenceAndAddPointer,\n\t\t\tscope: 'storage',\n\t\t\tup: (storage) => {\n\t\t\t\tfor (const [id, record] of storage.entries()) {\n\t\t\t\t\tif (record.typeName.match(/^(user|user_presence)$/)) {\n\t\t\t\t\t\tstorage.delete(id)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t// remove user document records\n\t\t\tid: Versions.RemoveUserDocument,\n\t\t\tscope: 'storage',\n\t\t\tup: (storage) => {\n\t\t\t\tfor (const [id, record] of storage.entries()) {\n\t\t\t\t\tif (record.typeName.match('user_document')) {\n\t\t\t\t\t\tstorage.delete(id)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: Versions.FixIndexKeys,\n\t\t\tscope: 'record',\n\t\t\tup: (record) => {\n\t\t\t\tif (['shape', 'page'].includes(record.typeName) && 'index' in record) {\n\t\t\t\t\tconst recordWithIndex = record as TLShape | TLPage\n\t\t\t\t\t// Our newer fractional indexed library (more correctly) validates that indices\n\t\t\t\t\t// do not end with 0. ('a0' being an exception)\n\t\t\t\t\tif (recordWithIndex.index.endsWith('0') && recordWithIndex.index !== 'a0') {\n\t\t\t\t\t\trecordWithIndex.index = (recordWithIndex.index.slice(0, -1) +\n\t\t\t\t\t\t\tgetNRandomBase62Digits(3)) as IndexKey\n\t\t\t\t\t}\n\t\t\t\t\t// Line shapes have 'points' that have indices as well.\n\t\t\t\t\tif (record.typeName === 'shape' && (recordWithIndex as TLShape).type === 'line') {\n\t\t\t\t\t\tconst lineShape = recordWithIndex as TLLineShape\n\t\t\t\t\t\tfor (const [_, point] of objectMapEntries(lineShape.props.points)) {\n\t\t\t\t\t\t\tif (point.index.endsWith('0') && point.index !== 'a0') {\n\t\t\t\t\t\t\t\tpoint.index = (point.index.slice(0, -1) + getNRandomBase62Digits(3)) as IndexKey\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\t\t\tdown: () => {\n\t\t\t\t// noop\n\t\t\t\t// Enables tlsync to support older clients so as to not force people to refresh immediately after deploying.\n\t\t\t},\n\t\t},\n\t],\n})\n\nconst BASE_62_DIGITS_WITHOUT_ZERO = '123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'\nconst getRandomBase62Digit = () => {\n\treturn BASE_62_DIGITS_WITHOUT_ZERO.charAt(\n\t\tMath.floor(Math.random() * BASE_62_DIGITS_WITHOUT_ZERO.length)\n\t)\n}\n\nconst getNRandomBase62Digits = (n: number) => {\n\treturn Array.from({ length: n }, getRandomBase62Digit).join('')\n}\n"],
5
+ "mappings": "AAAA,SAAS,oBAAoB,+BAA+B;AAC5D,SAAmB,wBAAwB;AAW3C,MAAM,WAAW,mBAAmB,oBAAoB;AAAA,EACvD,6BAA6B;AAAA,EAC7B,yBAAyB;AAAA,EACzB,sCAAsC;AAAA,EACtC,oBAAoB;AAAA,EACpB,cAAc;AACf,CAAU;AA6CH,MAAM,kBAAkB,wBAAwB;AAAA,EACtD,YAAY;AAAA,EACZ,aAAa;AAAA,EACb,UAAU;AAAA,IACT;AAAA,MACC,IAAI,SAAS;AAAA,MACb,OAAO;AAAA,MACP,IAAI,CAAC,YAAY;AAChB,mBAAW,CAAC,IAAI,MAAM,KAAK,QAAQ,QAAQ,GAAG;AAC7C,cACC,OAAO,aAAa,WACpB,UAAU,WACT,OAAO,SAAS,UAAU,OAAO,SAAS,SAC1C;AACD,oBAAQ,OAAO,EAAE;AAAA,UAClB;AAAA,QACD;AAAA,MACD;AAAA,IACD;AAAA,IACA;AAAA,MACC,IAAI,SAAS;AAAA,MACb,OAAO;AAAA,MACP,GAAG,UAAU;AAAA,MAIb;AAAA,IACD;AAAA,IACA;AAAA;AAAA,MAEC,IAAI,SAAS;AAAA,MACb,OAAO;AAAA,MACP,IAAI,CAAC,YAAY;AAChB,mBAAW,CAAC,IAAI,MAAM,KAAK,QAAQ,QAAQ,GAAG;AAC7C,cAAI,OAAO,SAAS,MAAM,wBAAwB,GAAG;AACpD,oBAAQ,OAAO,EAAE;AAAA,UAClB;AAAA,QACD;AAAA,MACD;AAAA,IACD;AAAA,IACA;AAAA;AAAA,MAEC,IAAI,SAAS;AAAA,MACb,OAAO;AAAA,MACP,IAAI,CAAC,YAAY;AAChB,mBAAW,CAAC,IAAI,MAAM,KAAK,QAAQ,QAAQ,GAAG;AAC7C,cAAI,OAAO,SAAS,MAAM,eAAe,GAAG;AAC3C,oBAAQ,OAAO,EAAE;AAAA,UAClB;AAAA,QACD;AAAA,MACD;AAAA,IACD;AAAA,IACA;AAAA,MACC,IAAI,SAAS;AAAA,MACb,OAAO;AAAA,MACP,IAAI,CAAC,WAAW;AACf,YAAI,CAAC,SAAS,MAAM,EAAE,SAAS,OAAO,QAAQ,KAAK,WAAW,QAAQ;AACrE,gBAAM,kBAAkB;AAGxB,cAAI,gBAAgB,MAAM,SAAS,GAAG,KAAK,gBAAgB,UAAU,MAAM;AAC1E,4BAAgB,QAAS,gBAAgB,MAAM,MAAM,GAAG,EAAE,IACzD,uBAAuB,CAAC;AAAA,UAC1B;AAEA,cAAI,OAAO,aAAa,WAAY,gBAA4B,SAAS,QAAQ;AAChF,kBAAM,YAAY;AAClB,uBAAW,CAAC,GAAG,KAAK,KAAK,iBAAiB,UAAU,MAAM,MAAM,GAAG;AAClE,kBAAI,MAAM,MAAM,SAAS,GAAG,KAAK,MAAM,UAAU,MAAM;AACtD,sBAAM,QAAS,MAAM,MAAM,MAAM,GAAG,EAAE,IAAI,uBAAuB,CAAC;AAAA,cACnE;AAAA,YACD;AAAA,UACD;AAAA,QACD;AAAA,MACD;AAAA,MACA,MAAM,MAAM;AAAA,MAGZ;AAAA,IACD;AAAA,EACD;AACD,CAAC;AAED,MAAM,8BAA8B;AACpC,MAAM,uBAAuB,MAAM;AAClC,SAAO,4BAA4B;AAAA,IAClC,KAAK,MAAM,KAAK,OAAO,IAAI,4BAA4B,MAAM;AAAA,EAC9D;AACD;AAEA,MAAM,yBAAyB,CAAC,MAAc;AAC7C,SAAO,MAAM,KAAK,EAAE,QAAQ,EAAE,GAAG,oBAAoB,EAAE,KAAK,EAAE;AAC/D;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tldraw/tlschema",
3
3
  "description": "tldraw infinite canvas SDK (schema).",
4
- "version": "4.3.0-next.2d181ae353a2",
4
+ "version": "4.3.0-next.40e4536afc8e",
5
5
  "author": {
6
6
  "name": "tldraw Inc.",
7
7
  "email": "hello@tldraw.com"
@@ -47,18 +47,18 @@
47
47
  "devDependencies": {
48
48
  "kleur": "^4.1.5",
49
49
  "lazyrepo": "0.0.0-alpha.27",
50
- "react": "^18.3.1",
50
+ "react": "^19.2.1",
51
51
  "vitest": "^3.2.4"
52
52
  },
53
53
  "dependencies": {
54
- "@tldraw/state": "4.3.0-next.2d181ae353a2",
55
- "@tldraw/store": "4.3.0-next.2d181ae353a2",
56
- "@tldraw/utils": "4.3.0-next.2d181ae353a2",
57
- "@tldraw/validate": "4.3.0-next.2d181ae353a2"
54
+ "@tldraw/state": "4.3.0-next.40e4536afc8e",
55
+ "@tldraw/store": "4.3.0-next.40e4536afc8e",
56
+ "@tldraw/utils": "4.3.0-next.40e4536afc8e",
57
+ "@tldraw/validate": "4.3.0-next.40e4536afc8e"
58
58
  },
59
59
  "peerDependencies": {
60
- "react": "^18.2.0 || ^19.0.0",
61
- "react-dom": "^18.2.0 || ^19.0.0"
60
+ "react": "^18.2.0 || ^19.2.1",
61
+ "react-dom": "^18.2.0 || ^19.2.1"
62
62
  },
63
63
  "module": "dist-esm/index.mjs",
64
64
  "source": "src/index.ts",
@@ -1,4 +1,4 @@
1
- import { Migration, MigrationId } from '@tldraw/store'
1
+ import { devFreeze, Migration, MigrationId } from '@tldraw/store'
2
2
  import { mockUniqueId, structuredClone } from '@tldraw/utils'
3
3
  import { vi } from 'vitest'
4
4
  import { createTLSchema } from '../createTLSchema'
@@ -25,8 +25,14 @@ export function getTestMigration(migrationId: MigrationId) {
25
25
  id: migrationId,
26
26
  up: (stuff: any) => {
27
27
  nextNanoId = 0
28
- const result = structuredClone(stuff)
29
- return migration.up(result) ?? result
28
+ if (migration.scope === 'record' || migration.scope === 'store') {
29
+ const result = structuredClone(stuff)
30
+ return migration.up(result) ?? result
31
+ }
32
+ const storage =
33
+ typeof stuff.entries === 'function' ? stuff : new Map(Object.entries(stuff).map(devFreeze))
34
+ migration.up(storage)
35
+ return typeof stuff.entries === 'function' ? storage : Object.fromEntries(storage.entries())
30
36
  },
31
37
  down: (stuff: any) => {
32
38
  nextNanoId = 0
package/src/index.ts CHANGED
@@ -191,6 +191,7 @@ export {
191
191
  type TLBookmarkShapeProps,
192
192
  } from './shapes/TLBookmarkShape'
193
193
  export {
194
+ compressLegacySegments,
194
195
  drawShapeMigrations,
195
196
  drawShapeProps,
196
197
  type TLDrawShape,
@@ -313,3 +314,5 @@ registerTldrawLibraryVersion(
313
314
  (globalThis as any).TLDRAW_LIBRARY_VERSION,
314
315
  (globalThis as any).TLDRAW_LIBRARY_MODULES
315
316
  )
317
+
318
+ export { b64Vecs } from './misc/b64Vecs'
@@ -16,7 +16,7 @@ import { instancePresenceVersions } from './records/TLPresence'
16
16
  import { TLShape, rootShapeVersions } from './records/TLShape'
17
17
  import { arrowShapeVersions } from './shapes/TLArrowShape'
18
18
  import { bookmarkShapeVersions } from './shapes/TLBookmarkShape'
19
- import { drawShapeVersions } from './shapes/TLDrawShape'
19
+ import { compressLegacySegments, drawShapeVersions } from './shapes/TLDrawShape'
20
20
  import { embedShapeVersions } from './shapes/TLEmbedShape'
21
21
  import { frameShapeVersions } from './shapes/TLFrameShape'
22
22
  import { geoShapeVersions } from './shapes/TLGeoShape'
@@ -1444,6 +1444,32 @@ describe('Add rich text', () => {
1444
1444
  }
1445
1445
  })
1446
1446
 
1447
+ describe('Add rich text attrs', () => {
1448
+ const migrations = [
1449
+ ['text shape', getTestMigration(textShapeVersions.AddRichTextAttrs)],
1450
+ ['geo shape', getTestMigration(geoShapeVersions.AddRichTextAttrs)],
1451
+ ['note shape', getTestMigration(noteShapeVersions.AddRichTextAttrs)],
1452
+ ['arrow shape', getTestMigration(arrowShapeVersions.AddRichTextAttrs)],
1453
+ ] as const
1454
+
1455
+ for (const [shapeName, { up, down }] of migrations) {
1456
+ it(`works for ${shapeName}`, () => {
1457
+ const shape = { props: { richText: toRichText('hello, world') } }
1458
+ const shapeWithAttrs = {
1459
+ props: { richText: { ...toRichText('hello, world'), attrs: { test: 'value' } } },
1460
+ }
1461
+
1462
+ // Up migration should be a noop
1463
+ expect(up(shape)).toEqual(shape)
1464
+ expect(up(shapeWithAttrs)).toEqual(shapeWithAttrs)
1465
+
1466
+ // Down migration should remove attrs
1467
+ expect(down(shapeWithAttrs)).toEqual(shape)
1468
+ expect(down(shape)).toEqual(shape)
1469
+ })
1470
+ }
1471
+ })
1472
+
1447
1473
  describe('Make urls valid for all the assets', () => {
1448
1474
  const migrations = [
1449
1475
  ['bookmark asset', getTestMigration(bookmarkAssetVersions.MakeUrlsValid)],
@@ -2242,6 +2268,128 @@ describe('TLVideoAsset AddAutoplay', () => {
2242
2268
  })
2243
2269
  })
2244
2270
 
2271
+ describe('Add scaleX, scaleY, and new base64 format to draw shape', () => {
2272
+ const { up, down } = getTestMigration(drawShapeVersions.Base64)
2273
+
2274
+ test('up works as expected', () => {
2275
+ const legacySegments = [
2276
+ {
2277
+ type: 'free',
2278
+ points: [
2279
+ { x: 0, y: 0, z: 0.5 },
2280
+ { x: 10, y: 10, z: 0.6 },
2281
+ { x: 20, y: 20, z: 0.7 },
2282
+ ],
2283
+ },
2284
+ {
2285
+ type: 'straight',
2286
+ points: [
2287
+ { x: 20, y: 20, z: 0.7 },
2288
+ { x: 30, y: 30, z: 0.8 },
2289
+ ],
2290
+ },
2291
+ ]
2292
+ expect(
2293
+ up({
2294
+ props: {
2295
+ segments: legacySegments,
2296
+ },
2297
+ })
2298
+ ).toEqual({
2299
+ props: {
2300
+ scaleX: 1,
2301
+ scaleY: 1,
2302
+ segments: compressLegacySegments(legacySegments as any),
2303
+ },
2304
+ })
2305
+ })
2306
+
2307
+ test('down works as expected', () => {
2308
+ const legacySegments = [
2309
+ {
2310
+ type: 'free',
2311
+ points: [
2312
+ { x: 0, y: 0, z: 0.5 },
2313
+ { x: 10, y: 10, z: 0.6 },
2314
+ ],
2315
+ },
2316
+ ]
2317
+ const compressed = compressLegacySegments(legacySegments as any)
2318
+ const result = down({
2319
+ props: {
2320
+ scaleX: 1,
2321
+ scaleY: 1,
2322
+ segments: compressed,
2323
+ },
2324
+ })
2325
+ expect(result.props.scaleX).toBeUndefined()
2326
+ expect(result.props.scaleY).toBeUndefined()
2327
+ expect(Array.isArray(result.props.segments[0].points)).toBe(true)
2328
+ expect(result.props.segments[0].points.length).toBe(2)
2329
+ })
2330
+ })
2331
+
2332
+ describe('Add scaleX, scaleY, and new base64 format to highlight shape', () => {
2333
+ const { up, down } = getTestMigration(highlightShapeVersions.Base64)
2334
+
2335
+ test('up works as expected', () => {
2336
+ const legacySegments = [
2337
+ {
2338
+ type: 'free',
2339
+ points: [
2340
+ { x: 0, y: 0, z: 0.5 },
2341
+ { x: 10, y: 10, z: 0.6 },
2342
+ { x: 20, y: 20, z: 0.7 },
2343
+ ],
2344
+ },
2345
+ {
2346
+ type: 'straight',
2347
+ points: [
2348
+ { x: 20, y: 20, z: 0.7 },
2349
+ { x: 30, y: 30, z: 0.8 },
2350
+ ],
2351
+ },
2352
+ ]
2353
+ expect(
2354
+ up({
2355
+ props: {
2356
+ segments: legacySegments,
2357
+ },
2358
+ })
2359
+ ).toEqual({
2360
+ props: {
2361
+ scaleX: 1,
2362
+ scaleY: 1,
2363
+ segments: compressLegacySegments(legacySegments as any),
2364
+ },
2365
+ })
2366
+ })
2367
+
2368
+ test('down works as expected', () => {
2369
+ const legacySegments = [
2370
+ {
2371
+ type: 'free',
2372
+ points: [
2373
+ { x: 0, y: 0, z: 0.5 },
2374
+ { x: 10, y: 10, z: 0.6 },
2375
+ ],
2376
+ },
2377
+ ]
2378
+ const compressed = compressLegacySegments(legacySegments as any)
2379
+ const result = down({
2380
+ props: {
2381
+ scaleX: 1,
2382
+ scaleY: 1,
2383
+ segments: compressed,
2384
+ },
2385
+ })
2386
+ expect(result.props.scaleX).toBeUndefined()
2387
+ expect(result.props.scaleY).toBeUndefined()
2388
+ expect(Array.isArray(result.props.segments[0].points)).toBe(true)
2389
+ expect(result.props.segments[0].points.length).toBe(2)
2390
+ })
2391
+ })
2392
+
2245
2393
  /* --- PUT YOUR MIGRATIONS TESTS ABOVE HERE --- */
2246
2394
 
2247
2395
  // check that all migrator fns were called at least once
@@ -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
+ }