@tldraw/tlschema
Version:
tldraw infinite canvas SDK (schema).
476 lines (475 loc) • 19.1 kB
JavaScript
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var b64Vecs_exports = {};
__export(b64Vecs_exports, {
DIM_2D: () => DIM_2D,
DIM_3D: () => DIM_3D,
b64Vecs: () => b64Vecs,
fallbackBase64ToUint8Array: () => fallbackBase64ToUint8Array,
fallbackUint8ArrayToBase64: () => fallbackUint8ArrayToBase64,
float16BitsToNumber: () => float16BitsToNumber,
numberToFloat16Bits: () => numberToFloat16Bits
});
module.exports = __toCommonJS(b64Vecs_exports);
const _POINT_B64_LENGTH = 8;
const FIRST_POINT_B64_LENGTH = 16;
const FIRST_POINT_2D_B64_LENGTH = 12;
const DEFAULT_PRESSURE = 0.5;
const DIM_2D = 2;
const DIM_3D = 3;
const BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
const B64_LOOKUP = new Uint8Array(128);
for (let i = 0; i < 64; i++) {
B64_LOOKUP[BASE64_CHARS.charCodeAt(i)] = i;
}
const SIX_BIT_MASK = 63;
const PADDING_CHAR_CODE = "=".charCodeAt(0);
const POW2 = new Float64Array(31);
for (let i = 0; i < 31; i++) {
POW2[i] = Math.pow(2, i - 15);
}
const POW2_SUBNORMAL = Math.pow(2, -14) / 1024;
const MANTISSA = new Float64Array(1024);
for (let i = 0; i < 1024; i++) {
MANTISSA[i] = 1 + i / 1024;
}
function nativeGetFloat16(dataView, offset) {
return dataView.getFloat16(offset, true);
}
function fallbackGetFloat16(dataView, offset) {
return float16BitsToNumber(dataView.getUint16(offset, true));
}
const getFloat16 = typeof DataView.prototype.getFloat16 === "function" ? nativeGetFloat16 : fallbackGetFloat16;
function nativeSetFloat16(dataView, offset, value) {
;
dataView.setFloat16(offset, value, true);
}
function fallbackSetFloat16(dataView, offset, value) {
dataView.setUint16(offset, numberToFloat16Bits(value), true);
}
const setFloat16 = typeof DataView.prototype.setFloat16 === "function" ? nativeSetFloat16 : fallbackSetFloat16;
function nativeBase64ToUint8Array(base64) {
return Uint8Array.fromBase64(base64);
}
function fallbackBase64ToUint8Array(base64) {
const paddedLength = base64.length;
let padding = 0;
if (paddedLength > 0 && base64.charCodeAt(paddedLength - 1) === PADDING_CHAR_CODE) {
padding++;
if (paddedLength > 1 && base64.charCodeAt(paddedLength - 2) === PADDING_CHAR_CODE) {
padding++;
}
}
const numBytes = Math.floor(paddedLength * 3 / 4) - padding;
const bytes = new Uint8Array(numBytes);
let byteIndex = 0;
const fullGroups = Math.floor((paddedLength - padding) / 4) * 4;
for (let i = 0; i < fullGroups; i += 4) {
const c0 = B64_LOOKUP[base64.charCodeAt(i)];
const c1 = B64_LOOKUP[base64.charCodeAt(i + 1)];
const c2 = B64_LOOKUP[base64.charCodeAt(i + 2)];
const c3 = B64_LOOKUP[base64.charCodeAt(i + 3)];
const bitmap = c0 << 18 | c1 << 12 | c2 << 6 | c3;
bytes[byteIndex++] = bitmap >> 16 & 255;
bytes[byteIndex++] = bitmap >> 8 & 255;
bytes[byteIndex++] = bitmap & 255;
}
if (padding === 1) {
const c0 = B64_LOOKUP[base64.charCodeAt(fullGroups)];
const c1 = B64_LOOKUP[base64.charCodeAt(fullGroups + 1)];
const c2 = B64_LOOKUP[base64.charCodeAt(fullGroups + 2)];
const bitmap = c0 << 18 | c1 << 12 | c2 << 6;
bytes[byteIndex++] = bitmap >> 16 & 255;
bytes[byteIndex++] = bitmap >> 8 & 255;
} else if (padding === 2) {
const c0 = B64_LOOKUP[base64.charCodeAt(fullGroups)];
const c1 = B64_LOOKUP[base64.charCodeAt(fullGroups + 1)];
const bitmap = c0 << 18 | c1 << 12;
bytes[byteIndex++] = bitmap >> 16 & 255;
}
return bytes;
}
function nativeUint8ArrayToBase64(uint8Array) {
return uint8Array.toBase64();
}
function fallbackUint8ArrayToBase64(uint8Array) {
const len = uint8Array.length;
const fullGroups = Math.floor(len / 3) * 3;
let result = "";
for (let i = 0; i < fullGroups; i += 3) {
const byte1 = uint8Array[i];
const byte2 = uint8Array[i + 1];
const byte3 = uint8Array[i + 2];
const bitmap = byte1 << 16 | byte2 << 8 | byte3;
result += BASE64_CHARS[bitmap >> 18 & SIX_BIT_MASK] + // bits 23–18 (top sextet)
BASE64_CHARS[bitmap >> 12 & SIX_BIT_MASK] + // bits 17–12
BASE64_CHARS[bitmap >> 6 & SIX_BIT_MASK] + // bits 11–6
BASE64_CHARS[bitmap & SIX_BIT_MASK];
}
const remaining = len - fullGroups;
if (remaining === 1) {
const bitmap = uint8Array[fullGroups] << 16;
result += BASE64_CHARS[bitmap >> 18 & SIX_BIT_MASK] + BASE64_CHARS[bitmap >> 12 & SIX_BIT_MASK] + "==";
} else if (remaining === 2) {
const bitmap = uint8Array[fullGroups] << 16 | uint8Array[fullGroups + 1] << 8;
result += BASE64_CHARS[bitmap >> 18 & SIX_BIT_MASK] + BASE64_CHARS[bitmap >> 12 & SIX_BIT_MASK] + BASE64_CHARS[bitmap >> 6 & SIX_BIT_MASK] + "=";
}
return result;
}
const uint8ArrayToBase64 = typeof Uint8Array.prototype.toBase64 === "function" ? nativeUint8ArrayToBase64 : fallbackUint8ArrayToBase64;
const base64ToUint8Array = typeof Uint8Array.fromBase64 === "function" ? nativeBase64ToUint8Array : fallbackBase64ToUint8Array;
function float16BitsToNumber(bits) {
const sign = bits >> 15;
const exp = bits >> 10 & 31;
const frac = bits & 1023;
if (exp === 0) {
return sign ? -frac * POW2_SUBNORMAL : frac * POW2_SUBNORMAL;
}
if (exp === 31) {
return frac ? NaN : sign ? -Infinity : Infinity;
}
const magnitude = POW2[exp] * MANTISSA[frac];
return sign ? -magnitude : magnitude;
}
function numberToFloat16Bits(value) {
if (value === 0) return Object.is(value, -0) ? 32768 : 0;
if (!Number.isFinite(value)) {
if (Number.isNaN(value)) return 32256;
return value > 0 ? 31744 : 64512;
}
const sign = value < 0 ? 1 : 0;
value = Math.abs(value);
const exp = Math.floor(Math.log2(value));
let expBiased = exp + 15;
if (expBiased >= 31) {
return sign << 15 | 31744;
}
if (expBiased <= 0) {
const frac2 = Math.round(value * Math.pow(2, 14) * 1024);
return sign << 15 | frac2 & 1023;
}
const mantissa = value / Math.pow(2, exp) - 1;
let frac = Math.round(mantissa * 1024);
if (frac >= 1024) {
frac = 0;
expBiased++;
if (expBiased >= 31) {
return sign << 15 | 31744;
}
}
return sign << 15 | expBiased << 10 | frac;
}
class b64Vecs {
/**
* Encode a single point (x, y, z) to 8 base64 characters using legacy Float16 encoding.
* Each coordinate is encoded as a Float16 value, resulting in 6 bytes total.
*
* @param x - The x coordinate
* @param y - The y coordinate
* @param z - The z coordinate
* @returns An 8-character base64 string representing the point
* @internal
*/
static _legacyEncodePoint(x, y, z) {
const buffer = new Uint8Array(6);
const dataView = new DataView(buffer.buffer);
setFloat16(dataView, 0, x);
setFloat16(dataView, 2, y);
setFloat16(dataView, 4, z);
return uint8ArrayToBase64(buffer);
}
/**
* Convert an array of VecModels to a base64 string using legacy Float16 encoding.
* Uses Float16 encoding for each coordinate (x, y, z). If a point's z value is
* undefined, it defaults to 0.5.
*
* @param points - An array of VecModel objects to encode
* @returns A base64-encoded string containing all points
* @internal Used only for migrations from legacy format
*/
static _legacyEncodePoints(points) {
if (points.length === 0) return "";
const buffer = new Uint8Array(points.length * 6);
const dataView = new DataView(buffer.buffer);
for (let i = 0; i < points.length; i++) {
const p = points[i];
const offset = i * 6;
setFloat16(dataView, offset, p.x);
setFloat16(dataView, offset + 2, p.y);
setFloat16(dataView, offset + 4, p.z ?? 0.5);
}
return uint8ArrayToBase64(buffer);
}
/**
* Convert a legacy base64 string back to an array of VecModels.
* Decodes Float16-encoded coordinates (x, y, z) from the base64 string.
*
* @param base64 - The base64-encoded string containing point data
* @returns An array of VecModel objects decoded from the string
* @internal Used only for migrations from legacy format
*/
static _legacyDecodePoints(base64) {
const bytes = base64ToUint8Array(base64);
const dataView = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
const result = [];
for (let offset = 0; offset < bytes.length; offset += 6) {
result.push({
x: getFloat16(dataView, offset),
y: getFloat16(dataView, offset + 2),
z: getFloat16(dataView, offset + 4)
});
}
return result;
}
/**
* Encode an array of VecModels using delta encoding for improved precision.
* The first point is stored as Float32 (high precision for absolute position),
* subsequent points are stored as Float16 deltas from the previous point.
* This provides full precision for the starting position and excellent precision
* for deltas between consecutive points (which are typically small values).
*
* Format:
* - First point: 3 Float32 values = 12 bytes = 16 base64 chars
* - Delta points: 3 Float16 values each = 6 bytes = 8 base64 chars each
*
* @param points - An array of VecModel objects to encode
* @param dim - Encoding dimension; `2` routes through the 2D variant (drops z), `3` (default) keeps x, y, z
* @returns A base64-encoded string containing delta-encoded points
* @public
*/
static encodePoints(points, dim) {
if (dim === DIM_2D) return b64Vecs.encodePoints2D(points);
if (points.length === 0) return "";
const firstPointBytes = 12;
const deltaBytes = (points.length - 1) * 6;
const totalBytes = firstPointBytes + deltaBytes;
const buffer = new Uint8Array(totalBytes);
const dataView = new DataView(buffer.buffer);
const first = points[0];
dataView.setFloat32(0, first.x, true);
dataView.setFloat32(4, first.y, true);
dataView.setFloat32(8, first.z ?? 0.5, true);
let prevX = first.x;
let prevY = first.y;
let prevZ = first.z ?? 0.5;
for (let i = 1; i < points.length; i++) {
const p = points[i];
const z = p.z ?? 0.5;
const offset = firstPointBytes + (i - 1) * 6;
setFloat16(dataView, offset, p.x - prevX);
setFloat16(dataView, offset + 2, p.y - prevY);
setFloat16(dataView, offset + 4, z - prevZ);
prevX = p.x;
prevY = p.y;
prevZ = z;
}
return uint8ArrayToBase64(buffer);
}
/**
* Decode a delta-encoded base64 string back to an array of absolute VecModels.
* The first point is stored as Float32 (high precision), subsequent points are
* Float16 deltas that are accumulated to reconstruct absolute positions.
*
* @param base64 - The base64-encoded string containing delta-encoded point data
* @param dim - Encoding dimension; `2` expects x/y only (z supplied as 0.5), `3` (default) expects x/y/z
* @returns An array of VecModel objects with absolute coordinates
* @public
*/
static decodePoints(base64, dim) {
if (dim === DIM_2D) return b64Vecs.decodePoints2D(base64);
if (base64.length === 0) return [];
const bytes = base64ToUint8Array(base64);
const dataView = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
const result = [];
let x = dataView.getFloat32(0, true);
let y = dataView.getFloat32(4, true);
let z = dataView.getFloat32(8, true);
result.push({ x, y, z });
const firstPointBytes = 12;
for (let offset = firstPointBytes; offset < bytes.length; offset += 6) {
x += getFloat16(dataView, offset);
y += getFloat16(dataView, offset + 2);
z += getFloat16(dataView, offset + 4);
result.push({ x, y, z });
}
return result;
}
/**
* Get the first point from a delta-encoded base64 string.
* The first point is stored as Float32 for full precision.
*
* @param b64Points - The delta-encoded base64 string
* @param dim - Encoding dimension; `2` expects x/y only (z supplied as 0.5), `3` (default) expects x/y/z
* @returns The first point as a VecModel, or null if the string is too short
* @public
*/
static decodeFirstPoint(b64Points, dim) {
if (dim === DIM_2D) return b64Vecs.decodeFirstPoint2D(b64Points);
if (b64Points.length < FIRST_POINT_B64_LENGTH) return null;
const bytes = base64ToUint8Array(b64Points.slice(0, FIRST_POINT_B64_LENGTH));
const dataView = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
return {
x: dataView.getFloat32(0, true),
y: dataView.getFloat32(4, true),
z: dataView.getFloat32(8, true)
};
}
/**
* Get the last point from a delta-encoded base64 string.
* Requires decoding all points to accumulate deltas.
*
* @param b64Points - The delta-encoded base64 string
* @param dim - Encoding dimension; `2` expects x/y only (z supplied as 0.5), `3` (default) expects x/y/z
* @returns The last point as a VecModel, or null if the string is too short
* @public
*/
static decodeLastPoint(b64Points, dim) {
if (dim === DIM_2D) return b64Vecs.decodeLastPoint2D(b64Points);
if (b64Points.length < FIRST_POINT_B64_LENGTH) return null;
const bytes = base64ToUint8Array(b64Points);
const dataView = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
let x = dataView.getFloat32(0, true);
let y = dataView.getFloat32(4, true);
let z = dataView.getFloat32(8, true);
const firstPointBytes = 12;
for (let offset = firstPointBytes; offset < bytes.length; offset += 6) {
x += getFloat16(dataView, offset);
y += getFloat16(dataView, offset + 2);
z += getFloat16(dataView, offset + 4);
}
return { x, y, z };
}
/**
* Encode an array of VecModels as 2D delta-encoded points, dropping z entirely.
* Use for draw shapes from devices that don't report pressure, where z is a
* constant 0.5 and storing it wastes ~33% of per-point bytes.
*
* Format:
* - First point: 2 Float32 values (x, y) = 8 bytes
* - Delta points: 2 Float16 values (dx, dy) = 4 bytes each
*
* @param points - An array of VecModel objects to encode (z is discarded)
* @returns A base64-encoded string containing 2D delta-encoded points
* @public
*/
static encodePoints2D(points) {
if (points.length === 0) return "";
const firstPointBytes = 8;
const deltaBytes = (points.length - 1) * 4;
const buffer = new Uint8Array(firstPointBytes + deltaBytes);
const dataView = new DataView(buffer.buffer);
const first = points[0];
dataView.setFloat32(0, first.x, true);
dataView.setFloat32(4, first.y, true);
let prevX = first.x;
let prevY = first.y;
for (let i = 1; i < points.length; i++) {
const p = points[i];
const offset = firstPointBytes + (i - 1) * 4;
setFloat16(dataView, offset, p.x - prevX);
setFloat16(dataView, offset + 2, p.y - prevY);
prevX = p.x;
prevY = p.y;
}
return uint8ArrayToBase64(buffer);
}
/**
* Decode a 2D delta-encoded base64 string back to an array of absolute VecModels.
* The z coordinate is always set to 0.5 (the default pressure value) so downstream
* consumers don't need a separate code path.
*
* @param base64 - The base64-encoded string containing 2D delta-encoded point data
* @returns An array of VecModel objects with absolute (x, y) and z = 0.5
* @public
*/
static decodePoints2D(base64) {
if (base64.length === 0) return [];
const bytes = base64ToUint8Array(base64);
const dataView = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
const result = [];
let x = dataView.getFloat32(0, true);
let y = dataView.getFloat32(4, true);
result.push({ x, y, z: DEFAULT_PRESSURE });
const firstPointBytes = 8;
for (let offset = firstPointBytes; offset < bytes.length; offset += 4) {
x += getFloat16(dataView, offset);
y += getFloat16(dataView, offset + 2);
result.push({ x, y, z: DEFAULT_PRESSURE });
}
return result;
}
/**
* Get the first point from a 2D delta-encoded base64 string.
*
* @param b64Points - The 2D delta-encoded base64 string
* @returns The first point with z = 0.5, or null if the string is too short
* @public
*/
static decodeFirstPoint2D(b64Points) {
if (b64Points.length < FIRST_POINT_2D_B64_LENGTH) return null;
const bytes = base64ToUint8Array(b64Points.slice(0, FIRST_POINT_2D_B64_LENGTH));
const dataView = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
return {
x: dataView.getFloat32(0, true),
y: dataView.getFloat32(4, true),
z: DEFAULT_PRESSURE
};
}
/**
* Get the last point from a 2D delta-encoded base64 string.
* Requires decoding all points to accumulate deltas.
*
* @param b64Points - The 2D delta-encoded base64 string
* @returns The last point with z = 0.5, or null if the string is too short
* @public
*/
static decodeLastPoint2D(b64Points) {
if (b64Points.length < FIRST_POINT_2D_B64_LENGTH) return null;
const bytes = base64ToUint8Array(b64Points);
const dataView = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
let x = dataView.getFloat32(0, true);
let y = dataView.getFloat32(4, true);
const firstPointBytes = 8;
for (let offset = firstPointBytes; offset < bytes.length; offset += 4) {
x += getFloat16(dataView, offset);
y += getFloat16(dataView, offset + 2);
}
return { x, y, z: DEFAULT_PRESSURE };
}
/**
* Whether an encoded path contains only a single point (a "dot"), inferred from
* the encoded length without decoding — cheap enough for the render path.
*
* The single-point length depends on the encoding dimension, so this takes the
* segment's `dim`: a one-point path is `FIRST_POINT_B64_LENGTH` chars (3D) or
* `FIRST_POINT_2D_B64_LENGTH` chars (2D). Keeping this beside the layout constants
* is deliberate — it is the single source of truth for "how long is one point", so
* callers never hard-code a length threshold (which silently breaks when a new
* encoding is added).
*
* @param b64Points - The encoded path string
* @param dim - Encoding dimension; `2` for (x, y), `3` (default) for (x, y, z)
* @returns true if the path encodes exactly one point
* @public
*/
static isSinglePoint(b64Points, dim) {
return b64Points.length <= (dim === DIM_2D ? FIRST_POINT_2D_B64_LENGTH : FIRST_POINT_B64_LENGTH);
}
}
//# sourceMappingURL=b64Vecs.js.map