@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.
- package/dist-cjs/index.d.ts +82 -34
- package/dist-cjs/index.js +4 -1
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/misc/TLOpacity.js +1 -5
- package/dist-cjs/misc/TLOpacity.js.map +2 -2
- package/dist-cjs/misc/TLRichText.js +5 -1
- package/dist-cjs/misc/TLRichText.js.map +2 -2
- package/dist-cjs/misc/b64Vecs.js +224 -0
- package/dist-cjs/misc/b64Vecs.js.map +7 -0
- package/dist-cjs/shapes/TLArrowShape.js +30 -13
- package/dist-cjs/shapes/TLArrowShape.js.map +2 -2
- package/dist-cjs/shapes/TLDrawShape.js +37 -4
- package/dist-cjs/shapes/TLDrawShape.js.map +2 -2
- package/dist-cjs/shapes/TLEmbedShape.js +17 -0
- package/dist-cjs/shapes/TLEmbedShape.js.map +2 -2
- package/dist-cjs/shapes/TLGeoShape.js +12 -1
- package/dist-cjs/shapes/TLGeoShape.js.map +2 -2
- package/dist-cjs/shapes/TLHighlightShape.js +29 -2
- package/dist-cjs/shapes/TLHighlightShape.js.map +2 -2
- package/dist-cjs/shapes/TLNoteShape.js +12 -1
- package/dist-cjs/shapes/TLNoteShape.js.map +2 -2
- package/dist-cjs/shapes/TLTextShape.js +12 -1
- package/dist-cjs/shapes/TLTextShape.js.map +2 -2
- package/dist-cjs/store-migrations.js +14 -14
- package/dist-cjs/store-migrations.js.map +2 -2
- package/dist-esm/index.d.mts +82 -34
- package/dist-esm/index.mjs +5 -1
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/misc/TLOpacity.mjs +1 -5
- package/dist-esm/misc/TLOpacity.mjs.map +2 -2
- package/dist-esm/misc/TLRichText.mjs +5 -1
- package/dist-esm/misc/TLRichText.mjs.map +2 -2
- package/dist-esm/misc/b64Vecs.mjs +204 -0
- package/dist-esm/misc/b64Vecs.mjs.map +7 -0
- package/dist-esm/shapes/TLArrowShape.mjs +30 -13
- package/dist-esm/shapes/TLArrowShape.mjs.map +2 -2
- package/dist-esm/shapes/TLDrawShape.mjs +37 -4
- package/dist-esm/shapes/TLDrawShape.mjs.map +2 -2
- package/dist-esm/shapes/TLEmbedShape.mjs +17 -0
- package/dist-esm/shapes/TLEmbedShape.mjs.map +2 -2
- package/dist-esm/shapes/TLGeoShape.mjs +12 -1
- package/dist-esm/shapes/TLGeoShape.mjs.map +2 -2
- package/dist-esm/shapes/TLHighlightShape.mjs +29 -2
- package/dist-esm/shapes/TLHighlightShape.mjs.map +2 -2
- package/dist-esm/shapes/TLNoteShape.mjs +12 -1
- package/dist-esm/shapes/TLNoteShape.mjs.map +2 -2
- package/dist-esm/shapes/TLTextShape.mjs +12 -1
- package/dist-esm/shapes/TLTextShape.mjs.map +2 -2
- package/dist-esm/store-migrations.mjs +14 -14
- package/dist-esm/store-migrations.mjs.map +2 -2
- package/package.json +8 -8
- package/src/__tests__/migrationTestUtils.ts +9 -3
- package/src/index.ts +3 -0
- package/src/migrations.test.ts +149 -1
- package/src/misc/TLOpacity.ts +1 -5
- package/src/misc/TLRichText.ts +6 -1
- package/src/misc/b64Vecs.ts +308 -0
- package/src/shapes/TLArrowShape.ts +36 -13
- package/src/shapes/TLDrawShape.ts +59 -12
- package/src/shapes/TLEmbedShape.ts +17 -0
- package/src/shapes/TLGeoShape.ts +14 -1
- package/src/shapes/TLHighlightShape.ts +37 -0
- package/src/shapes/TLNoteShape.ts +15 -1
- package/src/shapes/TLTextShape.ts +16 -2
- package/src/store-migrations.ts +15 -15
- package/src/assets/TLBookmarkAsset.test.ts +0 -96
- package/src/assets/TLImageAsset.test.ts +0 -213
- package/src/assets/TLVideoAsset.test.ts +0 -105
- package/src/bindings/TLArrowBinding.test.ts +0 -55
- package/src/misc/id-validator.test.ts +0 -50
- package/src/records/TLAsset.test.ts +0 -234
- package/src/records/TLBinding.test.ts +0 -22
- package/src/records/TLCamera.test.ts +0 -19
- package/src/records/TLDocument.test.ts +0 -35
- package/src/records/TLInstance.test.ts +0 -201
- package/src/records/TLPage.test.ts +0 -110
- package/src/records/TLPageState.test.ts +0 -228
- package/src/records/TLPointer.test.ts +0 -63
- package/src/records/TLPresence.test.ts +0 -190
- package/src/records/TLRecord.test.ts +0 -82
- package/src/records/TLShape.test.ts +0 -232
- package/src/shapes/ShapeWithCrop.test.ts +0 -18
- package/src/shapes/TLArrowShape.test.ts +0 -505
- package/src/shapes/TLBaseShape.test.ts +0 -142
- package/src/shapes/TLBookmarkShape.test.ts +0 -122
- package/src/shapes/TLDrawShape.test.ts +0 -177
- package/src/shapes/TLEmbedShape.test.ts +0 -286
- package/src/shapes/TLFrameShape.test.ts +0 -71
- package/src/shapes/TLGeoShape.test.ts +0 -247
- package/src/shapes/TLGroupShape.test.ts +0 -59
- package/src/shapes/TLHighlightShape.test.ts +0 -325
- package/src/shapes/TLImageShape.test.ts +0 -534
- package/src/shapes/TLLineShape.test.ts +0 -269
- package/src/shapes/TLNoteShape.test.ts +0 -1568
- package/src/shapes/TLTextShape.test.ts +0 -407
- package/src/shapes/TLVideoShape.test.ts +0 -112
- 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: '
|
|
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,
|
|
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.
|
|
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": "^
|
|
50
|
+
"react": "^19.2.1",
|
|
51
51
|
"vitest": "^3.2.4"
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
|
-
"@tldraw/state": "4.3.0-next.
|
|
55
|
-
"@tldraw/store": "4.3.0-next.
|
|
56
|
-
"@tldraw/utils": "4.3.0-next.
|
|
57
|
-
"@tldraw/validate": "4.3.0-next.
|
|
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.
|
|
61
|
-
"react-dom": "^18.2.0 || ^19.
|
|
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
|
-
|
|
29
|
-
|
|
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'
|
package/src/migrations.test.ts
CHANGED
|
@@ -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
|
package/src/misc/TLOpacity.ts
CHANGED
|
@@ -53,8 +53,4 @@ export type TLOpacityType = number
|
|
|
53
53
|
*
|
|
54
54
|
* @public
|
|
55
55
|
*/
|
|
56
|
-
export const opacityValidator = T.
|
|
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
|
package/src/misc/TLRichText.ts
CHANGED
|
@@ -12,7 +12,12 @@ import { T } from '@tldraw/validate'
|
|
|
12
12
|
* const isValid = richTextValidator.check(richText) // true
|
|
13
13
|
* ```
|
|
14
14
|
*/
|
|
15
|
-
|
|
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
|
+
}
|