import { a as reactDomExports, d as commonjsGlobal, g as getDefaultExportFromCjs, b as React, r as reactExports, R as React$1, c as ReactDOM } from './vendor-react.js';
import { j as jsxRuntimeExports, u as useFloating, o as offset, s as shift, f as flip, a as size, b as arrow, h as hide, l as limitShift, c as autoUpdate } from './vendor-mui.js';
var client = {};
var createRoot;
var m = reactDomExports;
{
createRoot = client.createRoot = m.createRoot;
client.hydrateRoot = m.hydrateRoot;
}
const TLDRAW_LIBRARY_VERSION_KEY = "__TLDRAW_LIBRARY_VERSIONS__";
function getLibraryVersions() {
if (globalThis[TLDRAW_LIBRARY_VERSION_KEY]) {
return globalThis[TLDRAW_LIBRARY_VERSION_KEY];
}
const info = {
versions: [],
didWarn: false,
scheduledNotice: null
};
Object.defineProperty(globalThis, TLDRAW_LIBRARY_VERSION_KEY, {
value: info,
writable: false,
configurable: false,
enumerable: false
});
return info;
}
function registerTldrawLibraryVersion(name, version, modules) {
if (!name || !version || !modules) {
{
throw new Error("Missing name/version/module system in built version of tldraw library");
}
}
const info = getLibraryVersions();
info.versions.push({ name, version, modules });
if (!info.scheduledNotice) {
try {
info.scheduledNotice = setTimeout(() => {
info.scheduledNotice = null;
checkLibraryVersions(info);
}, 100);
} catch {
checkLibraryVersions(info);
}
}
}
function checkLibraryVersions(info) {
if (!info.versions.length) return;
if (info.didWarn) return;
const sorted = info.versions.sort((a, b) => compareVersions(a.version, b.version));
const latestVersion = sorted[sorted.length - 1].version;
const matchingVersions = /* @__PURE__ */ new Set();
const nonMatchingVersions = /* @__PURE__ */ new Map();
for (const lib of sorted) {
if (nonMatchingVersions.has(lib.name)) {
matchingVersions.delete(lib.name);
entry(nonMatchingVersions, lib.name, /* @__PURE__ */ new Set()).add(lib.version);
continue;
}
if (lib.version === latestVersion) {
matchingVersions.add(lib.name);
} else {
matchingVersions.delete(lib.name);
entry(nonMatchingVersions, lib.name, /* @__PURE__ */ new Set()).add(lib.version);
}
}
if (nonMatchingVersions.size > 0) {
const message = [
`${format("[tldraw]", ["bold", "bgRed", "textWhite"])} ${format("You have multiple versions of tldraw libraries installed. This can lead to bugs and unexpected behavior.", ["textRed", "bold"])}`,
"",
`The latest version you have installed is ${format(`v${latestVersion}`, ["bold", "textBlue"])}. The following libraries are on the latest version:`,
...Array.from(matchingVersions, (name) => ` \u2022 \u2705 ${format(name, ["bold"])}`),
"",
`The following libraries are not on the latest version, or have multiple versions installed:`,
...Array.from(nonMatchingVersions, ([name, versions]) => {
const sortedVersions = Array.from(versions).sort(compareVersions).map((v) => format(`v${v}`, v === latestVersion ? ["textGreen"] : ["textRed"]));
return ` \u2022 \u274C ${format(name, ["bold"])} (${sortedVersions.join(", ")})`;
})
];
console.log(message.join("\n"));
info.didWarn = true;
return;
}
const potentialDuplicates = /* @__PURE__ */ new Map();
for (const lib of sorted) {
entry(potentialDuplicates, lib.name, { version: lib.version, modules: [] }).modules.push(
lib.modules
);
}
const duplicates = /* @__PURE__ */ new Map();
for (const [name, lib] of potentialDuplicates) {
if (lib.modules.length > 1) duplicates.set(name, lib);
}
if (duplicates.size > 0) {
const message = [
`${format("[tldraw]", ["bold", "bgRed", "textWhite"])} ${format("You have multiple instances of some tldraw libraries active. This can lead to bugs and unexpected behavior. ", ["textRed", "bold"])}`,
"",
"This usually means that your bundler is misconfigured, and is importing the same library multiple times - usually once as an ES Module, and once as a CommonJS module.",
"",
"The following libraries have been imported multiple times:",
...Array.from(duplicates, ([name, lib]) => {
const modules = lib.modules.map((m, i) => m === "esm" ? ` ${i + 1}. ES Modules` : ` ${i + 1}. CommonJS`).join("\n");
return ` \u2022 \u274C ${format(name, ["bold"])} v${lib.version}:
${modules}`;
}),
"",
"You should configure your bundler to only import one version of each library."
];
console.log(message.join("\n"));
info.didWarn = true;
return;
}
}
function compareVersions(a, b) {
const aMatch = a.match(/^(\d+)\.(\d+)\.(\d+)(?:-(\w+))?$/);
const bMatch = b.match(/^(\d+)\.(\d+)\.(\d+)(?:-(\w+))?$/);
if (!aMatch || !bMatch) return a.localeCompare(b);
if (aMatch[1] !== bMatch[1]) return Number(aMatch[1]) - Number(bMatch[1]);
if (aMatch[2] !== bMatch[2]) return Number(aMatch[2]) - Number(bMatch[2]);
if (aMatch[3] !== bMatch[3]) return Number(aMatch[3]) - Number(bMatch[3]);
if (aMatch[4] && bMatch[4]) return aMatch[4].localeCompare(bMatch[4]);
if (aMatch[4]) return 1;
if (bMatch[4]) return -1;
return 0;
}
const formats = {
bold: "1",
textBlue: "94",
textRed: "31",
textGreen: "32",
bgRed: "41",
textWhite: "97"
};
function format(value, formatters = []) {
return `\x1B[${formatters.map((f) => formats[f]).join(";")}m${value}\x1B[m`;
}
function entry(map, key, defaultValue) {
if (map.has(key)) {
return map.get(key);
}
map.set(key, defaultValue);
return defaultValue;
}
/**
* lodash (Custom Build)
* Build: `lodash modularize exports="npm" -o ./`
* Copyright jQuery Foundation and other contributors
* Released under MIT license
* Based on Underscore.js 1.8.3
* Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
*/
/** Used as the `TypeError` message for "Functions" methods. */
var FUNC_ERROR_TEXT = 'Expected a function';
/** Used as references for various `Number` constants. */
var NAN = 0 / 0;
/** `Object#toString` result references. */
var symbolTag = '[object Symbol]';
/** Used to match leading and trailing whitespace. */
var reTrim = /^\s+|\s+$/g;
/** Used to detect bad signed hexadecimal string values. */
var reIsBadHex = /^[-+]0x[0-9a-f]+$/i;
/** Used to detect binary string values. */
var reIsBinary = /^0b[01]+$/i;
/** Used to detect octal string values. */
var reIsOctal = /^0o[0-7]+$/i;
/** Built-in method references without a dependency on `root`. */
var freeParseInt = parseInt;
/** Detect free variable `global` from Node.js. */
var freeGlobal$1 = typeof commonjsGlobal == 'object' && commonjsGlobal && commonjsGlobal.Object === Object && commonjsGlobal;
/** Detect free variable `self`. */
var freeSelf$1 = typeof self == 'object' && self && self.Object === Object && self;
/** Used as a reference to the global object. */
var root$1 = freeGlobal$1 || freeSelf$1 || Function('return this')();
/** Used for built-in method references. */
var objectProto$1 = Object.prototype;
/**
* Used to resolve the
* [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring)
* of values.
*/
var objectToString$1 = objectProto$1.toString;
/* Built-in method references for those with the same name as other `lodash` methods. */
var nativeMax = Math.max,
nativeMin = Math.min;
/**
* Gets the timestamp of the number of milliseconds that have elapsed since
* the Unix epoch (1 January 1970 00:00:00 UTC).
*
* @static
* @memberOf _
* @since 2.4.0
* @category Date
* @returns {number} Returns the timestamp.
* @example
*
* _.defer(function(stamp) {
* console.log(_.now() - stamp);
* }, _.now());
* // => Logs the number of milliseconds it took for the deferred invocation.
*/
var now = function() {
return root$1.Date.now();
};
/**
* Creates a debounced function that delays invoking `func` until after `wait`
* milliseconds have elapsed since the last time the debounced function was
* invoked. The debounced function comes with a `cancel` method to cancel
* delayed `func` invocations and a `flush` method to immediately invoke them.
* Provide `options` to indicate whether `func` should be invoked on the
* leading and/or trailing edge of the `wait` timeout. The `func` is invoked
* with the last arguments provided to the debounced function. Subsequent
* calls to the debounced function return the result of the last `func`
* invocation.
*
* **Note:** If `leading` and `trailing` options are `true`, `func` is
* invoked on the trailing edge of the timeout only if the debounced function
* is invoked more than once during the `wait` timeout.
*
* If `wait` is `0` and `leading` is `false`, `func` invocation is deferred
* until to the next tick, similar to `setTimeout` with a timeout of `0`.
*
* See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/)
* for details over the differences between `_.debounce` and `_.throttle`.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Function
* @param {Function} func The function to debounce.
* @param {number} [wait=0] The number of milliseconds to delay.
* @param {Object} [options={}] The options object.
* @param {boolean} [options.leading=false]
* Specify invoking on the leading edge of the timeout.
* @param {number} [options.maxWait]
* The maximum time `func` is allowed to be delayed before it's invoked.
* @param {boolean} [options.trailing=true]
* Specify invoking on the trailing edge of the timeout.
* @returns {Function} Returns the new debounced function.
* @example
*
* // Avoid costly calculations while the window size is in flux.
* jQuery(window).on('resize', _.debounce(calculateLayout, 150));
*
* // Invoke `sendMail` when clicked, debouncing subsequent calls.
* jQuery(element).on('click', _.debounce(sendMail, 300, {
* 'leading': true,
* 'trailing': false
* }));
*
* // Ensure `batchLog` is invoked once after 1 second of debounced calls.
* var debounced = _.debounce(batchLog, 250, { 'maxWait': 1000 });
* var source = new EventSource('/stream');
* jQuery(source).on('message', debounced);
*
* // Cancel the trailing debounced invocation.
* jQuery(window).on('popstate', debounced.cancel);
*/
function debounce$1(func, wait, options) {
var lastArgs,
lastThis,
maxWait,
result,
timerId,
lastCallTime,
lastInvokeTime = 0,
leading = false,
maxing = false,
trailing = true;
if (typeof func != 'function') {
throw new TypeError(FUNC_ERROR_TEXT);
}
wait = toNumber(wait) || 0;
if (isObject$a(options)) {
leading = !!options.leading;
maxing = 'maxWait' in options;
maxWait = maxing ? nativeMax(toNumber(options.maxWait) || 0, wait) : maxWait;
trailing = 'trailing' in options ? !!options.trailing : trailing;
}
function invokeFunc(time) {
var args = lastArgs,
thisArg = lastThis;
lastArgs = lastThis = undefined;
lastInvokeTime = time;
result = func.apply(thisArg, args);
return result;
}
function leadingEdge(time) {
// Reset any `maxWait` timer.
lastInvokeTime = time;
// Start the timer for the trailing edge.
timerId = setTimeout(timerExpired, wait);
// Invoke the leading edge.
return leading ? invokeFunc(time) : result;
}
function remainingWait(time) {
var timeSinceLastCall = time - lastCallTime,
timeSinceLastInvoke = time - lastInvokeTime,
result = wait - timeSinceLastCall;
return maxing ? nativeMin(result, maxWait - timeSinceLastInvoke) : result;
}
function shouldInvoke(time) {
var timeSinceLastCall = time - lastCallTime,
timeSinceLastInvoke = time - lastInvokeTime;
// Either this is the first call, activity has stopped and we're at the
// trailing edge, the system time has gone backwards and we're treating
// it as the trailing edge, or we've hit the `maxWait` limit.
return (lastCallTime === undefined || (timeSinceLastCall >= wait) ||
(timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait));
}
function timerExpired() {
var time = now();
if (shouldInvoke(time)) {
return trailingEdge(time);
}
// Restart the timer.
timerId = setTimeout(timerExpired, remainingWait(time));
}
function trailingEdge(time) {
timerId = undefined;
// Only invoke if we have `lastArgs` which means `func` has been
// debounced at least once.
if (trailing && lastArgs) {
return invokeFunc(time);
}
lastArgs = lastThis = undefined;
return result;
}
function cancel() {
if (timerId !== undefined) {
clearTimeout(timerId);
}
lastInvokeTime = 0;
lastArgs = lastCallTime = lastThis = timerId = undefined;
}
function flush() {
return timerId === undefined ? result : trailingEdge(now());
}
function debounced() {
var time = now(),
isInvoking = shouldInvoke(time);
lastArgs = arguments;
lastThis = this;
lastCallTime = time;
if (isInvoking) {
if (timerId === undefined) {
return leadingEdge(lastCallTime);
}
if (maxing) {
// Handle invocations in a tight loop.
timerId = setTimeout(timerExpired, wait);
return invokeFunc(lastCallTime);
}
}
if (timerId === undefined) {
timerId = setTimeout(timerExpired, wait);
}
return result;
}
debounced.cancel = cancel;
debounced.flush = flush;
return debounced;
}
/**
* Creates a throttled function that only invokes `func` at most once per
* every `wait` milliseconds. The throttled function comes with a `cancel`
* method to cancel delayed `func` invocations and a `flush` method to
* immediately invoke them. Provide `options` to indicate whether `func`
* should be invoked on the leading and/or trailing edge of the `wait`
* timeout. The `func` is invoked with the last arguments provided to the
* throttled function. Subsequent calls to the throttled function return the
* result of the last `func` invocation.
*
* **Note:** If `leading` and `trailing` options are `true`, `func` is
* invoked on the trailing edge of the timeout only if the throttled function
* is invoked more than once during the `wait` timeout.
*
* If `wait` is `0` and `leading` is `false`, `func` invocation is deferred
* until to the next tick, similar to `setTimeout` with a timeout of `0`.
*
* See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/)
* for details over the differences between `_.throttle` and `_.debounce`.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Function
* @param {Function} func The function to throttle.
* @param {number} [wait=0] The number of milliseconds to throttle invocations to.
* @param {Object} [options={}] The options object.
* @param {boolean} [options.leading=true]
* Specify invoking on the leading edge of the timeout.
* @param {boolean} [options.trailing=true]
* Specify invoking on the trailing edge of the timeout.
* @returns {Function} Returns the new throttled function.
* @example
*
* // Avoid excessively updating the position while scrolling.
* jQuery(window).on('scroll', _.throttle(updatePosition, 100));
*
* // Invoke `renewToken` when the click event is fired, but not more than once every 5 minutes.
* var throttled = _.throttle(renewToken, 300000, { 'trailing': false });
* jQuery(element).on('click', throttled);
*
* // Cancel the trailing throttled invocation.
* jQuery(window).on('popstate', throttled.cancel);
*/
function throttle(func, wait, options) {
var leading = true,
trailing = true;
if (typeof func != 'function') {
throw new TypeError(FUNC_ERROR_TEXT);
}
if (isObject$a(options)) {
leading = 'leading' in options ? !!options.leading : leading;
trailing = 'trailing' in options ? !!options.trailing : trailing;
}
return debounce$1(func, wait, {
'leading': leading,
'maxWait': wait,
'trailing': trailing
});
}
/**
* Checks if `value` is the
* [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types)
* of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`)
*
* @static
* @memberOf _
* @since 0.1.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is an object, else `false`.
* @example
*
* _.isObject({});
* // => true
*
* _.isObject([1, 2, 3]);
* // => true
*
* _.isObject(_.noop);
* // => true
*
* _.isObject(null);
* // => false
*/
function isObject$a(value) {
var type = typeof value;
return !!value && (type == 'object' || type == 'function');
}
/**
* Checks if `value` is object-like. A value is object-like if it's not `null`
* and has a `typeof` result of "object".
*
* @static
* @memberOf _
* @since 4.0.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is object-like, else `false`.
* @example
*
* _.isObjectLike({});
* // => true
*
* _.isObjectLike([1, 2, 3]);
* // => true
*
* _.isObjectLike(_.noop);
* // => false
*
* _.isObjectLike(null);
* // => false
*/
function isObjectLike(value) {
return !!value && typeof value == 'object';
}
/**
* Checks if `value` is classified as a `Symbol` primitive or object.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a symbol, else `false`.
* @example
*
* _.isSymbol(Symbol.iterator);
* // => true
*
* _.isSymbol('abc');
* // => false
*/
function isSymbol$3(value) {
return typeof value == 'symbol' ||
(isObjectLike(value) && objectToString$1.call(value) == symbolTag);
}
/**
* Converts `value` to a number.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Lang
* @param {*} value The value to process.
* @returns {number} Returns the number.
* @example
*
* _.toNumber(3.2);
* // => 3.2
*
* _.toNumber(Number.MIN_VALUE);
* // => 5e-324
*
* _.toNumber(Infinity);
* // => Infinity
*
* _.toNumber('3.2');
* // => 3.2
*/
function toNumber(value) {
if (typeof value == 'number') {
return value;
}
if (isSymbol$3(value)) {
return NAN;
}
if (isObject$a(value)) {
var other = typeof value.valueOf == 'function' ? value.valueOf() : value;
value = isObject$a(other) ? (other + '') : other;
}
if (typeof value != 'string') {
return value === 0 ? value : +value;
}
value = value.replace(reTrim, '');
var isBinary = reIsBinary.test(value);
return (isBinary || reIsOctal.test(value))
? freeParseInt(value.slice(2), isBinary ? 2 : 8)
: (reIsBadHex.test(value) ? NAN : +value);
}
var lodash_throttle = throttle;
const throttle$1 = /*@__PURE__*/getDefaultExportFromCjs(lodash_throttle);
/**
* lodash (Custom Build)
* Build: `lodash modularize exports="npm" -o ./`
* Copyright jQuery Foundation and other contributors
* Released under MIT license
* Based on Underscore.js 1.8.3
* Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
*/
/** Used as the size to enable large array optimizations. */
var LARGE_ARRAY_SIZE = 200;
/** Used to stand-in for `undefined` hash values. */
var HASH_UNDEFINED = '__lodash_hash_undefined__';
/** Used as references for various `Number` constants. */
var INFINITY = 1 / 0;
/** `Object#toString` result references. */
var funcTag = '[object Function]',
genTag = '[object GeneratorFunction]';
/**
* Used to match `RegExp`
* [syntax characters](http://ecma-international.org/ecma-262/7.0/#sec-patterns).
*/
var reRegExpChar = /[\\^$.*+?()[\]{}|]/g;
/** Used to detect host constructors (Safari). */
var reIsHostCtor = /^\[object .+?Constructor\]$/;
/** Detect free variable `global` from Node.js. */
var freeGlobal = typeof commonjsGlobal == 'object' && commonjsGlobal && commonjsGlobal.Object === Object && commonjsGlobal;
/** Detect free variable `self`. */
var freeSelf = typeof self == 'object' && self && self.Object === Object && self;
/** Used as a reference to the global object. */
var root = freeGlobal || freeSelf || Function('return this')();
/**
* A specialized version of `_.includes` for arrays without support for
* specifying an index to search from.
*
* @private
* @param {Array} [array] The array to inspect.
* @param {*} target The value to search for.
* @returns {boolean} Returns `true` if `target` is found, else `false`.
*/
function arrayIncludes$1(array, value) {
var length = array ? array.length : 0;
return !!length && baseIndexOf(array, value, 0) > -1;
}
/**
* The base implementation of `_.findIndex` and `_.findLastIndex` without
* support for iteratee shorthands.
*
* @private
* @param {Array} array The array to inspect.
* @param {Function} predicate The function invoked per iteration.
* @param {number} fromIndex The index to search from.
* @param {boolean} [fromRight] Specify iterating from right to left.
* @returns {number} Returns the index of the matched value, else `-1`.
*/
function baseFindIndex(array, predicate, fromIndex, fromRight) {
var length = array.length,
index = fromIndex + (-1);
while ((++index < length)) {
if (predicate(array[index], index, array)) {
return index;
}
}
return -1;
}
/**
* The base implementation of `_.indexOf` without `fromIndex` bounds checks.
*
* @private
* @param {Array} array The array to inspect.
* @param {*} value The value to search for.
* @param {number} fromIndex The index to search from.
* @returns {number} Returns the index of the matched value, else `-1`.
*/
function baseIndexOf(array, value, fromIndex) {
if (value !== value) {
return baseFindIndex(array, baseIsNaN, fromIndex);
}
var index = fromIndex - 1,
length = array.length;
while (++index < length) {
if (array[index] === value) {
return index;
}
}
return -1;
}
/**
* The base implementation of `_.isNaN` without support for number objects.
*
* @private
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is `NaN`, else `false`.
*/
function baseIsNaN(value) {
return value !== value;
}
/**
* Checks if a cache value for `key` exists.
*
* @private
* @param {Object} cache The cache to query.
* @param {string} key The key of the entry to check.
* @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.
*/
function cacheHas(cache, key) {
return cache.has(key);
}
/**
* Gets the value at `key` of `object`.
*
* @private
* @param {Object} [object] The object to query.
* @param {string} key The key of the property to get.
* @returns {*} Returns the property value.
*/
function getValue(object, key) {
return object == null ? undefined : object[key];
}
/**
* Checks if `value` is a host object in IE < 9.
*
* @private
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a host object, else `false`.
*/
function isHostObject(value) {
// Many host objects are `Object` objects that can coerce to strings
// despite having improperly defined `toString` methods.
var result = false;
if (value != null && typeof value.toString != 'function') {
try {
result = !!(value + '');
} catch (e) {}
}
return result;
}
/**
* Converts `set` to an array of its values.
*
* @private
* @param {Object} set The set to convert.
* @returns {Array} Returns the values.
*/
function setToArray(set) {
var index = -1,
result = Array(set.size);
set.forEach(function(value) {
result[++index] = value;
});
return result;
}
/** Used for built-in method references. */
var arrayProto = Array.prototype,
funcProto = Function.prototype,
objectProto = Object.prototype;
/** Used to detect overreaching core-js shims. */
var coreJsData = root['__core-js_shared__'];
/** Used to detect methods masquerading as native. */
var maskSrcKey = (function() {
var uid = /[^.]+$/.exec(coreJsData && coreJsData.keys && coreJsData.keys.IE_PROTO || '');
return uid ? ('Symbol(src)_1.' + uid) : '';
}());
/** Used to resolve the decompiled source of functions. */
var funcToString = funcProto.toString;
/** Used to check objects for own properties. */
var hasOwnProperty$2 = objectProto.hasOwnProperty;
/**
* Used to resolve the
* [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring)
* of values.
*/
var objectToString = objectProto.toString;
/** Used to detect if a method is native. */
var reIsNative = RegExp('^' +
funcToString.call(hasOwnProperty$2).replace(reRegExpChar, '\\$&')
.replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, '$1.*?') + '$'
);
/** Built-in value references. */
var splice = arrayProto.splice;
/* Built-in method references that are verified to be native. */
var Map$1 = getNative(root, 'Map'),
Set$1 = getNative(root, 'Set'),
nativeCreate = getNative(Object, 'create');
/**
* Creates a hash object.
*
* @private
* @constructor
* @param {Array} [entries] The key-value pairs to cache.
*/
function Hash(entries) {
var index = -1,
length = entries ? entries.length : 0;
this.clear();
while (++index < length) {
var entry = entries[index];
this.set(entry[0], entry[1]);
}
}
/**
* Removes all key-value entries from the hash.
*
* @private
* @name clear
* @memberOf Hash
*/
function hashClear() {
this.__data__ = nativeCreate ? nativeCreate(null) : {};
}
/**
* Removes `key` and its value from the hash.
*
* @private
* @name delete
* @memberOf Hash
* @param {Object} hash The hash to modify.
* @param {string} key The key of the value to remove.
* @returns {boolean} Returns `true` if the entry was removed, else `false`.
*/
function hashDelete(key) {
return this.has(key) && delete this.__data__[key];
}
/**
* Gets the hash value for `key`.
*
* @private
* @name get
* @memberOf Hash
* @param {string} key The key of the value to get.
* @returns {*} Returns the entry value.
*/
function hashGet(key) {
var data = this.__data__;
if (nativeCreate) {
var result = data[key];
return result === HASH_UNDEFINED ? undefined : result;
}
return hasOwnProperty$2.call(data, key) ? data[key] : undefined;
}
/**
* Checks if a hash value for `key` exists.
*
* @private
* @name has
* @memberOf Hash
* @param {string} key The key of the entry to check.
* @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.
*/
function hashHas(key) {
var data = this.__data__;
return nativeCreate ? data[key] !== undefined : hasOwnProperty$2.call(data, key);
}
/**
* Sets the hash `key` to `value`.
*
* @private
* @name set
* @memberOf Hash
* @param {string} key The key of the value to set.
* @param {*} value The value to set.
* @returns {Object} Returns the hash instance.
*/
function hashSet(key, value) {
var data = this.__data__;
data[key] = (nativeCreate && value === undefined) ? HASH_UNDEFINED : value;
return this;
}
// Add methods to `Hash`.
Hash.prototype.clear = hashClear;
Hash.prototype['delete'] = hashDelete;
Hash.prototype.get = hashGet;
Hash.prototype.has = hashHas;
Hash.prototype.set = hashSet;
/**
* Creates an list cache object.
*
* @private
* @constructor
* @param {Array} [entries] The key-value pairs to cache.
*/
function ListCache(entries) {
var index = -1,
length = entries ? entries.length : 0;
this.clear();
while (++index < length) {
var entry = entries[index];
this.set(entry[0], entry[1]);
}
}
/**
* Removes all key-value entries from the list cache.
*
* @private
* @name clear
* @memberOf ListCache
*/
function listCacheClear() {
this.__data__ = [];
}
/**
* Removes `key` and its value from the list cache.
*
* @private
* @name delete
* @memberOf ListCache
* @param {string} key The key of the value to remove.
* @returns {boolean} Returns `true` if the entry was removed, else `false`.
*/
function listCacheDelete(key) {
var data = this.__data__,
index = assocIndexOf(data, key);
if (index < 0) {
return false;
}
var lastIndex = data.length - 1;
if (index == lastIndex) {
data.pop();
} else {
splice.call(data, index, 1);
}
return true;
}
/**
* Gets the list cache value for `key`.
*
* @private
* @name get
* @memberOf ListCache
* @param {string} key The key of the value to get.
* @returns {*} Returns the entry value.
*/
function listCacheGet(key) {
var data = this.__data__,
index = assocIndexOf(data, key);
return index < 0 ? undefined : data[index][1];
}
/**
* Checks if a list cache value for `key` exists.
*
* @private
* @name has
* @memberOf ListCache
* @param {string} key The key of the entry to check.
* @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.
*/
function listCacheHas(key) {
return assocIndexOf(this.__data__, key) > -1;
}
/**
* Sets the list cache `key` to `value`.
*
* @private
* @name set
* @memberOf ListCache
* @param {string} key The key of the value to set.
* @param {*} value The value to set.
* @returns {Object} Returns the list cache instance.
*/
function listCacheSet(key, value) {
var data = this.__data__,
index = assocIndexOf(data, key);
if (index < 0) {
data.push([key, value]);
} else {
data[index][1] = value;
}
return this;
}
// Add methods to `ListCache`.
ListCache.prototype.clear = listCacheClear;
ListCache.prototype['delete'] = listCacheDelete;
ListCache.prototype.get = listCacheGet;
ListCache.prototype.has = listCacheHas;
ListCache.prototype.set = listCacheSet;
/**
* Creates a map cache object to store key-value pairs.
*
* @private
* @constructor
* @param {Array} [entries] The key-value pairs to cache.
*/
function MapCache(entries) {
var index = -1,
length = entries ? entries.length : 0;
this.clear();
while (++index < length) {
var entry = entries[index];
this.set(entry[0], entry[1]);
}
}
/**
* Removes all key-value entries from the map.
*
* @private
* @name clear
* @memberOf MapCache
*/
function mapCacheClear() {
this.__data__ = {
'hash': new Hash,
'map': new (Map$1 || ListCache),
'string': new Hash
};
}
/**
* Removes `key` and its value from the map.
*
* @private
* @name delete
* @memberOf MapCache
* @param {string} key The key of the value to remove.
* @returns {boolean} Returns `true` if the entry was removed, else `false`.
*/
function mapCacheDelete(key) {
return getMapData(this, key)['delete'](key);
}
/**
* Gets the map value for `key`.
*
* @private
* @name get
* @memberOf MapCache
* @param {string} key The key of the value to get.
* @returns {*} Returns the entry value.
*/
function mapCacheGet(key) {
return getMapData(this, key).get(key);
}
/**
* Checks if a map value for `key` exists.
*
* @private
* @name has
* @memberOf MapCache
* @param {string} key The key of the entry to check.
* @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.
*/
function mapCacheHas(key) {
return getMapData(this, key).has(key);
}
/**
* Sets the map `key` to `value`.
*
* @private
* @name set
* @memberOf MapCache
* @param {string} key The key of the value to set.
* @param {*} value The value to set.
* @returns {Object} Returns the map cache instance.
*/
function mapCacheSet(key, value) {
getMapData(this, key).set(key, value);
return this;
}
// Add methods to `MapCache`.
MapCache.prototype.clear = mapCacheClear;
MapCache.prototype['delete'] = mapCacheDelete;
MapCache.prototype.get = mapCacheGet;
MapCache.prototype.has = mapCacheHas;
MapCache.prototype.set = mapCacheSet;
/**
*
* Creates an array cache object to store unique values.
*
* @private
* @constructor
* @param {Array} [values] The values to cache.
*/
function SetCache(values) {
var index = -1,
length = values ? values.length : 0;
this.__data__ = new MapCache;
while (++index < length) {
this.add(values[index]);
}
}
/**
* Adds `value` to the array cache.
*
* @private
* @name add
* @memberOf SetCache
* @alias push
* @param {*} value The value to cache.
* @returns {Object} Returns the cache instance.
*/
function setCacheAdd(value) {
this.__data__.set(value, HASH_UNDEFINED);
return this;
}
/**
* Checks if `value` is in the array cache.
*
* @private
* @name has
* @memberOf SetCache
* @param {*} value The value to search for.
* @returns {number} Returns `true` if `value` is found, else `false`.
*/
function setCacheHas(value) {
return this.__data__.has(value);
}
// Add methods to `SetCache`.
SetCache.prototype.add = SetCache.prototype.push = setCacheAdd;
SetCache.prototype.has = setCacheHas;
/**
* Gets the index at which the `key` is found in `array` of key-value pairs.
*
* @private
* @param {Array} array The array to inspect.
* @param {*} key The key to search for.
* @returns {number} Returns the index of the matched value, else `-1`.
*/
function assocIndexOf(array, key) {
var length = array.length;
while (length--) {
if (eq(array[length][0], key)) {
return length;
}
}
return -1;
}
/**
* The base implementation of `_.isNative` without bad shim checks.
*
* @private
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a native function,
* else `false`.
*/
function baseIsNative(value) {
if (!isObject$9(value) || isMasked(value)) {
return false;
}
var pattern = (isFunction$1(value) || isHostObject(value)) ? reIsNative : reIsHostCtor;
return pattern.test(toSource(value));
}
/**
* The base implementation of `_.uniqBy` without support for iteratee shorthands.
*
* @private
* @param {Array} array The array to inspect.
* @param {Function} [iteratee] The iteratee invoked per element.
* @param {Function} [comparator] The comparator invoked per element.
* @returns {Array} Returns the new duplicate free array.
*/
function baseUniq(array, iteratee, comparator) {
var index = -1,
includes = arrayIncludes$1,
length = array.length,
isCommon = true,
result = [],
seen = result;
if (length >= LARGE_ARRAY_SIZE) {
var set = createSet(array);
if (set) {
return setToArray(set);
}
isCommon = false;
includes = cacheHas;
seen = new SetCache;
}
else {
seen = result;
}
outer:
while (++index < length) {
var value = array[index],
computed = value;
value = (value !== 0) ? value : 0;
if (isCommon && computed === computed) {
var seenIndex = seen.length;
while (seenIndex--) {
if (seen[seenIndex] === computed) {
continue outer;
}
}
result.push(value);
}
else if (!includes(seen, computed, comparator)) {
if (seen !== result) {
seen.push(computed);
}
result.push(value);
}
}
return result;
}
/**
* Creates a set object of `values`.
*
* @private
* @param {Array} values The values to add to the set.
* @returns {Object} Returns the new set.
*/
var createSet = !(Set$1 && (1 / setToArray(new Set$1([,-0]))[1]) == INFINITY) ? noop$3 : function(values) {
return new Set$1(values);
};
/**
* Gets the data for `map`.
*
* @private
* @param {Object} map The map to query.
* @param {string} key The reference key.
* @returns {*} Returns the map data.
*/
function getMapData(map, key) {
var data = map.__data__;
return isKeyable(key)
? data[typeof key == 'string' ? 'string' : 'hash']
: data.map;
}
/**
* Gets the native function at `key` of `object`.
*
* @private
* @param {Object} object The object to query.
* @param {string} key The key of the method to get.
* @returns {*} Returns the function if it's native, else `undefined`.
*/
function getNative(object, key) {
var value = getValue(object, key);
return baseIsNative(value) ? value : undefined;
}
/**
* Checks if `value` is suitable for use as unique object key.
*
* @private
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is suitable, else `false`.
*/
function isKeyable(value) {
var type = typeof value;
return (type == 'string' || type == 'number' || type == 'symbol' || type == 'boolean')
? (value !== '__proto__')
: (value === null);
}
/**
* Checks if `func` has its source masked.
*
* @private
* @param {Function} func The function to check.
* @returns {boolean} Returns `true` if `func` is masked, else `false`.
*/
function isMasked(func) {
return !!maskSrcKey && (maskSrcKey in func);
}
/**
* Converts `func` to its source code.
*
* @private
* @param {Function} func The function to process.
* @returns {string} Returns the source code.
*/
function toSource(func) {
if (func != null) {
try {
return funcToString.call(func);
} catch (e) {}
try {
return (func + '');
} catch (e) {}
}
return '';
}
/**
* Creates a duplicate-free version of an array, using
* [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)
* for equality comparisons, in which only the first occurrence of each
* element is kept.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Array
* @param {Array} array The array to inspect.
* @returns {Array} Returns the new duplicate free array.
* @example
*
* _.uniq([2, 1, 2]);
* // => [2, 1]
*/
function uniq$1(array) {
return (array && array.length)
? baseUniq(array)
: [];
}
/**
* Performs a
* [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)
* comparison between two values to determine if they are equivalent.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Lang
* @param {*} value The value to compare.
* @param {*} other The other value to compare.
* @returns {boolean} Returns `true` if the values are equivalent, else `false`.
* @example
*
* var object = { 'a': 1 };
* var other = { 'a': 1 };
*
* _.eq(object, object);
* // => true
*
* _.eq(object, other);
* // => false
*
* _.eq('a', 'a');
* // => true
*
* _.eq('a', Object('a'));
* // => false
*
* _.eq(NaN, NaN);
* // => true
*/
function eq(value, other) {
return value === other || (value !== value && other !== other);
}
/**
* Checks if `value` is classified as a `Function` object.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a function, else `false`.
* @example
*
* _.isFunction(_);
* // => true
*
* _.isFunction(/abc/);
* // => false
*/
function isFunction$1(value) {
// The use of `Object#toString` avoids issues with the `typeof` operator
// in Safari 8-9 which returns 'object' for typed array and other constructors.
var tag = isObject$9(value) ? objectToString.call(value) : '';
return tag == funcTag || tag == genTag;
}
/**
* Checks if `value` is the
* [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types)
* of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`)
*
* @static
* @memberOf _
* @since 0.1.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is an object, else `false`.
* @example
*
* _.isObject({});
* // => true
*
* _.isObject([1, 2, 3]);
* // => true
*
* _.isObject(_.noop);
* // => true
*
* _.isObject(null);
* // => false
*/
function isObject$9(value) {
var type = typeof value;
return !!value && (type == 'object' || type == 'function');
}
/**
* This method returns `undefined`.
*
* @static
* @memberOf _
* @since 2.3.0
* @category Util
* @example
*
* _.times(2, _.noop);
* // => [undefined, undefined]
*/
function noop$3() {
// No operation performed.
}
var lodash_uniq = uniq$1;
const _uniq = /*@__PURE__*/getDefaultExportFromCjs(lodash_uniq);
const PERFORMANCE_COLORS = {
Good: "#40C057",
Mid: "#FFC078",
Poor: "#E03131"
};
const PERFORMANCE_PREFIX_COLOR = PERFORMANCE_COLORS.Good;
class PerformanceTracker {
startTime = 0;
name = "";
frames = 0;
started = false;
frame = null;
// eslint-disable-next-line local/prefer-class-methods
recordFrame = () => {
this.frames++;
if (!this.started) return;
this.frame = requestAnimationFrame(this.recordFrame);
};
start(name) {
this.name = name;
this.frames = 0;
this.started = true;
if (this.frame !== null) cancelAnimationFrame(this.frame);
this.frame = requestAnimationFrame(this.recordFrame);
this.startTime = performance.now();
}
stop() {
this.started = false;
if (this.frame !== null) cancelAnimationFrame(this.frame);
const duration = (performance.now() - this.startTime) / 1e3;
const fps = duration === 0 ? 0 : Math.floor(this.frames / duration);
const background = fps > 55 ? PERFORMANCE_COLORS.Good : fps > 30 ? PERFORMANCE_COLORS.Mid : PERFORMANCE_COLORS.Poor;
const color = background === PERFORMANCE_COLORS.Mid ? "black" : "white";
const capitalized = this.name[0].toUpperCase() + this.name.slice(1);
console.debug(
`%cPerf%c ${capitalized} %c${fps}%c fps`,
`color: white; background: ${PERFORMANCE_PREFIX_COLOR};padding: 2px;border-radius: 3px;`,
"font-weight: normal",
`font-weight: bold; padding: 2px; background: ${background};color: ${color};`,
"font-weight: normal"
);
}
isStarted() {
return this.started;
}
}
function dedupe(input, equals) {
const result = [];
mainLoop: for (const item of input) {
for (const existing of result) {
if (equals ? equals(item, existing) : item === existing) {
continue mainLoop;
}
}
result.push(item);
}
return result;
}
function compact(arr) {
return arr.filter((i) => i !== void 0 && i !== null);
}
function last$1(arr) {
return arr[arr.length - 1];
}
function minBy(arr, fn) {
let min;
let minVal = Infinity;
for (const item of arr) {
const val = fn(item);
if (val < minVal) {
min = item;
minVal = val;
}
}
return min;
}
function areArraysShallowEqual(arr1, arr2) {
if (arr1 === arr2) return true;
if (arr1.length !== arr2.length) return false;
for (let i = 0; i < arr1.length; i++) {
if (!Object.is(arr1[i], arr2[i])) {
return false;
}
}
return true;
}
function omitFromStackTrace(fn) {
const wrappedFn = (...args) => {
try {
return fn(...args);
} catch (error) {
if (error instanceof Error && Error.captureStackTrace) {
Error.captureStackTrace(error, wrappedFn);
}
throw error;
}
};
return wrappedFn;
}
const noop$2 = () => {
};
const Result = {
ok(value) {
return { ok: true, value };
},
err(error) {
return { ok: false, error };
}
};
function exhaustiveSwitchError(value, property) {
const debugValue = property && value && typeof value === "object" && property in value ? value[property] : value;
throw new Error(`Unknown switch case ${debugValue}`);
}
const assert = omitFromStackTrace(
(value, message) => {
if (!value) {
throw new Error(message || "Assertion Error");
}
}
);
const assertExists = omitFromStackTrace((value, message) => {
if (value == null) {
throw new Error(message ?? "value must be defined");
}
return value;
});
function promiseWithResolve() {
let resolve;
let reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
return Object.assign(promise, {
resolve,
reject
});
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function bind$2(...args) {
if (args.length === 2) {
const [originalMethod, context] = args;
context.addInitializer(function initializeMethod() {
assert(Reflect.isExtensible(this), "Cannot bind to a non-extensible class.");
const value = originalMethod.bind(this);
const ok = Reflect.defineProperty(this, context.name, {
value,
writable: true,
configurable: true
});
assert(ok, "Cannot bind a non-configurable class method.");
});
} else {
const [_target, propertyKey, descriptor] = args;
if (!descriptor || typeof descriptor.value !== "function") {
throw new TypeError(
`Only methods can be decorated with @bind. <${propertyKey}> is not a method!`
);
}
return {
configurable: true,
get() {
const bound = descriptor.value.bind(this);
Object.defineProperty(this, propertyKey, {
value: bound,
configurable: true,
writable: true
});
return bound;
}
};
}
}
class WeakCache {
/** The map of items to their cached values. */
items = /* @__PURE__ */ new WeakMap();
/**
* Get the cached value for a given record. If the record is not present in the map, the callback
* will be used to create the value (with the result being stored in the cache for next time).
*
* @param item - The item to get.
* @param cb - The callback to use to create the value when a cached value is not found.
*/
get(item, cb) {
if (!this.items.has(item)) {
this.items.set(item, cb(item));
}
return this.items.get(item);
}
}
function debounce(callback, wait) {
let state = void 0;
const fn = (...args) => {
if (!state) {
state = {};
state.promise = new Promise((resolve, reject) => {
state.resolve = resolve;
state.reject = reject;
});
}
clearTimeout(state.timeout);
state.latestArgs = args;
state.timeout = setTimeout(() => {
const s = state;
state = void 0;
try {
s.resolve(callback(...s.latestArgs));
} catch (e) {
s.reject(e);
}
}, wait);
return state.promise;
};
fn.cancel = () => {
if (!state) return;
clearTimeout(state.timeout);
};
return fn;
}
const annotationsByError = /* @__PURE__ */ new WeakMap();
function annotateError(error, annotations) {
if (typeof error !== "object" || error === null) return;
let currentAnnotations = annotationsByError.get(error);
if (!currentAnnotations) {
currentAnnotations = { tags: {}, extras: {} };
annotationsByError.set(error, currentAnnotations);
}
if (annotations.tags) {
currentAnnotations.tags = {
...currentAnnotations.tags,
...annotations.tags
};
}
if (annotations.extras) {
currentAnnotations.extras = {
...currentAnnotations.extras,
...annotations.extras
};
}
}
async function fetch(input, init) {
return window.fetch(input, {
// We want to make sure that the referrer is not sent to other domains.
referrerPolicy: "strict-origin-when-cross-origin",
...init
});
}
const Image = (width, height) => {
const img = new window.Image(width, height);
img.referrerPolicy = "strict-origin-when-cross-origin";
return img;
};
class FileHelpers {
/**
* @param dataURL - The file as a string.
*
* from https://stackoverflow.com/a/53817185
*/
static async dataUrlToArrayBuffer(dataURL) {
return fetch(dataURL).then(function(result) {
return result.arrayBuffer();
});
}
/**
* Convert a file to a base64 encoded data url.
*
* @example
*
* ```ts
* const A = FileHelpers.toDataUrl(myImageFile)
* ```
*
* @param file - The file as a blob.
*/
static async blobToDataUrl(file) {
return await new Promise((resolve, reject) => {
if (file) {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = (error) => reject(error);
reader.onabort = (error) => reject(error);
reader.readAsDataURL(file);
}
});
}
/**
* Convert a file to a unicode text string.
*
* @example
*
* ```ts
* const A = FileHelpers.fileToDataUrl(myTextFile)
* ```
*
* @param file - The file as a blob.
*/
static async blobToText(file) {
return await new Promise((resolve, reject) => {
if (file) {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = (error) => reject(error);
reader.onabort = (error) => reject(error);
reader.readAsText(file);
}
});
}
static rewriteMimeType(blob, newMimeType) {
if (blob.type === newMimeType) return blob;
if (blob instanceof File) {
return new File([blob], blob.name, { type: newMimeType });
}
return new Blob([blob], { type: newMimeType });
}
}
function getHashForString(string) {
let hash = 0;
for (let i = 0; i < string.length; i++) {
hash = (hash << 5) - hash + string.charCodeAt(i);
hash |= 0;
}
return hash + "";
}
function getHashForBuffer(buffer) {
const view = new DataView(buffer);
let hash = 0;
for (let i = 0; i < view.byteLength; i++) {
hash = (hash << 5) - hash + view.getUint8(i);
hash |= 0;
}
return hash + "";
}
/*!
* MIT License: https://github.com/ai/nanoid/blob/main/LICENSE
* Modified code originally from
* Copyright 2017 Andrey Sitnik
*
* `nanoid` is currently only distributed as an ES module. Some tools (jest, playwright) don't
* properly support ESM-only code yet, and tldraw itself is distributed as both an ES module and a
* CommonJS module. By including nanoid here, we can make sure it works well in every environment
* where tldraw is used. We can also remove some unused features like custom alphabets.
*/
const crypto$1 = globalThis.crypto;
const urlAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
const POOL_SIZE_MULTIPLIER = 128;
let pool, poolOffset;
function fillPool(bytes) {
if (!pool || pool.length < bytes) {
pool = new Uint8Array(bytes * POOL_SIZE_MULTIPLIER);
crypto$1.getRandomValues(pool);
poolOffset = 0;
} else if (poolOffset + bytes > pool.length) {
crypto$1.getRandomValues(pool);
poolOffset = 0;
}
poolOffset += bytes;
}
function nanoid(size = 21) {
fillPool(size -= 0);
let id = "";
for (let i = poolOffset - size; i < poolOffset; i++) {
id += urlAlphabet[pool[i] & 63];
}
return id;
}
let impl = nanoid;
function uniqueId(size) {
return impl(size);
}
/*!
* MIT License: https://github.com/vHeemstra/is-apng/blob/main/license
* Copyright (c) Philip van Heemstra
*/
function isApngAnimated(buffer) {
const view = new Uint8Array(buffer);
if (!view || !(typeof Buffer !== "undefined" && Buffer.isBuffer(view) || view instanceof Uint8Array) || view.length < 16) {
return false;
}
const isPNG = view[0] === 137 && view[1] === 80 && view[2] === 78 && view[3] === 71 && view[4] === 13 && view[5] === 10 && view[6] === 26 && view[7] === 10;
if (!isPNG) {
return false;
}
function indexOfSubstring(haystack, needle, fromIndex, upToIndex, chunksize = 1024) {
if (!needle) {
return -1;
}
needle = new RegExp(needle, "g");
const needle_length = needle.source.length;
const decoder = new TextDecoder();
const full_haystack_length = haystack.length;
if (typeof upToIndex === "undefined") {
upToIndex = full_haystack_length;
}
if (fromIndex >= full_haystack_length || upToIndex <= 0 || fromIndex >= upToIndex) {
return -1;
}
haystack = haystack.subarray(fromIndex, upToIndex);
let position = -1;
let current_index = 0;
let full_length = 0;
let needle_buffer = "";
outer: while (current_index < haystack.length) {
const next_index = current_index + chunksize;
const chunk = haystack.subarray(current_index, next_index);
const decoded = decoder.decode(chunk, { stream: true });
const text = needle_buffer + decoded;
let match;
let last_index = -1;
while ((match = needle.exec(text)) !== null) {
last_index = match.index - needle_buffer.length;
position = full_length + last_index;
break outer;
}
current_index = next_index;
full_length += decoded.length;
const needle_index = last_index > -1 ? last_index + needle_length : decoded.length - needle_length;
needle_buffer = decoded.slice(needle_index);
}
if (position >= 0) {
position += fromIndex >= 0 ? fromIndex : full_haystack_length + fromIndex;
}
return position;
}
const idatIdx = indexOfSubstring(view, "IDAT", 12);
if (idatIdx >= 12) {
const actlIdx = indexOfSubstring(view, "acTL", 8, idatIdx);
return actlIdx >= 8;
}
return false;
}
const isAvifAnimated = (buffer) => {
const view = new Uint8Array(buffer);
return view[3] === 44;
};
/*!
* MIT License
* Modified code originally from
* Copyright (c) 2016 Józef Sokołowski
*/
function getDataBlocksLength(buffer, offset) {
let length = 0;
while (buffer[offset + length]) {
length += buffer[offset + length] + 1;
}
return length + 1;
}
function isGIF(buffer) {
const enc = new TextDecoder("ascii");
const header = enc.decode(buffer.slice(0, 3));
return header === "GIF";
}
function isGifAnimated(buffer) {
const view = new Uint8Array(buffer);
let hasColorTable, colorTableSize;
let offset = 0;
let imagesCount = 0;
if (!isGIF(buffer)) {
return false;
}
hasColorTable = view[10] & 128;
colorTableSize = view[10] & 7;
offset += 6;
offset += 7;
offset += hasColorTable ? 3 * Math.pow(2, colorTableSize + 1) : 0;
while (imagesCount < 2 && offset < view.length) {
switch (view[offset]) {
case 44:
imagesCount += 1;
hasColorTable = view[offset + 9] & 128;
colorTableSize = view[offset + 9] & 7;
offset += 10;
offset += hasColorTable ? 3 * Math.pow(2, colorTableSize + 1) : 0;
offset += getDataBlocksLength(view, offset + 1) + 1;
break;
case 33:
offset += 2;
offset += getDataBlocksLength(view, offset);
break;
case 59:
offset = view.length;
break;
default:
offset = view.length;
break;
}
}
return imagesCount > 1;
}
/*!
* MIT License: https://github.com/alexgorbatchev/crc/blob/master/LICENSE
* Copyright: 2014 Alex Gorbatchev
* Code: crc32, https://github.com/alexgorbatchev/crc/blob/master/src/calculators/crc32.ts
*/
let TABLE = [
0,
1996959894,
3993919788,
2567524794,
124634137,
1886057615,
3915621685,
2657392035,
249268274,
2044508324,
3772115230,
2547177864,
162941995,
2125561021,
3887607047,
2428444049,
498536548,
1789927666,
4089016648,
2227061214,
450548861,
1843258603,
4107580753,
2211677639,
325883990,
1684777152,
4251122042,
2321926636,
335633487,
1661365465,
4195302755,
2366115317,
997073096,
1281953886,
3579855332,
2724688242,
1006888145,
1258607687,
3524101629,
2768942443,
901097722,
1119000684,
3686517206,
2898065728,
853044451,
1172266101,
3705015759,
2882616665,
651767980,
1373503546,
3369554304,
3218104598,
565507253,
1454621731,
3485111705,
3099436303,
671266974,
1594198024,
3322730930,
2970347812,
795835527,
1483230225,
3244367275,
3060149565,
1994146192,
31158534,
2563907772,
4023717930,
1907459465,
112637215,
2680153253,
3904427059,
2013776290,
251722036,
2517215374,
3775830040,
2137656763,
141376813,
2439277719,
3865271297,
1802195444,
476864866,
2238001368,
4066508878,
1812370925,
453092731,
2181625025,
4111451223,
1706088902,
314042704,
2344532202,
4240017532,
1658658271,
366619977,
2362670323,
4224994405,
1303535960,
984961486,
2747007092,
3569037538,
1256170817,
1037604311,
2765210733,
3554079995,
1131014506,
879679996,
2909243462,
3663771856,
1141124467,
855842277,
2852801631,
3708648649,
1342533948,
654459306,
3188396048,
3373015174,
1466479909,
544179635,
3110523913,
3462522015,
1591671054,
702138776,
2966460450,
3352799412,
1504918807,
783551873,
3082640443,
3233442989,
3988292384,
2596254646,
62317068,
1957810842,
3939845945,
2647816111,
81470997,
1943803523,
3814918930,
2489596804,
225274430,
2053790376,
3826175755,
2466906013,
167816743,
2097651377,
4027552580,
2265490386,
503444072,
1762050814,
4150417245,
2154129355,
426522225,
1852507879,
4275313526,
2312317920,
282753626,
1742555852,
4189708143,
2394877945,
397917763,
1622183637,
3604390888,
2714866558,
953729732,
1340076626,
3518719985,
2797360999,
1068828381,
1219638859,
3624741850,
2936675148,
906185462,
1090812512,
3747672003,
2825379669,
829329135,
1181335161,
3412177804,
3160834842,
628085408,
1382605366,
3423369109,
3138078467,
570562233,
1426400815,
3317316542,
2998733608,
733239954,
1555261956,
3268935591,
3050360625,
752459403,
1541320221,
2607071920,
3965973030,
1969922972,
40735498,
2617837225,
3943577151,
1913087877,
83908371,
2512341634,
3803740692,
2075208622,
213261112,
2463272603,
3855990285,
2094854071,
198958881,
2262029012,
4057260610,
1759359992,
534414190,
2176718541,
4139329115,
1873836001,
414664567,
2282248934,
4279200368,
1711684554,
285281116,
2405801727,
4167216745,
1634467795,
376229701,
2685067896,
3608007406,
1308918612,
956543938,
2808555105,
3495958263,
1231636301,
1047427035,
2932959818,
3654703836,
1088359270,
936918e3,
2847714899,
3736837829,
1202900863,
817233897,
3183342108,
3401237130,
1404277552,
615818150,
3134207493,
3453421203,
1423857449,
601450431,
3009837614,
3294710456,
1567103746,
711928724,
3020668471,
3272380065,
1510334235,
755167117
];
if (typeof Int32Array !== "undefined") {
TABLE = new Int32Array(TABLE);
}
const crc = (current, previous) => {
let crc2 = 0 ^ -1;
for (let index = 0; index < current.length; index++) {
crc2 = TABLE[(crc2 ^ current[index]) & 255] ^ crc2 >>> 8;
}
return crc2 ^ -1;
};
const LEN_SIZE = 4;
const CRC_SIZE = 4;
class PngHelpers {
static isPng(view, offset) {
if (view.getUint8(offset + 0) === 137 && view.getUint8(offset + 1) === 80 && view.getUint8(offset + 2) === 78 && view.getUint8(offset + 3) === 71 && view.getUint8(offset + 4) === 13 && view.getUint8(offset + 5) === 10 && view.getUint8(offset + 6) === 26 && view.getUint8(offset + 7) === 10) {
return true;
}
return false;
}
static getChunkType(view, offset) {
return [
String.fromCharCode(view.getUint8(offset)),
String.fromCharCode(view.getUint8(offset + 1)),
String.fromCharCode(view.getUint8(offset + 2)),
String.fromCharCode(view.getUint8(offset + 3))
].join("");
}
static readChunks(view, offset = 0) {
const chunks = {};
if (!PngHelpers.isPng(view, offset)) {
throw new Error("Not a PNG");
}
offset += 8;
while (offset <= view.buffer.byteLength) {
const start = offset;
const len = view.getInt32(offset);
offset += 4;
const chunkType = PngHelpers.getChunkType(view, offset);
if (chunkType === "IDAT" && chunks[chunkType]) {
offset += len + LEN_SIZE + CRC_SIZE;
continue;
}
if (chunkType === "IEND") {
break;
}
chunks[chunkType] = {
start,
dataOffset: offset + 4,
size: len
};
offset += len + LEN_SIZE + CRC_SIZE;
}
return chunks;
}
static parsePhys(view, offset) {
return {
ppux: view.getUint32(offset),
ppuy: view.getUint32(offset + 4),
unit: view.getUint8(offset + 4)
};
}
static findChunk(view, type) {
const chunks = PngHelpers.readChunks(view);
return chunks[type];
}
static setPhysChunk(view, dpr = 1, options) {
let offset = 46;
let size = 0;
const res1 = PngHelpers.findChunk(view, "pHYs");
if (res1) {
offset = res1.start;
size = res1.size;
}
const res2 = PngHelpers.findChunk(view, "IDAT");
if (res2) {
offset = res2.start;
size = 0;
}
const pHYsData = new ArrayBuffer(21);
const pHYsDataView = new DataView(pHYsData);
pHYsDataView.setUint32(0, 9);
pHYsDataView.setUint8(4, "p".charCodeAt(0));
pHYsDataView.setUint8(5, "H".charCodeAt(0));
pHYsDataView.setUint8(6, "Y".charCodeAt(0));
pHYsDataView.setUint8(7, "s".charCodeAt(0));
const DPI_96 = 2835.5;
pHYsDataView.setInt32(8, DPI_96 * dpr);
pHYsDataView.setInt32(12, DPI_96 * dpr);
pHYsDataView.setInt8(16, 1);
const crcBit = new Uint8Array(pHYsData.slice(4, 17));
pHYsDataView.setInt32(17, crc(crcBit));
const startBuf = view.buffer.slice(0, offset);
const endBuf = view.buffer.slice(offset + size);
return new Blob([startBuf, pHYsData, endBuf], options);
}
}
/*!
* MIT License: https://github.com/sindresorhus/is-webp/blob/main/license
* Copyright (c) Sindre Sorhus (https://sindresorhus.com)
*/
function isWebp(view) {
if (!view || view.length < 12) {
return false;
}
return view[8] === 87 && view[9] === 69 && view[10] === 66 && view[11] === 80;
}
function isWebpAnimated(buffer) {
const view = new Uint8Array(buffer);
if (!isWebp(view)) {
return false;
}
if (!view || view.length < 21) {
return false;
}
return (view[20] >> 1 & 1) === 1;
}
const DEFAULT_SUPPORTED_VECTOR_IMAGE_TYPES = Object.freeze(["image/svg+xml"]);
const DEFAULT_SUPPORTED_STATIC_IMAGE_TYPES = Object.freeze([
"image/jpeg",
"image/png",
"image/webp"
]);
const DEFAULT_SUPPORTED_ANIMATED_IMAGE_TYPES = Object.freeze([
"image/gif",
"image/apng",
"image/avif"
]);
const DEFAULT_SUPPORTED_IMAGE_TYPES = Object.freeze([
...DEFAULT_SUPPORTED_STATIC_IMAGE_TYPES,
...DEFAULT_SUPPORTED_VECTOR_IMAGE_TYPES,
...DEFAULT_SUPPORTED_ANIMATED_IMAGE_TYPES
]);
const DEFAULT_SUPPORT_VIDEO_TYPES = Object.freeze([
"video/mp4",
"video/webm",
"video/quicktime"
]);
const DEFAULT_SUPPORTED_MEDIA_TYPE_LIST = [
...DEFAULT_SUPPORTED_IMAGE_TYPES,
...DEFAULT_SUPPORT_VIDEO_TYPES
].join(",");
class MediaHelpers {
/**
* Load a video from a url.
* @public
*/
static loadVideo(src) {
return new Promise((resolve, reject) => {
const video = document.createElement("video");
video.onloadeddata = () => resolve(video);
video.onerror = (e) => {
console.error(e);
reject(new Error("Could not load video"));
};
video.crossOrigin = "anonymous";
video.src = src;
});
}
static async getVideoFrameAsDataUrl(video, time = 0) {
const promise = promiseWithResolve();
let didSetTime = false;
const onReadyStateChanged = () => {
if (!didSetTime) {
if (video.readyState >= video.HAVE_METADATA) {
didSetTime = true;
video.currentTime = time;
} else {
return;
}
}
if (video.readyState >= video.HAVE_CURRENT_DATA) {
const canvas = document.createElement("canvas");
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
const ctx = canvas.getContext("2d");
if (!ctx) {
throw new Error("Could not get 2d context");
}
ctx.drawImage(video, 0, 0);
promise.resolve(canvas.toDataURL());
}
};
const onError = (e) => {
console.error(e);
promise.reject(new Error("Could not get video frame"));
};
video.addEventListener("loadedmetadata", onReadyStateChanged);
video.addEventListener("loadeddata", onReadyStateChanged);
video.addEventListener("canplay", onReadyStateChanged);
video.addEventListener("seeked", onReadyStateChanged);
video.addEventListener("error", onError);
video.addEventListener("stalled", onError);
onReadyStateChanged();
try {
return await promise;
} finally {
video.removeEventListener("loadedmetadata", onReadyStateChanged);
video.removeEventListener("loadeddata", onReadyStateChanged);
video.removeEventListener("canplay", onReadyStateChanged);
video.removeEventListener("seeked", onReadyStateChanged);
video.removeEventListener("error", onError);
video.removeEventListener("stalled", onError);
}
}
/**
* Load an image from a url.
* @public
*/
static loadImage(src) {
return new Promise((resolve, reject) => {
const img = Image();
img.onload = () => resolve(img);
img.onerror = (e) => {
console.error(e);
reject(new Error("Could not load image"));
};
img.crossOrigin = "anonymous";
img.referrerPolicy = "strict-origin-when-cross-origin";
img.src = src;
});
}
/**
* Get the size of a video blob
*
* @param blob - A SharedBlob containing the video
* @public
*/
static async getVideoSize(blob) {
return MediaHelpers.usingObjectURL(blob, async (url) => {
const video = await MediaHelpers.loadVideo(url);
return { w: video.videoWidth, h: video.videoHeight };
});
}
/**
* Get the size of an image blob
*
* @param blob - A Blob containing the image.
* @public
*/
static async getImageSize(blob) {
const image = await MediaHelpers.usingObjectURL(blob, MediaHelpers.loadImage);
try {
if (blob.type === "image/png") {
const view = new DataView(await blob.arrayBuffer());
if (PngHelpers.isPng(view, 0)) {
const physChunk = PngHelpers.findChunk(view, "pHYs");
if (physChunk) {
const physData = PngHelpers.parsePhys(view, physChunk.dataOffset);
if (physData.unit === 0 && physData.ppux === physData.ppuy) {
const pixelRatio = Math.max(physData.ppux / 2834.5, 1);
return {
w: Math.round(image.naturalWidth / pixelRatio),
h: Math.round(image.naturalHeight / pixelRatio)
};
}
}
}
}
} catch (err) {
console.error(err);
return { w: image.naturalWidth, h: image.naturalHeight };
}
return { w: image.naturalWidth, h: image.naturalHeight };
}
static async isAnimated(file) {
if (file.type === "image/gif") {
return isGifAnimated(await file.arrayBuffer());
}
if (file.type === "image/avif") {
return isAvifAnimated(await file.arrayBuffer());
}
if (file.type === "image/webp") {
return isWebpAnimated(await file.arrayBuffer());
}
if (file.type === "image/apng") {
return isApngAnimated(await file.arrayBuffer());
}
return false;
}
static isAnimatedImageType(mimeType) {
return DEFAULT_SUPPORTED_ANIMATED_IMAGE_TYPES.includes(mimeType || "");
}
static isStaticImageType(mimeType) {
return DEFAULT_SUPPORTED_STATIC_IMAGE_TYPES.includes(mimeType || "");
}
static isVectorImageType(mimeType) {
return DEFAULT_SUPPORTED_VECTOR_IMAGE_TYPES.includes(mimeType || "");
}
static isImageType(mimeType) {
return DEFAULT_SUPPORTED_IMAGE_TYPES.includes(mimeType);
}
static async usingObjectURL(blob, fn) {
const url = URL.createObjectURL(blob);
try {
return await fn(url);
} finally {
URL.revokeObjectURL(url);
}
}
}
function lerp(a, b, t) {
return a + (b - a) * t;
}
function rng(seed = "") {
let x = 0;
let y = 0;
let z = 0;
let w = 0;
function next() {
const t = x ^ x << 11;
x = y;
y = z;
z = w;
w ^= (w >>> 19 ^ t ^ t >>> 8) >>> 0;
return w / 4294967296 * 2;
}
for (let k = 0; k < seed.length + 64; k++) {
x ^= seed.charCodeAt(k) | 0;
next();
}
return next;
}
function modulate(value, rangeA, rangeB, clamp = false) {
const [fromLow, fromHigh] = rangeA;
const [v0, v1] = rangeB;
const result = v0 + (value - fromLow) / (fromHigh - fromLow) * (v1 - v0);
return clamp ? v0 < v1 ? Math.max(Math.min(result, v1), v0) : Math.max(Math.min(result, v0), v1) : result;
}
function hasOwnProperty$1(obj, key) {
return Object.prototype.hasOwnProperty.call(obj, key);
}
function getOwnProperty(obj, key) {
if (!hasOwnProperty$1(obj, key)) {
return void 0;
}
return obj[key];
}
function objectMapKeys(object) {
return Object.keys(object);
}
function objectMapValues(object) {
return Object.values(object);
}
function objectMapEntries(object) {
return Object.entries(object);
}
function objectMapFromEntries(entries) {
return Object.fromEntries(entries);
}
function filterEntries(object, predicate) {
const result = {};
let didChange = false;
for (const [key, value] of objectMapEntries(object)) {
if (predicate(key, value)) {
result[key] = value;
} else {
didChange = true;
}
}
return didChange ? result : object;
}
function mapObjectMapValues(object, mapper) {
const result = {};
for (const [key, value] of objectMapEntries(object)) {
const newValue = mapper(key, value);
result[key] = newValue;
}
return result;
}
function areObjectsShallowEqual(obj1, obj2) {
if (obj1 === obj2) return true;
const keys1 = new Set(Object.keys(obj1));
const keys2 = new Set(Object.keys(obj2));
if (keys1.size !== keys2.size) return false;
for (const key of keys1) {
if (!keys2.has(key)) return false;
if (!Object.is(obj1[key], obj2[key])) return false;
}
return true;
}
// src/charSet.ts
function indexCharacterSet(options) {
const dicts = createCharSetDicts(options.chars);
const limits = integerLimits(
dicts,
options.firstPositive,
options.mostPositive,
options.mostNegative
);
const jitterRange = options.jitterRange ?? Math.floor(Math.pow(dicts.length, 3) / 5);
const paddingRange = paddingDict(jitterRange, dicts.length);
return {
chars: options.chars,
byChar: dicts.byChar,
byCode: dicts.byCode,
length: dicts.length,
first: dicts.byCode[0],
last: dicts.byCode[dicts.length - 1],
firstPositive: limits.firstPositive,
mostPositive: limits.mostPositive,
firstNegative: limits.firstNegative,
mostNegative: limits.mostNegative,
jitterRange,
paddingDict: paddingRange
};
}
function createCharSetDicts(charSet) {
const byCode = {};
const byChar = {};
const length = charSet.length;
for (let i = 0; i < length; i++) {
const char = charSet[i];
byCode[i] = char;
byChar[char] = i;
}
return {
byCode,
byChar,
length
};
}
function integerLimits(dicts, firstPositive, mostPositive, mostNegative) {
const firstPositiveIndex = dicts.byChar[firstPositive] ;
const mostPositiveIndex = dicts.byChar[mostPositive] ;
const mostNegativeIndex = dicts.byChar[mostNegative] ;
if (firstPositiveIndex === void 0 || mostPositiveIndex === void 0 || mostNegativeIndex === void 0) {
throw new Error("invalid charSet");
}
if (mostPositiveIndex - firstPositiveIndex < 3) {
throw new Error(
"mostPositive must be at least 3 characters away from neutral"
);
}
if (firstPositiveIndex - mostNegativeIndex < 3) {
throw new Error(
"mostNegative must be at least 3 characters away from neutral"
);
}
return {
firstPositive: dicts.byCode[firstPositiveIndex],
mostPositive: dicts.byCode[mostPositiveIndex],
firstNegative: dicts.byCode[firstPositiveIndex - 1],
mostNegative: dicts.byCode[mostNegativeIndex]
};
}
function paddingDict(jitterRange, charSetLength) {
const paddingDict2 = {};
for (let i = 0; i < 100; i++) {
paddingDict2[i] = Math.pow(charSetLength, i);
if (paddingDict2[i] > jitterRange) {
break;
}
}
return paddingDict2;
}
var _base62CharSet = null;
function base62CharSet() {
if (_base62CharSet)
return _base62CharSet;
return _base62CharSet = indexCharacterSet({
// Base62 are all the alphanumeric characters, database and user friendly
// For shorter strings and more room you could opt for more characters
chars: "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
// This gives us nice human readable keys to start with a0 a1 etc
firstPositive: "a",
mostPositive: "z",
mostNegative: "A"
});
}
// src/integerLength.ts
function distanceBetween(a, b, charSet) {
const indexA = charSet.byChar[a];
const indexB = charSet.byChar[b];
return Math.abs(indexA - indexB);
}
function integerLength(head, charSet) {
const firstChar = head[0];
if (firstChar > charSet.mostPositive || firstChar < charSet.mostNegative) {
throw new Error("invalid firstChar on key");
}
if (firstChar === charSet.mostPositive) {
const firstLevel = distanceBetween(firstChar, charSet.firstPositive, charSet) + 1;
return firstLevel + integerLengthFromSecondLevel(head.slice(1), "positive", charSet);
}
if (firstChar === charSet.mostNegative) {
const firstLevel = distanceBetween(firstChar, charSet.firstNegative, charSet) + 1;
return firstLevel + integerLengthFromSecondLevel(head.slice(1), "negative", charSet);
}
const isPositiveRange = firstChar >= charSet.firstPositive;
if (isPositiveRange) {
return distanceBetween(firstChar, charSet.firstPositive, charSet) + 2;
} else {
return distanceBetween(firstChar, charSet.firstNegative, charSet) + 2;
}
}
function integerLengthFromSecondLevel(key, direction, charSet) {
const firstChar = key[0];
if (firstChar > charSet.mostPositive || firstChar < charSet.mostNegative) {
throw new Error("invalid firstChar on key");
}
if (firstChar === charSet.mostPositive && direction === "positive") {
const totalPositiveRoom = distanceBetween(firstChar, charSet.mostNegative, charSet) + 1;
return totalPositiveRoom + integerLengthFromSecondLevel(key.slice(1), direction, charSet);
}
if (firstChar === charSet.mostNegative && direction === "negative") {
const totalNegativeRoom = distanceBetween(firstChar, charSet.mostPositive, charSet) + 1;
return totalNegativeRoom + integerLengthFromSecondLevel(key.slice(1), direction, charSet);
}
if (direction === "positive") {
return distanceBetween(firstChar, charSet.mostNegative, charSet) + 2;
} else {
return distanceBetween(firstChar, charSet.mostPositive, charSet) + 2;
}
}
// src/padToSameLength.ts
function makeSameLength(a, b, pad, fillChar, forceLength) {
const max = Math.max(a.length, b.length);
if (pad === "start") {
return [a.padStart(max, fillChar), b.padStart(max, fillChar)];
}
return [a.padEnd(max, fillChar), b.padEnd(max, fillChar)];
}
// src/keyAsNumber.ts
function midPoint(lower, upper, charSet) {
let [paddedLower, paddedUpper] = makeSameLength(
lower,
upper,
"end",
charSet.first
);
let distance = lexicalDistance(paddedLower, paddedUpper, charSet);
if (distance === 1) {
paddedLower = paddedLower.padEnd(paddedLower.length + 1, charSet.first);
distance = charSet.length;
}
const mid = encodeToCharSet(Math.floor(distance / 2), charSet);
return addCharSetKeys(paddedLower, mid, charSet);
}
function lexicalDistance(a, b, charSet) {
const [lower, upper] = makeSameLength(a, b, "end", charSet.first).sort();
const distance = subtractCharSetKeys(upper, lower, charSet);
return decodeCharSetToNumber(distance, charSet);
}
function addCharSetKeys(a, b, charSet) {
const base = charSet.length;
const [paddedA, paddedB] = makeSameLength(a, b, "start", charSet.first);
const result = [];
let carry = 0;
for (let i = paddedA.length - 1; i >= 0; i--) {
const digitA = charSet.byChar[paddedA[i]];
const digitB = charSet.byChar[paddedB[i]];
const sum = digitA + digitB + carry;
carry = Math.floor(sum / base);
const remainder = sum % base;
result.unshift(charSet.byCode[remainder]);
}
if (carry > 0) {
result.unshift(charSet.byCode[carry]);
}
return result.join("");
}
function subtractCharSetKeys(a, b, charSet) {
const base = charSet.length;
const [paddedA, paddedB] = makeSameLength(a, b, "start", charSet.first);
const result = [];
let borrow = 0;
for (let i = paddedA.length - 1; i >= 0; i--) {
let digitA = charSet.byChar[paddedA[i]];
const digitB = charSet.byChar[paddedB[i]] + borrow;
if (digitA < digitB) {
borrow = 1;
digitA += base;
} else {
borrow = 0;
}
const difference = digitA - digitB;
result.unshift(charSet.byCode[difference]);
}
if (borrow > 0) {
throw new Error(
"Subtraction result is negative. Ensure a is greater than or equal to b."
);
}
while (result.length > 1 && result[0] === charSet.byCode[0]) {
result.shift();
}
return result.join("");
}
function incrementKey(key, charSet) {
return addCharSetKeys(key, charSet.byCode[1], charSet);
}
function decrementKey(key, charSet) {
return subtractCharSetKeys(key, charSet.byCode[1], charSet);
}
function encodeToCharSet(int, charSet) {
if (int === 0) {
return charSet.byCode[0];
}
let res = "";
const max = charSet.length;
while (int > 0) {
res = charSet.byCode[int % max] + res;
int = Math.floor(int / max);
}
return res;
}
function decodeCharSetToNumber(key, charSet) {
let res = 0;
const length = key.length;
const max = charSet.length;
for (let i = 0; i < length; i++) {
res += charSet.byChar[key[i]] * Math.pow(max, length - i - 1);
}
return res;
}
// src/integer.ts
function startKey(charSet) {
return charSet.firstPositive + charSet.byCode[0];
}
function validInteger(integer, charSet) {
const length = integerLength(integer, charSet);
return length === integer.length;
}
function validateOrderKey(orderKey, charSet) {
getIntegerPart(orderKey, charSet);
}
function getIntegerPart(orderKey, charSet) {
const head = integerHead(orderKey, charSet);
const integerPartLength = integerLength(head, charSet);
if (integerPartLength > orderKey.length) {
throw new Error("invalid order key length: " + orderKey);
}
return orderKey.slice(0, integerPartLength);
}
function validateInteger(integer, charSet) {
if (!validInteger(integer, charSet)) {
throw new Error("invalid integer length: " + integer);
}
}
function incrementInteger(integer, charSet) {
validateInteger(integer, charSet);
const [head, digs] = splitInteger(integer, charSet);
const anyNonMaxedDigit = digs.split("").some((d) => d !== charSet.byCode[charSet.length - 1]);
if (anyNonMaxedDigit) {
const newDigits = incrementKey(digs, charSet);
return head + newDigits;
}
const nextHead = incrementIntegerHead(head, charSet);
return startOnNewHead(nextHead, "lower", charSet);
}
function decrementInteger(integer, charSet) {
validateInteger(integer, charSet);
const [head, digs] = splitInteger(integer, charSet);
const anyNonLimitDigit = digs.split("").some((d) => d !== charSet.byCode[0]);
if (anyNonLimitDigit) {
const newDigits = decrementKey(digs, charSet);
return head + newDigits;
}
const nextHead = decrementIntegerHead(head, charSet);
return startOnNewHead(nextHead, "upper", charSet);
}
function integerHead(integer, charSet) {
let i = 0;
if (integer[0] === charSet.mostPositive) {
while (integer[i] === charSet.mostPositive) {
i = i + 1;
}
}
if (integer[0] === charSet.mostNegative) {
while (integer[i] === charSet.mostNegative) {
i = i + 1;
}
}
return integer.slice(0, i + 1);
}
function splitInteger(integer, charSet) {
const head = integerHead(integer, charSet);
const tail = integer.slice(head.length);
return [head, tail];
}
function incrementIntegerHead(head, charSet) {
const inPositiveRange = head >= charSet.firstPositive;
const nextHead = incrementKey(head, charSet);
const headIsLimitMax = head[head.length - 1] === charSet.mostPositive;
const nextHeadIsLimitMax = nextHead[nextHead.length - 1] === charSet.mostPositive;
if (inPositiveRange && nextHeadIsLimitMax) {
return nextHead + charSet.mostNegative;
}
if (!inPositiveRange && headIsLimitMax) {
return head.slice(0, head.length - 1);
}
return nextHead;
}
function decrementIntegerHead(head, charSet) {
const inPositiveRange = head >= charSet.firstPositive;
const headIsLimitMin = head[head.length - 1] === charSet.mostNegative;
if (inPositiveRange && headIsLimitMin) {
const nextLevel = head.slice(0, head.length - 1);
return decrementKey(nextLevel, charSet);
}
if (!inPositiveRange && headIsLimitMin) {
return head + charSet.mostPositive;
}
return decrementKey(head, charSet);
}
function startOnNewHead(head, limit, charSet) {
const newLength = integerLength(head, charSet);
const fillChar = limit === "upper" ? charSet.byCode[charSet.length - 1] : charSet.byCode[0];
return head + fillChar.repeat(newLength - head.length);
}
// src/jittering.ts
function jitterString(orderKey, charSet) {
const shift = encodeToCharSet(
Math.floor(Math.random() * charSet.jitterRange),
charSet
);
return addCharSetKeys(orderKey, shift, charSet);
}
function padAndJitterString(orderKey, numberOfChars, charSet) {
const paddedKey = orderKey.padEnd(
orderKey.length + numberOfChars,
charSet.first
);
return jitterString(paddedKey, charSet);
}
function paddingNeededForJitter(orderKey, b, charSet) {
const integer = getIntegerPart(orderKey, charSet);
const nextInteger = incrementInteger(integer, charSet);
let needed = 0;
if (b !== null) {
const distanceToB = lexicalDistance(orderKey, b, charSet);
if (distanceToB < charSet.jitterRange + 1) {
needed = Math.max(needed, paddingNeededForDistance(distanceToB, charSet));
}
}
const distanceToNextInteger = lexicalDistance(orderKey, nextInteger, charSet);
if (distanceToNextInteger < charSet.jitterRange + 1) {
needed = Math.max(
needed,
paddingNeededForDistance(distanceToNextInteger, charSet)
);
}
return needed;
}
function paddingNeededForDistance(distance, charSet) {
const gap = charSet.jitterRange - distance;
const firstBigger = Object.entries(charSet.paddingDict).find(
([_key, value]) => {
return value > gap;
}
);
return firstBigger ? parseInt(firstBigger[0]) : 0;
}
// src/generateKeyBetween.ts
function generateKeyBetween(lower, upper, charSet = base62CharSet()) {
if (lower !== null) {
validateOrderKey(lower, charSet);
}
if (upper !== null) {
validateOrderKey(upper, charSet);
}
if (lower === null && upper === null) {
return startKey(charSet);
}
if (lower === null) {
const integer = getIntegerPart(upper, charSet);
return decrementInteger(integer, charSet);
}
if (upper === null) {
const integer = getIntegerPart(lower, charSet);
return incrementInteger(integer, charSet);
}
if (lower >= upper) {
throw new Error(lower + " >= " + upper);
}
return midPoint(lower, upper, charSet);
}
function generateJitteredKeyBetween(lower, upper, charSet = base62CharSet()) {
const key = generateKeyBetween(lower, upper, charSet);
const paddingNeeded = paddingNeededForJitter(key, upper, charSet);
if (paddingNeeded) {
return padAndJitterString(key, paddingNeeded, charSet);
}
return jitterString(key, charSet);
}
function generateNJitteredKeysBetween(lower, upper, n, charSet = base62CharSet()) {
return spreadGeneratorResults(
lower,
upper,
n,
charSet,
generateJitteredKeyBetween,
generateNJitteredKeysBetween
);
}
function spreadGeneratorResults(lower, upper, n, charSet, generateKey, generateNKeys) {
if (n === 0) {
return [];
}
if (n === 1) {
return [generateKey(lower, upper, charSet)];
}
if (upper == null) {
let newUpper = generateKey(lower, upper, charSet);
const result = [newUpper];
for (let i = 0; i < n - 1; i++) {
newUpper = generateKey(newUpper, upper, charSet);
result.push(newUpper);
}
return result;
}
if (lower == null) {
let newLower = generateKey(lower, upper, charSet);
const result = [newLower];
for (let i = 0; i < n - 1; i++) {
newLower = generateKey(lower, newLower, charSet);
result.push(newLower);
}
result.reverse();
return result;
}
const mid = Math.floor(n / 2);
const midOrderKey = generateKey(lower, upper, charSet);
return [
...generateNKeys(lower, midOrderKey, mid, charSet),
midOrderKey,
...generateNKeys(midOrderKey, upper, n - mid - 1, charSet)
];
}
const generateKeysFn = generateNJitteredKeysBetween;
const ZERO_INDEX_KEY = "a0";
function validateIndexKey(index) {
try {
generateJitteredKeyBetween(index, null);
} catch {
throw new Error("invalid index: " + index);
}
}
function getIndicesBetween(below, above, n) {
return generateKeysFn(below ?? null, above ?? null, n);
}
function getIndicesAbove(below, n) {
return generateKeysFn(below ?? null, null, n);
}
function getIndexBetween(below, above) {
return generateKeysFn(below ?? null, above ?? null, 1)[0];
}
function getIndexAbove(below = null) {
return generateKeysFn(below, null, 1)[0];
}
function getIndexBelow(above = null) {
return generateKeysFn(null, above, 1)[0];
}
function getIndices(n, start = "a1") {
return [start, ...generateKeysFn(start, null, n)];
}
function sortByIndex$1(a, b) {
if (a.index < b.index) {
return -1;
} else if (a.index > b.index) {
return 1;
}
return 0;
}
function sortById(a, b) {
return a.id > b.id ? 1 : -1;
}
function getFromLocalStorage(key) {
try {
return localStorage.getItem(key);
} catch {
return null;
}
}
function setInLocalStorage(key, value) {
try {
localStorage.setItem(key, value);
} catch {
}
}
function clearLocalStorage() {
try {
localStorage.clear();
} catch {
}
}
function getFromSessionStorage(key) {
try {
return sessionStorage.getItem(key);
} catch {
return null;
}
}
function setInSessionStorage(key, value) {
try {
sessionStorage.setItem(key, value);
} catch {
}
}
function deleteFromSessionStorage(key) {
try {
sessionStorage.removeItem(key);
} catch {
}
}
function clearSessionStorage() {
try {
sessionStorage.clear();
} catch {
}
}
const fpsQueue = [];
const targetFps = 60;
const targetTimePerFrame = Math.ceil(1e3 / targetFps);
let frame;
let time = 0;
let last = 0;
const flush = () => {
const queue = fpsQueue.splice(0, fpsQueue.length);
for (const fn of queue) {
fn();
}
};
function tick() {
if (frame) {
return;
}
const now = Date.now();
const elapsed = now - last;
if (time + elapsed < targetTimePerFrame) {
frame = requestAnimationFrame(() => {
frame = void 0;
tick();
});
return;
}
frame = requestAnimationFrame(() => {
frame = void 0;
last = now;
time = Math.min(time + elapsed - targetTimePerFrame, targetTimePerFrame * 10);
flush();
});
}
let started = false;
function fpsThrottle(fn) {
const throttledFn = () => {
if (fpsQueue.includes(fn)) {
return;
}
fpsQueue.push(fn);
if (!started) {
started = true;
last = Date.now() - targetTimePerFrame - 1;
}
tick();
};
throttledFn.cancel = () => {
const index = fpsQueue.indexOf(fn);
if (index > -1) {
fpsQueue.splice(index, 1);
}
};
return throttledFn;
}
function throttleToNextFrame$1(fn) {
if (!fpsQueue.includes(fn)) {
fpsQueue.push(fn);
if (!started) {
started = true;
last = Date.now() - targetTimePerFrame - 1;
}
tick();
}
return () => {
const index = fpsQueue.indexOf(fn);
if (index > -1) {
fpsQueue.splice(index, 1);
}
};
}
class Timers {
timeouts = /* @__PURE__ */ new Map();
intervals = /* @__PURE__ */ new Map();
rafs = /* @__PURE__ */ new Map();
constructor() {
this.setTimeout = this.setTimeout.bind(this);
this.setInterval = this.setInterval.bind(this);
this.requestAnimationFrame = this.requestAnimationFrame.bind(this);
this.dispose = this.dispose.bind(this);
}
/** @public */
setTimeout(contextId, handler, timeout, ...args) {
const id = window.setTimeout(handler, timeout, args);
const current = this.timeouts.get(contextId) ?? [];
this.timeouts.set(contextId, [...current, id]);
return id;
}
/** @public */
setInterval(contextId, handler, timeout, ...args) {
const id = window.setInterval(handler, timeout, args);
const current = this.intervals.get(contextId) ?? [];
this.intervals.set(contextId, [...current, id]);
return id;
}
/** @public */
requestAnimationFrame(contextId, callback) {
const id = window.requestAnimationFrame(callback);
const current = this.rafs.get(contextId) ?? [];
this.rafs.set(contextId, [...current, id]);
return id;
}
/** @public */
dispose(contextId) {
this.timeouts.get(contextId)?.forEach((id) => clearTimeout(id));
this.intervals.get(contextId)?.forEach((id) => clearInterval(id));
this.rafs.get(contextId)?.forEach((id) => cancelAnimationFrame(id));
this.timeouts.delete(contextId);
this.intervals.delete(contextId);
this.rafs.delete(contextId);
}
disposeAll() {
for (const contextId of this.timeouts.keys()) {
this.dispose(contextId);
}
}
forContext(contextId) {
return {
setTimeout: (handler, timeout, ...args) => this.setTimeout(contextId, handler, timeout, args),
setInterval: (handler, timeout, ...args) => this.setInterval(contextId, handler, timeout, args),
requestAnimationFrame: (callback) => this.requestAnimationFrame(contextId, callback),
dispose: () => this.dispose(contextId)
};
}
}
const safeParseUrl = (url, baseUrl) => {
try {
return new URL(url, baseUrl);
} catch {
return;
}
};
function isDefined(value) {
return value !== void 0;
}
function getStructuredClone() {
if (typeof globalThis !== "undefined" && globalThis.structuredClone) {
return [globalThis.structuredClone, true];
}
if (typeof global !== "undefined" && global.structuredClone) {
return [global.structuredClone, true];
}
if (typeof window !== "undefined" && window.structuredClone) {
return [window.structuredClone, true];
}
return [(i) => i ? JSON.parse(JSON.stringify(i)) : i, false];
}
const _structuredClone = getStructuredClone();
const structuredClone = _structuredClone[0];
_structuredClone[1];
const STRUCTURED_CLONE_OBJECT_PROTOTYPE = Object.getPrototypeOf(structuredClone({}));
const usedWarnings = /* @__PURE__ */ new Set();
function warnOnce(message) {
if (usedWarnings.has(message)) return;
usedWarnings.add(message);
console.warn(`[tldraw] ${message}`);
}
registerTldrawLibraryVersion(
"@tldraw/utils",
"3.6.1",
"esm"
);
var check = function (it) {
return it && it.Math === Math && it;
};
// https://github.com/zloirock/core-js/issues/86#issuecomment-115759028
var globalThis_1 =
// eslint-disable-next-line es/no-global-this -- safe
check(typeof globalThis == 'object' && globalThis) ||
check(typeof window == 'object' && window) ||
// eslint-disable-next-line no-restricted-globals -- safe
check(typeof self == 'object' && self) ||
check(typeof commonjsGlobal == 'object' && commonjsGlobal) ||
check(typeof commonjsGlobal == 'object' && commonjsGlobal) ||
// eslint-disable-next-line no-new-func -- fallback
(function () { return this; })() || Function('return this')();
var objectGetOwnPropertyDescriptor = {};
var fails$g = function (exec) {
try {
return !!exec();
} catch (error) {
return true;
}
};
var fails$f = fails$g;
// Detect IE8's incomplete defineProperty implementation
var descriptors = !fails$f(function () {
// eslint-disable-next-line es/no-object-defineproperty -- required for testing
return Object.defineProperty({}, 1, { get: function () { return 7; } })[1] !== 7;
});
var fails$e = fails$g;
var functionBindNative = !fails$e(function () {
// eslint-disable-next-line es/no-function-prototype-bind -- safe
var test = (function () { /* empty */ }).bind();
// eslint-disable-next-line no-prototype-builtins -- safe
return typeof test != 'function' || test.hasOwnProperty('prototype');
});
var NATIVE_BIND$3 = functionBindNative;
var call$c = Function.prototype.call;
// eslint-disable-next-line es/no-function-prototype-bind -- safe
var functionCall = NATIVE_BIND$3 ? call$c.bind(call$c) : function () {
return call$c.apply(call$c, arguments);
};
var objectPropertyIsEnumerable = {};
var $propertyIsEnumerable = {}.propertyIsEnumerable;
// eslint-disable-next-line es/no-object-getownpropertydescriptor -- safe
var getOwnPropertyDescriptor$1 = Object.getOwnPropertyDescriptor;
// Nashorn ~ JDK8 bug
var NASHORN_BUG = getOwnPropertyDescriptor$1 && !$propertyIsEnumerable.call({ 1: 2 }, 1);
// `Object.prototype.propertyIsEnumerable` method implementation
// https://tc39.es/ecma262/#sec-object.prototype.propertyisenumerable
objectPropertyIsEnumerable.f = NASHORN_BUG ? function propertyIsEnumerable(V) {
var descriptor = getOwnPropertyDescriptor$1(this, V);
return !!descriptor && descriptor.enumerable;
} : $propertyIsEnumerable;
var createPropertyDescriptor$2 = function (bitmap, value) {
return {
enumerable: !(bitmap & 1),
configurable: !(bitmap & 2),
writable: !(bitmap & 4),
value: value
};
};
var NATIVE_BIND$2 = functionBindNative;
var FunctionPrototype$2 = Function.prototype;
var call$b = FunctionPrototype$2.call;
// eslint-disable-next-line es/no-function-prototype-bind -- safe
var uncurryThisWithBind = NATIVE_BIND$2 && FunctionPrototype$2.bind.bind(call$b, call$b);
var functionUncurryThis = NATIVE_BIND$2 ? uncurryThisWithBind : function (fn) {
return function () {
return call$b.apply(fn, arguments);
};
};
var uncurryThis$i = functionUncurryThis;
var toString$7 = uncurryThis$i({}.toString);
var stringSlice$6 = uncurryThis$i(''.slice);
var classofRaw$2 = function (it) {
return stringSlice$6(toString$7(it), 8, -1);
};
var uncurryThis$h = functionUncurryThis;
var fails$d = fails$g;
var classof$6 = classofRaw$2;
var $Object$3 = Object;
var split = uncurryThis$h(''.split);
// fallback for non-array-like ES3 and non-enumerable old V8 strings
var indexedObject = fails$d(function () {
// throws an error in rhino, see https://github.com/mozilla/rhino/issues/346
// eslint-disable-next-line no-prototype-builtins -- safe
return !$Object$3('z').propertyIsEnumerable(0);
}) ? function (it) {
return classof$6(it) === 'String' ? split(it, '') : $Object$3(it);
} : $Object$3;
// we can't use just `it == null` since of `document.all` special case
// https://tc39.es/ecma262/#sec-IsHTMLDDA-internal-slot-aec
var isNullOrUndefined$2 = function (it) {
return it === null || it === undefined;
};
var isNullOrUndefined$1 = isNullOrUndefined$2;
var $TypeError$8 = TypeError;
// `RequireObjectCoercible` abstract operation
// https://tc39.es/ecma262/#sec-requireobjectcoercible
var requireObjectCoercible$6 = function (it) {
if (isNullOrUndefined$1(it)) throw new $TypeError$8("Can't call method on " + it);
return it;
};
// toObject with fallback for non-array-like ES3 strings
var IndexedObject = indexedObject;
var requireObjectCoercible$5 = requireObjectCoercible$6;
var toIndexedObject$4 = function (it) {
return IndexedObject(requireObjectCoercible$5(it));
};
// https://tc39.es/ecma262/#sec-IsHTMLDDA-internal-slot
var documentAll = typeof document == 'object' && document.all;
// `IsCallable` abstract operation
// https://tc39.es/ecma262/#sec-iscallable
// eslint-disable-next-line unicorn/no-typeof-undefined -- required for testing
var isCallable$f = typeof documentAll == 'undefined' && documentAll !== undefined ? function (argument) {
return typeof argument == 'function' || argument === documentAll;
} : function (argument) {
return typeof argument == 'function';
};
var isCallable$e = isCallable$f;
var isObject$8 = function (it) {
return typeof it == 'object' ? it !== null : isCallable$e(it);
};
var globalThis$g = globalThis_1;
var isCallable$d = isCallable$f;
var aFunction = function (argument) {
return isCallable$d(argument) ? argument : undefined;
};
var getBuiltIn$4 = function (namespace, method) {
return arguments.length < 2 ? aFunction(globalThis$g[namespace]) : globalThis$g[namespace] && globalThis$g[namespace][method];
};
var uncurryThis$g = functionUncurryThis;
var objectIsPrototypeOf = uncurryThis$g({}.isPrototypeOf);
var globalThis$f = globalThis_1;
var navigator$1 = globalThis$f.navigator;
var userAgent$1 = navigator$1 && navigator$1.userAgent;
var environmentUserAgent = userAgent$1 ? String(userAgent$1) : '';
var globalThis$e = globalThis_1;
var userAgent = environmentUserAgent;
var process$1 = globalThis$e.process;
var Deno = globalThis$e.Deno;
var versions = process$1 && process$1.versions || Deno && Deno.version;
var v8 = versions && versions.v8;
var match, version$1;
if (v8) {
match = v8.split('.');
// in old Chrome, versions of V8 isn't V8 = Chrome / 10
// but their correct versions are not interesting for us
version$1 = match[0] > 0 && match[0] < 4 ? 1 : +(match[0] + match[1]);
}
// BrowserFS NodeJS `process` polyfill incorrectly set `.v8` to `0.0`
// so check `userAgent` even if `.v8` exists, but 0
if (!version$1 && userAgent) {
match = userAgent.match(/Edge\/(\d+)/);
if (!match || match[1] >= 74) {
match = userAgent.match(/Chrome\/(\d+)/);
if (match) version$1 = +match[1];
}
}
var environmentV8Version = version$1;
/* eslint-disable es/no-symbol -- required for testing */
var V8_VERSION = environmentV8Version;
var fails$c = fails$g;
var globalThis$d = globalThis_1;
var $String$4 = globalThis$d.String;
// eslint-disable-next-line es/no-object-getownpropertysymbols -- required for testing
var symbolConstructorDetection = !!Object.getOwnPropertySymbols && !fails$c(function () {
var symbol = Symbol('symbol detection');
// Chrome 38 Symbol has incorrect toString conversion
// `get-own-property-symbols` polyfill symbols converted to object are not Symbol instances
// nb: Do not call `String` directly to avoid this being optimized out to `symbol+''` which will,
// of course, fail.
return !$String$4(symbol) || !(Object(symbol) instanceof Symbol) ||
// Chrome 38-40 symbols are not inherited from DOM collections prototypes to instances
!Symbol.sham && V8_VERSION && V8_VERSION < 41;
});
/* eslint-disable es/no-symbol -- required for testing */
var NATIVE_SYMBOL$1 = symbolConstructorDetection;
var useSymbolAsUid = NATIVE_SYMBOL$1 &&
!Symbol.sham &&
typeof Symbol.iterator == 'symbol';
var getBuiltIn$3 = getBuiltIn$4;
var isCallable$c = isCallable$f;
var isPrototypeOf$1 = objectIsPrototypeOf;
var USE_SYMBOL_AS_UID$1 = useSymbolAsUid;
var $Object$2 = Object;
var isSymbol$2 = USE_SYMBOL_AS_UID$1 ? function (it) {
return typeof it == 'symbol';
} : function (it) {
var $Symbol = getBuiltIn$3('Symbol');
return isCallable$c($Symbol) && isPrototypeOf$1($Symbol.prototype, $Object$2(it));
};
var $String$3 = String;
var tryToString$1 = function (argument) {
try {
return $String$3(argument);
} catch (error) {
return 'Object';
}
};
var isCallable$b = isCallable$f;
var tryToString = tryToString$1;
var $TypeError$7 = TypeError;
// `Assert: IsCallable(argument) is true`
var aCallable$3 = function (argument) {
if (isCallable$b(argument)) return argument;
throw new $TypeError$7(tryToString(argument) + ' is not a function');
};
var aCallable$2 = aCallable$3;
var isNullOrUndefined = isNullOrUndefined$2;
// `GetMethod` abstract operation
// https://tc39.es/ecma262/#sec-getmethod
var getMethod$4 = function (V, P) {
var func = V[P];
return isNullOrUndefined(func) ? undefined : aCallable$2(func);
};
var call$a = functionCall;
var isCallable$a = isCallable$f;
var isObject$7 = isObject$8;
var $TypeError$6 = TypeError;
// `OrdinaryToPrimitive` abstract operation
// https://tc39.es/ecma262/#sec-ordinarytoprimitive
var ordinaryToPrimitive$1 = function (input, pref) {
var fn, val;
if (pref === 'string' && isCallable$a(fn = input.toString) && !isObject$7(val = call$a(fn, input))) return val;
if (isCallable$a(fn = input.valueOf) && !isObject$7(val = call$a(fn, input))) return val;
if (pref !== 'string' && isCallable$a(fn = input.toString) && !isObject$7(val = call$a(fn, input))) return val;
throw new $TypeError$6("Can't convert object to primitive value");
};
var sharedStore = {exports: {}};
var globalThis$c = globalThis_1;
// eslint-disable-next-line es/no-object-defineproperty -- safe
var defineProperty$2 = Object.defineProperty;
var defineGlobalProperty$3 = function (key, value) {
try {
defineProperty$2(globalThis$c, key, { value: value, configurable: true, writable: true });
} catch (error) {
globalThis$c[key] = value;
} return value;
};
var globalThis$b = globalThis_1;
var defineGlobalProperty$2 = defineGlobalProperty$3;
var SHARED = '__core-js_shared__';
var store$3 = sharedStore.exports = globalThis$b[SHARED] || defineGlobalProperty$2(SHARED, {});
(store$3.versions || (store$3.versions = [])).push({
version: '3.45.1',
mode: 'global',
copyright: '© 2014-2025 Denis Pushkarev (zloirock.ru)',
license: 'https://github.com/zloirock/core-js/blob/v3.45.1/LICENSE',
source: 'https://github.com/zloirock/core-js'
});
var sharedStoreExports = sharedStore.exports;
var store$2 = sharedStoreExports;
var shared$4 = function (key, value) {
return store$2[key] || (store$2[key] = value || {});
};
var requireObjectCoercible$4 = requireObjectCoercible$6;
var $Object$1 = Object;
// `ToObject` abstract operation
// https://tc39.es/ecma262/#sec-toobject
var toObject$5 = function (argument) {
return $Object$1(requireObjectCoercible$4(argument));
};
var uncurryThis$f = functionUncurryThis;
var toObject$4 = toObject$5;
var hasOwnProperty = uncurryThis$f({}.hasOwnProperty);
// `HasOwnProperty` abstract operation
// https://tc39.es/ecma262/#sec-hasownproperty
// eslint-disable-next-line es/no-object-hasown -- safe
var hasOwnProperty_1 = Object.hasOwn || function hasOwn(it, key) {
return hasOwnProperty(toObject$4(it), key);
};
var uncurryThis$e = functionUncurryThis;
var id = 0;
var postfix = Math.random();
var toString$6 = uncurryThis$e(1.1.toString);
var uid$2 = function (key) {
return 'Symbol(' + (key === undefined ? '' : key) + ')_' + toString$6(++id + postfix, 36);
};
var globalThis$a = globalThis_1;
var shared$3 = shared$4;
var hasOwn$7 = hasOwnProperty_1;
var uid$1 = uid$2;
var NATIVE_SYMBOL = symbolConstructorDetection;
var USE_SYMBOL_AS_UID = useSymbolAsUid;
var Symbol$1 = globalThis$a.Symbol;
var WellKnownSymbolsStore = shared$3('wks');
var createWellKnownSymbol = USE_SYMBOL_AS_UID ? Symbol$1['for'] || Symbol$1 : Symbol$1 && Symbol$1.withoutSetter || uid$1;
var wellKnownSymbol$9 = function (name) {
if (!hasOwn$7(WellKnownSymbolsStore, name)) {
WellKnownSymbolsStore[name] = NATIVE_SYMBOL && hasOwn$7(Symbol$1, name)
? Symbol$1[name]
: createWellKnownSymbol('Symbol.' + name);
} return WellKnownSymbolsStore[name];
};
var call$9 = functionCall;
var isObject$6 = isObject$8;
var isSymbol$1 = isSymbol$2;
var getMethod$3 = getMethod$4;
var ordinaryToPrimitive = ordinaryToPrimitive$1;
var wellKnownSymbol$8 = wellKnownSymbol$9;
var $TypeError$5 = TypeError;
var TO_PRIMITIVE = wellKnownSymbol$8('toPrimitive');
// `ToPrimitive` abstract operation
// https://tc39.es/ecma262/#sec-toprimitive
var toPrimitive$1 = function (input, pref) {
if (!isObject$6(input) || isSymbol$1(input)) return input;
var exoticToPrim = getMethod$3(input, TO_PRIMITIVE);
var result;
if (exoticToPrim) {
if (pref === undefined) pref = 'default';
result = call$9(exoticToPrim, input, pref);
if (!isObject$6(result) || isSymbol$1(result)) return result;
throw new $TypeError$5("Can't convert object to primitive value");
}
if (pref === undefined) pref = 'number';
return ordinaryToPrimitive(input, pref);
};
var toPrimitive = toPrimitive$1;
var isSymbol = isSymbol$2;
// `ToPropertyKey` abstract operation
// https://tc39.es/ecma262/#sec-topropertykey
var toPropertyKey$2 = function (argument) {
var key = toPrimitive(argument, 'string');
return isSymbol(key) ? key : key + '';
};
var globalThis$9 = globalThis_1;
var isObject$5 = isObject$8;
var document$1 = globalThis$9.document;
// typeof document.createElement is 'object' in old IE
var EXISTS$1 = isObject$5(document$1) && isObject$5(document$1.createElement);
var documentCreateElement$1 = function (it) {
return EXISTS$1 ? document$1.createElement(it) : {};
};
var DESCRIPTORS$7 = descriptors;
var fails$b = fails$g;
var createElement = documentCreateElement$1;
// Thanks to IE8 for its funny defineProperty
var ie8DomDefine = !DESCRIPTORS$7 && !fails$b(function () {
// eslint-disable-next-line es/no-object-defineproperty -- required for testing
return Object.defineProperty(createElement('div'), 'a', {
get: function () { return 7; }
}).a !== 7;
});
var DESCRIPTORS$6 = descriptors;
var call$8 = functionCall;
var propertyIsEnumerableModule = objectPropertyIsEnumerable;
var createPropertyDescriptor$1 = createPropertyDescriptor$2;
var toIndexedObject$3 = toIndexedObject$4;
var toPropertyKey$1 = toPropertyKey$2;
var hasOwn$6 = hasOwnProperty_1;
var IE8_DOM_DEFINE$1 = ie8DomDefine;
// eslint-disable-next-line es/no-object-getownpropertydescriptor -- safe
var $getOwnPropertyDescriptor$1 = Object.getOwnPropertyDescriptor;
// `Object.getOwnPropertyDescriptor` method
// https://tc39.es/ecma262/#sec-object.getownpropertydescriptor
objectGetOwnPropertyDescriptor.f = DESCRIPTORS$6 ? $getOwnPropertyDescriptor$1 : function getOwnPropertyDescriptor(O, P) {
O = toIndexedObject$3(O);
P = toPropertyKey$1(P);
if (IE8_DOM_DEFINE$1) try {
return $getOwnPropertyDescriptor$1(O, P);
} catch (error) { /* empty */ }
if (hasOwn$6(O, P)) return createPropertyDescriptor$1(!call$8(propertyIsEnumerableModule.f, O, P), O[P]);
};
var objectDefineProperty = {};
var DESCRIPTORS$5 = descriptors;
var fails$a = fails$g;
// V8 ~ Chrome 36-
// https://bugs.chromium.org/p/v8/issues/detail?id=3334
var v8PrototypeDefineBug = DESCRIPTORS$5 && fails$a(function () {
// eslint-disable-next-line es/no-object-defineproperty -- required for testing
return Object.defineProperty(function () { /* empty */ }, 'prototype', {
value: 42,
writable: false
}).prototype !== 42;
});
var isObject$4 = isObject$8;
var $String$2 = String;
var $TypeError$4 = TypeError;
// `Assert: Type(argument) is Object`
var anObject$7 = function (argument) {
if (isObject$4(argument)) return argument;
throw new $TypeError$4($String$2(argument) + ' is not an object');
};
var DESCRIPTORS$4 = descriptors;
var IE8_DOM_DEFINE = ie8DomDefine;
var V8_PROTOTYPE_DEFINE_BUG$1 = v8PrototypeDefineBug;
var anObject$6 = anObject$7;
var toPropertyKey = toPropertyKey$2;
var $TypeError$3 = TypeError;
// eslint-disable-next-line es/no-object-defineproperty -- safe
var $defineProperty = Object.defineProperty;
// eslint-disable-next-line es/no-object-getownpropertydescriptor -- safe
var $getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
var ENUMERABLE = 'enumerable';
var CONFIGURABLE$1 = 'configurable';
var WRITABLE = 'writable';
// `Object.defineProperty` method
// https://tc39.es/ecma262/#sec-object.defineproperty
objectDefineProperty.f = DESCRIPTORS$4 ? V8_PROTOTYPE_DEFINE_BUG$1 ? function defineProperty(O, P, Attributes) {
anObject$6(O);
P = toPropertyKey(P);
anObject$6(Attributes);
if (typeof O === 'function' && P === 'prototype' && 'value' in Attributes && WRITABLE in Attributes && !Attributes[WRITABLE]) {
var current = $getOwnPropertyDescriptor(O, P);
if (current && current[WRITABLE]) {
O[P] = Attributes.value;
Attributes = {
configurable: CONFIGURABLE$1 in Attributes ? Attributes[CONFIGURABLE$1] : current[CONFIGURABLE$1],
enumerable: ENUMERABLE in Attributes ? Attributes[ENUMERABLE] : current[ENUMERABLE],
writable: false
};
}
} return $defineProperty(O, P, Attributes);
} : $defineProperty : function defineProperty(O, P, Attributes) {
anObject$6(O);
P = toPropertyKey(P);
anObject$6(Attributes);
if (IE8_DOM_DEFINE) try {
return $defineProperty(O, P, Attributes);
} catch (error) { /* empty */ }
if ('get' in Attributes || 'set' in Attributes) throw new $TypeError$3('Accessors not supported');
if ('value' in Attributes) O[P] = Attributes.value;
return O;
};
var DESCRIPTORS$3 = descriptors;
var definePropertyModule$3 = objectDefineProperty;
var createPropertyDescriptor = createPropertyDescriptor$2;
var createNonEnumerableProperty$3 = DESCRIPTORS$3 ? function (object, key, value) {
return definePropertyModule$3.f(object, key, createPropertyDescriptor(1, value));
} : function (object, key, value) {
object[key] = value;
return object;
};
var makeBuiltIn$2 = {exports: {}};
var DESCRIPTORS$2 = descriptors;
var hasOwn$5 = hasOwnProperty_1;
var FunctionPrototype$1 = Function.prototype;
// eslint-disable-next-line es/no-object-getownpropertydescriptor -- safe
var getDescriptor = DESCRIPTORS$2 && Object.getOwnPropertyDescriptor;
var EXISTS = hasOwn$5(FunctionPrototype$1, 'name');
var CONFIGURABLE = EXISTS && (!DESCRIPTORS$2 || (DESCRIPTORS$2 && getDescriptor(FunctionPrototype$1, 'name').configurable));
var functionName = {
CONFIGURABLE: CONFIGURABLE
};
var uncurryThis$d = functionUncurryThis;
var isCallable$9 = isCallable$f;
var store$1 = sharedStoreExports;
var functionToString = uncurryThis$d(Function.toString);
// this helper broken in `core-js@3.4.1-3.4.4`, so we can't use `shared` helper
if (!isCallable$9(store$1.inspectSource)) {
store$1.inspectSource = function (it) {
return functionToString(it);
};
}
var inspectSource$2 = store$1.inspectSource;
var globalThis$8 = globalThis_1;
var isCallable$8 = isCallable$f;
var WeakMap$2 = globalThis$8.WeakMap;
var weakMapBasicDetection = isCallable$8(WeakMap$2) && /native code/.test(String(WeakMap$2));
var shared$2 = shared$4;
var uid = uid$2;
var keys = shared$2('keys');
var sharedKey$2 = function (key) {
return keys[key] || (keys[key] = uid(key));
};
var hiddenKeys$4 = {};
var NATIVE_WEAK_MAP = weakMapBasicDetection;
var globalThis$7 = globalThis_1;
var createNonEnumerableProperty$2 = createNonEnumerableProperty$3;
var hasOwn$4 = hasOwnProperty_1;
var shared$1 = sharedStoreExports;
var sharedKey$1 = sharedKey$2;
var hiddenKeys$3 = hiddenKeys$4;
var OBJECT_ALREADY_INITIALIZED = 'Object already initialized';
var TypeError$1 = globalThis$7.TypeError;
var WeakMap$1 = globalThis$7.WeakMap;
var set, get, has;
var enforce = function (it) {
return has(it) ? get(it) : set(it, {});
};
if (NATIVE_WEAK_MAP || shared$1.state) {
var store = shared$1.state || (shared$1.state = new WeakMap$1());
/* eslint-disable no-self-assign -- prototype methods protection */
store.get = store.get;
store.has = store.has;
store.set = store.set;
/* eslint-enable no-self-assign -- prototype methods protection */
set = function (it, metadata) {
if (store.has(it)) throw new TypeError$1(OBJECT_ALREADY_INITIALIZED);
metadata.facade = it;
store.set(it, metadata);
return metadata;
};
get = function (it) {
return store.get(it) || {};
};
has = function (it) {
return store.has(it);
};
} else {
var STATE = sharedKey$1('state');
hiddenKeys$3[STATE] = true;
set = function (it, metadata) {
if (hasOwn$4(it, STATE)) throw new TypeError$1(OBJECT_ALREADY_INITIALIZED);
metadata.facade = it;
createNonEnumerableProperty$2(it, STATE, metadata);
return metadata;
};
get = function (it) {
return hasOwn$4(it, STATE) ? it[STATE] : {};
};
has = function (it) {
return hasOwn$4(it, STATE);
};
}
var internalState = {
get: get,
enforce: enforce};
var uncurryThis$c = functionUncurryThis;
var fails$9 = fails$g;
var isCallable$7 = isCallable$f;
var hasOwn$3 = hasOwnProperty_1;
var DESCRIPTORS$1 = descriptors;
var CONFIGURABLE_FUNCTION_NAME = functionName.CONFIGURABLE;
var inspectSource$1 = inspectSource$2;
var InternalStateModule = internalState;
var enforceInternalState = InternalStateModule.enforce;
var getInternalState$1 = InternalStateModule.get;
var $String$1 = String;
// eslint-disable-next-line es/no-object-defineproperty -- safe
var defineProperty$1 = Object.defineProperty;
var stringSlice$5 = uncurryThis$c(''.slice);
var replace$3 = uncurryThis$c(''.replace);
var join = uncurryThis$c([].join);
var CONFIGURABLE_LENGTH = DESCRIPTORS$1 && !fails$9(function () {
return defineProperty$1(function () { /* empty */ }, 'length', { value: 8 }).length !== 8;
});
var TEMPLATE = String(String).split('String');
var makeBuiltIn$1 = makeBuiltIn$2.exports = function (value, name, options) {
if (stringSlice$5($String$1(name), 0, 7) === 'Symbol(') {
name = '[' + replace$3($String$1(name), /^Symbol\(([^)]*)\).*$/, '$1') + ']';
}
if (options && options.getter) name = 'get ' + name;
if (options && options.setter) name = 'set ' + name;
if (!hasOwn$3(value, 'name') || (CONFIGURABLE_FUNCTION_NAME && value.name !== name)) {
if (DESCRIPTORS$1) defineProperty$1(value, 'name', { value: name, configurable: true });
else value.name = name;
}
if (CONFIGURABLE_LENGTH && options && hasOwn$3(options, 'arity') && value.length !== options.arity) {
defineProperty$1(value, 'length', { value: options.arity });
}
try {
if (options && hasOwn$3(options, 'constructor') && options.constructor) {
if (DESCRIPTORS$1) defineProperty$1(value, 'prototype', { writable: false });
// in V8 ~ Chrome 53, prototypes of some methods, like `Array.prototype.values`, are non-writable
} else if (value.prototype) value.prototype = undefined;
} catch (error) { /* empty */ }
var state = enforceInternalState(value);
if (!hasOwn$3(state, 'source')) {
state.source = join(TEMPLATE, typeof name == 'string' ? name : '');
} return value;
};
// add fake Function#toString for correct work wrapped methods / constructors with methods like LoDash isNative
// eslint-disable-next-line no-extend-native -- required
Function.prototype.toString = makeBuiltIn$1(function toString() {
return isCallable$7(this) && getInternalState$1(this).source || inspectSource$1(this);
}, 'toString');
var makeBuiltInExports = makeBuiltIn$2.exports;
var isCallable$6 = isCallable$f;
var definePropertyModule$2 = objectDefineProperty;
var makeBuiltIn = makeBuiltInExports;
var defineGlobalProperty$1 = defineGlobalProperty$3;
var defineBuiltIn$2 = function (O, key, value, options) {
if (!options) options = {};
var simple = options.enumerable;
var name = options.name !== undefined ? options.name : key;
if (isCallable$6(value)) makeBuiltIn(value, name, options);
if (options.global) {
if (simple) O[key] = value;
else defineGlobalProperty$1(key, value);
} else {
try {
if (!options.unsafe) delete O[key];
else if (O[key]) simple = true;
} catch (error) { /* empty */ }
if (simple) O[key] = value;
else definePropertyModule$2.f(O, key, {
value: value,
enumerable: false,
configurable: !options.nonConfigurable,
writable: !options.nonWritable
});
} return O;
};
var objectGetOwnPropertyNames = {};
var ceil = Math.ceil;
var floor$1 = Math.floor;
// `Math.trunc` method
// https://tc39.es/ecma262/#sec-math.trunc
// eslint-disable-next-line es/no-math-trunc -- safe
var mathTrunc = Math.trunc || function trunc(x) {
var n = +x;
return (n > 0 ? floor$1 : ceil)(n);
};
var trunc = mathTrunc;
// `ToIntegerOrInfinity` abstract operation
// https://tc39.es/ecma262/#sec-tointegerorinfinity
var toIntegerOrInfinity$7 = function (argument) {
var number = +argument;
// eslint-disable-next-line no-self-compare -- NaN check
return number !== number || number === 0 ? 0 : trunc(number);
};
var toIntegerOrInfinity$6 = toIntegerOrInfinity$7;
var max$2 = Math.max;
var min$3 = Math.min;
// Helper for a popular repeating case of the spec:
// Let integer be ? ToInteger(index).
// If integer < 0, let result be max((length + integer), 0); else let result be min(integer, length).
var toAbsoluteIndex$1 = function (index, length) {
var integer = toIntegerOrInfinity$6(index);
return integer < 0 ? max$2(integer + length, 0) : min$3(integer, length);
};
var toIntegerOrInfinity$5 = toIntegerOrInfinity$7;
var min$2 = Math.min;
// `ToLength` abstract operation
// https://tc39.es/ecma262/#sec-tolength
var toLength$2 = function (argument) {
var len = toIntegerOrInfinity$5(argument);
return len > 0 ? min$2(len, 0x1FFFFFFFFFFFFF) : 0; // 2 ** 53 - 1 == 9007199254740991
};
var toLength$1 = toLength$2;
// `LengthOfArrayLike` abstract operation
// https://tc39.es/ecma262/#sec-lengthofarraylike
var lengthOfArrayLike$5 = function (obj) {
return toLength$1(obj.length);
};
var toIndexedObject$2 = toIndexedObject$4;
var toAbsoluteIndex = toAbsoluteIndex$1;
var lengthOfArrayLike$4 = lengthOfArrayLike$5;
// `Array.prototype.{ indexOf, includes }` methods implementation
var createMethod$1 = function (IS_INCLUDES) {
return function ($this, el, fromIndex) {
var O = toIndexedObject$2($this);
var length = lengthOfArrayLike$4(O);
if (length === 0) return !IS_INCLUDES && -1;
var index = toAbsoluteIndex(fromIndex, length);
var value;
// Array#includes uses SameValueZero equality algorithm
// eslint-disable-next-line no-self-compare -- NaN check
if (IS_INCLUDES && el !== el) while (length > index) {
value = O[index++];
// eslint-disable-next-line no-self-compare -- NaN check
if (value !== value) return true;
// Array#indexOf ignores holes, Array#includes - not
} else for (;length > index; index++) {
if ((IS_INCLUDES || index in O) && O[index] === el) return IS_INCLUDES || index || 0;
} return !IS_INCLUDES && -1;
};
};
var arrayIncludes = {
// `Array.prototype.indexOf` method
// https://tc39.es/ecma262/#sec-array.prototype.indexof
indexOf: createMethod$1(false)
};
var uncurryThis$b = functionUncurryThis;
var hasOwn$2 = hasOwnProperty_1;
var toIndexedObject$1 = toIndexedObject$4;
var indexOf$2 = arrayIncludes.indexOf;
var hiddenKeys$2 = hiddenKeys$4;
var push$1 = uncurryThis$b([].push);
var objectKeysInternal = function (object, names) {
var O = toIndexedObject$1(object);
var i = 0;
var result = [];
var key;
for (key in O) !hasOwn$2(hiddenKeys$2, key) && hasOwn$2(O, key) && push$1(result, key);
// Don't enum bug & hidden keys
while (names.length > i) if (hasOwn$2(O, key = names[i++])) {
~indexOf$2(result, key) || push$1(result, key);
}
return result;
};
// IE8- don't enum bug keys
var enumBugKeys$3 = [
'constructor',
'hasOwnProperty',
'isPrototypeOf',
'propertyIsEnumerable',
'toLocaleString',
'toString',
'valueOf'
];
var internalObjectKeys$1 = objectKeysInternal;
var enumBugKeys$2 = enumBugKeys$3;
var hiddenKeys$1 = enumBugKeys$2.concat('length', 'prototype');
// `Object.getOwnPropertyNames` method
// https://tc39.es/ecma262/#sec-object.getownpropertynames
// eslint-disable-next-line es/no-object-getownpropertynames -- safe
objectGetOwnPropertyNames.f = Object.getOwnPropertyNames || function getOwnPropertyNames(O) {
return internalObjectKeys$1(O, hiddenKeys$1);
};
var objectGetOwnPropertySymbols = {};
// eslint-disable-next-line es/no-object-getownpropertysymbols -- safe
objectGetOwnPropertySymbols.f = Object.getOwnPropertySymbols;
var getBuiltIn$2 = getBuiltIn$4;
var uncurryThis$a = functionUncurryThis;
var getOwnPropertyNamesModule = objectGetOwnPropertyNames;
var getOwnPropertySymbolsModule = objectGetOwnPropertySymbols;
var anObject$5 = anObject$7;
var concat$1 = uncurryThis$a([].concat);
// all object keys, includes non-enumerable and symbols
var ownKeys$3 = getBuiltIn$2('Reflect', 'ownKeys') || function ownKeys(it) {
var keys = getOwnPropertyNamesModule.f(anObject$5(it));
var getOwnPropertySymbols = getOwnPropertySymbolsModule.f;
return getOwnPropertySymbols ? concat$1(keys, getOwnPropertySymbols(it)) : keys;
};
var hasOwn$1 = hasOwnProperty_1;
var ownKeys$2 = ownKeys$3;
var getOwnPropertyDescriptorModule = objectGetOwnPropertyDescriptor;
var definePropertyModule$1 = objectDefineProperty;
var copyConstructorProperties$1 = function (target, source, exceptions) {
var keys = ownKeys$2(source);
var defineProperty = definePropertyModule$1.f;
var getOwnPropertyDescriptor = getOwnPropertyDescriptorModule.f;
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
if (!hasOwn$1(target, key) && !(exceptions && hasOwn$1(exceptions, key))) {
defineProperty(target, key, getOwnPropertyDescriptor(source, key));
}
}
};
var fails$8 = fails$g;
var isCallable$5 = isCallable$f;
var replacement = /#|\.prototype\./;
var isForced$1 = function (feature, detection) {
var value = data[normalize(feature)];
return value === POLYFILL ? true
: value === NATIVE ? false
: isCallable$5(detection) ? fails$8(detection)
: !!detection;
};
var normalize = isForced$1.normalize = function (string) {
return String(string).replace(replacement, '.').toLowerCase();
};
var data = isForced$1.data = {};
var NATIVE = isForced$1.NATIVE = 'N';
var POLYFILL = isForced$1.POLYFILL = 'P';
var isForced_1 = isForced$1;
var globalThis$6 = globalThis_1;
var getOwnPropertyDescriptor = objectGetOwnPropertyDescriptor.f;
var createNonEnumerableProperty$1 = createNonEnumerableProperty$3;
var defineBuiltIn$1 = defineBuiltIn$2;
var defineGlobalProperty = defineGlobalProperty$3;
var copyConstructorProperties = copyConstructorProperties$1;
var isForced = isForced_1;
/*
options.target - name of the target object
options.global - target is the global object
options.stat - export as static methods of target
options.proto - export as prototype methods of target
options.real - real prototype method for the `pure` version
options.forced - export even if the native feature is available
options.bind - bind methods to the target, required for the `pure` version
options.wrap - wrap constructors to preventing global pollution, required for the `pure` version
options.unsafe - use the simple assignment of property instead of delete + defineProperty
options.sham - add a flag to not completely full polyfills
options.enumerable - export as enumerable property
options.dontCallGetSet - prevent calling a getter on target
options.name - the .name of the function if it does not match the key
*/
var _export = function (options, source) {
var TARGET = options.target;
var GLOBAL = options.global;
var STATIC = options.stat;
var FORCED, target, key, targetProperty, sourceProperty, descriptor;
if (GLOBAL) {
target = globalThis$6;
} else if (STATIC) {
target = globalThis$6[TARGET] || defineGlobalProperty(TARGET, {});
} else {
target = globalThis$6[TARGET] && globalThis$6[TARGET].prototype;
}
if (target) for (key in source) {
sourceProperty = source[key];
if (options.dontCallGetSet) {
descriptor = getOwnPropertyDescriptor(target, key);
targetProperty = descriptor && descriptor.value;
} else targetProperty = target[key];
FORCED = isForced(GLOBAL ? key : TARGET + (STATIC ? '.' : '#') + key, options.forced);
// contained in target
if (!FORCED && targetProperty !== undefined) {
if (typeof sourceProperty == typeof targetProperty) continue;
copyConstructorProperties(sourceProperty, targetProperty);
}
// add a flag to not completely full polyfills
if (options.sham || (targetProperty && targetProperty.sham)) {
createNonEnumerableProperty$1(sourceProperty, 'sham', true);
}
defineBuiltIn$1(target, key, sourceProperty, options);
}
};
var objectDefineProperties = {};
var internalObjectKeys = objectKeysInternal;
var enumBugKeys$1 = enumBugKeys$3;
// `Object.keys` method
// https://tc39.es/ecma262/#sec-object.keys
// eslint-disable-next-line es/no-object-keys -- safe
var objectKeys$1 = Object.keys || function keys(O) {
return internalObjectKeys(O, enumBugKeys$1);
};
var DESCRIPTORS = descriptors;
var V8_PROTOTYPE_DEFINE_BUG = v8PrototypeDefineBug;
var definePropertyModule = objectDefineProperty;
var anObject$4 = anObject$7;
var toIndexedObject = toIndexedObject$4;
var objectKeys = objectKeys$1;
// `Object.defineProperties` method
// https://tc39.es/ecma262/#sec-object.defineproperties
// eslint-disable-next-line es/no-object-defineproperties -- safe
objectDefineProperties.f = DESCRIPTORS && !V8_PROTOTYPE_DEFINE_BUG ? Object.defineProperties : function defineProperties(O, Properties) {
anObject$4(O);
var props = toIndexedObject(Properties);
var keys = objectKeys(Properties);
var length = keys.length;
var index = 0;
var key;
while (length > index) definePropertyModule.f(O, key = keys[index++], props[key]);
return O;
};
var getBuiltIn$1 = getBuiltIn$4;
var html$1 = getBuiltIn$1('document', 'documentElement');
/* global ActiveXObject -- old IE, WSH */
var anObject$3 = anObject$7;
var definePropertiesModule = objectDefineProperties;
var enumBugKeys = enumBugKeys$3;
var hiddenKeys = hiddenKeys$4;
var html = html$1;
var documentCreateElement = documentCreateElement$1;
var sharedKey = sharedKey$2;
var GT = '>';
var LT = '<';
var PROTOTYPE = 'prototype';
var SCRIPT = 'script';
var IE_PROTO = sharedKey('IE_PROTO');
var EmptyConstructor = function () { /* empty */ };
var scriptTag = function (content) {
return LT + SCRIPT + GT + content + LT + '/' + SCRIPT + GT;
};
// Create object with fake `null` prototype: use ActiveX Object with cleared prototype
var NullProtoObjectViaActiveX = function (activeXDocument) {
activeXDocument.write(scriptTag(''));
activeXDocument.close();
var temp = activeXDocument.parentWindow.Object;
// eslint-disable-next-line no-useless-assignment -- avoid memory leak
activeXDocument = null;
return temp;
};
// Create object with fake `null` prototype: use iframe Object with cleared prototype
var NullProtoObjectViaIFrame = function () {
// Thrash, waste and sodomy: IE GC bug
var iframe = documentCreateElement('iframe');
var JS = 'java' + SCRIPT + ':';
var iframeDocument;
iframe.style.display = 'none';
html.appendChild(iframe);
// https://github.com/zloirock/core-js/issues/475
iframe.src = String(JS);
iframeDocument = iframe.contentWindow.document;
iframeDocument.open();
iframeDocument.write(scriptTag('document.F=Object'));
iframeDocument.close();
return iframeDocument.F;
};
// Check for document.domain and active x support
// No need to use active x approach when document.domain is not set
// see https://github.com/es-shims/es5-shim/issues/150
// variation of https://github.com/kitcambridge/es5-shim/commit/4f738ac066346
// avoid IE GC bug
var activeXDocument;
var NullProtoObject = function () {
try {
activeXDocument = new ActiveXObject('htmlfile');
} catch (error) { /* ignore */ }
NullProtoObject = typeof document != 'undefined'
? document.domain && activeXDocument
? NullProtoObjectViaActiveX(activeXDocument) // old IE
: NullProtoObjectViaIFrame()
: NullProtoObjectViaActiveX(activeXDocument); // WSH
var length = enumBugKeys.length;
while (length--) delete NullProtoObject[PROTOTYPE][enumBugKeys[length]];
return NullProtoObject();
};
hiddenKeys[IE_PROTO] = true;
// `Object.create` method
// https://tc39.es/ecma262/#sec-object.create
// eslint-disable-next-line es/no-object-create -- safe
var objectCreate = Object.create || function create(O, Properties) {
var result;
if (O !== null) {
EmptyConstructor[PROTOTYPE] = anObject$3(O);
result = new EmptyConstructor();
EmptyConstructor[PROTOTYPE] = null;
// add "__proto__" for Object.getPrototypeOf polyfill
result[IE_PROTO] = O;
} else result = NullProtoObject();
return Properties === undefined ? result : definePropertiesModule.f(result, Properties);
};
var wellKnownSymbol$7 = wellKnownSymbol$9;
var create$1 = objectCreate;
var defineProperty = objectDefineProperty.f;
var UNSCOPABLES = wellKnownSymbol$7('unscopables');
var ArrayPrototype = Array.prototype;
// Array.prototype[@@unscopables]
// https://tc39.es/ecma262/#sec-array.prototype-@@unscopables
if (ArrayPrototype[UNSCOPABLES] === undefined) {
defineProperty(ArrayPrototype, UNSCOPABLES, {
configurable: true,
value: create$1(null)
});
}
// add a key to Array.prototype[@@unscopables]
var addToUnscopables$3 = function (key) {
ArrayPrototype[UNSCOPABLES][key] = true;
};
var $$5 = _export;
var toObject$3 = toObject$5;
var lengthOfArrayLike$3 = lengthOfArrayLike$5;
var toIntegerOrInfinity$4 = toIntegerOrInfinity$7;
var addToUnscopables$2 = addToUnscopables$3;
// `Array.prototype.at` method
// https://tc39.es/ecma262/#sec-array.prototype.at
$$5({ target: 'Array', proto: true }, {
at: function at(index) {
var O = toObject$3(this);
var len = lengthOfArrayLike$3(O);
var relativeIndex = toIntegerOrInfinity$4(index);
var k = relativeIndex >= 0 ? relativeIndex : len + relativeIndex;
return (k < 0 || k >= len) ? undefined : O[k];
}
});
addToUnscopables$2('at');
var globalThis$5 = globalThis_1;
var uncurryThis$9 = functionUncurryThis;
var entryUnbind$5 = function (CONSTRUCTOR, METHOD) {
return uncurryThis$9(globalThis$5[CONSTRUCTOR].prototype[METHOD]);
};
var entryUnbind$4 = entryUnbind$5;
entryUnbind$4('Array', 'at');
var classof$5 = classofRaw$2;
// `IsArray` abstract operation
// https://tc39.es/ecma262/#sec-isarray
// eslint-disable-next-line es/no-array-isarray -- safe
var isArray$2 = Array.isArray || function isArray(argument) {
return classof$5(argument) === 'Array';
};
var $TypeError$2 = TypeError;
var MAX_SAFE_INTEGER = 0x1FFFFFFFFFFFFF; // 2 ** 53 - 1 == 9007199254740991
var doesNotExceedSafeInteger$1 = function (it) {
if (it > MAX_SAFE_INTEGER) throw $TypeError$2('Maximum allowed index exceeded');
return it;
};
var classofRaw$1 = classofRaw$2;
var uncurryThis$8 = functionUncurryThis;
var functionUncurryThisClause = function (fn) {
// Nashorn bug:
// https://github.com/zloirock/core-js/issues/1128
// https://github.com/zloirock/core-js/issues/1130
if (classofRaw$1(fn) === 'Function') return uncurryThis$8(fn);
};
var uncurryThis$7 = functionUncurryThisClause;
var aCallable$1 = aCallable$3;
var NATIVE_BIND$1 = functionBindNative;
var bind$1 = uncurryThis$7(uncurryThis$7.bind);
// optional / simple context binding
var functionBindContext = function (fn, that) {
aCallable$1(fn);
return that === undefined ? fn : NATIVE_BIND$1 ? bind$1(fn, that) : function (/* ...args */) {
return fn.apply(that, arguments);
};
};
var isArray$1 = isArray$2;
var lengthOfArrayLike$2 = lengthOfArrayLike$5;
var doesNotExceedSafeInteger = doesNotExceedSafeInteger$1;
var bind = functionBindContext;
// `FlattenIntoArray` abstract operation
// https://tc39.es/ecma262/#sec-flattenintoarray
var flattenIntoArray$2 = function (target, original, source, sourceLen, start, depth, mapper, thisArg) {
var targetIndex = start;
var sourceIndex = 0;
var mapFn = mapper ? bind(mapper, thisArg) : false;
var element, elementLen;
while (sourceIndex < sourceLen) {
if (sourceIndex in source) {
element = mapFn ? mapFn(source[sourceIndex], sourceIndex, original) : source[sourceIndex];
if (depth > 0 && isArray$1(element)) {
elementLen = lengthOfArrayLike$2(element);
targetIndex = flattenIntoArray$2(target, original, element, elementLen, targetIndex, depth - 1) - 1;
} else {
doesNotExceedSafeInteger(targetIndex + 1);
target[targetIndex] = element;
}
targetIndex++;
}
sourceIndex++;
}
return targetIndex;
};
var flattenIntoArray_1 = flattenIntoArray$2;
var wellKnownSymbol$6 = wellKnownSymbol$9;
var TO_STRING_TAG$1 = wellKnownSymbol$6('toStringTag');
var test = {};
test[TO_STRING_TAG$1] = 'z';
var toStringTagSupport = String(test) === '[object z]';
var TO_STRING_TAG_SUPPORT = toStringTagSupport;
var isCallable$4 = isCallable$f;
var classofRaw = classofRaw$2;
var wellKnownSymbol$5 = wellKnownSymbol$9;
var TO_STRING_TAG = wellKnownSymbol$5('toStringTag');
var $Object = Object;
// ES3 wrong here
var CORRECT_ARGUMENTS = classofRaw(function () { return arguments; }()) === 'Arguments';
// fallback for IE11 Script Access Denied error
var tryGet = function (it, key) {
try {
return it[key];
} catch (error) { /* empty */ }
};
// getting tag from ES6+ `Object.prototype.toString`
var classof$4 = TO_STRING_TAG_SUPPORT ? classofRaw : function (it) {
var O, tag, result;
return it === undefined ? 'Undefined' : it === null ? 'Null'
// @@toStringTag case
: typeof (tag = tryGet(O = $Object(it), TO_STRING_TAG)) == 'string' ? tag
// builtinTag case
: CORRECT_ARGUMENTS ? classofRaw(O)
// ES3 arguments fallback
: (result = classofRaw(O)) === 'Object' && isCallable$4(O.callee) ? 'Arguments' : result;
};
var uncurryThis$6 = functionUncurryThis;
var fails$7 = fails$g;
var isCallable$3 = isCallable$f;
var classof$3 = classof$4;
var getBuiltIn = getBuiltIn$4;
var inspectSource = inspectSource$2;
var noop$1 = function () { /* empty */ };
var construct = getBuiltIn('Reflect', 'construct');
var constructorRegExp = /^\s*(?:class|function)\b/;
var exec$1 = uncurryThis$6(constructorRegExp.exec);
var INCORRECT_TO_STRING = !constructorRegExp.test(noop$1);
var isConstructorModern = function isConstructor(argument) {
if (!isCallable$3(argument)) return false;
try {
construct(noop$1, [], argument);
return true;
} catch (error) {
return false;
}
};
var isConstructorLegacy = function isConstructor(argument) {
if (!isCallable$3(argument)) return false;
switch (classof$3(argument)) {
case 'AsyncFunction':
case 'GeneratorFunction':
case 'AsyncGeneratorFunction': return false;
}
try {
// we can't check .prototype since constructors produced by .bind haven't it
// `Function#toString` throws on some built-it function in some legacy engines
// (for example, `DOMQuad` and similar in FF41-)
return INCORRECT_TO_STRING || !!exec$1(constructorRegExp, inspectSource(argument));
} catch (error) {
return true;
}
};
isConstructorLegacy.sham = true;
// `IsConstructor` abstract operation
// https://tc39.es/ecma262/#sec-isconstructor
var isConstructor$1 = !construct || fails$7(function () {
var called;
return isConstructorModern(isConstructorModern.call)
|| !isConstructorModern(Object)
|| !isConstructorModern(function () { called = true; })
|| called;
}) ? isConstructorLegacy : isConstructorModern;
var isArray = isArray$2;
var isConstructor = isConstructor$1;
var isObject$3 = isObject$8;
var wellKnownSymbol$4 = wellKnownSymbol$9;
var SPECIES$1 = wellKnownSymbol$4('species');
var $Array = Array;
// a part of `ArraySpeciesCreate` abstract operation
// https://tc39.es/ecma262/#sec-arrayspeciescreate
var arraySpeciesConstructor$1 = function (originalArray) {
var C;
if (isArray(originalArray)) {
C = originalArray.constructor;
// cross-realm fallback
if (isConstructor(C) && (C === $Array || isArray(C.prototype))) C = undefined;
else if (isObject$3(C)) {
C = C[SPECIES$1];
if (C === null) C = undefined;
}
} return C === undefined ? $Array : C;
};
var arraySpeciesConstructor = arraySpeciesConstructor$1;
// `ArraySpeciesCreate` abstract operation
// https://tc39.es/ecma262/#sec-arrayspeciescreate
var arraySpeciesCreate$2 = function (originalArray, length) {
return new (arraySpeciesConstructor(originalArray))(length === 0 ? 0 : length);
};
var $$4 = _export;
var flattenIntoArray$1 = flattenIntoArray_1;
var aCallable = aCallable$3;
var toObject$2 = toObject$5;
var lengthOfArrayLike$1 = lengthOfArrayLike$5;
var arraySpeciesCreate$1 = arraySpeciesCreate$2;
// `Array.prototype.flatMap` method
// https://tc39.es/ecma262/#sec-array.prototype.flatmap
$$4({ target: 'Array', proto: true }, {
flatMap: function flatMap(callbackfn /* , thisArg */) {
var O = toObject$2(this);
var sourceLen = lengthOfArrayLike$1(O);
var A;
aCallable(callbackfn);
A = arraySpeciesCreate$1(O, 0);
A.length = flattenIntoArray$1(A, O, O, sourceLen, 0, 1, callbackfn, arguments.length > 1 ? arguments[1] : undefined);
return A;
}
});
// this method was added to unscopables after implementation
// in popular engines, so it's moved to a separate module
var addToUnscopables$1 = addToUnscopables$3;
// https://tc39.es/ecma262/#sec-array.prototype-@@unscopables
addToUnscopables$1('flatMap');
var entryUnbind$3 = entryUnbind$5;
entryUnbind$3('Array', 'flatMap');
var $$3 = _export;
var flattenIntoArray = flattenIntoArray_1;
var toObject$1 = toObject$5;
var lengthOfArrayLike = lengthOfArrayLike$5;
var toIntegerOrInfinity$3 = toIntegerOrInfinity$7;
var arraySpeciesCreate = arraySpeciesCreate$2;
// `Array.prototype.flat` method
// https://tc39.es/ecma262/#sec-array.prototype.flat
$$3({ target: 'Array', proto: true }, {
flat: function flat(/* depthArg = 1 */) {
var depthArg = arguments.length ? arguments[0] : undefined;
var O = toObject$1(this);
var sourceLen = lengthOfArrayLike(O);
var A = arraySpeciesCreate(O, 0);
A.length = flattenIntoArray(A, O, O, sourceLen, 0, depthArg === undefined ? 1 : toIntegerOrInfinity$3(depthArg));
return A;
}
});
// this method was added to unscopables after implementation
// in popular engines, so it's moved to a separate module
var addToUnscopables = addToUnscopables$3;
// https://tc39.es/ecma262/#sec-array.prototype-@@unscopables
addToUnscopables('flat');
var entryUnbind$2 = entryUnbind$5;
entryUnbind$2('Array', 'flat');
var classof$2 = classof$4;
var $String = String;
var toString$5 = function (argument) {
if (classof$2(argument) === 'Symbol') throw new TypeError('Cannot convert a Symbol value to a string');
return $String(argument);
};
var $$2 = _export;
var uncurryThis$5 = functionUncurryThis;
var requireObjectCoercible$3 = requireObjectCoercible$6;
var toIntegerOrInfinity$2 = toIntegerOrInfinity$7;
var toString$4 = toString$5;
var fails$6 = fails$g;
var charAt$4 = uncurryThis$5(''.charAt);
var FORCED = fails$6(function () {
// eslint-disable-next-line es/no-string-prototype-at -- safe
return '𠮷'.at(-2) !== '\uD842';
});
// `String.prototype.at` method
// https://tc39.es/ecma262/#sec-string.prototype.at
$$2({ target: 'String', proto: true, forced: FORCED }, {
at: function at(index) {
var S = toString$4(requireObjectCoercible$3(this));
var len = S.length;
var relativeIndex = toIntegerOrInfinity$2(index);
var k = relativeIndex >= 0 ? relativeIndex : len + relativeIndex;
return (k < 0 || k >= len) ? undefined : charAt$4(S, k);
}
});
var entryUnbind$1 = entryUnbind$5;
entryUnbind$1('String', 'at');
var anObject$2 = anObject$7;
// `RegExp.prototype.flags` getter implementation
// https://tc39.es/ecma262/#sec-get-regexp.prototype.flags
var regexpFlags$1 = function () {
var that = anObject$2(this);
var result = '';
if (that.hasIndices) result += 'd';
if (that.global) result += 'g';
if (that.ignoreCase) result += 'i';
if (that.multiline) result += 'm';
if (that.dotAll) result += 's';
if (that.unicode) result += 'u';
if (that.unicodeSets) result += 'v';
if (that.sticky) result += 'y';
return result;
};
var fails$5 = fails$g;
var globalThis$4 = globalThis_1;
// babel-minify and Closure Compiler transpiles RegExp('a', 'y') -> /a/y and it causes SyntaxError
var $RegExp$2 = globalThis$4.RegExp;
var UNSUPPORTED_Y$1 = fails$5(function () {
var re = $RegExp$2('a', 'y');
re.lastIndex = 2;
return re.exec('abcd') !== null;
});
// UC Browser bug
// https://github.com/zloirock/core-js/issues/1008
UNSUPPORTED_Y$1 || fails$5(function () {
return !$RegExp$2('a', 'y').sticky;
});
var BROKEN_CARET = UNSUPPORTED_Y$1 || fails$5(function () {
// https://bugzilla.mozilla.org/show_bug.cgi?id=773687
var re = $RegExp$2('^r', 'gy');
re.lastIndex = 2;
return re.exec('str') !== null;
});
var regexpStickyHelpers = {
BROKEN_CARET: BROKEN_CARET};
var fails$4 = fails$g;
var globalThis$3 = globalThis_1;
// babel-minify and Closure Compiler transpiles RegExp('.', 's') -> /./s and it causes SyntaxError
var $RegExp$1 = globalThis$3.RegExp;
var regexpUnsupportedDotAll = fails$4(function () {
var re = $RegExp$1('.', 's');
return !(re.dotAll && re.test('\n') && re.flags === 's');
});
var fails$3 = fails$g;
var globalThis$2 = globalThis_1;
// babel-minify and Closure Compiler transpiles RegExp('(?b)', 'g') -> /(?b)/g and it causes SyntaxError
var $RegExp = globalThis$2.RegExp;
var regexpUnsupportedNcg = fails$3(function () {
var re = $RegExp('(?b)', 'g');
return re.exec('b').groups.a !== 'b' ||
'b'.replace(re, '$c') !== 'bc';
});
/* eslint-disable regexp/no-empty-capturing-group, regexp/no-empty-group, regexp/no-lazy-ends -- testing */
/* eslint-disable regexp/no-useless-quantifier -- testing */
var call$7 = functionCall;
var uncurryThis$4 = functionUncurryThis;
var toString$3 = toString$5;
var regexpFlags = regexpFlags$1;
var stickyHelpers = regexpStickyHelpers;
var shared = shared$4;
var create = objectCreate;
var getInternalState = internalState.get;
var UNSUPPORTED_DOT_ALL = regexpUnsupportedDotAll;
var UNSUPPORTED_NCG = regexpUnsupportedNcg;
var nativeReplace = shared('native-string-replace', String.prototype.replace);
var nativeExec = RegExp.prototype.exec;
var patchedExec = nativeExec;
var charAt$3 = uncurryThis$4(''.charAt);
var indexOf$1 = uncurryThis$4(''.indexOf);
var replace$2 = uncurryThis$4(''.replace);
var stringSlice$4 = uncurryThis$4(''.slice);
var UPDATES_LAST_INDEX_WRONG = (function () {
var re1 = /a/;
var re2 = /b*/g;
call$7(nativeExec, re1, 'a');
call$7(nativeExec, re2, 'a');
return re1.lastIndex !== 0 || re2.lastIndex !== 0;
})();
var UNSUPPORTED_Y = stickyHelpers.BROKEN_CARET;
// nonparticipating capturing group, copied from es5-shim's String#split patch.
var NPCG_INCLUDED = /()??/.exec('')[1] !== undefined;
var PATCH = UPDATES_LAST_INDEX_WRONG || NPCG_INCLUDED || UNSUPPORTED_Y || UNSUPPORTED_DOT_ALL || UNSUPPORTED_NCG;
if (PATCH) {
patchedExec = function exec(string) {
var re = this;
var state = getInternalState(re);
var str = toString$3(string);
var raw = state.raw;
var result, reCopy, lastIndex, match, i, object, group;
if (raw) {
raw.lastIndex = re.lastIndex;
result = call$7(patchedExec, raw, str);
re.lastIndex = raw.lastIndex;
return result;
}
var groups = state.groups;
var sticky = UNSUPPORTED_Y && re.sticky;
var flags = call$7(regexpFlags, re);
var source = re.source;
var charsAdded = 0;
var strCopy = str;
if (sticky) {
flags = replace$2(flags, 'y', '');
if (indexOf$1(flags, 'g') === -1) {
flags += 'g';
}
strCopy = stringSlice$4(str, re.lastIndex);
// Support anchored sticky behavior.
if (re.lastIndex > 0 && (!re.multiline || re.multiline && charAt$3(str, re.lastIndex - 1) !== '\n')) {
source = '(?: ' + source + ')';
strCopy = ' ' + strCopy;
charsAdded++;
}
// ^(? + rx + ) is needed, in combination with some str slicing, to
// simulate the 'y' flag.
reCopy = new RegExp('^(?:' + source + ')', flags);
}
if (NPCG_INCLUDED) {
reCopy = new RegExp('^' + source + '$(?!\\s)', flags);
}
if (UPDATES_LAST_INDEX_WRONG) lastIndex = re.lastIndex;
match = call$7(nativeExec, sticky ? reCopy : re, strCopy);
if (sticky) {
if (match) {
match.input = stringSlice$4(match.input, charsAdded);
match[0] = stringSlice$4(match[0], charsAdded);
match.index = re.lastIndex;
re.lastIndex += match[0].length;
} else re.lastIndex = 0;
} else if (UPDATES_LAST_INDEX_WRONG && match) {
re.lastIndex = re.global ? match.index + match[0].length : lastIndex;
}
if (NPCG_INCLUDED && match && match.length > 1) {
// Fix browsers whose `exec` methods don't consistently return `undefined`
// for NPCG, like IE8. NOTE: This doesn't work for /(.?)?/
call$7(nativeReplace, match[0], reCopy, function () {
for (i = 1; i < arguments.length - 2; i++) {
if (arguments[i] === undefined) match[i] = undefined;
}
});
}
if (match && groups) {
match.groups = object = create(null);
for (i = 0; i < groups.length; i++) {
group = groups[i];
object[group[0]] = match[group[1]];
}
}
return match;
};
}
var regexpExec$2 = patchedExec;
var $$1 = _export;
var exec = regexpExec$2;
// `RegExp.prototype.exec` method
// https://tc39.es/ecma262/#sec-regexp.prototype.exec
$$1({ target: 'RegExp', proto: true, forced: /./.exec !== exec }, {
exec: exec
});
var NATIVE_BIND = functionBindNative;
var FunctionPrototype = Function.prototype;
var apply$1 = FunctionPrototype.apply;
var call$6 = FunctionPrototype.call;
// eslint-disable-next-line es/no-function-prototype-bind, es/no-reflect -- safe
var functionApply = typeof Reflect == 'object' && Reflect.apply || (NATIVE_BIND ? call$6.bind(apply$1) : function () {
return call$6.apply(apply$1, arguments);
});
// TODO: Remove from `core-js@4` since it's moved to entry points
var call$5 = functionCall;
var defineBuiltIn = defineBuiltIn$2;
var regexpExec$1 = regexpExec$2;
var fails$2 = fails$g;
var wellKnownSymbol$3 = wellKnownSymbol$9;
var createNonEnumerableProperty = createNonEnumerableProperty$3;
var SPECIES = wellKnownSymbol$3('species');
var RegExpPrototype$1 = RegExp.prototype;
var fixRegexpWellKnownSymbolLogic = function (KEY, exec, FORCED, SHAM) {
var SYMBOL = wellKnownSymbol$3(KEY);
var DELEGATES_TO_SYMBOL = !fails$2(function () {
// String methods call symbol-named RegExp methods
var O = {};
O[SYMBOL] = function () { return 7; };
return ''[KEY](O) !== 7;
});
var DELEGATES_TO_EXEC = DELEGATES_TO_SYMBOL && !fails$2(function () {
// Symbol-named RegExp methods call .exec
var execCalled = false;
var re = /a/;
if (KEY === 'split') {
// We can't use real regex here since it causes deoptimization
// and serious performance degradation in V8
// https://github.com/zloirock/core-js/issues/306
re = {};
// RegExp[@@split] doesn't call the regex's exec method, but first creates
// a new one. We need to return the patched regex when creating the new one.
re.constructor = {};
re.constructor[SPECIES] = function () { return re; };
re.flags = '';
re[SYMBOL] = /./[SYMBOL];
}
re.exec = function () {
execCalled = true;
return null;
};
re[SYMBOL]('');
return !execCalled;
});
if (
!DELEGATES_TO_SYMBOL ||
!DELEGATES_TO_EXEC ||
FORCED
) {
var nativeRegExpMethod = /./[SYMBOL];
var methods = exec(SYMBOL, ''[KEY], function (nativeMethod, regexp, str, arg2, forceStringMethod) {
var $exec = regexp.exec;
if ($exec === regexpExec$1 || $exec === RegExpPrototype$1.exec) {
if (DELEGATES_TO_SYMBOL && !forceStringMethod) {
// The native String method already delegates to @@method (this
// polyfilled function), leasing to infinite recursion.
// We avoid it by directly calling the native @@method method.
return { done: true, value: call$5(nativeRegExpMethod, regexp, str, arg2) };
}
return { done: true, value: call$5(nativeMethod, str, regexp, arg2) };
}
return { done: false };
});
defineBuiltIn(String.prototype, KEY, methods[0]);
defineBuiltIn(RegExpPrototype$1, SYMBOL, methods[1]);
}
if (SHAM) createNonEnumerableProperty(RegExpPrototype$1[SYMBOL], 'sham', true);
};
var uncurryThis$3 = functionUncurryThis;
var toIntegerOrInfinity$1 = toIntegerOrInfinity$7;
var toString$2 = toString$5;
var requireObjectCoercible$2 = requireObjectCoercible$6;
var charAt$2 = uncurryThis$3(''.charAt);
var charCodeAt = uncurryThis$3(''.charCodeAt);
var stringSlice$3 = uncurryThis$3(''.slice);
var createMethod = function (CONVERT_TO_STRING) {
return function ($this, pos) {
var S = toString$2(requireObjectCoercible$2($this));
var position = toIntegerOrInfinity$1(pos);
var size = S.length;
var first, second;
if (position < 0 || position >= size) return CONVERT_TO_STRING ? '' : undefined;
first = charCodeAt(S, position);
return first < 0xD800 || first > 0xDBFF || position + 1 === size
|| (second = charCodeAt(S, position + 1)) < 0xDC00 || second > 0xDFFF
? CONVERT_TO_STRING
? charAt$2(S, position)
: first
: CONVERT_TO_STRING
? stringSlice$3(S, position, position + 2)
: (first - 0xD800 << 10) + (second - 0xDC00) + 0x10000;
};
};
var stringMultibyte = {
// `String.prototype.at` method
// https://github.com/mathiasbynens/String.prototype.at
charAt: createMethod(true)
};
var charAt$1 = stringMultibyte.charAt;
// `AdvanceStringIndex` abstract operation
// https://tc39.es/ecma262/#sec-advancestringindex
var advanceStringIndex$1 = function (S, index, unicode) {
return index + (unicode ? charAt$1(S, index).length : 1);
};
var uncurryThis$2 = functionUncurryThis;
var toObject = toObject$5;
var floor = Math.floor;
var charAt = uncurryThis$2(''.charAt);
var replace$1 = uncurryThis$2(''.replace);
var stringSlice$2 = uncurryThis$2(''.slice);
// eslint-disable-next-line redos/no-vulnerable -- safe
var SUBSTITUTION_SYMBOLS = /\$([$&'`]|\d{1,2}|<[^>]*>)/g;
var SUBSTITUTION_SYMBOLS_NO_NAMED = /\$([$&'`]|\d{1,2})/g;
// `GetSubstitution` abstract operation
// https://tc39.es/ecma262/#sec-getsubstitution
var getSubstitution$2 = function (matched, str, position, captures, namedCaptures, replacement) {
var tailPos = position + matched.length;
var m = captures.length;
var symbols = SUBSTITUTION_SYMBOLS_NO_NAMED;
if (namedCaptures !== undefined) {
namedCaptures = toObject(namedCaptures);
symbols = SUBSTITUTION_SYMBOLS;
}
return replace$1(replacement, symbols, function (match, ch) {
var capture;
switch (charAt(ch, 0)) {
case '$': return '$';
case '&': return matched;
case '`': return stringSlice$2(str, 0, position);
case "'": return stringSlice$2(str, tailPos);
case '<':
capture = namedCaptures[stringSlice$2(ch, 1, -1)];
break;
default: // \d\d?
var n = +ch;
if (n === 0) return match;
if (n > m) {
var f = floor(n / 10);
if (f === 0) return match;
if (f <= m) return captures[f - 1] === undefined ? charAt(ch, 1) : captures[f - 1] + charAt(ch, 1);
return match;
}
capture = captures[n - 1];
}
return capture === undefined ? '' : capture;
});
};
var globalThis$1 = globalThis_1;
var fails$1 = fails$g;
// babel-minify and Closure Compiler transpiles RegExp('.', 'd') -> /./d and it causes SyntaxError
var RegExp$1 = globalThis$1.RegExp;
var FLAGS_GETTER_IS_CORRECT = !fails$1(function () {
var INDICES_SUPPORT = true;
try {
RegExp$1('.', 'd');
} catch (error) {
INDICES_SUPPORT = false;
}
var O = {};
// modern V8 bug
var calls = '';
var expected = INDICES_SUPPORT ? 'dgimsy' : 'gimsy';
var addGetter = function (key, chr) {
// eslint-disable-next-line es/no-object-defineproperty -- safe
Object.defineProperty(O, key, { get: function () {
calls += chr;
return true;
} });
};
var pairs = {
dotAll: 's',
global: 'g',
ignoreCase: 'i',
multiline: 'm',
sticky: 'y'
};
if (INDICES_SUPPORT) pairs.hasIndices = 'd';
for (var key in pairs) addGetter(key, pairs[key]);
// eslint-disable-next-line es/no-object-getownpropertydescriptor -- safe
var result = Object.getOwnPropertyDescriptor(RegExp$1.prototype, 'flags').get.call(O);
return result !== expected || calls !== expected;
});
var regexpFlagsDetection = { correct: FLAGS_GETTER_IS_CORRECT };
var call$4 = functionCall;
var hasOwn = hasOwnProperty_1;
var isPrototypeOf = objectIsPrototypeOf;
var regExpFlagsDetection = regexpFlagsDetection;
var regExpFlagsGetterImplementation = regexpFlags$1;
var RegExpPrototype = RegExp.prototype;
var regexpGetFlags = regExpFlagsDetection.correct ? function (it) {
return it.flags;
} : function (it) {
return (!regExpFlagsDetection.correct && isPrototypeOf(RegExpPrototype, it) && !hasOwn(it, 'flags'))
? call$4(regExpFlagsGetterImplementation, it)
: it.flags;
};
var call$3 = functionCall;
var anObject$1 = anObject$7;
var isCallable$2 = isCallable$f;
var classof$1 = classofRaw$2;
var regexpExec = regexpExec$2;
var $TypeError$1 = TypeError;
// `RegExpExec` abstract operation
// https://tc39.es/ecma262/#sec-regexpexec
var regexpExecAbstract = function (R, S) {
var exec = R.exec;
if (isCallable$2(exec)) {
var result = call$3(exec, R, S);
if (result !== null) anObject$1(result);
return result;
}
if (classof$1(R) === 'RegExp') return call$3(regexpExec, R, S);
throw new $TypeError$1('RegExp#exec called on incompatible receiver');
};
var apply = functionApply;
var call$2 = functionCall;
var uncurryThis$1 = functionUncurryThis;
var fixRegExpWellKnownSymbolLogic = fixRegexpWellKnownSymbolLogic;
var fails = fails$g;
var anObject = anObject$7;
var isCallable$1 = isCallable$f;
var isObject$2 = isObject$8;
var toIntegerOrInfinity = toIntegerOrInfinity$7;
var toLength = toLength$2;
var toString$1 = toString$5;
var requireObjectCoercible$1 = requireObjectCoercible$6;
var advanceStringIndex = advanceStringIndex$1;
var getMethod$2 = getMethod$4;
var getSubstitution$1 = getSubstitution$2;
var getRegExpFlags$1 = regexpGetFlags;
var regExpExec = regexpExecAbstract;
var wellKnownSymbol$2 = wellKnownSymbol$9;
var REPLACE$1 = wellKnownSymbol$2('replace');
var max$1 = Math.max;
var min$1 = Math.min;
var concat = uncurryThis$1([].concat);
var push = uncurryThis$1([].push);
var stringIndexOf = uncurryThis$1(''.indexOf);
var stringSlice$1 = uncurryThis$1(''.slice);
var maybeToString = function (it) {
return it === undefined ? it : String(it);
};
// IE <= 11 replaces $0 with the whole match, as if it was $&
// https://stackoverflow.com/questions/6024666/getting-ie-to-replace-a-regex-with-the-literal-string-0
var REPLACE_KEEPS_$0 = (function () {
// eslint-disable-next-line regexp/prefer-escape-replacement-dollar-char -- required for testing
return 'a'.replace(/./, '$0') === '$0';
})();
// Safari <= 13.0.3(?) substitutes nth capture where n>m with an empty string
var REGEXP_REPLACE_SUBSTITUTES_UNDEFINED_CAPTURE = (function () {
if (/./[REPLACE$1]) {
return /./[REPLACE$1]('a', '$0') === '';
}
return false;
})();
var REPLACE_SUPPORTS_NAMED_GROUPS = !fails(function () {
var re = /./;
re.exec = function () {
var result = [];
result.groups = { a: '7' };
return result;
};
// eslint-disable-next-line regexp/no-useless-dollar-replacements -- false positive
return ''.replace(re, '$') !== '7';
});
// @@replace logic
fixRegExpWellKnownSymbolLogic('replace', function (_, nativeReplace, maybeCallNative) {
var UNSAFE_SUBSTITUTE = REGEXP_REPLACE_SUBSTITUTES_UNDEFINED_CAPTURE ? '$' : '$0';
return [
// `String.prototype.replace` method
// https://tc39.es/ecma262/#sec-string.prototype.replace
function replace(searchValue, replaceValue) {
var O = requireObjectCoercible$1(this);
var replacer = isObject$2(searchValue) ? getMethod$2(searchValue, REPLACE$1) : undefined;
return replacer
? call$2(replacer, searchValue, O, replaceValue)
: call$2(nativeReplace, toString$1(O), searchValue, replaceValue);
},
// `RegExp.prototype[@@replace]` method
// https://tc39.es/ecma262/#sec-regexp.prototype-@@replace
function (string, replaceValue) {
var rx = anObject(this);
var S = toString$1(string);
if (
typeof replaceValue == 'string' &&
stringIndexOf(replaceValue, UNSAFE_SUBSTITUTE) === -1 &&
stringIndexOf(replaceValue, '$<') === -1
) {
var res = maybeCallNative(nativeReplace, rx, S, replaceValue);
if (res.done) return res.value;
}
var functionalReplace = isCallable$1(replaceValue);
if (!functionalReplace) replaceValue = toString$1(replaceValue);
var flags = toString$1(getRegExpFlags$1(rx));
var global = stringIndexOf(flags, 'g') !== -1;
var fullUnicode;
if (global) {
fullUnicode = stringIndexOf(flags, 'u') !== -1;
rx.lastIndex = 0;
}
var results = [];
var result;
while (true) {
result = regExpExec(rx, S);
if (result === null) break;
push(results, result);
if (!global) break;
var matchStr = toString$1(result[0]);
if (matchStr === '') rx.lastIndex = advanceStringIndex(S, toLength(rx.lastIndex), fullUnicode);
}
var accumulatedResult = '';
var nextSourcePosition = 0;
for (var i = 0; i < results.length; i++) {
result = results[i];
var matched = toString$1(result[0]);
var position = max$1(min$1(toIntegerOrInfinity(result.index), S.length), 0);
var captures = [];
var replacement;
// NOTE: This is equivalent to
// captures = result.slice(1).map(maybeToString)
// but for some reason `nativeSlice.call(result, 1, result.length)` (called in
// the slice polyfill when slicing native arrays) "doesn't work" in safari 9 and
// causes a crash (https://pastebin.com/N21QzeQA) when trying to debug it.
for (var j = 1; j < result.length; j++) push(captures, maybeToString(result[j]));
var namedCaptures = result.groups;
if (functionalReplace) {
var replacerArgs = concat([matched], captures, position, S);
if (namedCaptures !== undefined) push(replacerArgs, namedCaptures);
replacement = toString$1(apply(replaceValue, undefined, replacerArgs));
} else {
replacement = getSubstitution$1(matched, S, position, captures, namedCaptures, replaceValue);
}
if (position >= nextSourcePosition) {
accumulatedResult += stringSlice$1(S, nextSourcePosition, position) + replacement;
nextSourcePosition = position + matched.length;
}
}
return accumulatedResult + stringSlice$1(S, nextSourcePosition);
}
];
}, !REPLACE_SUPPORTS_NAMED_GROUPS || !REPLACE_KEEPS_$0 || REGEXP_REPLACE_SUBSTITUTES_UNDEFINED_CAPTURE);
var isObject$1 = isObject$8;
var classof = classofRaw$2;
var wellKnownSymbol$1 = wellKnownSymbol$9;
var MATCH = wellKnownSymbol$1('match');
// `IsRegExp` abstract operation
// https://tc39.es/ecma262/#sec-isregexp
var isRegexp = function (it) {
var isRegExp;
return isObject$1(it) && ((isRegExp = it[MATCH]) !== undefined ? !!isRegExp : classof(it) === 'RegExp');
};
var $ = _export;
var call$1 = functionCall;
var uncurryThis = functionUncurryThis;
var requireObjectCoercible = requireObjectCoercible$6;
var isCallable = isCallable$f;
var isObject = isObject$8;
var isRegExp = isRegexp;
var toString = toString$5;
var getMethod$1 = getMethod$4;
var getRegExpFlags = regexpGetFlags;
var getSubstitution = getSubstitution$2;
var wellKnownSymbol = wellKnownSymbol$9;
var REPLACE = wellKnownSymbol('replace');
var $TypeError = TypeError;
var indexOf = uncurryThis(''.indexOf);
uncurryThis(''.replace);
var stringSlice = uncurryThis(''.slice);
var max = Math.max;
// `String.prototype.replaceAll` method
// https://tc39.es/ecma262/#sec-string.prototype.replaceall
$({ target: 'String', proto: true }, {
replaceAll: function replaceAll(searchValue, replaceValue) {
var O = requireObjectCoercible(this);
var IS_REG_EXP, flags, replacer, string, searchString, functionalReplace, searchLength, advanceBy, position, replacement;
var endOfLastMatch = 0;
var result = '';
if (isObject(searchValue)) {
IS_REG_EXP = isRegExp(searchValue);
if (IS_REG_EXP) {
flags = toString(requireObjectCoercible(getRegExpFlags(searchValue)));
if (!~indexOf(flags, 'g')) throw new $TypeError('`.replaceAll` does not allow non-global regexes');
}
replacer = getMethod$1(searchValue, REPLACE);
if (replacer) return call$1(replacer, searchValue, O, replaceValue);
}
string = toString(O);
searchString = toString(searchValue);
functionalReplace = isCallable(replaceValue);
if (!functionalReplace) replaceValue = toString(replaceValue);
searchLength = searchString.length;
advanceBy = max(1, searchLength);
position = indexOf(string, searchString);
while (position !== -1) {
replacement = functionalReplace
? toString(replaceValue(searchString, position, string))
: getSubstitution(searchString, string, position, [], undefined, replaceValue);
result += stringSlice(string, endOfLastMatch, position) + replacement;
endOfLastMatch = position + searchLength;
position = position + advanceBy > string.length ? -1 : indexOf(string, searchString, position + advanceBy);
}
if (endOfLastMatch < string.length) {
result += stringSlice(string, endOfLastMatch);
}
return result;
}
});
var entryUnbind = entryUnbind$5;
entryUnbind('String', 'replaceAll');
function isChild(x) {
return x && typeof x === "object" && "parents" in x;
}
function haveParentsChanged(child) {
for (let i = 0, n = child.parents.length; i < n; i++) {
child.parents[i].__unsafe__getWithoutCapture(true);
if (child.parents[i].lastChangedEpoch !== child.parentEpochs[i]) {
return true;
}
}
return false;
}
function detach(parent, child) {
if (!parent.children.remove(child)) {
return;
}
if (parent.children.isEmpty && isChild(parent)) {
for (let i = 0, n = parent.parents.length; i < n; i++) {
detach(parent.parents[i], parent);
}
}
}
function attach(parent, child) {
if (!parent.children.add(child)) {
return;
}
if (isChild(parent)) {
for (let i = 0, n = parent.parents.length; i < n; i++) {
attach(parent.parents[i], parent);
}
}
}
function equals(a, b) {
const shallowEquals = a === b || Object.is(a, b) || Boolean(a && b && typeof a.equals === "function" && a.equals(b));
return shallowEquals;
}
function singleton(key, init) {
const symbol = Symbol.for(`com.tldraw.state/${key}`);
const global = globalThis;
global[symbol] ??= init();
return global[symbol];
}
const EMPTY_ARRAY = singleton("empty_array", () => Object.freeze([]));
const ARRAY_SIZE_THRESHOLD = 8;
class ArraySet {
arraySize = 0;
array = Array(ARRAY_SIZE_THRESHOLD);
set = null;
/**
* Get whether this ArraySet has any elements.
*
* @returns True if this ArraySet has any elements, false otherwise.
*/
// eslint-disable-next-line no-restricted-syntax
get isEmpty() {
if (this.array) {
return this.arraySize === 0;
}
if (this.set) {
return this.set.size === 0;
}
throw new Error("no set or array");
}
/**
* Add an item to the ArraySet if it is not already present.
*
* @param elem - The element to add.
*/
add(elem) {
if (this.array) {
const idx = this.array.indexOf(elem);
if (idx !== -1) {
return false;
}
if (this.arraySize < ARRAY_SIZE_THRESHOLD) {
this.array[this.arraySize] = elem;
this.arraySize++;
return true;
} else {
this.set = new Set(this.array);
this.array = null;
this.set.add(elem);
return true;
}
}
if (this.set) {
if (this.set.has(elem)) {
return false;
}
this.set.add(elem);
return true;
}
throw new Error("no set or array");
}
/**
* Remove an item from the ArraySet if it is present.
*
* @param elem - The element to remove
*/
remove(elem) {
if (this.array) {
const idx = this.array.indexOf(elem);
if (idx === -1) {
return false;
}
this.array[idx] = void 0;
this.arraySize--;
if (idx !== this.arraySize) {
this.array[idx] = this.array[this.arraySize];
this.array[this.arraySize] = void 0;
}
return true;
}
if (this.set) {
if (!this.set.has(elem)) {
return false;
}
this.set.delete(elem);
return true;
}
throw new Error("no set or array");
}
/**
* Run a callback for each element in the ArraySet.
*
* @param visitor - The callback to run for each element.
*/
visit(visitor) {
if (this.array) {
for (let i = 0; i < this.arraySize; i++) {
const elem = this.array[i];
if (typeof elem !== "undefined") {
visitor(elem);
}
}
return;
}
if (this.set) {
this.set.forEach(visitor);
return;
}
throw new Error("no set or array");
}
has(elem) {
if (this.array) {
return this.array.indexOf(elem) !== -1;
} else {
return this.set.has(elem);
}
}
clear() {
if (this.set) {
this.set.clear();
} else {
this.arraySize = 0;
this.array = [];
}
}
size() {
if (this.set) {
return this.set.size;
} else {
return this.arraySize;
}
}
}
const RESET_VALUE = Symbol.for("com.tldraw.state/RESET_VALUE");
class HistoryBuffer {
constructor(capacity) {
this.capacity = capacity;
this.buffer = new Array(capacity);
}
index = 0;
// use a wrap around buffer to store the last N values
buffer;
/**
* Add a diff to the history buffer.
*
* @param lastComputedEpoch - The epoch when the diff was computed.
* @param currentEpoch - The current epoch.
* @param diff - The diff to add, or else a reset value.
*/
pushEntry(lastComputedEpoch, currentEpoch, diff) {
if (diff === void 0) {
return;
}
if (diff === RESET_VALUE) {
this.clear();
return;
}
this.buffer[this.index] = [lastComputedEpoch, currentEpoch, diff];
this.index = (this.index + 1) % this.capacity;
}
/**
* Clear the history buffer.
*/
clear() {
this.index = 0;
this.buffer.fill(void 0);
}
/**
* Get the diffs since the given epoch.
*
* @param sinceEpoch - The epoch to get diffs since.
* @returns An array of diffs or a flag to reset the history buffer.
*/
getChangesSince(sinceEpoch) {
const { index, capacity, buffer } = this;
for (let i = 0; i < capacity; i++) {
const offset = (index - 1 + capacity - i) % capacity;
const elem = buffer[offset];
if (!elem) {
return RESET_VALUE;
}
const [fromEpoch, toEpoch] = elem;
if (i === 0 && sinceEpoch >= toEpoch) {
return [];
}
if (fromEpoch <= sinceEpoch && sinceEpoch < toEpoch) {
const len = i + 1;
const result = new Array(len);
for (let j = 0; j < len; j++) {
result[j] = buffer[(offset + j) % capacity][2];
}
return result;
}
}
return RESET_VALUE;
}
}
class CaptureStackFrame {
constructor(below, child) {
this.below = below;
this.child = child;
}
offset = 0;
maybeRemoved;
}
const inst$1 = singleton("capture", () => ({ stack: null }));
function unsafe__withoutCapture(fn) {
const oldStack = inst$1.stack;
inst$1.stack = null;
try {
return fn();
} finally {
inst$1.stack = oldStack;
}
}
function startCapturingParents(child) {
inst$1.stack = new CaptureStackFrame(inst$1.stack, child);
child.parentSet.clear();
}
function stopCapturingParents() {
const frame = inst$1.stack;
inst$1.stack = frame.below;
if (frame.offset < frame.child.parents.length) {
for (let i = frame.offset; i < frame.child.parents.length; i++) {
const maybeRemovedParent = frame.child.parents[i];
if (!frame.child.parentSet.has(maybeRemovedParent)) {
detach(maybeRemovedParent, frame.child);
}
}
frame.child.parents.length = frame.offset;
frame.child.parentEpochs.length = frame.offset;
}
if (frame.maybeRemoved) {
for (let i = 0; i < frame.maybeRemoved.length; i++) {
const maybeRemovedParent = frame.maybeRemoved[i];
if (!frame.child.parentSet.has(maybeRemovedParent)) {
detach(maybeRemovedParent, frame.child);
}
}
}
}
function maybeCaptureParent(p) {
if (inst$1.stack) {
const wasCapturedAlready = inst$1.stack.child.parentSet.has(p);
if (wasCapturedAlready) {
return;
}
inst$1.stack.child.parentSet.add(p);
if (inst$1.stack.child.isActivelyListening) {
attach(p, inst$1.stack.child);
}
if (inst$1.stack.offset < inst$1.stack.child.parents.length) {
const maybeRemovedParent = inst$1.stack.child.parents[inst$1.stack.offset];
if (maybeRemovedParent !== p) {
if (!inst$1.stack.maybeRemoved) {
inst$1.stack.maybeRemoved = [maybeRemovedParent];
} else {
inst$1.stack.maybeRemoved.push(maybeRemovedParent);
}
}
}
inst$1.stack.child.parents[inst$1.stack.offset] = p;
inst$1.stack.child.parentEpochs[inst$1.stack.offset] = p.lastChangedEpoch;
inst$1.stack.offset++;
}
}
const GLOBAL_START_EPOCH = -1;
class __EffectScheduler__ {
constructor(name, runEffect, options) {
this.name = name;
this.runEffect = runEffect;
this._scheduleEffect = options?.scheduleEffect;
}
_isActivelyListening = false;
/**
* Whether this scheduler is attached and actively listening to its parents.
* @public
*/
// eslint-disable-next-line no-restricted-syntax
get isActivelyListening() {
return this._isActivelyListening;
}
/** @internal */
lastTraversedEpoch = GLOBAL_START_EPOCH;
lastReactedEpoch = GLOBAL_START_EPOCH;
_scheduleCount = 0;
/**
* The number of times this effect has been scheduled.
* @public
*/
// eslint-disable-next-line no-restricted-syntax
get scheduleCount() {
return this._scheduleCount;
}
/** @internal */
parentSet = new ArraySet();
/** @internal */
parentEpochs = [];
/** @internal */
parents = [];
_scheduleEffect;
/** @internal */
maybeScheduleEffect() {
if (!this._isActivelyListening) return;
if (this.lastReactedEpoch === getGlobalEpoch()) return;
if (this.parents.length && !haveParentsChanged(this)) {
this.lastReactedEpoch = getGlobalEpoch();
return;
}
this.scheduleEffect();
}
/** @internal */
scheduleEffect() {
this._scheduleCount++;
if (this._scheduleEffect) {
this._scheduleEffect(this.maybeExecute);
} else {
this.execute();
}
}
/** @internal */
// eslint-disable-next-line local/prefer-class-methods
maybeExecute = () => {
if (!this._isActivelyListening) return;
this.execute();
};
/**
* Makes this scheduler become 'actively listening' to its parents.
* If it has been executed before it will immediately become eligible to receive 'maybeScheduleEffect' calls.
* If it has not executed before it will need to be manually executed once to become eligible for scheduling, i.e. by calling [[EffectScheduler.execute]].
* @public
*/
attach() {
this._isActivelyListening = true;
for (let i = 0, n = this.parents.length; i < n; i++) {
attach(this.parents[i], this);
}
}
/**
* Makes this scheduler stop 'actively listening' to its parents.
* It will no longer be eligible to receive 'maybeScheduleEffect' calls until [[EffectScheduler.attach]] is called again.
*/
detach() {
this._isActivelyListening = false;
for (let i = 0, n = this.parents.length; i < n; i++) {
detach(this.parents[i], this);
}
}
/**
* Executes the effect immediately and returns the result.
* @returns The result of the effect.
*/
execute() {
try {
startCapturingParents(this);
const currentEpoch = getGlobalEpoch();
const result = this.runEffect(this.lastReactedEpoch);
this.lastReactedEpoch = currentEpoch;
return result;
} finally {
stopCapturingParents();
}
}
}
const EffectScheduler = singleton(
"EffectScheduler",
() => __EffectScheduler__
);
function react(name, fn, options) {
const scheduler = new EffectScheduler(name, fn, options);
scheduler.attach();
scheduler.scheduleEffect();
return () => {
scheduler.detach();
};
}
function reactor(name, fn, options) {
const scheduler = new EffectScheduler(name, fn, options);
return {
scheduler,
start: (options2) => {
const force = options2?.force ?? false;
scheduler.attach();
if (force) {
scheduler.scheduleEffect();
} else {
scheduler.maybeScheduleEffect();
}
},
stop: () => {
scheduler.detach();
}
};
}
class Transaction {
constructor(parent) {
this.parent = parent;
}
initialAtomValues = /* @__PURE__ */ new Map();
/**
* Get whether this transaction is a root (no parents).
*
* @public
*/
// eslint-disable-next-line no-restricted-syntax
get isRoot() {
return this.parent === null;
}
/**
* Commit the transaction's changes.
*
* @public
*/
commit() {
if (inst.globalIsReacting) {
for (const atom of this.initialAtomValues.keys()) {
traverseAtomForCleanup(atom);
}
} else if (this.isRoot) {
flushChanges(this.initialAtomValues.keys());
} else {
this.initialAtomValues.forEach((value, atom) => {
if (!this.parent.initialAtomValues.has(atom)) {
this.parent.initialAtomValues.set(atom, value);
}
});
}
}
/**
* Abort the transaction.
*
* @public
*/
abort() {
inst.globalEpoch++;
this.initialAtomValues.forEach((value, atom) => {
atom.set(value);
atom.historyBuffer?.clear();
});
this.commit();
}
}
const inst = singleton("transactions", () => ({
// The current epoch (global to all atoms).
globalEpoch: GLOBAL_START_EPOCH + 1,
// Whether any transaction is reacting.
globalIsReacting: false,
currentTransaction: null,
cleanupReactors: null,
reactionEpoch: GLOBAL_START_EPOCH + 1
}));
function getReactionEpoch() {
return inst.reactionEpoch;
}
function getGlobalEpoch() {
return inst.globalEpoch;
}
function getIsReacting() {
return inst.globalIsReacting;
}
function traverse(reactors, child) {
if (child.lastTraversedEpoch === inst.globalEpoch) {
return;
}
child.lastTraversedEpoch = inst.globalEpoch;
if (child instanceof EffectScheduler) {
reactors.add(child);
} else {
child.children.visit((c) => traverse(reactors, c));
}
}
function flushChanges(atoms) {
if (inst.globalIsReacting) {
throw new Error("flushChanges cannot be called during a reaction");
}
const outerTxn = inst.currentTransaction;
try {
inst.currentTransaction = null;
inst.globalIsReacting = true;
inst.reactionEpoch = inst.globalEpoch;
const reactors = /* @__PURE__ */ new Set();
for (const atom of atoms) {
atom.children.visit((child) => traverse(reactors, child));
}
for (const r of reactors) {
r.maybeScheduleEffect();
}
let updateDepth = 0;
while (inst.cleanupReactors?.size) {
if (updateDepth++ > 1e3) {
throw new Error("Reaction update depth limit exceeded");
}
const reactors2 = inst.cleanupReactors;
inst.cleanupReactors = null;
for (const r of reactors2) {
r.maybeScheduleEffect();
}
}
} finally {
inst.cleanupReactors = null;
inst.globalIsReacting = false;
inst.currentTransaction = outerTxn;
}
}
function atomDidChange(atom, previousValue) {
if (inst.currentTransaction) {
if (!inst.currentTransaction.initialAtomValues.has(atom)) {
inst.currentTransaction.initialAtomValues.set(atom, previousValue);
}
} else if (inst.globalIsReacting) {
traverseAtomForCleanup(atom);
} else {
flushChanges([atom]);
}
}
function traverseAtomForCleanup(atom) {
const rs = inst.cleanupReactors ??= /* @__PURE__ */ new Set();
atom.children.visit((child) => traverse(rs, child));
}
function advanceGlobalEpoch() {
inst.globalEpoch++;
}
function transaction(fn) {
const txn = new Transaction(inst.currentTransaction);
inst.currentTransaction = txn;
try {
let result = void 0;
let rollback = false;
try {
result = fn(() => rollback = true);
} catch (e) {
txn.abort();
throw e;
}
if (rollback) {
txn.abort();
} else {
txn.commit();
}
return result;
} finally {
inst.currentTransaction = inst.currentTransaction.parent;
}
}
function transact(fn) {
if (inst.currentTransaction) {
return fn();
}
return transaction(fn);
}
class __Atom__ {
constructor(name, current, options) {
this.name = name;
this.current = current;
this.isEqual = options?.isEqual ?? null;
if (!options) return;
if (options.historyLength) {
this.historyBuffer = new HistoryBuffer(options.historyLength);
}
this.computeDiff = options.computeDiff;
}
isEqual;
computeDiff;
lastChangedEpoch = getGlobalEpoch();
children = new ArraySet();
historyBuffer;
__unsafe__getWithoutCapture(_ignoreErrors) {
return this.current;
}
get() {
maybeCaptureParent(this);
return this.current;
}
set(value, diff) {
if (this.isEqual?.(this.current, value) ?? equals(this.current, value)) {
return this.current;
}
advanceGlobalEpoch();
if (this.historyBuffer) {
this.historyBuffer.pushEntry(
this.lastChangedEpoch,
getGlobalEpoch(),
diff ?? this.computeDiff?.(this.current, value, this.lastChangedEpoch, getGlobalEpoch()) ?? RESET_VALUE
);
}
this.lastChangedEpoch = getGlobalEpoch();
const oldValue = this.current;
this.current = value;
atomDidChange(this, oldValue);
return value;
}
update(updater) {
return this.set(updater(this.current));
}
getDiffSince(epoch) {
maybeCaptureParent(this);
if (epoch >= this.lastChangedEpoch) {
return EMPTY_ARRAY;
}
return this.historyBuffer?.getChangesSince(epoch) ?? RESET_VALUE;
}
}
const _Atom = singleton("Atom", () => __Atom__);
function atom(name, initialValue, options) {
return new _Atom(name, initialValue, options);
}
let didWarnComputedGetter = false;
function logComputedGetterWarning() {
if (didWarnComputedGetter) return;
didWarnComputedGetter = true;
console.warn(
`Using \`@computed\` as a decorator for getters is deprecated and will be removed in the near future. Please refactor to use \`@computed\` as a decorator for methods.
// Before
@computed
get foo() {
return 'foo'
}
// After
@computed
getFoo() {
return 'foo'
}
`
);
}
const UNINITIALIZED = Symbol.for("com.tldraw.state/UNINITIALIZED");
function isUninitialized(value) {
return value === UNINITIALIZED;
}
const WithDiff = singleton(
"WithDiff",
() => (class WithDiff {
constructor(value, diff) {
this.value = value;
this.diff = diff;
}
})
);
function withDiff(value, diff) {
return new WithDiff(value, diff);
}
class __UNSAFE__Computed {
constructor(name, derive, options) {
this.name = name;
this.derive = derive;
if (options?.historyLength) {
this.historyBuffer = new HistoryBuffer(options.historyLength);
}
this.computeDiff = options?.computeDiff;
this.isEqual = options?.isEqual ?? equals;
}
lastChangedEpoch = GLOBAL_START_EPOCH;
lastTraversedEpoch = GLOBAL_START_EPOCH;
/**
* The epoch when the reactor was last checked.
*/
lastCheckedEpoch = GLOBAL_START_EPOCH;
parentSet = new ArraySet();
parents = [];
parentEpochs = [];
children = new ArraySet();
// eslint-disable-next-line no-restricted-syntax
get isActivelyListening() {
return !this.children.isEmpty;
}
historyBuffer;
// The last-computed value of this signal.
state = UNINITIALIZED;
// If the signal throws an error we stash it so we can rethrow it on the next get()
error = null;
computeDiff;
isEqual;
__unsafe__getWithoutCapture(ignoreErrors) {
const isNew = this.lastChangedEpoch === GLOBAL_START_EPOCH;
const globalEpoch = getGlobalEpoch();
if (!isNew && (this.lastCheckedEpoch === globalEpoch || this.isActivelyListening && getIsReacting() && this.lastTraversedEpoch < getReactionEpoch() || !haveParentsChanged(this))) {
this.lastCheckedEpoch = globalEpoch;
if (this.error) {
if (!ignoreErrors) {
throw this.error.thrownValue;
} else {
return this.state;
}
} else {
return this.state;
}
}
try {
startCapturingParents(this);
const result = this.derive(this.state, this.lastCheckedEpoch);
const newState = result instanceof WithDiff ? result.value : result;
const isUninitialized2 = this.state === UNINITIALIZED;
if (isUninitialized2 || !this.isEqual(newState, this.state)) {
if (this.historyBuffer && !isUninitialized2) {
const diff = result instanceof WithDiff ? result.diff : void 0;
this.historyBuffer.pushEntry(
this.lastChangedEpoch,
getGlobalEpoch(),
diff ?? this.computeDiff?.(this.state, newState, this.lastCheckedEpoch, getGlobalEpoch()) ?? RESET_VALUE
);
}
this.lastChangedEpoch = getGlobalEpoch();
this.state = newState;
}
this.error = null;
this.lastCheckedEpoch = getGlobalEpoch();
return this.state;
} catch (e) {
if (this.state !== UNINITIALIZED) {
this.state = UNINITIALIZED;
this.lastChangedEpoch = getGlobalEpoch();
}
this.lastCheckedEpoch = getGlobalEpoch();
if (this.historyBuffer) {
this.historyBuffer.clear();
}
this.error = { thrownValue: e };
if (!ignoreErrors) throw e;
return this.state;
} finally {
stopCapturingParents();
}
}
get() {
try {
return this.__unsafe__getWithoutCapture();
} finally {
maybeCaptureParent(this);
}
}
getDiffSince(epoch) {
this.__unsafe__getWithoutCapture(true);
maybeCaptureParent(this);
if (epoch >= this.lastChangedEpoch) {
return EMPTY_ARRAY;
}
return this.historyBuffer?.getChangesSince(epoch) ?? RESET_VALUE;
}
}
const _Computed = singleton("Computed", () => __UNSAFE__Computed);
function computedMethodLegacyDecorator(options = {}, _target, key, descriptor) {
const originalMethod = descriptor.value;
const derivationKey = Symbol.for("__@tldraw/state__computed__" + key);
descriptor.value = function() {
let d = this[derivationKey];
if (!d) {
d = new _Computed(key, originalMethod.bind(this), options);
Object.defineProperty(this, derivationKey, {
enumerable: false,
configurable: false,
writable: false,
value: d
});
}
return d.get();
};
descriptor.value[isComputedMethodKey] = true;
return descriptor;
}
function computedGetterLegacyDecorator(options = {}, _target, key, descriptor) {
const originalMethod = descriptor.get;
const derivationKey = Symbol.for("__@tldraw/state__computed__" + key);
descriptor.get = function() {
let d = this[derivationKey];
if (!d) {
d = new _Computed(key, originalMethod.bind(this), options);
Object.defineProperty(this, derivationKey, {
enumerable: false,
configurable: false,
writable: false,
value: d
});
}
return d.get();
};
return descriptor;
}
function computedMethodTc39Decorator(options, compute, context) {
assert(context.kind === "method", "@computed can only be used on methods");
const derivationKey = Symbol.for("__@tldraw/state__computed__" + String(context.name));
const fn = function() {
let d = this[derivationKey];
if (!d) {
d = new _Computed(String(context.name), compute.bind(this), options);
Object.defineProperty(this, derivationKey, {
enumerable: false,
configurable: false,
writable: false,
value: d
});
}
return d.get();
};
fn[isComputedMethodKey] = true;
return fn;
}
function computedDecorator(options = {}, args) {
if (args.length === 2) {
const [originalMethod, context] = args;
return computedMethodTc39Decorator(options, originalMethod, context);
} else {
const [_target, key, descriptor] = args;
if (descriptor.get) {
logComputedGetterWarning();
return computedGetterLegacyDecorator(options, _target, key, descriptor);
} else {
return computedMethodLegacyDecorator(options, _target, key, descriptor);
}
}
}
const isComputedMethodKey = "@@__isComputedMethod__@@";
function computed() {
if (arguments.length === 1) {
const options = arguments[0];
return (...args) => computedDecorator(options, args);
} else if (typeof arguments[0] === "string") {
return new _Computed(arguments[0], arguments[1], arguments[2]);
} else {
return computedDecorator(void 0, arguments);
}
}
function isSignal(value) {
return value instanceof _Atom || value instanceof _Computed;
}
const currentApiVersion = 1;
const actualApiVersion = singleton("apiVersion", () => currentApiVersion);
if (actualApiVersion !== currentApiVersion) {
throw new Error(
`You have multiple incompatible versions of @tldraw/state in your app. Please deduplicate the package.`
);
}
registerTldrawLibraryVersion(
"@tldraw/state",
"3.6.1",
"esm"
);
function useStateTracking(name, render, deps = []) {
const renderRef = React.useRef(render);
renderRef.current = render;
const [scheduler, subscribe, getSnapshot] = React.useMemo(() => {
let scheduleUpdate = null;
const subscribe2 = (cb) => {
scheduleUpdate = cb;
return () => {
scheduleUpdate = null;
};
};
const scheduler2 = new EffectScheduler(
`useStateTracking(${name})`,
// this is what `scheduler.execute()` will call
() => renderRef.current?.(),
// this is what will be invoked when @tldraw/state detects a change in an upstream reactive value
{
scheduleEffect() {
scheduleUpdate?.();
}
}
);
const getSnapshot2 = () => scheduler2.scheduleCount;
return [scheduler2, subscribe2, getSnapshot2];
}, [name, ...deps]);
React.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
React.useEffect(() => {
scheduler.attach();
scheduler.maybeScheduleEffect();
return () => {
scheduler.detach();
};
}, [scheduler]);
return scheduler.execute();
}
const ProxyHandlers = {
/**
* This is a function call trap for functional components. When this is called, we know it means
* React did run 'Component()', that means we can use any hooks here to setup our effect and
* store.
*
* With the native Proxy, all other calls such as access/setting to/of properties will be
* forwarded to the target Component, so we don't need to copy the Component's own or inherited
* properties.
*
* @see https://github.com/facebook/react/blob/2d80a0cd690bb5650b6c8a6c079a87b5dc42bd15/packages/react-reconciler/src/ReactFiberHooks.old.js#L460
*/
apply(Component, thisArg, argumentsList) {
return useStateTracking(
Component.displayName ?? Component.name ?? "tracked(???)",
() => Component.apply(thisArg, argumentsList)
);
}
};
const ReactMemoSymbol = Symbol.for("react.memo");
const ReactForwardRefSymbol = Symbol.for("react.forward_ref");
function track(baseComponent) {
let compare = null;
const $$typeof = baseComponent["$$typeof"];
if ($$typeof === ReactMemoSymbol) {
baseComponent = baseComponent.type;
compare = baseComponent.compare;
}
if ($$typeof === ReactForwardRefSymbol) {
return reactExports.memo(reactExports.forwardRef(new Proxy(baseComponent.render, ProxyHandlers)));
}
return reactExports.memo(new Proxy(baseComponent, ProxyHandlers), compare);
}
function useAtom(name, valueOrInitialiser, options) {
return reactExports.useState(() => {
const initialValue = typeof valueOrInitialiser === "function" ? valueOrInitialiser() : valueOrInitialiser;
return atom(`useAtom(${name})`, initialValue, options);
})[0];
}
function useComputed() {
const name = arguments[0];
const compute = arguments[1];
const opts = arguments.length === 3 ? void 0 : arguments[2];
const deps = arguments.length === 3 ? arguments[2] : arguments[3];
return reactExports.useMemo(() => computed(`useComputed(${name})`, compute, opts), deps);
}
function useQuickReactor(name, reactFn, deps = EMPTY_ARRAY) {
reactExports.useEffect(() => {
const scheduler = new EffectScheduler(name, reactFn);
scheduler.attach();
scheduler.execute();
return () => {
scheduler.detach();
};
}, deps);
}
function useValue() {
const args = arguments;
const deps = args.length === 3 ? args[2] : [args[0]];
const name = args.length === 3 ? args[0] : `useValue(${args[0].name})`;
const isInRender = reactExports.useRef(true);
isInRender.current = true;
const $val = reactExports.useMemo(() => {
if (args.length === 1) {
return args[0];
}
return computed(name, () => {
if (isInRender.current) {
return args[1]();
} else {
try {
return args[1]();
} catch {
return {};
}
}
});
}, deps);
try {
const { subscribe, getSnapshot } = reactExports.useMemo(() => {
return {
subscribe: (listen) => {
return react(`useValue(${name})`, () => {
$val.get();
listen();
});
},
getSnapshot: () => $val.get()
};
}, [$val]);
return reactExports.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
} finally {
isInRender.current = false;
}
}
registerTldrawLibraryVersion(
"@tldraw/state-react",
"3.6.1",
"esm"
);
class IncrementalSetConstructor {
constructor(previousValue) {
this.previousValue = previousValue;
}
/**
* The next value of the set.
*
* @internal
*/
nextValue;
/**
* The diff of the set.
*
* @internal
*/
diff;
/**
* Get the next value of the set.
*
* @public
*/
get() {
const numRemoved = this.diff?.removed?.size ?? 0;
const numAdded = this.diff?.added?.size ?? 0;
if (numRemoved === 0 && numAdded === 0) {
return void 0;
}
return { value: this.nextValue, diff: this.diff };
}
/**
* Add an item to the set.
*
* @param item - The item to add.
* @param wasAlreadyPresent - Whether the item was already present in the set.
* @internal
*/
_add(item, wasAlreadyPresent) {
this.nextValue ??= new Set(this.previousValue);
this.nextValue.add(item);
this.diff ??= {};
if (wasAlreadyPresent) {
this.diff.removed?.delete(item);
} else {
this.diff.added ??= /* @__PURE__ */ new Set();
this.diff.added.add(item);
}
}
/**
* Add an item to the set.
*
* @param item - The item to add.
* @public
*/
add(item) {
const wasAlreadyPresent = this.previousValue.has(item);
if (wasAlreadyPresent) {
const wasRemoved = this.diff?.removed?.has(item);
if (!wasRemoved) return;
return this._add(item, wasAlreadyPresent);
}
const isCurrentlyPresent = this.nextValue?.has(item);
if (isCurrentlyPresent) return;
this._add(item, wasAlreadyPresent);
}
/**
* Remove an item from the set.
*
* @param item - The item to remove.
* @param wasAlreadyPresent - Whether the item was already present in the set.
* @internal
*/
_remove(item, wasAlreadyPresent) {
this.nextValue ??= new Set(this.previousValue);
this.nextValue.delete(item);
this.diff ??= {};
if (wasAlreadyPresent) {
this.diff.removed ??= /* @__PURE__ */ new Set();
this.diff.removed.add(item);
} else {
this.diff.added?.delete(item);
}
}
/**
* Remove an item from the set.
*
* @param item - The item to remove.
* @public
*/
remove(item) {
const wasAlreadyPresent = this.previousValue.has(item);
if (!wasAlreadyPresent) {
const wasAdded = this.diff?.added?.has(item);
if (!wasAdded) return;
return this._remove(item, wasAlreadyPresent);
}
const hasAlreadyBeenRemoved = this.diff?.removed?.has(item);
if (hasAlreadyBeenRemoved) return;
this._remove(item, wasAlreadyPresent);
}
}
class RecordType {
constructor(typeName, config) {
this.typeName = typeName;
this.createDefaultProperties = config.createDefaultProperties;
this.validator = config.validator ?? { validate: (r) => r };
this.scope = config.scope ?? "document";
this.ephemeralKeys = config.ephemeralKeys;
const ephemeralKeySet = /* @__PURE__ */ new Set();
if (config.ephemeralKeys) {
for (const [key, isEphemeral] of objectMapEntries(config.ephemeralKeys)) {
if (isEphemeral) ephemeralKeySet.add(key);
}
}
this.ephemeralKeySet = ephemeralKeySet;
}
createDefaultProperties;
validator;
ephemeralKeys;
ephemeralKeySet;
scope;
/**
* Create a new record of this type.
*
* @param properties - The properties of the record.
* @returns The new record.
*/
create(properties) {
const result = { ...this.createDefaultProperties(), id: this.createId() };
for (const [k, v] of Object.entries(properties)) {
if (v !== void 0) {
result[k] = v;
}
}
result.typeName = this.typeName;
return result;
}
/**
* Clone a record of this type.
*
* @param record - The record to clone.
* @returns The cloned record.
* @public
*/
clone(record) {
return { ...structuredClone(record), id: this.createId() };
}
/**
* Create a new ID for this record type.
*
* @example
*
* ```ts
* const id = recordType.createId()
* ```
*
* @returns The new ID.
* @public
*/
createId(customUniquePart) {
return this.typeName + ":" + (customUniquePart ?? uniqueId());
}
/**
* Create a new ID for this record type based on the given ID.
*
* @example
*
* ```ts
* const id = recordType.createCustomId('myId')
* ```
*
* @deprecated - Use `createId` instead.
* @param id - The ID to base the new ID on.
* @returns The new ID.
*/
createCustomId(id) {
return this.typeName + ":" + id;
}
/**
* Takes an id like `user:123` and returns the part after the colon `123`
*
* @param id - The id
* @returns
*/
parseId(id) {
if (!this.isId(id)) {
throw new Error(`ID "${id}" is not a valid ID for type "${this.typeName}"`);
}
return id.slice(this.typeName.length + 1);
}
/**
* Check whether a record is an instance of this record type.
*
* @example
*
* ```ts
* const result = recordType.isInstance(someRecord)
* ```
*
* @param record - The record to check.
* @returns Whether the record is an instance of this record type.
*/
isInstance(record) {
return record?.typeName === this.typeName;
}
/**
* Check whether an id is an id of this type.
*
* @example
*
* ```ts
* const result = recordType.isIn('someId')
* ```
*
* @param id - The id to check.
* @returns Whether the id is an id of this type.
*/
isId(id) {
if (!id) return false;
for (let i = 0; i < this.typeName.length; i++) {
if (id[i] !== this.typeName[i]) return false;
}
return id[this.typeName.length] === ":";
}
/**
* Create a new RecordType that has the same type name as this RecordType and includes the given
* default properties.
*
* @example
*
* ```ts
* const authorType = createRecordType('author', () => ({ living: true }))
* const deadAuthorType = authorType.withDefaultProperties({ living: false })
* ```
*
* @param createDefaultProperties - A function that returns the default properties of the new RecordType.
* @returns The new RecordType.
*/
withDefaultProperties(createDefaultProperties) {
return new RecordType(this.typeName, {
createDefaultProperties,
validator: this.validator,
scope: this.scope,
ephemeralKeys: this.ephemeralKeys
});
}
/**
* Check that the passed in record passes the validations for this type. Returns its input
* correctly typed if it does, but throws an error otherwise.
*/
validate(record, recordBefore) {
if (recordBefore && this.validator.validateUsingKnownGoodVersion) {
return this.validator.validateUsingKnownGoodVersion(recordBefore, record);
}
return this.validator.validate(record);
}
}
function createRecordType(typeName, config) {
return new RecordType(typeName, {
createDefaultProperties: () => ({}),
validator: config.validator,
scope: config.scope,
ephemeralKeys: config.ephemeralKeys
});
}
function createEmptyRecordsDiff() {
return { added: {}, updated: {}, removed: {} };
}
function reverseRecordsDiff(diff) {
const result = { added: diff.removed, removed: diff.added, updated: {} };
for (const [from, to] of Object.values(diff.updated)) {
result.updated[from.id] = [to, from];
}
return result;
}
function isRecordsDiffEmpty(diff) {
return Object.keys(diff.added).length === 0 && Object.keys(diff.updated).length === 0 && Object.keys(diff.removed).length === 0;
}
function squashRecordDiffs(diffs) {
const result = { added: {}, removed: {}, updated: {} };
squashRecordDiffsMutable(result, diffs);
return result;
}
function squashRecordDiffsMutable(target, diffs) {
for (const diff of diffs) {
for (const [id, value] of objectMapEntries(diff.added)) {
if (target.removed[id]) {
const original = target.removed[id];
delete target.removed[id];
if (original !== value) {
target.updated[id] = [original, value];
}
} else {
target.added[id] = value;
}
}
for (const [id, [_from, to]] of objectMapEntries(diff.updated)) {
if (target.added[id]) {
target.added[id] = to;
delete target.updated[id];
delete target.removed[id];
continue;
}
if (target.updated[id]) {
target.updated[id] = [target.updated[id][0], to];
delete target.removed[id];
continue;
}
target.updated[id] = diff.updated[id];
delete target.removed[id];
}
for (const [id, value] of objectMapEntries(diff.removed)) {
if (target.added[id]) {
delete target.added[id];
} else if (target.updated[id]) {
target.removed[id] = target.updated[id][0];
delete target.updated[id];
} else {
target.removed[id] = value;
}
}
}
}
var lodash_isequal = {exports: {}};
/**
* Lodash (Custom Build)
* Build: `lodash modularize exports="npm" -o ./`
* Copyright JS Foundation and other contributors
* Released under MIT license
* Based on Underscore.js 1.8.3
* Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
*/
lodash_isequal.exports;
(function (module, exports) {
/** Used as the size to enable large array optimizations. */
var LARGE_ARRAY_SIZE = 200;
/** Used to stand-in for `undefined` hash values. */
var HASH_UNDEFINED = '__lodash_hash_undefined__';
/** Used to compose bitmasks for value comparisons. */
var COMPARE_PARTIAL_FLAG = 1,
COMPARE_UNORDERED_FLAG = 2;
/** Used as references for various `Number` constants. */
var MAX_SAFE_INTEGER = 9007199254740991;
/** `Object#toString` result references. */
var argsTag = '[object Arguments]',
arrayTag = '[object Array]',
asyncTag = '[object AsyncFunction]',
boolTag = '[object Boolean]',
dateTag = '[object Date]',
errorTag = '[object Error]',
funcTag = '[object Function]',
genTag = '[object GeneratorFunction]',
mapTag = '[object Map]',
numberTag = '[object Number]',
nullTag = '[object Null]',
objectTag = '[object Object]',
promiseTag = '[object Promise]',
proxyTag = '[object Proxy]',
regexpTag = '[object RegExp]',
setTag = '[object Set]',
stringTag = '[object String]',
symbolTag = '[object Symbol]',
undefinedTag = '[object Undefined]',
weakMapTag = '[object WeakMap]';
var arrayBufferTag = '[object ArrayBuffer]',
dataViewTag = '[object DataView]',
float32Tag = '[object Float32Array]',
float64Tag = '[object Float64Array]',
int8Tag = '[object Int8Array]',
int16Tag = '[object Int16Array]',
int32Tag = '[object Int32Array]',
uint8Tag = '[object Uint8Array]',
uint8ClampedTag = '[object Uint8ClampedArray]',
uint16Tag = '[object Uint16Array]',
uint32Tag = '[object Uint32Array]';
/**
* Used to match `RegExp`
* [syntax characters](http://ecma-international.org/ecma-262/7.0/#sec-patterns).
*/
var reRegExpChar = /[\\^$.*+?()[\]{}|]/g;
/** Used to detect host constructors (Safari). */
var reIsHostCtor = /^\[object .+?Constructor\]$/;
/** Used to detect unsigned integer values. */
var reIsUint = /^(?:0|[1-9]\d*)$/;
/** Used to identify `toStringTag` values of typed arrays. */
var typedArrayTags = {};
typedArrayTags[float32Tag] = typedArrayTags[float64Tag] =
typedArrayTags[int8Tag] = typedArrayTags[int16Tag] =
typedArrayTags[int32Tag] = typedArrayTags[uint8Tag] =
typedArrayTags[uint8ClampedTag] = typedArrayTags[uint16Tag] =
typedArrayTags[uint32Tag] = true;
typedArrayTags[argsTag] = typedArrayTags[arrayTag] =
typedArrayTags[arrayBufferTag] = typedArrayTags[boolTag] =
typedArrayTags[dataViewTag] = typedArrayTags[dateTag] =
typedArrayTags[errorTag] = typedArrayTags[funcTag] =
typedArrayTags[mapTag] = typedArrayTags[numberTag] =
typedArrayTags[objectTag] = typedArrayTags[regexpTag] =
typedArrayTags[setTag] = typedArrayTags[stringTag] =
typedArrayTags[weakMapTag] = false;
/** Detect free variable `global` from Node.js. */
var freeGlobal = typeof commonjsGlobal == 'object' && commonjsGlobal && commonjsGlobal.Object === Object && commonjsGlobal;
/** Detect free variable `self`. */
var freeSelf = typeof self == 'object' && self && self.Object === Object && self;
/** Used as a reference to the global object. */
var root = freeGlobal || freeSelf || Function('return this')();
/** Detect free variable `exports`. */
var freeExports = exports && !exports.nodeType && exports;
/** Detect free variable `module`. */
var freeModule = freeExports && 'object' == 'object' && module && !module.nodeType && module;
/** Detect the popular CommonJS extension `module.exports`. */
var moduleExports = freeModule && freeModule.exports === freeExports;
/** Detect free variable `process` from Node.js. */
var freeProcess = moduleExports && freeGlobal.process;
/** Used to access faster Node.js helpers. */
var nodeUtil = (function() {
try {
return freeProcess && freeProcess.binding && freeProcess.binding('util');
} catch (e) {}
}());
/* Node.js helper references. */
var nodeIsTypedArray = nodeUtil && nodeUtil.isTypedArray;
/**
* A specialized version of `_.filter` for arrays without support for
* iteratee shorthands.
*
* @private
* @param {Array} [array] The array to iterate over.
* @param {Function} predicate The function invoked per iteration.
* @returns {Array} Returns the new filtered array.
*/
function arrayFilter(array, predicate) {
var index = -1,
length = array == null ? 0 : array.length,
resIndex = 0,
result = [];
while (++index < length) {
var value = array[index];
if (predicate(value, index, array)) {
result[resIndex++] = value;
}
}
return result;
}
/**
* Appends the elements of `values` to `array`.
*
* @private
* @param {Array} array The array to modify.
* @param {Array} values The values to append.
* @returns {Array} Returns `array`.
*/
function arrayPush(array, values) {
var index = -1,
length = values.length,
offset = array.length;
while (++index < length) {
array[offset + index] = values[index];
}
return array;
}
/**
* A specialized version of `_.some` for arrays without support for iteratee
* shorthands.
*
* @private
* @param {Array} [array] The array to iterate over.
* @param {Function} predicate The function invoked per iteration.
* @returns {boolean} Returns `true` if any element passes the predicate check,
* else `false`.
*/
function arraySome(array, predicate) {
var index = -1,
length = array == null ? 0 : array.length;
while (++index < length) {
if (predicate(array[index], index, array)) {
return true;
}
}
return false;
}
/**
* The base implementation of `_.times` without support for iteratee shorthands
* or max array length checks.
*
* @private
* @param {number} n The number of times to invoke `iteratee`.
* @param {Function} iteratee The function invoked per iteration.
* @returns {Array} Returns the array of results.
*/
function baseTimes(n, iteratee) {
var index = -1,
result = Array(n);
while (++index < n) {
result[index] = iteratee(index);
}
return result;
}
/**
* The base implementation of `_.unary` without support for storing metadata.
*
* @private
* @param {Function} func The function to cap arguments for.
* @returns {Function} Returns the new capped function.
*/
function baseUnary(func) {
return function(value) {
return func(value);
};
}
/**
* Checks if a `cache` value for `key` exists.
*
* @private
* @param {Object} cache The cache to query.
* @param {string} key The key of the entry to check.
* @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.
*/
function cacheHas(cache, key) {
return cache.has(key);
}
/**
* Gets the value at `key` of `object`.
*
* @private
* @param {Object} [object] The object to query.
* @param {string} key The key of the property to get.
* @returns {*} Returns the property value.
*/
function getValue(object, key) {
return object == null ? undefined : object[key];
}
/**
* Converts `map` to its key-value pairs.
*
* @private
* @param {Object} map The map to convert.
* @returns {Array} Returns the key-value pairs.
*/
function mapToArray(map) {
var index = -1,
result = Array(map.size);
map.forEach(function(value, key) {
result[++index] = [key, value];
});
return result;
}
/**
* Creates a unary function that invokes `func` with its argument transformed.
*
* @private
* @param {Function} func The function to wrap.
* @param {Function} transform The argument transform.
* @returns {Function} Returns the new function.
*/
function overArg(func, transform) {
return function(arg) {
return func(transform(arg));
};
}
/**
* Converts `set` to an array of its values.
*
* @private
* @param {Object} set The set to convert.
* @returns {Array} Returns the values.
*/
function setToArray(set) {
var index = -1,
result = Array(set.size);
set.forEach(function(value) {
result[++index] = value;
});
return result;
}
/** Used for built-in method references. */
var arrayProto = Array.prototype,
funcProto = Function.prototype,
objectProto = Object.prototype;
/** Used to detect overreaching core-js shims. */
var coreJsData = root['__core-js_shared__'];
/** Used to resolve the decompiled source of functions. */
var funcToString = funcProto.toString;
/** Used to check objects for own properties. */
var hasOwnProperty = objectProto.hasOwnProperty;
/** Used to detect methods masquerading as native. */
var maskSrcKey = (function() {
var uid = /[^.]+$/.exec(coreJsData && coreJsData.keys && coreJsData.keys.IE_PROTO || '');
return uid ? ('Symbol(src)_1.' + uid) : '';
}());
/**
* Used to resolve the
* [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring)
* of values.
*/
var nativeObjectToString = objectProto.toString;
/** Used to detect if a method is native. */
var reIsNative = RegExp('^' +
funcToString.call(hasOwnProperty).replace(reRegExpChar, '\\$&')
.replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, '$1.*?') + '$'
);
/** Built-in value references. */
var Buffer = moduleExports ? root.Buffer : undefined,
Symbol = root.Symbol,
Uint8Array = root.Uint8Array,
propertyIsEnumerable = objectProto.propertyIsEnumerable,
splice = arrayProto.splice,
symToStringTag = Symbol ? Symbol.toStringTag : undefined;
/* Built-in method references for those with the same name as other `lodash` methods. */
var nativeGetSymbols = Object.getOwnPropertySymbols,
nativeIsBuffer = Buffer ? Buffer.isBuffer : undefined,
nativeKeys = overArg(Object.keys, Object);
/* Built-in method references that are verified to be native. */
var DataView = getNative(root, 'DataView'),
Map = getNative(root, 'Map'),
Promise = getNative(root, 'Promise'),
Set = getNative(root, 'Set'),
WeakMap = getNative(root, 'WeakMap'),
nativeCreate = getNative(Object, 'create');
/** Used to detect maps, sets, and weakmaps. */
var dataViewCtorString = toSource(DataView),
mapCtorString = toSource(Map),
promiseCtorString = toSource(Promise),
setCtorString = toSource(Set),
weakMapCtorString = toSource(WeakMap);
/** Used to convert symbols to primitives and strings. */
var symbolProto = Symbol ? Symbol.prototype : undefined,
symbolValueOf = symbolProto ? symbolProto.valueOf : undefined;
/**
* Creates a hash object.
*
* @private
* @constructor
* @param {Array} [entries] The key-value pairs to cache.
*/
function Hash(entries) {
var index = -1,
length = entries == null ? 0 : entries.length;
this.clear();
while (++index < length) {
var entry = entries[index];
this.set(entry[0], entry[1]);
}
}
/**
* Removes all key-value entries from the hash.
*
* @private
* @name clear
* @memberOf Hash
*/
function hashClear() {
this.__data__ = nativeCreate ? nativeCreate(null) : {};
this.size = 0;
}
/**
* Removes `key` and its value from the hash.
*
* @private
* @name delete
* @memberOf Hash
* @param {Object} hash The hash to modify.
* @param {string} key The key of the value to remove.
* @returns {boolean} Returns `true` if the entry was removed, else `false`.
*/
function hashDelete(key) {
var result = this.has(key) && delete this.__data__[key];
this.size -= result ? 1 : 0;
return result;
}
/**
* Gets the hash value for `key`.
*
* @private
* @name get
* @memberOf Hash
* @param {string} key The key of the value to get.
* @returns {*} Returns the entry value.
*/
function hashGet(key) {
var data = this.__data__;
if (nativeCreate) {
var result = data[key];
return result === HASH_UNDEFINED ? undefined : result;
}
return hasOwnProperty.call(data, key) ? data[key] : undefined;
}
/**
* Checks if a hash value for `key` exists.
*
* @private
* @name has
* @memberOf Hash
* @param {string} key The key of the entry to check.
* @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.
*/
function hashHas(key) {
var data = this.__data__;
return nativeCreate ? (data[key] !== undefined) : hasOwnProperty.call(data, key);
}
/**
* Sets the hash `key` to `value`.
*
* @private
* @name set
* @memberOf Hash
* @param {string} key The key of the value to set.
* @param {*} value The value to set.
* @returns {Object} Returns the hash instance.
*/
function hashSet(key, value) {
var data = this.__data__;
this.size += this.has(key) ? 0 : 1;
data[key] = (nativeCreate && value === undefined) ? HASH_UNDEFINED : value;
return this;
}
// Add methods to `Hash`.
Hash.prototype.clear = hashClear;
Hash.prototype['delete'] = hashDelete;
Hash.prototype.get = hashGet;
Hash.prototype.has = hashHas;
Hash.prototype.set = hashSet;
/**
* Creates an list cache object.
*
* @private
* @constructor
* @param {Array} [entries] The key-value pairs to cache.
*/
function ListCache(entries) {
var index = -1,
length = entries == null ? 0 : entries.length;
this.clear();
while (++index < length) {
var entry = entries[index];
this.set(entry[0], entry[1]);
}
}
/**
* Removes all key-value entries from the list cache.
*
* @private
* @name clear
* @memberOf ListCache
*/
function listCacheClear() {
this.__data__ = [];
this.size = 0;
}
/**
* Removes `key` and its value from the list cache.
*
* @private
* @name delete
* @memberOf ListCache
* @param {string} key The key of the value to remove.
* @returns {boolean} Returns `true` if the entry was removed, else `false`.
*/
function listCacheDelete(key) {
var data = this.__data__,
index = assocIndexOf(data, key);
if (index < 0) {
return false;
}
var lastIndex = data.length - 1;
if (index == lastIndex) {
data.pop();
} else {
splice.call(data, index, 1);
}
--this.size;
return true;
}
/**
* Gets the list cache value for `key`.
*
* @private
* @name get
* @memberOf ListCache
* @param {string} key The key of the value to get.
* @returns {*} Returns the entry value.
*/
function listCacheGet(key) {
var data = this.__data__,
index = assocIndexOf(data, key);
return index < 0 ? undefined : data[index][1];
}
/**
* Checks if a list cache value for `key` exists.
*
* @private
* @name has
* @memberOf ListCache
* @param {string} key The key of the entry to check.
* @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.
*/
function listCacheHas(key) {
return assocIndexOf(this.__data__, key) > -1;
}
/**
* Sets the list cache `key` to `value`.
*
* @private
* @name set
* @memberOf ListCache
* @param {string} key The key of the value to set.
* @param {*} value The value to set.
* @returns {Object} Returns the list cache instance.
*/
function listCacheSet(key, value) {
var data = this.__data__,
index = assocIndexOf(data, key);
if (index < 0) {
++this.size;
data.push([key, value]);
} else {
data[index][1] = value;
}
return this;
}
// Add methods to `ListCache`.
ListCache.prototype.clear = listCacheClear;
ListCache.prototype['delete'] = listCacheDelete;
ListCache.prototype.get = listCacheGet;
ListCache.prototype.has = listCacheHas;
ListCache.prototype.set = listCacheSet;
/**
* Creates a map cache object to store key-value pairs.
*
* @private
* @constructor
* @param {Array} [entries] The key-value pairs to cache.
*/
function MapCache(entries) {
var index = -1,
length = entries == null ? 0 : entries.length;
this.clear();
while (++index < length) {
var entry = entries[index];
this.set(entry[0], entry[1]);
}
}
/**
* Removes all key-value entries from the map.
*
* @private
* @name clear
* @memberOf MapCache
*/
function mapCacheClear() {
this.size = 0;
this.__data__ = {
'hash': new Hash,
'map': new (Map || ListCache),
'string': new Hash
};
}
/**
* Removes `key` and its value from the map.
*
* @private
* @name delete
* @memberOf MapCache
* @param {string} key The key of the value to remove.
* @returns {boolean} Returns `true` if the entry was removed, else `false`.
*/
function mapCacheDelete(key) {
var result = getMapData(this, key)['delete'](key);
this.size -= result ? 1 : 0;
return result;
}
/**
* Gets the map value for `key`.
*
* @private
* @name get
* @memberOf MapCache
* @param {string} key The key of the value to get.
* @returns {*} Returns the entry value.
*/
function mapCacheGet(key) {
return getMapData(this, key).get(key);
}
/**
* Checks if a map value for `key` exists.
*
* @private
* @name has
* @memberOf MapCache
* @param {string} key The key of the entry to check.
* @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.
*/
function mapCacheHas(key) {
return getMapData(this, key).has(key);
}
/**
* Sets the map `key` to `value`.
*
* @private
* @name set
* @memberOf MapCache
* @param {string} key The key of the value to set.
* @param {*} value The value to set.
* @returns {Object} Returns the map cache instance.
*/
function mapCacheSet(key, value) {
var data = getMapData(this, key),
size = data.size;
data.set(key, value);
this.size += data.size == size ? 0 : 1;
return this;
}
// Add methods to `MapCache`.
MapCache.prototype.clear = mapCacheClear;
MapCache.prototype['delete'] = mapCacheDelete;
MapCache.prototype.get = mapCacheGet;
MapCache.prototype.has = mapCacheHas;
MapCache.prototype.set = mapCacheSet;
/**
*
* Creates an array cache object to store unique values.
*
* @private
* @constructor
* @param {Array} [values] The values to cache.
*/
function SetCache(values) {
var index = -1,
length = values == null ? 0 : values.length;
this.__data__ = new MapCache;
while (++index < length) {
this.add(values[index]);
}
}
/**
* Adds `value` to the array cache.
*
* @private
* @name add
* @memberOf SetCache
* @alias push
* @param {*} value The value to cache.
* @returns {Object} Returns the cache instance.
*/
function setCacheAdd(value) {
this.__data__.set(value, HASH_UNDEFINED);
return this;
}
/**
* Checks if `value` is in the array cache.
*
* @private
* @name has
* @memberOf SetCache
* @param {*} value The value to search for.
* @returns {number} Returns `true` if `value` is found, else `false`.
*/
function setCacheHas(value) {
return this.__data__.has(value);
}
// Add methods to `SetCache`.
SetCache.prototype.add = SetCache.prototype.push = setCacheAdd;
SetCache.prototype.has = setCacheHas;
/**
* Creates a stack cache object to store key-value pairs.
*
* @private
* @constructor
* @param {Array} [entries] The key-value pairs to cache.
*/
function Stack(entries) {
var data = this.__data__ = new ListCache(entries);
this.size = data.size;
}
/**
* Removes all key-value entries from the stack.
*
* @private
* @name clear
* @memberOf Stack
*/
function stackClear() {
this.__data__ = new ListCache;
this.size = 0;
}
/**
* Removes `key` and its value from the stack.
*
* @private
* @name delete
* @memberOf Stack
* @param {string} key The key of the value to remove.
* @returns {boolean} Returns `true` if the entry was removed, else `false`.
*/
function stackDelete(key) {
var data = this.__data__,
result = data['delete'](key);
this.size = data.size;
return result;
}
/**
* Gets the stack value for `key`.
*
* @private
* @name get
* @memberOf Stack
* @param {string} key The key of the value to get.
* @returns {*} Returns the entry value.
*/
function stackGet(key) {
return this.__data__.get(key);
}
/**
* Checks if a stack value for `key` exists.
*
* @private
* @name has
* @memberOf Stack
* @param {string} key The key of the entry to check.
* @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.
*/
function stackHas(key) {
return this.__data__.has(key);
}
/**
* Sets the stack `key` to `value`.
*
* @private
* @name set
* @memberOf Stack
* @param {string} key The key of the value to set.
* @param {*} value The value to set.
* @returns {Object} Returns the stack cache instance.
*/
function stackSet(key, value) {
var data = this.__data__;
if (data instanceof ListCache) {
var pairs = data.__data__;
if (!Map || (pairs.length < LARGE_ARRAY_SIZE - 1)) {
pairs.push([key, value]);
this.size = ++data.size;
return this;
}
data = this.__data__ = new MapCache(pairs);
}
data.set(key, value);
this.size = data.size;
return this;
}
// Add methods to `Stack`.
Stack.prototype.clear = stackClear;
Stack.prototype['delete'] = stackDelete;
Stack.prototype.get = stackGet;
Stack.prototype.has = stackHas;
Stack.prototype.set = stackSet;
/**
* Creates an array of the enumerable property names of the array-like `value`.
*
* @private
* @param {*} value The value to query.
* @param {boolean} inherited Specify returning inherited property names.
* @returns {Array} Returns the array of property names.
*/
function arrayLikeKeys(value, inherited) {
var isArr = isArray(value),
isArg = !isArr && isArguments(value),
isBuff = !isArr && !isArg && isBuffer(value),
isType = !isArr && !isArg && !isBuff && isTypedArray(value),
skipIndexes = isArr || isArg || isBuff || isType,
result = skipIndexes ? baseTimes(value.length, String) : [],
length = result.length;
for (var key in value) {
if ((hasOwnProperty.call(value, key)) &&
!(skipIndexes && (
// Safari 9 has enumerable `arguments.length` in strict mode.
key == 'length' ||
// Node.js 0.10 has enumerable non-index properties on buffers.
(isBuff && (key == 'offset' || key == 'parent')) ||
// PhantomJS 2 has enumerable non-index properties on typed arrays.
(isType && (key == 'buffer' || key == 'byteLength' || key == 'byteOffset')) ||
// Skip index properties.
isIndex(key, length)
))) {
result.push(key);
}
}
return result;
}
/**
* Gets the index at which the `key` is found in `array` of key-value pairs.
*
* @private
* @param {Array} array The array to inspect.
* @param {*} key The key to search for.
* @returns {number} Returns the index of the matched value, else `-1`.
*/
function assocIndexOf(array, key) {
var length = array.length;
while (length--) {
if (eq(array[length][0], key)) {
return length;
}
}
return -1;
}
/**
* The base implementation of `getAllKeys` and `getAllKeysIn` which uses
* `keysFunc` and `symbolsFunc` to get the enumerable property names and
* symbols of `object`.
*
* @private
* @param {Object} object The object to query.
* @param {Function} keysFunc The function to get the keys of `object`.
* @param {Function} symbolsFunc The function to get the symbols of `object`.
* @returns {Array} Returns the array of property names and symbols.
*/
function baseGetAllKeys(object, keysFunc, symbolsFunc) {
var result = keysFunc(object);
return isArray(object) ? result : arrayPush(result, symbolsFunc(object));
}
/**
* The base implementation of `getTag` without fallbacks for buggy environments.
*
* @private
* @param {*} value The value to query.
* @returns {string} Returns the `toStringTag`.
*/
function baseGetTag(value) {
if (value == null) {
return value === undefined ? undefinedTag : nullTag;
}
return (symToStringTag && symToStringTag in Object(value))
? getRawTag(value)
: objectToString(value);
}
/**
* The base implementation of `_.isArguments`.
*
* @private
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is an `arguments` object,
*/
function baseIsArguments(value) {
return isObjectLike(value) && baseGetTag(value) == argsTag;
}
/**
* The base implementation of `_.isEqual` which supports partial comparisons
* and tracks traversed objects.
*
* @private
* @param {*} value The value to compare.
* @param {*} other The other value to compare.
* @param {boolean} bitmask The bitmask flags.
* 1 - Unordered comparison
* 2 - Partial comparison
* @param {Function} [customizer] The function to customize comparisons.
* @param {Object} [stack] Tracks traversed `value` and `other` objects.
* @returns {boolean} Returns `true` if the values are equivalent, else `false`.
*/
function baseIsEqual(value, other, bitmask, customizer, stack) {
if (value === other) {
return true;
}
if (value == null || other == null || (!isObjectLike(value) && !isObjectLike(other))) {
return value !== value && other !== other;
}
return baseIsEqualDeep(value, other, bitmask, customizer, baseIsEqual, stack);
}
/**
* A specialized version of `baseIsEqual` for arrays and objects which performs
* deep comparisons and tracks traversed objects enabling objects with circular
* references to be compared.
*
* @private
* @param {Object} object The object to compare.
* @param {Object} other The other object to compare.
* @param {number} bitmask The bitmask flags. See `baseIsEqual` for more details.
* @param {Function} customizer The function to customize comparisons.
* @param {Function} equalFunc The function to determine equivalents of values.
* @param {Object} [stack] Tracks traversed `object` and `other` objects.
* @returns {boolean} Returns `true` if the objects are equivalent, else `false`.
*/
function baseIsEqualDeep(object, other, bitmask, customizer, equalFunc, stack) {
var objIsArr = isArray(object),
othIsArr = isArray(other),
objTag = objIsArr ? arrayTag : getTag(object),
othTag = othIsArr ? arrayTag : getTag(other);
objTag = objTag == argsTag ? objectTag : objTag;
othTag = othTag == argsTag ? objectTag : othTag;
var objIsObj = objTag == objectTag,
othIsObj = othTag == objectTag,
isSameTag = objTag == othTag;
if (isSameTag && isBuffer(object)) {
if (!isBuffer(other)) {
return false;
}
objIsArr = true;
objIsObj = false;
}
if (isSameTag && !objIsObj) {
stack || (stack = new Stack);
return (objIsArr || isTypedArray(object))
? equalArrays(object, other, bitmask, customizer, equalFunc, stack)
: equalByTag(object, other, objTag, bitmask, customizer, equalFunc, stack);
}
if (!(bitmask & COMPARE_PARTIAL_FLAG)) {
var objIsWrapped = objIsObj && hasOwnProperty.call(object, '__wrapped__'),
othIsWrapped = othIsObj && hasOwnProperty.call(other, '__wrapped__');
if (objIsWrapped || othIsWrapped) {
var objUnwrapped = objIsWrapped ? object.value() : object,
othUnwrapped = othIsWrapped ? other.value() : other;
stack || (stack = new Stack);
return equalFunc(objUnwrapped, othUnwrapped, bitmask, customizer, stack);
}
}
if (!isSameTag) {
return false;
}
stack || (stack = new Stack);
return equalObjects(object, other, bitmask, customizer, equalFunc, stack);
}
/**
* The base implementation of `_.isNative` without bad shim checks.
*
* @private
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a native function,
* else `false`.
*/
function baseIsNative(value) {
if (!isObject(value) || isMasked(value)) {
return false;
}
var pattern = isFunction(value) ? reIsNative : reIsHostCtor;
return pattern.test(toSource(value));
}
/**
* The base implementation of `_.isTypedArray` without Node.js optimizations.
*
* @private
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a typed array, else `false`.
*/
function baseIsTypedArray(value) {
return isObjectLike(value) &&
isLength(value.length) && !!typedArrayTags[baseGetTag(value)];
}
/**
* The base implementation of `_.keys` which doesn't treat sparse arrays as dense.
*
* @private
* @param {Object} object The object to query.
* @returns {Array} Returns the array of property names.
*/
function baseKeys(object) {
if (!isPrototype(object)) {
return nativeKeys(object);
}
var result = [];
for (var key in Object(object)) {
if (hasOwnProperty.call(object, key) && key != 'constructor') {
result.push(key);
}
}
return result;
}
/**
* A specialized version of `baseIsEqualDeep` for arrays with support for
* partial deep comparisons.
*
* @private
* @param {Array} array The array to compare.
* @param {Array} other The other array to compare.
* @param {number} bitmask The bitmask flags. See `baseIsEqual` for more details.
* @param {Function} customizer The function to customize comparisons.
* @param {Function} equalFunc The function to determine equivalents of values.
* @param {Object} stack Tracks traversed `array` and `other` objects.
* @returns {boolean} Returns `true` if the arrays are equivalent, else `false`.
*/
function equalArrays(array, other, bitmask, customizer, equalFunc, stack) {
var isPartial = bitmask & COMPARE_PARTIAL_FLAG,
arrLength = array.length,
othLength = other.length;
if (arrLength != othLength && !(isPartial && othLength > arrLength)) {
return false;
}
// Assume cyclic values are equal.
var stacked = stack.get(array);
if (stacked && stack.get(other)) {
return stacked == other;
}
var index = -1,
result = true,
seen = (bitmask & COMPARE_UNORDERED_FLAG) ? new SetCache : undefined;
stack.set(array, other);
stack.set(other, array);
// Ignore non-index properties.
while (++index < arrLength) {
var arrValue = array[index],
othValue = other[index];
if (customizer) {
var compared = isPartial
? customizer(othValue, arrValue, index, other, array, stack)
: customizer(arrValue, othValue, index, array, other, stack);
}
if (compared !== undefined) {
if (compared) {
continue;
}
result = false;
break;
}
// Recursively compare arrays (susceptible to call stack limits).
if (seen) {
if (!arraySome(other, function(othValue, othIndex) {
if (!cacheHas(seen, othIndex) &&
(arrValue === othValue || equalFunc(arrValue, othValue, bitmask, customizer, stack))) {
return seen.push(othIndex);
}
})) {
result = false;
break;
}
} else if (!(
arrValue === othValue ||
equalFunc(arrValue, othValue, bitmask, customizer, stack)
)) {
result = false;
break;
}
}
stack['delete'](array);
stack['delete'](other);
return result;
}
/**
* A specialized version of `baseIsEqualDeep` for comparing objects of
* the same `toStringTag`.
*
* **Note:** This function only supports comparing values with tags of
* `Boolean`, `Date`, `Error`, `Number`, `RegExp`, or `String`.
*
* @private
* @param {Object} object The object to compare.
* @param {Object} other The other object to compare.
* @param {string} tag The `toStringTag` of the objects to compare.
* @param {number} bitmask The bitmask flags. See `baseIsEqual` for more details.
* @param {Function} customizer The function to customize comparisons.
* @param {Function} equalFunc The function to determine equivalents of values.
* @param {Object} stack Tracks traversed `object` and `other` objects.
* @returns {boolean} Returns `true` if the objects are equivalent, else `false`.
*/
function equalByTag(object, other, tag, bitmask, customizer, equalFunc, stack) {
switch (tag) {
case dataViewTag:
if ((object.byteLength != other.byteLength) ||
(object.byteOffset != other.byteOffset)) {
return false;
}
object = object.buffer;
other = other.buffer;
case arrayBufferTag:
if ((object.byteLength != other.byteLength) ||
!equalFunc(new Uint8Array(object), new Uint8Array(other))) {
return false;
}
return true;
case boolTag:
case dateTag:
case numberTag:
// Coerce booleans to `1` or `0` and dates to milliseconds.
// Invalid dates are coerced to `NaN`.
return eq(+object, +other);
case errorTag:
return object.name == other.name && object.message == other.message;
case regexpTag:
case stringTag:
// Coerce regexes to strings and treat strings, primitives and objects,
// as equal. See http://www.ecma-international.org/ecma-262/7.0/#sec-regexp.prototype.tostring
// for more details.
return object == (other + '');
case mapTag:
var convert = mapToArray;
case setTag:
var isPartial = bitmask & COMPARE_PARTIAL_FLAG;
convert || (convert = setToArray);
if (object.size != other.size && !isPartial) {
return false;
}
// Assume cyclic values are equal.
var stacked = stack.get(object);
if (stacked) {
return stacked == other;
}
bitmask |= COMPARE_UNORDERED_FLAG;
// Recursively compare objects (susceptible to call stack limits).
stack.set(object, other);
var result = equalArrays(convert(object), convert(other), bitmask, customizer, equalFunc, stack);
stack['delete'](object);
return result;
case symbolTag:
if (symbolValueOf) {
return symbolValueOf.call(object) == symbolValueOf.call(other);
}
}
return false;
}
/**
* A specialized version of `baseIsEqualDeep` for objects with support for
* partial deep comparisons.
*
* @private
* @param {Object} object The object to compare.
* @param {Object} other The other object to compare.
* @param {number} bitmask The bitmask flags. See `baseIsEqual` for more details.
* @param {Function} customizer The function to customize comparisons.
* @param {Function} equalFunc The function to determine equivalents of values.
* @param {Object} stack Tracks traversed `object` and `other` objects.
* @returns {boolean} Returns `true` if the objects are equivalent, else `false`.
*/
function equalObjects(object, other, bitmask, customizer, equalFunc, stack) {
var isPartial = bitmask & COMPARE_PARTIAL_FLAG,
objProps = getAllKeys(object),
objLength = objProps.length,
othProps = getAllKeys(other),
othLength = othProps.length;
if (objLength != othLength && !isPartial) {
return false;
}
var index = objLength;
while (index--) {
var key = objProps[index];
if (!(isPartial ? key in other : hasOwnProperty.call(other, key))) {
return false;
}
}
// Assume cyclic values are equal.
var stacked = stack.get(object);
if (stacked && stack.get(other)) {
return stacked == other;
}
var result = true;
stack.set(object, other);
stack.set(other, object);
var skipCtor = isPartial;
while (++index < objLength) {
key = objProps[index];
var objValue = object[key],
othValue = other[key];
if (customizer) {
var compared = isPartial
? customizer(othValue, objValue, key, other, object, stack)
: customizer(objValue, othValue, key, object, other, stack);
}
// Recursively compare objects (susceptible to call stack limits).
if (!(compared === undefined
? (objValue === othValue || equalFunc(objValue, othValue, bitmask, customizer, stack))
: compared
)) {
result = false;
break;
}
skipCtor || (skipCtor = key == 'constructor');
}
if (result && !skipCtor) {
var objCtor = object.constructor,
othCtor = other.constructor;
// Non `Object` object instances with different constructors are not equal.
if (objCtor != othCtor &&
('constructor' in object && 'constructor' in other) &&
!(typeof objCtor == 'function' && objCtor instanceof objCtor &&
typeof othCtor == 'function' && othCtor instanceof othCtor)) {
result = false;
}
}
stack['delete'](object);
stack['delete'](other);
return result;
}
/**
* Creates an array of own enumerable property names and symbols of `object`.
*
* @private
* @param {Object} object The object to query.
* @returns {Array} Returns the array of property names and symbols.
*/
function getAllKeys(object) {
return baseGetAllKeys(object, keys, getSymbols);
}
/**
* Gets the data for `map`.
*
* @private
* @param {Object} map The map to query.
* @param {string} key The reference key.
* @returns {*} Returns the map data.
*/
function getMapData(map, key) {
var data = map.__data__;
return isKeyable(key)
? data[typeof key == 'string' ? 'string' : 'hash']
: data.map;
}
/**
* Gets the native function at `key` of `object`.
*
* @private
* @param {Object} object The object to query.
* @param {string} key The key of the method to get.
* @returns {*} Returns the function if it's native, else `undefined`.
*/
function getNative(object, key) {
var value = getValue(object, key);
return baseIsNative(value) ? value : undefined;
}
/**
* A specialized version of `baseGetTag` which ignores `Symbol.toStringTag` values.
*
* @private
* @param {*} value The value to query.
* @returns {string} Returns the raw `toStringTag`.
*/
function getRawTag(value) {
var isOwn = hasOwnProperty.call(value, symToStringTag),
tag = value[symToStringTag];
try {
value[symToStringTag] = undefined;
var unmasked = true;
} catch (e) {}
var result = nativeObjectToString.call(value);
if (unmasked) {
if (isOwn) {
value[symToStringTag] = tag;
} else {
delete value[symToStringTag];
}
}
return result;
}
/**
* Creates an array of the own enumerable symbols of `object`.
*
* @private
* @param {Object} object The object to query.
* @returns {Array} Returns the array of symbols.
*/
var getSymbols = !nativeGetSymbols ? stubArray : function(object) {
if (object == null) {
return [];
}
object = Object(object);
return arrayFilter(nativeGetSymbols(object), function(symbol) {
return propertyIsEnumerable.call(object, symbol);
});
};
/**
* Gets the `toStringTag` of `value`.
*
* @private
* @param {*} value The value to query.
* @returns {string} Returns the `toStringTag`.
*/
var getTag = baseGetTag;
// Fallback for data views, maps, sets, and weak maps in IE 11 and promises in Node.js < 6.
if ((DataView && getTag(new DataView(new ArrayBuffer(1))) != dataViewTag) ||
(Map && getTag(new Map) != mapTag) ||
(Promise && getTag(Promise.resolve()) != promiseTag) ||
(Set && getTag(new Set) != setTag) ||
(WeakMap && getTag(new WeakMap) != weakMapTag)) {
getTag = function(value) {
var result = baseGetTag(value),
Ctor = result == objectTag ? value.constructor : undefined,
ctorString = Ctor ? toSource(Ctor) : '';
if (ctorString) {
switch (ctorString) {
case dataViewCtorString: return dataViewTag;
case mapCtorString: return mapTag;
case promiseCtorString: return promiseTag;
case setCtorString: return setTag;
case weakMapCtorString: return weakMapTag;
}
}
return result;
};
}
/**
* Checks if `value` is a valid array-like index.
*
* @private
* @param {*} value The value to check.
* @param {number} [length=MAX_SAFE_INTEGER] The upper bounds of a valid index.
* @returns {boolean} Returns `true` if `value` is a valid index, else `false`.
*/
function isIndex(value, length) {
length = length == null ? MAX_SAFE_INTEGER : length;
return !!length &&
(typeof value == 'number' || reIsUint.test(value)) &&
(value > -1 && value % 1 == 0 && value < length);
}
/**
* Checks if `value` is suitable for use as unique object key.
*
* @private
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is suitable, else `false`.
*/
function isKeyable(value) {
var type = typeof value;
return (type == 'string' || type == 'number' || type == 'symbol' || type == 'boolean')
? (value !== '__proto__')
: (value === null);
}
/**
* Checks if `func` has its source masked.
*
* @private
* @param {Function} func The function to check.
* @returns {boolean} Returns `true` if `func` is masked, else `false`.
*/
function isMasked(func) {
return !!maskSrcKey && (maskSrcKey in func);
}
/**
* Checks if `value` is likely a prototype object.
*
* @private
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a prototype, else `false`.
*/
function isPrototype(value) {
var Ctor = value && value.constructor,
proto = (typeof Ctor == 'function' && Ctor.prototype) || objectProto;
return value === proto;
}
/**
* Converts `value` to a string using `Object.prototype.toString`.
*
* @private
* @param {*} value The value to convert.
* @returns {string} Returns the converted string.
*/
function objectToString(value) {
return nativeObjectToString.call(value);
}
/**
* Converts `func` to its source code.
*
* @private
* @param {Function} func The function to convert.
* @returns {string} Returns the source code.
*/
function toSource(func) {
if (func != null) {
try {
return funcToString.call(func);
} catch (e) {}
try {
return (func + '');
} catch (e) {}
}
return '';
}
/**
* Performs a
* [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)
* comparison between two values to determine if they are equivalent.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Lang
* @param {*} value The value to compare.
* @param {*} other The other value to compare.
* @returns {boolean} Returns `true` if the values are equivalent, else `false`.
* @example
*
* var object = { 'a': 1 };
* var other = { 'a': 1 };
*
* _.eq(object, object);
* // => true
*
* _.eq(object, other);
* // => false
*
* _.eq('a', 'a');
* // => true
*
* _.eq('a', Object('a'));
* // => false
*
* _.eq(NaN, NaN);
* // => true
*/
function eq(value, other) {
return value === other || (value !== value && other !== other);
}
/**
* Checks if `value` is likely an `arguments` object.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is an `arguments` object,
* else `false`.
* @example
*
* _.isArguments(function() { return arguments; }());
* // => true
*
* _.isArguments([1, 2, 3]);
* // => false
*/
var isArguments = baseIsArguments(function() { return arguments; }()) ? baseIsArguments : function(value) {
return isObjectLike(value) && hasOwnProperty.call(value, 'callee') &&
!propertyIsEnumerable.call(value, 'callee');
};
/**
* Checks if `value` is classified as an `Array` object.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is an array, else `false`.
* @example
*
* _.isArray([1, 2, 3]);
* // => true
*
* _.isArray(document.body.children);
* // => false
*
* _.isArray('abc');
* // => false
*
* _.isArray(_.noop);
* // => false
*/
var isArray = Array.isArray;
/**
* Checks if `value` is array-like. A value is considered array-like if it's
* not a function and has a `value.length` that's an integer greater than or
* equal to `0` and less than or equal to `Number.MAX_SAFE_INTEGER`.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is array-like, else `false`.
* @example
*
* _.isArrayLike([1, 2, 3]);
* // => true
*
* _.isArrayLike(document.body.children);
* // => true
*
* _.isArrayLike('abc');
* // => true
*
* _.isArrayLike(_.noop);
* // => false
*/
function isArrayLike(value) {
return value != null && isLength(value.length) && !isFunction(value);
}
/**
* Checks if `value` is a buffer.
*
* @static
* @memberOf _
* @since 4.3.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a buffer, else `false`.
* @example
*
* _.isBuffer(new Buffer(2));
* // => true
*
* _.isBuffer(new Uint8Array(2));
* // => false
*/
var isBuffer = nativeIsBuffer || stubFalse;
/**
* Performs a deep comparison between two values to determine if they are
* equivalent.
*
* **Note:** This method supports comparing arrays, array buffers, booleans,
* date objects, error objects, maps, numbers, `Object` objects, regexes,
* sets, strings, symbols, and typed arrays. `Object` objects are compared
* by their own, not inherited, enumerable properties. Functions and DOM
* nodes are compared by strict equality, i.e. `===`.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Lang
* @param {*} value The value to compare.
* @param {*} other The other value to compare.
* @returns {boolean} Returns `true` if the values are equivalent, else `false`.
* @example
*
* var object = { 'a': 1 };
* var other = { 'a': 1 };
*
* _.isEqual(object, other);
* // => true
*
* object === other;
* // => false
*/
function isEqual(value, other) {
return baseIsEqual(value, other);
}
/**
* Checks if `value` is classified as a `Function` object.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a function, else `false`.
* @example
*
* _.isFunction(_);
* // => true
*
* _.isFunction(/abc/);
* // => false
*/
function isFunction(value) {
if (!isObject(value)) {
return false;
}
// The use of `Object#toString` avoids issues with the `typeof` operator
// in Safari 9 which returns 'object' for typed arrays and other constructors.
var tag = baseGetTag(value);
return tag == funcTag || tag == genTag || tag == asyncTag || tag == proxyTag;
}
/**
* Checks if `value` is a valid array-like length.
*
* **Note:** This method is loosely based on
* [`ToLength`](http://ecma-international.org/ecma-262/7.0/#sec-tolength).
*
* @static
* @memberOf _
* @since 4.0.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a valid length, else `false`.
* @example
*
* _.isLength(3);
* // => true
*
* _.isLength(Number.MIN_VALUE);
* // => false
*
* _.isLength(Infinity);
* // => false
*
* _.isLength('3');
* // => false
*/
function isLength(value) {
return typeof value == 'number' &&
value > -1 && value % 1 == 0 && value <= MAX_SAFE_INTEGER;
}
/**
* Checks if `value` is the
* [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types)
* of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`)
*
* @static
* @memberOf _
* @since 0.1.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is an object, else `false`.
* @example
*
* _.isObject({});
* // => true
*
* _.isObject([1, 2, 3]);
* // => true
*
* _.isObject(_.noop);
* // => true
*
* _.isObject(null);
* // => false
*/
function isObject(value) {
var type = typeof value;
return value != null && (type == 'object' || type == 'function');
}
/**
* Checks if `value` is object-like. A value is object-like if it's not `null`
* and has a `typeof` result of "object".
*
* @static
* @memberOf _
* @since 4.0.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is object-like, else `false`.
* @example
*
* _.isObjectLike({});
* // => true
*
* _.isObjectLike([1, 2, 3]);
* // => true
*
* _.isObjectLike(_.noop);
* // => false
*
* _.isObjectLike(null);
* // => false
*/
function isObjectLike(value) {
return value != null && typeof value == 'object';
}
/**
* Checks if `value` is classified as a typed array.
*
* @static
* @memberOf _
* @since 3.0.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a typed array, else `false`.
* @example
*
* _.isTypedArray(new Uint8Array);
* // => true
*
* _.isTypedArray([]);
* // => false
*/
var isTypedArray = nodeIsTypedArray ? baseUnary(nodeIsTypedArray) : baseIsTypedArray;
/**
* Creates an array of the own enumerable property names of `object`.
*
* **Note:** Non-object values are coerced to objects. See the
* [ES spec](http://ecma-international.org/ecma-262/7.0/#sec-object.keys)
* for more details.
*
* @static
* @since 0.1.0
* @memberOf _
* @category Object
* @param {Object} object The object to query.
* @returns {Array} Returns the array of property names.
* @example
*
* function Foo() {
* this.a = 1;
* this.b = 2;
* }
*
* Foo.prototype.c = 3;
*
* _.keys(new Foo);
* // => ['a', 'b'] (iteration order is not guaranteed)
*
* _.keys('hi');
* // => ['0', '1']
*/
function keys(object) {
return isArrayLike(object) ? arrayLikeKeys(object) : baseKeys(object);
}
/**
* This method returns a new empty array.
*
* @static
* @memberOf _
* @since 4.13.0
* @category Util
* @returns {Array} Returns the new empty array.
* @example
*
* var arrays = _.times(2, _.stubArray);
*
* console.log(arrays);
* // => [[], []]
*
* console.log(arrays[0] === arrays[1]);
* // => false
*/
function stubArray() {
return [];
}
/**
* This method returns `false`.
*
* @static
* @memberOf _
* @since 4.13.0
* @category Util
* @returns {boolean} Returns `false`.
* @example
*
* _.times(2, _.stubFalse);
* // => [false, false]
*/
function stubFalse() {
return false;
}
module.exports = isEqual;
} (lodash_isequal, lodash_isequal.exports));
var lodash_isequalExports = lodash_isequal.exports;
const isEqual = /*@__PURE__*/getDefaultExportFromCjs(lodash_isequalExports);
function intersectSets(sets) {
if (sets.length === 0) return /* @__PURE__ */ new Set();
const first = sets[0];
const rest = sets.slice(1);
const result = /* @__PURE__ */ new Set();
for (const val of first) {
if (rest.every((set) => set.has(val))) {
result.add(val);
}
}
return result;
}
function diffSets(prev, next) {
const result = {};
for (const val of next) {
if (!prev.has(val)) {
result.added ??= /* @__PURE__ */ new Set();
result.added.add(val);
}
}
for (const val of prev) {
if (!next.has(val)) {
result.removed ??= /* @__PURE__ */ new Set();
result.removed.add(val);
}
}
return result.added || result.removed ? result : void 0;
}
function objectMatchesQuery(query, object) {
for (const [key, _matcher] of Object.entries(query)) {
const matcher = _matcher;
const value = object[key];
if ("eq" in matcher && value !== matcher.eq) return false;
if ("neq" in matcher && value === matcher.neq) return false;
if ("gt" in matcher && (typeof value !== "number" || value <= matcher.gt)) return false;
}
return true;
}
function executeQuery(store, typeName, query) {
const matchIds = Object.fromEntries(Object.keys(query).map((key) => [key, /* @__PURE__ */ new Set()]));
for (const [k, matcher] of Object.entries(query)) {
if ("eq" in matcher) {
const index = store.index(typeName, k);
const ids = index.get().get(matcher.eq);
if (ids) {
for (const id of ids) {
matchIds[k].add(id);
}
}
} else if ("neq" in matcher) {
const index = store.index(typeName, k);
for (const [value, ids] of index.get()) {
if (value !== matcher.neq) {
for (const id of ids) {
matchIds[k].add(id);
}
}
}
} else if ("gt" in matcher) {
const index = store.index(typeName, k);
for (const [value, ids] of index.get()) {
if (value > matcher.gt) {
for (const id of ids) {
matchIds[k].add(id);
}
}
}
}
}
return intersectSets(Object.values(matchIds));
}
class StoreQueries {
constructor(atoms, history) {
this.atoms = atoms;
this.history = history;
}
/**
* A cache of derivations (indexes).
*
* @internal
*/
indexCache = /* @__PURE__ */ new Map();
/**
* A cache of derivations (filtered histories).
*
* @internal
*/
historyCache = /* @__PURE__ */ new Map();
/**
* Create a derivation that contains the history for a given type
*
* @param typeName - The name of the type to filter by.
* @returns A derivation that returns the ids of all records of the given type.
* @public
*/
filterHistory(typeName) {
if (this.historyCache.has(typeName)) {
return this.historyCache.get(typeName);
}
const filtered = computed(
"filterHistory:" + typeName,
(lastValue, lastComputedEpoch) => {
if (isUninitialized(lastValue)) {
return this.history.get();
}
const diff = this.history.getDiffSince(lastComputedEpoch);
if (diff === RESET_VALUE) return this.history.get();
const res = { added: {}, removed: {}, updated: {} };
let numAdded = 0;
let numRemoved = 0;
let numUpdated = 0;
for (const changes of diff) {
for (const added of objectMapValues(changes.added)) {
if (added.typeName === typeName) {
if (res.removed[added.id]) {
const original = res.removed[added.id];
delete res.removed[added.id];
numRemoved--;
if (original !== added) {
res.updated[added.id] = [original, added];
numUpdated++;
}
} else {
res.added[added.id] = added;
numAdded++;
}
}
}
for (const [from, to] of objectMapValues(changes.updated)) {
if (to.typeName === typeName) {
if (res.added[to.id]) {
res.added[to.id] = to;
} else if (res.updated[to.id]) {
res.updated[to.id] = [res.updated[to.id][0], to];
} else {
res.updated[to.id] = [from, to];
numUpdated++;
}
}
}
for (const removed of objectMapValues(changes.removed)) {
if (removed.typeName === typeName) {
if (res.added[removed.id]) {
delete res.added[removed.id];
numAdded--;
} else if (res.updated[removed.id]) {
res.removed[removed.id] = res.updated[removed.id][0];
delete res.updated[removed.id];
numUpdated--;
numRemoved++;
} else {
res.removed[removed.id] = removed;
numRemoved++;
}
}
}
}
if (numAdded || numRemoved || numUpdated) {
return withDiff(this.history.get(), res);
} else {
return lastValue;
}
},
{ historyLength: 100 }
);
this.historyCache.set(typeName, filtered);
return filtered;
}
/**
* Create a derivation that returns an index on a property for the given type.
*
* @param typeName - The name of the type.
* @param property - The name of the property.
* @public
*/
index(typeName, property) {
const cacheKey = typeName + ":" + property;
if (this.indexCache.has(cacheKey)) {
return this.indexCache.get(cacheKey);
}
const index = this.__uncached_createIndex(typeName, property);
this.indexCache.set(cacheKey, index);
return index;
}
/**
* Create a derivation that returns an index on a property for the given type.
*
* @param typeName - The name of the type?.
* @param property - The name of the property?.
* @internal
*/
__uncached_createIndex(typeName, property) {
const typeHistory = this.filterHistory(typeName);
const fromScratch = () => {
typeHistory.get();
const res = /* @__PURE__ */ new Map();
for (const atom of objectMapValues(this.atoms.get())) {
const record = atom.get();
if (record.typeName === typeName) {
const value = record[property];
if (!res.has(value)) {
res.set(value, /* @__PURE__ */ new Set());
}
res.get(value).add(record.id);
}
}
return res;
};
return computed(
"index:" + typeName + ":" + property,
(prevValue, lastComputedEpoch) => {
if (isUninitialized(prevValue)) return fromScratch();
const history = typeHistory.getDiffSince(lastComputedEpoch);
if (history === RESET_VALUE) {
return fromScratch();
}
const setConstructors = /* @__PURE__ */ new Map();
const add = (value, id) => {
let setConstructor = setConstructors.get(value);
if (!setConstructor)
setConstructor = new IncrementalSetConstructor(
prevValue.get(value) ?? /* @__PURE__ */ new Set()
);
setConstructor.add(id);
setConstructors.set(value, setConstructor);
};
const remove = (value, id) => {
let set = setConstructors.get(value);
if (!set) set = new IncrementalSetConstructor(prevValue.get(value) ?? /* @__PURE__ */ new Set());
set.remove(id);
setConstructors.set(value, set);
};
for (const changes of history) {
for (const record of objectMapValues(changes.added)) {
if (record.typeName === typeName) {
const value = record[property];
add(value, record.id);
}
}
for (const [from, to] of objectMapValues(changes.updated)) {
if (to.typeName === typeName) {
const prev = from[property];
const next = to[property];
if (prev !== next) {
remove(prev, to.id);
add(next, to.id);
}
}
}
for (const record of objectMapValues(changes.removed)) {
if (record.typeName === typeName) {
const value = record[property];
remove(value, record.id);
}
}
}
let nextValue = void 0;
let nextDiff = void 0;
for (const [value, setConstructor] of setConstructors) {
const result = setConstructor.get();
if (!result) continue;
if (!nextValue) nextValue = new Map(prevValue);
if (!nextDiff) nextDiff = /* @__PURE__ */ new Map();
if (result.value.size === 0) {
nextValue.delete(value);
} else {
nextValue.set(value, result.value);
}
nextDiff.set(value, result.diff);
}
if (nextValue && nextDiff) {
return withDiff(nextValue, nextDiff);
}
return prevValue;
},
{ historyLength: 100 }
);
}
/**
* Create a derivation that will return a signle record matching the given query.
*
* It will return undefined if there is no matching record
*
* @param typeName - The name of the type?
* @param queryCreator - A function that returns the query expression.
* @param name - (optinal) The name of the query.
*/
record(typeName, queryCreator = () => ({}), name = "record:" + typeName + (queryCreator ? ":" + queryCreator.toString() : "")) {
const ids = this.ids(typeName, queryCreator, name);
return computed(name, () => {
for (const id of ids.get()) {
return this.atoms.get()[id]?.get();
}
return void 0;
});
}
/**
* Create a derivation that will return an array of records matching the given query
*
* @param typeName - The name of the type?
* @param queryCreator - A function that returns the query expression.
* @param name - (optinal) The name of the query.
*/
records(typeName, queryCreator = () => ({}), name = "records:" + typeName + (queryCreator ? ":" + queryCreator.toString() : "")) {
const ids = this.ids(typeName, queryCreator, "ids:" + name);
return computed(name, () => {
return [...ids.get()].map((id) => {
const atom = this.atoms.get()[id];
if (!atom) {
throw new Error("no atom found for record id: " + id);
}
return atom.get();
});
});
}
/**
* Create a derivation that will return the ids of all records of the given type.
*
* @param typeName - The name of the type.
* @param queryCreator - A function that returns the query expression.
* @param name - (optinal) The name of the query.
*/
ids(typeName, queryCreator = () => ({}), name = "ids:" + typeName + (queryCreator ? ":" + queryCreator.toString() : "")) {
const typeHistory = this.filterHistory(typeName);
const fromScratch = () => {
typeHistory.get();
const query = queryCreator();
if (Object.keys(query).length === 0) {
return new Set(
objectMapValues(this.atoms.get()).flatMap((v) => {
const r = v.get();
if (r.typeName === typeName) {
return r.id;
} else {
return [];
}
})
);
}
return executeQuery(this, typeName, query);
};
const fromScratchWithDiff = (prevValue) => {
const nextValue = fromScratch();
const diff = diffSets(prevValue, nextValue);
if (diff) {
return withDiff(nextValue, diff);
} else {
return prevValue;
}
};
const cachedQuery = computed("ids_query:" + name, queryCreator, {
isEqual
});
return computed(
"query:" + name,
(prevValue, lastComputedEpoch) => {
const query = cachedQuery.get();
if (isUninitialized(prevValue)) {
return fromScratch();
}
if (lastComputedEpoch < cachedQuery.lastChangedEpoch) {
return fromScratchWithDiff(prevValue);
}
const history = typeHistory.getDiffSince(lastComputedEpoch);
if (history === RESET_VALUE) {
return fromScratchWithDiff(prevValue);
}
const setConstructor = new IncrementalSetConstructor(
prevValue
);
for (const changes of history) {
for (const added of objectMapValues(changes.added)) {
if (added.typeName === typeName && objectMatchesQuery(query, added)) {
setConstructor.add(added.id);
}
}
for (const [_, updated] of objectMapValues(changes.updated)) {
if (updated.typeName === typeName) {
if (objectMatchesQuery(query, updated)) {
setConstructor.add(updated.id);
} else {
setConstructor.remove(updated.id);
}
}
}
for (const removed of objectMapValues(changes.removed)) {
if (removed.typeName === typeName) {
setConstructor.remove(removed.id);
}
}
}
const result = setConstructor.get();
if (!result) {
return prevValue;
}
return withDiff(result.value, result.diff);
},
{ historyLength: 50 }
);
}
exec(typeName, query) {
const ids = executeQuery(this, typeName, query);
if (ids.size === 0) {
return EMPTY_ARRAY;
}
const atoms = this.atoms.get();
return [...ids].map((id) => atoms[id].get());
}
}
class StoreSideEffects {
constructor(store) {
this.store = store;
}
_beforeCreateHandlers = {};
_afterCreateHandlers = {};
_beforeChangeHandlers = {};
_afterChangeHandlers = {};
_beforeDeleteHandlers = {};
_afterDeleteHandlers = {};
_operationCompleteHandlers = [];
_isEnabled = true;
/** @internal */
isEnabled() {
return this._isEnabled;
}
/** @internal */
setIsEnabled(enabled) {
this._isEnabled = enabled;
}
/** @internal */
handleBeforeCreate(record, source) {
if (!this._isEnabled) return record;
const handlers = this._beforeCreateHandlers[record.typeName];
if (handlers) {
let r = record;
for (const handler of handlers) {
r = handler(r, source);
}
return r;
}
return record;
}
/** @internal */
handleAfterCreate(record, source) {
if (!this._isEnabled) return;
const handlers = this._afterCreateHandlers[record.typeName];
if (handlers) {
for (const handler of handlers) {
handler(record, source);
}
}
}
/** @internal */
handleBeforeChange(prev, next, source) {
if (!this._isEnabled) return next;
const handlers = this._beforeChangeHandlers[next.typeName];
if (handlers) {
let r = next;
for (const handler of handlers) {
r = handler(prev, r, source);
}
return r;
}
return next;
}
/** @internal */
handleAfterChange(prev, next, source) {
if (!this._isEnabled) return;
const handlers = this._afterChangeHandlers[next.typeName];
if (handlers) {
for (const handler of handlers) {
handler(prev, next, source);
}
}
}
/** @internal */
handleBeforeDelete(record, source) {
if (!this._isEnabled) return true;
const handlers = this._beforeDeleteHandlers[record.typeName];
if (handlers) {
for (const handler of handlers) {
if (handler(record, source) === false) {
return false;
}
}
}
return true;
}
/** @internal */
handleAfterDelete(record, source) {
if (!this._isEnabled) return;
const handlers = this._afterDeleteHandlers[record.typeName];
if (handlers) {
for (const handler of handlers) {
handler(record, source);
}
}
}
/** @internal */
handleOperationComplete(source) {
if (!this._isEnabled) return;
for (const handler of this._operationCompleteHandlers) {
handler(source);
}
}
/**
* Internal helper for registering a bunch of side effects at once and keeping them organized.
* @internal
*/
register(handlersByType) {
const disposes = [];
for (const [type, handlers] of Object.entries(handlersByType)) {
if (handlers?.beforeCreate) {
disposes.push(this.registerBeforeCreateHandler(type, handlers.beforeCreate));
}
if (handlers?.afterCreate) {
disposes.push(this.registerAfterCreateHandler(type, handlers.afterCreate));
}
if (handlers?.beforeChange) {
disposes.push(this.registerBeforeChangeHandler(type, handlers.beforeChange));
}
if (handlers?.afterChange) {
disposes.push(this.registerAfterChangeHandler(type, handlers.afterChange));
}
if (handlers?.beforeDelete) {
disposes.push(this.registerBeforeDeleteHandler(type, handlers.beforeDelete));
}
if (handlers?.afterDelete) {
disposes.push(this.registerAfterDeleteHandler(type, handlers.afterDelete));
}
}
return () => {
for (const dispose of disposes) dispose();
};
}
/**
* Register a handler to be called before a record of a certain type is created. Return a
* modified record from the handler to change the record that will be created.
*
* Use this handle only to modify the creation of the record itself. If you want to trigger a
* side-effect on a different record (for example, moving one shape when another is created),
* use {@link StoreSideEffects.registerAfterCreateHandler} instead.
*
* @example
* ```ts
* editor.sideEffects.registerBeforeCreateHandler('shape', (shape, source) => {
* // only modify shapes created by the user
* if (source !== 'user') return shape
*
* //by default, arrow shapes have no label. Let's make sure they always have a label.
* if (shape.type === 'arrow') {
* return {...shape, props: {...shape.props, text: 'an arrow'}}
* }
*
* // other shapes get returned unmodified
* return shape
* })
* ```
*
* @param typeName - The type of record to listen for
* @param handler - The handler to call
*
* @returns A callback that removes the handler.
*/
registerBeforeCreateHandler(typeName, handler) {
const handlers = this._beforeCreateHandlers[typeName];
if (!handlers) this._beforeCreateHandlers[typeName] = [];
this._beforeCreateHandlers[typeName].push(handler);
return () => remove(this._beforeCreateHandlers[typeName], handler);
}
/**
* Register a handler to be called after a record is created. This is useful for side-effects
* that would update _other_ records. If you want to modify the record being created use
* {@link StoreSideEffects.registerBeforeCreateHandler} instead.
*
* @example
* ```ts
* editor.sideEffects.registerAfterCreateHandler('page', (page, source) => {
* // Automatically create a shape when a page is created
* editor.createShape({
* id: createShapeId(),
* type: 'text',
* props: { text: page.name },
* })
* })
* ```
*
* @param typeName - The type of record to listen for
* @param handler - The handler to call
*
* @returns A callback that removes the handler.
*/
registerAfterCreateHandler(typeName, handler) {
const handlers = this._afterCreateHandlers[typeName];
if (!handlers) this._afterCreateHandlers[typeName] = [];
this._afterCreateHandlers[typeName].push(handler);
return () => remove(this._afterCreateHandlers[typeName], handler);
}
/**
* Register a handler to be called before a record is changed. The handler is given the old and
* new record - you can return a modified record to apply a different update, or the old record
* to block the update entirely.
*
* Use this handler only for intercepting updates to the record itself. If you want to update
* other records in response to a change, use
* {@link StoreSideEffects.registerAfterChangeHandler} instead.
*
* @example
* ```ts
* editor.sideEffects.registerBeforeChangeHandler('shape', (prev, next, source) => {
* if (next.isLocked && !prev.isLocked) {
* // prevent shapes from ever being locked:
* return prev
* }
* // other types of change are allowed
* return next
* })
* ```
*
* @param typeName - The type of record to listen for
* @param handler - The handler to call
*
* @returns A callback that removes the handler.
*/
registerBeforeChangeHandler(typeName, handler) {
const handlers = this._beforeChangeHandlers[typeName];
if (!handlers) this._beforeChangeHandlers[typeName] = [];
this._beforeChangeHandlers[typeName].push(handler);
return () => remove(this._beforeChangeHandlers[typeName], handler);
}
/**
* Register a handler to be called after a record is changed. This is useful for side-effects
* that would update _other_ records - if you want to modify the record being changed, use
* {@link StoreSideEffects.registerBeforeChangeHandler} instead.
*
* @example
* ```ts
* editor.sideEffects.registerAfterChangeHandler('shape', (prev, next, source) => {
* if (next.props.color === 'red') {
* // there can only be one red shape at a time:
* const otherRedShapes = editor.getCurrentPageShapes().filter(s => s.props.color === 'red' && s.id !== next.id)
* editor.updateShapes(otherRedShapes.map(s => ({...s, props: {...s.props, color: 'blue'}})))
* }
* })
* ```
*
* @param typeName - The type of record to listen for
* @param handler - The handler to call
*
* @returns A callback that removes the handler.
*/
registerAfterChangeHandler(typeName, handler) {
const handlers = this._afterChangeHandlers[typeName];
if (!handlers) this._afterChangeHandlers[typeName] = [];
this._afterChangeHandlers[typeName].push(handler);
return () => remove(this._afterChangeHandlers[typeName], handler);
}
/**
* Register a handler to be called before a record is deleted. The handler can return `false` to
* prevent the deletion.
*
* Use this handler only for intercepting deletions of the record itself. If you want to do
* something to other records in response to a deletion, use
* {@link StoreSideEffects.registerAfterDeleteHandler} instead.
*
* @example
* ```ts
* editor.sideEffects.registerBeforeDeleteHandler('shape', (shape, source) => {
* if (shape.props.color === 'red') {
* // prevent red shapes from being deleted
* return false
* }
* })
* ```
*
* @param typeName - The type of record to listen for
* @param handler - The handler to call
*
* @returns A callback that removes the handler.
*/
registerBeforeDeleteHandler(typeName, handler) {
const handlers = this._beforeDeleteHandlers[typeName];
if (!handlers) this._beforeDeleteHandlers[typeName] = [];
this._beforeDeleteHandlers[typeName].push(handler);
return () => remove(this._beforeDeleteHandlers[typeName], handler);
}
/**
* Register a handler to be called after a record is deleted. This is useful for side-effects
* that would update _other_ records - if you want to block the deletion of the record itself,
* use {@link StoreSideEffects.registerBeforeDeleteHandler} instead.
*
* @example
* ```ts
* editor.sideEffects.registerAfterDeleteHandler('shape', (shape, source) => {
* // if the last shape in a frame is deleted, delete the frame too:
* const parentFrame = editor.getShape(shape.parentId)
* if (!parentFrame || parentFrame.type !== 'frame') return
*
* const siblings = editor.getSortedChildIdsForParent(parentFrame)
* if (siblings.length === 0) {
* editor.deleteShape(parentFrame.id)
* }
* })
* ```
*
* @param typeName - The type of record to listen for
* @param handler - The handler to call
*
* @returns A callback that removes the handler.
*/
registerAfterDeleteHandler(typeName, handler) {
const handlers = this._afterDeleteHandlers[typeName];
if (!handlers) this._afterDeleteHandlers[typeName] = [];
this._afterDeleteHandlers[typeName].push(handler);
return () => remove(this._afterDeleteHandlers[typeName], handler);
}
/**
* Register a handler to be called when a store completes an atomic operation.
*
* @example
* ```ts
* let count = 0
*
* editor.sideEffects.registerOperationCompleteHandler(() => count++)
*
* editor.selectAll()
* expect(count).toBe(1)
*
* editor.store.atomic(() => {
* editor.selectNone()
* editor.selectAll()
* })
*
* expect(count).toBe(2)
* ```
*
* @param handler - The handler to call
*
* @returns A callback that removes the handler.
*
* @public
*/
registerOperationCompleteHandler(handler) {
this._operationCompleteHandlers.push(handler);
return () => remove(this._operationCompleteHandlers, handler);
}
}
function remove(array, item) {
const index = array.indexOf(item);
if (index >= 0) {
array.splice(index, 1);
}
}
function devFreeze(object) {
{
return object;
}
}
class Store {
/**
* The random id of the store.
*/
id;
/**
* An atom containing the store's atoms.
*
* @internal
* @readonly
*/
atoms = atom("store_atoms", {});
/**
* An atom containing the store's history.
*
* @public
* @readonly
*/
history = atom("history", 0, {
historyLength: 1e3
});
/**
* A StoreQueries instance for this store.
*
* @public
* @readonly
*/
query = new StoreQueries(this.atoms, this.history);
/**
* A set containing listeners that have been added to this store.
*
* @internal
*/
listeners = /* @__PURE__ */ new Set();
/**
* An array of history entries that have not yet been flushed.
*
* @internal
*/
historyAccumulator = new HistoryAccumulator();
/**
* A reactor that responds to changes to the history by squashing the accumulated history and
* notifying listeners of the changes.
*
* @internal
*/
historyReactor;
/**
* Function to dispose of any in-flight timeouts.
*
* @internal
*/
cancelHistoryReactor() {
}
schema;
props;
scopedTypes;
sideEffects = new StoreSideEffects(this);
constructor(config) {
const { initialData, schema, id } = config;
this.id = id ?? uniqueId();
this.schema = schema;
this.props = config.props;
if (initialData) {
this.atoms.set(
objectMapFromEntries(
objectMapEntries(initialData).map(([id2, record]) => [
id2,
atom(
"atom:" + id2,
devFreeze(this.schema.validateRecord(this, record, "initialize", null))
)
])
)
);
}
this.historyReactor = reactor(
"Store.historyReactor",
() => {
this.history.get();
this._flushHistory();
},
{ scheduleEffect: (cb) => this.cancelHistoryReactor = throttleToNextFrame$1(cb) }
);
this.scopedTypes = {
document: new Set(
objectMapValues(this.schema.types).filter((t) => t.scope === "document").map((t) => t.typeName)
),
session: new Set(
objectMapValues(this.schema.types).filter((t) => t.scope === "session").map((t) => t.typeName)
),
presence: new Set(
objectMapValues(this.schema.types).filter((t) => t.scope === "presence").map((t) => t.typeName)
)
};
}
_flushHistory() {
if (this.historyAccumulator.hasChanges()) {
const entries = this.historyAccumulator.flush();
for (const { changes, source } of entries) {
let instanceChanges = null;
let documentChanges = null;
let presenceChanges = null;
for (const { onHistory, filters } of this.listeners) {
if (filters.source !== "all" && filters.source !== source) {
continue;
}
if (filters.scope !== "all") {
if (filters.scope === "document") {
documentChanges ??= this.filterChangesByScope(changes, "document");
if (!documentChanges) continue;
onHistory({ changes: documentChanges, source });
} else if (filters.scope === "session") {
instanceChanges ??= this.filterChangesByScope(changes, "session");
if (!instanceChanges) continue;
onHistory({ changes: instanceChanges, source });
} else {
presenceChanges ??= this.filterChangesByScope(changes, "presence");
if (!presenceChanges) continue;
onHistory({ changes: presenceChanges, source });
}
} else {
onHistory({ changes, source });
}
}
}
}
}
dispose() {
this.cancelHistoryReactor();
}
/**
* Filters out non-document changes from a diff. Returns null if there are no changes left.
* @param change - the records diff
* @param scope - the records scope
* @returns
*/
filterChangesByScope(change, scope) {
const result = {
added: filterEntries(change.added, (_, r) => this.scopedTypes[scope].has(r.typeName)),
updated: filterEntries(change.updated, (_, r) => this.scopedTypes[scope].has(r[1].typeName)),
removed: filterEntries(change.removed, (_, r) => this.scopedTypes[scope].has(r.typeName))
};
if (Object.keys(result.added).length === 0 && Object.keys(result.updated).length === 0 && Object.keys(result.removed).length === 0) {
return null;
}
return result;
}
/**
* Update the history with a diff of changes.
*
* @param changes - The changes to add to the history.
*/
updateHistory(changes) {
this.historyAccumulator.add({
changes,
source: this.isMergingRemoteChanges ? "remote" : "user"
});
if (this.listeners.size === 0) {
this.historyAccumulator.clear();
}
this.history.set(this.history.get() + 1, changes);
}
validate(phase) {
this.allRecords().forEach((record) => this.schema.validateRecord(this, record, phase, null));
}
/**
* Add some records to the store. It's an error if they already exist.
*
* @param records - The records to add.
* @param phaseOverride - The phase override.
* @public
*/
put(records, phaseOverride) {
this.atomic(() => {
const updates = {};
const additions = {};
const currentMap = this.atoms.__unsafe__getWithoutCapture();
let map = null;
let record;
let didChange = false;
const source = this.isMergingRemoteChanges ? "remote" : "user";
for (let i = 0, n = records.length; i < n; i++) {
record = records[i];
const recordAtom = (map ?? currentMap)[record.id];
if (recordAtom) {
const initialValue = recordAtom.__unsafe__getWithoutCapture();
record = this.sideEffects.handleBeforeChange(initialValue, record, source);
const validated = this.schema.validateRecord(
this,
record,
phaseOverride ?? "updateRecord",
initialValue
);
if (validated === initialValue) continue;
recordAtom.set(devFreeze(record));
didChange = true;
const updated = recordAtom.__unsafe__getWithoutCapture();
updates[record.id] = [initialValue, updated];
this.addDiffForAfterEvent(initialValue, updated);
} else {
record = this.sideEffects.handleBeforeCreate(record, source);
didChange = true;
record = this.schema.validateRecord(
this,
record,
phaseOverride ?? "createRecord",
null
);
additions[record.id] = record;
this.addDiffForAfterEvent(null, record);
if (!map) {
map = { ...currentMap };
}
map[record.id] = atom("atom:" + record.id, record);
}
}
if (map) {
this.atoms.set(map);
}
if (!didChange) return;
this.updateHistory({
added: additions,
updated: updates,
removed: {}
});
});
}
/**
* Remove some records from the store via their ids.
*
* @param ids - The ids of the records to remove.
* @public
*/
remove(ids) {
this.atomic(() => {
const cancelled = /* @__PURE__ */ new Set();
const source = this.isMergingRemoteChanges ? "remote" : "user";
if (this.sideEffects.isEnabled()) {
for (const id of ids) {
const atom2 = this.atoms.__unsafe__getWithoutCapture()[id];
if (!atom2) continue;
if (this.sideEffects.handleBeforeDelete(atom2.get(), source) === false) {
cancelled.add(id);
}
}
}
let removed = void 0;
this.atoms.update((atoms) => {
let result = void 0;
for (const id of ids) {
if (cancelled.has(id)) continue;
if (!(id in atoms)) continue;
if (!result) result = { ...atoms };
if (!removed) removed = {};
delete result[id];
const record = atoms[id].get();
removed[id] = record;
this.addDiffForAfterEvent(record, null);
}
return result ?? atoms;
});
if (!removed) return;
this.updateHistory({ added: {}, updated: {}, removed });
});
}
/**
* Get the value of a store record by its id.
*
* @param id - The id of the record to get.
* @public
*/
get(id) {
return this.atoms.get()[id]?.get();
}
/**
* Get the value of a store record by its id without updating its epoch.
*
* @param id - The id of the record to get.
* @public
*/
unsafeGetWithoutCapture(id) {
return this.atoms.__unsafe__getWithoutCapture()[id]?.__unsafe__getWithoutCapture();
}
/**
* Creates a JSON payload from the record store.
*
* @param scope - The scope of records to serialize. Defaults to 'document'.
* @returns The record store snapshot as a JSON payload.
*/
serialize(scope = "document") {
const result = {};
for (const [id, atom2] of objectMapEntries(this.atoms.get())) {
const record = atom2.get();
if (scope === "all" || this.scopedTypes[scope].has(record.typeName)) {
result[id] = record;
}
}
return result;
}
/**
* Get a serialized snapshot of the store and its schema.
*
* ```ts
* const snapshot = store.getStoreSnapshot()
* store.loadStoreSnapshot(snapshot)
* ```
*
* @param scope - The scope of records to serialize. Defaults to 'document'.
*
* @public
*/
getStoreSnapshot(scope = "document") {
return {
store: this.serialize(scope),
schema: this.schema.serialize()
};
}
/**
* @deprecated use `getSnapshot` from the 'tldraw' package instead.
*/
getSnapshot(scope = "document") {
console.warn(
"[tldraw] `Store.getSnapshot` is deprecated and will be removed in a future release. Use `getSnapshot` from the `tldraw` package instead."
);
return this.getStoreSnapshot(scope);
}
/**
* Migrate a serialized snapshot of the store and its schema.
*
* ```ts
* const snapshot = store.getSnapshot()
* store.migrateSnapshot(snapshot)
* ```
*
* @param snapshot - The snapshot to load.
* @public
*/
migrateSnapshot(snapshot) {
const migrationResult = this.schema.migrateStoreSnapshot(snapshot);
if (migrationResult.type === "error") {
throw new Error(`Failed to migrate snapshot: ${migrationResult.reason}`);
}
return {
store: migrationResult.value,
schema: this.schema.serialize()
};
}
/**
* Load a serialized snapshot.
*
* ```ts
* const snapshot = store.getStoreSnapshot()
* store.loadStoreSnapshot(snapshot)
* ```
*
* @param snapshot - The snapshot to load.
* @public
*/
loadStoreSnapshot(snapshot) {
const migrationResult = this.schema.migrateStoreSnapshot(snapshot);
if (migrationResult.type === "error") {
throw new Error(`Failed to migrate snapshot: ${migrationResult.reason}`);
}
const prevSideEffectsEnabled = this.sideEffects.isEnabled();
try {
this.sideEffects.setIsEnabled(false);
this.atomic(() => {
this.clear();
this.put(Object.values(migrationResult.value));
this.ensureStoreIsUsable();
});
} finally {
this.sideEffects.setIsEnabled(prevSideEffectsEnabled);
}
}
/**
* @public
* @deprecated use `loadSnapshot` from the 'tldraw' package instead.
*/
loadSnapshot(snapshot) {
console.warn(
"[tldraw] `Store.loadSnapshot` is deprecated and will be removed in a future release. Use `loadSnapshot` from the 'tldraw' package instead."
);
this.loadStoreSnapshot(snapshot);
}
/**
* Get an array of all values in the store.
*
* @returns An array of all values in the store.
* @public
*/
allRecords() {
return objectMapValues(this.atoms.get()).map((atom2) => atom2.get());
}
/**
* Removes all records from the store.
*
* @public
*/
clear() {
this.remove(objectMapKeys(this.atoms.get()));
}
/**
* Update a record. To update multiple records at once, use the `update` method of the
* `TypedStore` class.
*
* @param id - The id of the record to update.
* @param updater - A function that updates the record.
*/
update(id, updater) {
const atom2 = this.atoms.get()[id];
if (!atom2) {
console.error(`Record ${id} not found. This is probably an error`);
return;
}
this.put([updater(atom2.__unsafe__getWithoutCapture())]);
}
/**
* Get whether the record store has a id.
*
* @param id - The id of the record to check.
* @public
*/
has(id) {
return !!this.atoms.get()[id];
}
/**
* Add a new listener to the store.
*
* @param onHistory - The listener to call when the store updates.
* @param filters - Filters to apply to the listener.
* @returns A function to remove the listener.
*/
listen(onHistory, filters) {
this._flushHistory();
const listener = {
onHistory,
filters: {
source: filters?.source ?? "all",
scope: filters?.scope ?? "all"
}
};
this.listeners.add(listener);
if (!this.historyReactor.scheduler.isActivelyListening) {
this.historyReactor.start();
}
return () => {
this.listeners.delete(listener);
if (this.listeners.size === 0) {
this.historyReactor.stop();
}
};
}
isMergingRemoteChanges = false;
/**
* Merge changes from a remote source without triggering listeners.
*
* @param fn - A function that merges the external changes.
* @public
*/
mergeRemoteChanges(fn) {
if (this.isMergingRemoteChanges) {
return fn();
}
if (this._isInAtomicOp) {
throw new Error("Cannot merge remote changes while in atomic operation");
}
try {
this.isMergingRemoteChanges = true;
transact(fn);
} finally {
this.isMergingRemoteChanges = false;
this.ensureStoreIsUsable();
}
}
/**
* Run `fn` and return a {@link RecordsDiff} of the changes that occurred as a result.
*/
extractingChanges(fn) {
const changes = [];
const dispose = this.historyAccumulator.addInterceptor((entry) => changes.push(entry.changes));
try {
transact(fn);
return squashRecordDiffs(changes);
} finally {
dispose();
}
}
applyDiff(diff, {
runCallbacks = true,
ignoreEphemeralKeys = false
} = {}) {
this.atomic(() => {
const toPut = objectMapValues(diff.added);
for (const [_from, to] of objectMapValues(diff.updated)) {
const type = this.schema.getType(to.typeName);
if (ignoreEphemeralKeys && type.ephemeralKeySet.size) {
const existing = this.get(to.id);
if (!existing) {
toPut.push(to);
continue;
}
let changed = null;
for (const [key, value] of Object.entries(to)) {
if (type.ephemeralKeySet.has(key) || Object.is(value, getOwnProperty(existing, key))) {
continue;
}
if (!changed) changed = { ...existing };
changed[key] = value;
}
if (changed) toPut.push(changed);
} else {
toPut.push(to);
}
}
const toRemove = objectMapKeys(diff.removed);
if (toPut.length) {
this.put(toPut);
}
if (toRemove.length) {
this.remove(toRemove);
}
}, runCallbacks);
}
/**
* Create a computed cache.
*
* @param name - The name of the derivation cache.
* @param derive - A function used to derive the value of the cache.
* @param isEqual - A function that determins equality between two records.
* @public
*/
createComputedCache(name, derive, isEqual) {
const cache = new WeakCache();
return {
get: (id) => {
const atom2 = this.atoms.get()[id];
if (!atom2) {
return void 0;
}
return cache.get(atom2, () => {
const recordSignal = isEqual ? computed(atom2.name + ":equals", () => atom2.get(), { isEqual }) : atom2;
return computed(name + ":" + id, () => {
return derive(recordSignal.get());
});
}).get();
}
};
}
/**
* Create a computed cache from a selector
*
* @param name - The name of the derivation cache.
* @param selector - A function that returns a subset of the original shape
* @param derive - A function used to derive the value of the cache.
* @public
*/
createSelectedComputedCache(name, selector, derive) {
const cache = new WeakCache();
return {
get: (id) => {
const atom2 = this.atoms.get()[id];
if (!atom2) {
return void 0;
}
return cache.get(atom2, () => {
const d = computed(
name + ":" + id + ":selector",
() => selector(atom2.get())
);
return computed(name + ":" + id, () => derive(d.get()));
}).get();
}
};
}
_integrityChecker;
/** @internal */
ensureStoreIsUsable() {
this.atomic(() => {
this._integrityChecker ??= this.schema.createIntegrityChecker(this);
this._integrityChecker?.();
});
}
_isPossiblyCorrupted = false;
/** @internal */
markAsPossiblyCorrupted() {
this._isPossiblyCorrupted = true;
}
/** @internal */
isPossiblyCorrupted() {
return this._isPossiblyCorrupted;
}
pendingAfterEvents = null;
addDiffForAfterEvent(before, after) {
assert(this.pendingAfterEvents, "must be in event operation");
if (before === after) return;
if (before && after) assert(before.id === after.id);
if (!before && !after) return;
const id = (before || after).id;
const existing = this.pendingAfterEvents.get(id);
if (existing) {
existing.after = after;
} else {
this.pendingAfterEvents.set(id, { before, after });
}
}
flushAtomicCallbacks() {
let updateDepth = 0;
const source = this.isMergingRemoteChanges ? "remote" : "user";
while (this.pendingAfterEvents) {
const events = this.pendingAfterEvents;
this.pendingAfterEvents = null;
if (!this.sideEffects.isEnabled()) continue;
updateDepth++;
if (updateDepth > 100) {
throw new Error("Maximum store update depth exceeded, bailing out");
}
for (const { before, after } of events.values()) {
if (before && after) {
this.sideEffects.handleAfterChange(before, after, source);
} else if (before && !after) {
this.sideEffects.handleAfterDelete(before, source);
} else if (!before && after) {
this.sideEffects.handleAfterCreate(after, source);
}
}
if (!this.pendingAfterEvents) {
this.sideEffects.handleOperationComplete(source);
}
}
}
_isInAtomicOp = false;
/** @internal */
atomic(fn, runCallbacks = true) {
return transact(() => {
if (this._isInAtomicOp) {
if (!this.pendingAfterEvents) this.pendingAfterEvents = /* @__PURE__ */ new Map();
return fn();
}
this.pendingAfterEvents = /* @__PURE__ */ new Map();
const prevSideEffectsEnabled = this.sideEffects.isEnabled();
this.sideEffects.setIsEnabled(runCallbacks ?? prevSideEffectsEnabled);
this._isInAtomicOp = true;
try {
const result = fn();
this.flushAtomicCallbacks();
return result;
} finally {
this.pendingAfterEvents = null;
this.sideEffects.setIsEnabled(prevSideEffectsEnabled);
this._isInAtomicOp = false;
}
});
}
/** @internal */
addHistoryInterceptor(fn) {
return this.historyAccumulator.addInterceptor(
(entry) => fn(entry, this.isMergingRemoteChanges ? "remote" : "user")
);
}
}
function squashHistoryEntries(entries) {
if (entries.length === 0) return [];
const chunked = [];
let chunk = [entries[0]];
let entry;
for (let i = 1, n = entries.length; i < n; i++) {
entry = entries[i];
if (chunk[0].source !== entry.source) {
chunked.push(chunk);
chunk = [];
}
chunk.push(entry);
}
chunked.push(chunk);
return devFreeze(
chunked.map((chunk2) => ({
source: chunk2[0].source,
changes: squashRecordDiffs(chunk2.map((e) => e.changes))
}))
);
}
class HistoryAccumulator {
_history = [];
_interceptors = /* @__PURE__ */ new Set();
addInterceptor(fn) {
this._interceptors.add(fn);
return () => {
this._interceptors.delete(fn);
};
}
add(entry) {
this._history.push(entry);
for (const interceptor of this._interceptors) {
interceptor(entry);
}
}
flush() {
const history = squashHistoryEntries(this._history);
this._history = [];
return history;
}
clear() {
this._history = [];
}
hasChanges() {
return this._history.length > 0;
}
}
function createComputedCache(name, derive, isEqual) {
const cache = new WeakCache();
return {
get(context, id) {
const computedCache = cache.get(context, () => {
const store = context instanceof Store ? context : context.store;
return store.createComputedCache(name, (record) => derive(context, record), isEqual);
});
return computedCache.get(id);
}
};
}
function squashDependsOn(sequence) {
const result = [];
for (let i = sequence.length - 1; i >= 0; i--) {
const elem = sequence[i];
if (!("id" in elem)) {
const dependsOn = elem.dependsOn;
const prev = result[0];
if (prev) {
result[0] = {
...prev,
dependsOn: dependsOn.concat(prev.dependsOn ?? [])
};
}
} else {
result.unshift(elem);
}
}
return result;
}
function createMigrationSequence({
sequence,
sequenceId,
retroactive = true
}) {
const migrations = {
sequenceId,
retroactive,
sequence: squashDependsOn(sequence)
};
validateMigrations(migrations);
return migrations;
}
function createMigrationIds(sequenceId, versions) {
return Object.fromEntries(
objectMapEntries(versions).map(([key, version]) => [key, `${sequenceId}/${version}`])
);
}
function createRecordMigrationSequence(opts) {
const sequenceId = opts.sequenceId;
return createMigrationSequence({
sequenceId,
retroactive: opts.retroactive ?? true,
sequence: opts.sequence.map(
(m) => "id" in m ? {
...m,
scope: "record",
filter: (r) => r.typeName === opts.recordType && (m.filter?.(r) ?? true) && (opts.filter?.(r) ?? true)
} : m
)
});
}
function sortMigrations(migrations) {
const byId = new Map(migrations.map((m) => [m.id, m]));
const isProcessing = /* @__PURE__ */ new Set();
const result = [];
function process(m) {
assert(!isProcessing.has(m.id), `Circular dependency in migrations: ${m.id}`);
isProcessing.add(m.id);
const { version, sequenceId } = parseMigrationId(m.id);
const parent = byId.get(`${sequenceId}/${version - 1}`);
if (parent) {
process(parent);
}
if (m.dependsOn) {
for (const dep of m.dependsOn) {
const depMigration = byId.get(dep);
if (depMigration) {
process(depMigration);
}
}
}
byId.delete(m.id);
result.push(m);
}
for (const m of byId.values()) {
process(m);
}
return result;
}
function parseMigrationId(id) {
const [sequenceId, version] = id.split("/");
return { sequenceId, version: parseInt(version) };
}
function validateMigrationId(id, expectedSequenceId) {
if (expectedSequenceId) {
assert(
id.startsWith(expectedSequenceId + "/"),
`Every migration in sequence '${expectedSequenceId}' must have an id starting with '${expectedSequenceId}/'. Got invalid id: '${id}'`
);
}
assert(id.match(/^(.*?)\/(0|[1-9]\d*)$/), `Invalid migration id: '${id}'`);
}
function validateMigrations(migrations) {
assert(
!migrations.sequenceId.includes("/"),
`sequenceId cannot contain a '/', got ${migrations.sequenceId}`
);
assert(migrations.sequenceId.length, "sequenceId must be a non-empty string");
if (migrations.sequence.length === 0) {
return;
}
validateMigrationId(migrations.sequence[0].id, migrations.sequenceId);
let n = parseMigrationId(migrations.sequence[0].id).version;
assert(
n === 1,
`Expected the first migrationId to be '${migrations.sequenceId}/1' but got '${migrations.sequence[0].id}'`
);
for (let i = 1; i < migrations.sequence.length; i++) {
const id = migrations.sequence[i].id;
validateMigrationId(id, migrations.sequenceId);
const m = parseMigrationId(id).version;
assert(
m === n + 1,
`Migration id numbers must increase in increments of 1, expected ${migrations.sequenceId}/${n + 1} but got '${migrations.sequence[i].id}'`
);
n = m;
}
}
var MigrationFailureReason = /* @__PURE__ */ ((MigrationFailureReason2) => {
MigrationFailureReason2["IncompatibleSubtype"] = "incompatible-subtype";
MigrationFailureReason2["UnknownType"] = "unknown-type";
MigrationFailureReason2["TargetVersionTooNew"] = "target-version-too-new";
MigrationFailureReason2["TargetVersionTooOld"] = "target-version-too-old";
MigrationFailureReason2["MigrationError"] = "migration-error";
MigrationFailureReason2["UnrecognizedSubtype"] = "unrecognized-subtype";
return MigrationFailureReason2;
})(MigrationFailureReason || {});
function upgradeSchema(schema) {
if (schema.schemaVersion > 2 || schema.schemaVersion < 1) return Result.err("Bad schema version");
if (schema.schemaVersion === 2) return Result.ok(schema);
const result = {
schemaVersion: 2,
sequences: {
"com.tldraw.store": schema.storeVersion
}
};
for (const [typeName, recordVersion] of Object.entries(schema.recordVersions)) {
result.sequences[`com.tldraw.${typeName}`] = recordVersion.version;
if ("subTypeKey" in recordVersion) {
for (const [subType, version] of Object.entries(recordVersion.subTypeVersions)) {
result.sequences[`com.tldraw.${typeName}.${subType}`] = version;
}
}
}
return Result.ok(result);
}
class StoreSchema {
constructor(types, options) {
this.types = types;
this.options = options;
for (const m of options.migrations ?? []) {
assert(!this.migrations[m.sequenceId], `Duplicate migration sequenceId ${m.sequenceId}`);
validateMigrations(m);
this.migrations[m.sequenceId] = m;
}
const allMigrations = Object.values(this.migrations).flatMap((m) => m.sequence);
this.sortedMigrations = sortMigrations(allMigrations);
for (const migration of this.sortedMigrations) {
if (!migration.dependsOn?.length) continue;
for (const dep of migration.dependsOn) {
const depMigration = allMigrations.find((m) => m.id === dep);
assert(depMigration, `Migration '${migration.id}' depends on missing migration '${dep}'`);
}
}
}
static create(types, options) {
return new StoreSchema(types, options ?? {});
}
migrations = {};
sortedMigrations;
validateRecord(store, record, phase, recordBefore) {
try {
const recordType = getOwnProperty(this.types, record.typeName);
if (!recordType) {
throw new Error(`Missing definition for record type ${record.typeName}`);
}
return recordType.validate(record, recordBefore ?? void 0);
} catch (error) {
if (this.options.onValidationFailure) {
return this.options.onValidationFailure({
store,
record,
phase,
recordBefore,
error
});
} else {
throw error;
}
}
}
// TODO: use a weakmap to store the result of this function
getMigrationsSince(persistedSchema) {
const upgradeResult = upgradeSchema(persistedSchema);
if (!upgradeResult.ok) {
return upgradeResult;
}
const schema = upgradeResult.value;
const sequenceIdsToInclude = new Set(
// start with any shared sequences
Object.keys(schema.sequences).filter((sequenceId) => this.migrations[sequenceId])
);
for (const sequenceId in this.migrations) {
if (schema.sequences[sequenceId] === void 0 && this.migrations[sequenceId].retroactive) {
sequenceIdsToInclude.add(sequenceId);
}
}
if (sequenceIdsToInclude.size === 0) {
return Result.ok([]);
}
const allMigrationsToInclude = /* @__PURE__ */ new Set();
for (const sequenceId of sequenceIdsToInclude) {
const theirVersion = schema.sequences[sequenceId];
if (typeof theirVersion !== "number" && this.migrations[sequenceId].retroactive || theirVersion === 0) {
for (const migration of this.migrations[sequenceId].sequence) {
allMigrationsToInclude.add(migration.id);
}
continue;
}
const theirVersionId = `${sequenceId}/${theirVersion}`;
const idx = this.migrations[sequenceId].sequence.findIndex((m) => m.id === theirVersionId);
if (idx === -1) {
return Result.err("Incompatible schema?");
}
for (const migration of this.migrations[sequenceId].sequence.slice(idx + 1)) {
allMigrationsToInclude.add(migration.id);
}
}
return Result.ok(this.sortedMigrations.filter(({ id }) => allMigrationsToInclude.has(id)));
}
migratePersistedRecord(record, persistedSchema, direction = "up") {
const migrations = this.getMigrationsSince(persistedSchema);
if (!migrations.ok) {
console.error("Error migrating record", migrations.error);
return { type: "error", reason: MigrationFailureReason.MigrationError };
}
let migrationsToApply = migrations.value;
if (migrationsToApply.length === 0) {
return { type: "success", value: record };
}
if (migrationsToApply.some((m) => m.scope === "store")) {
return {
type: "error",
reason: direction === "down" ? MigrationFailureReason.TargetVersionTooOld : MigrationFailureReason.TargetVersionTooNew
};
}
if (direction === "down") {
if (!migrationsToApply.every((m) => m.down)) {
return {
type: "error",
reason: MigrationFailureReason.TargetVersionTooOld
};
}
migrationsToApply = migrationsToApply.slice().reverse();
}
record = structuredClone(record);
try {
for (const migration of migrationsToApply) {
if (migration.scope === "store") throw new Error(
/* won't happen, just for TS */
);
const shouldApply = migration.filter ? migration.filter(record) : true;
if (!shouldApply) continue;
const result = migration[direction](record);
if (result) {
record = structuredClone(result);
}
}
} catch (e) {
console.error("Error migrating record", e);
return { type: "error", reason: MigrationFailureReason.MigrationError };
}
return { type: "success", value: record };
}
migrateStoreSnapshot(snapshot) {
let { store } = snapshot;
const migrations = this.getMigrationsSince(snapshot.schema);
if (!migrations.ok) {
console.error("Error migrating store", migrations.error);
return { type: "error", reason: MigrationFailureReason.MigrationError };
}
const migrationsToApply = migrations.value;
if (migrationsToApply.length === 0) {
return { type: "success", value: store };
}
store = structuredClone(store);
try {
for (const migration of migrationsToApply) {
if (migration.scope === "record") {
for (const [id, record] of Object.entries(store)) {
const shouldApply = migration.filter ? migration.filter(record) : true;
if (!shouldApply) continue;
const result = migration.up(record);
if (result) {
store[id] = structuredClone(result);
}
}
} else if (migration.scope === "store") {
const result = migration.up(store);
if (result) {
store = structuredClone(result);
}
} else {
exhaustiveSwitchError(migration);
}
}
} catch (e) {
console.error("Error migrating store", e);
return { type: "error", reason: MigrationFailureReason.MigrationError };
}
return { type: "success", value: store };
}
/** @internal */
createIntegrityChecker(store) {
return this.options.createIntegrityChecker?.(store) ?? void 0;
}
serialize() {
return {
schemaVersion: 2,
sequences: Object.fromEntries(
Object.values(this.migrations).map(({ sequenceId, sequence }) => [
sequenceId,
sequence.length ? parseMigrationId(sequence.at(-1).id).version : 0
])
)
};
}
/**
* @deprecated This is only here for legacy reasons, don't use it unless you have david's blessing!
*/
serializeEarliestVersion() {
return {
schemaVersion: 2,
sequences: Object.fromEntries(
Object.values(this.migrations).map(({ sequenceId }) => [sequenceId, 0])
)
};
}
/** @internal */
getType(typeName) {
const type = getOwnProperty(this.types, typeName);
assert(type, "record type does not exists");
return type;
}
}
registerTldrawLibraryVersion(
"@tldraw/store",
"3.6.1",
"esm"
);
function formatPath(path) {
if (!path.length) {
return null;
}
let formattedPath = "";
for (const item of path) {
if (typeof item === "number") {
formattedPath += `.${item}`;
} else if (item.startsWith("(")) {
if (formattedPath.endsWith(")")) {
formattedPath = `${formattedPath.slice(0, -1)}, ${item.slice(1)}`;
} else {
formattedPath += item;
}
} else {
formattedPath += `.${item}`;
}
}
formattedPath = formattedPath.replace(/id = [^,]+, /, "").replace(/id = [^)]+/, "");
if (formattedPath.startsWith(".")) {
return formattedPath.slice(1);
}
return formattedPath;
}
class ValidationError extends Error {
constructor(rawMessage, path = []) {
const formattedPath = formatPath(path);
const indentedMessage = rawMessage.split("\n").map((line, i) => i === 0 ? line : ` ${line}`).join("\n");
super(path ? `At ${formattedPath}: ${indentedMessage}` : indentedMessage);
this.rawMessage = rawMessage;
this.path = path;
}
name = "ValidationError";
}
function prefixError(path, fn) {
try {
return fn();
} catch (err) {
if (err instanceof ValidationError) {
throw new ValidationError(err.rawMessage, [path, ...err.path]);
}
throw new ValidationError(err.toString(), [path]);
}
}
function typeToString(value) {
if (value === null) return "null";
if (Array.isArray(value)) return "an array";
const type = typeof value;
switch (type) {
case "bigint":
case "boolean":
case "function":
case "number":
case "string":
case "symbol":
return `a ${type}`;
case "object":
return `an ${type}`;
case "undefined":
return "undefined";
default:
exhaustiveSwitchError(type);
}
}
class Validator {
constructor(validationFn, validateUsingKnownGoodVersionFn) {
this.validationFn = validationFn;
this.validateUsingKnownGoodVersionFn = validateUsingKnownGoodVersionFn;
}
/**
* Asserts that the passed value is of the correct type and returns it. The returned value is
* guaranteed to be referentially equal to the passed value.
*/
validate(value) {
const validated = this.validationFn(value);
return validated;
}
validateUsingKnownGoodVersion(knownGoodValue, newValue) {
if (Object.is(knownGoodValue, newValue)) {
return knownGoodValue;
}
if (this.validateUsingKnownGoodVersionFn) {
return this.validateUsingKnownGoodVersionFn(knownGoodValue, newValue);
}
return this.validate(newValue);
}
/** Checks that the passed value is of the correct type. */
isValid(value) {
try {
this.validate(value);
return true;
} catch {
return false;
}
}
/**
* Returns a new validator that also accepts null or undefined. The resulting value will always be
* null.
*/
nullable() {
return nullable(this);
}
/**
* Returns a new validator that also accepts null or undefined. The resulting value will always be
* null.
*/
optional() {
return optional(this);
}
/**
* Refine this validation to a new type. The passed-in validation function should throw an error
* if the value can't be converted to the new type, or return the new type otherwise.
*/
refine(otherValidationFn) {
return new Validator(
(value) => {
return otherValidationFn(this.validate(value));
},
(knownGoodValue, newValue) => {
const validated = this.validateUsingKnownGoodVersion(knownGoodValue, newValue);
if (Object.is(knownGoodValue, validated)) {
return knownGoodValue;
}
return otherValidationFn(validated);
}
);
}
check(nameOrCheckFn, checkFn) {
if (typeof nameOrCheckFn === "string") {
return this.refine((value) => {
prefixError(`(check ${nameOrCheckFn})`, () => checkFn(value));
return value;
});
} else {
return this.refine((value) => {
nameOrCheckFn(value);
return value;
});
}
}
}
class ArrayOfValidator extends Validator {
constructor(itemValidator) {
super(
(value) => {
const arr = array.validate(value);
for (let i = 0; i < arr.length; i++) {
prefixError(i, () => itemValidator.validate(arr[i]));
}
return arr;
},
(knownGoodValue, newValue) => {
if (!itemValidator.validateUsingKnownGoodVersion) return this.validate(newValue);
const arr = array.validate(newValue);
let isDifferent = knownGoodValue.length !== arr.length;
for (let i = 0; i < arr.length; i++) {
const item = arr[i];
if (i >= knownGoodValue.length) {
isDifferent = true;
prefixError(i, () => itemValidator.validate(item));
continue;
}
if (Object.is(knownGoodValue[i], item)) {
continue;
}
const checkedItem = prefixError(
i,
() => itemValidator.validateUsingKnownGoodVersion(knownGoodValue[i], item)
);
if (!Object.is(checkedItem, knownGoodValue[i])) {
isDifferent = true;
}
}
return isDifferent ? newValue : knownGoodValue;
}
);
this.itemValidator = itemValidator;
}
nonEmpty() {
return this.check((value) => {
if (value.length === 0) {
throw new ValidationError("Expected a non-empty array");
}
});
}
lengthGreaterThan1() {
return this.check((value) => {
if (value.length <= 1) {
throw new ValidationError("Expected an array with length greater than 1");
}
});
}
}
class ObjectValidator extends Validator {
constructor(config, shouldAllowUnknownProperties = false) {
super(
(object2) => {
if (typeof object2 !== "object" || object2 === null) {
throw new ValidationError(`Expected object, got ${typeToString(object2)}`);
}
for (const [key, validator] of Object.entries(config)) {
prefixError(key, () => {
;
validator.validate(getOwnProperty(object2, key));
});
}
if (!shouldAllowUnknownProperties) {
for (const key of Object.keys(object2)) {
if (!hasOwnProperty$1(config, key)) {
throw new ValidationError(`Unexpected property`, [key]);
}
}
}
return object2;
},
(knownGoodValue, newValue) => {
if (typeof newValue !== "object" || newValue === null) {
throw new ValidationError(`Expected object, got ${typeToString(newValue)}`);
}
let isDifferent = false;
for (const [key, validator] of Object.entries(config)) {
const prev = getOwnProperty(knownGoodValue, key);
const next = getOwnProperty(newValue, key);
if (Object.is(prev, next)) {
continue;
}
const checked = prefixError(key, () => {
const validatable = validator;
if (validatable.validateUsingKnownGoodVersion) {
return validatable.validateUsingKnownGoodVersion(prev, next);
} else {
return validatable.validate(next);
}
});
if (!Object.is(checked, prev)) {
isDifferent = true;
}
}
if (!shouldAllowUnknownProperties) {
for (const key of Object.keys(newValue)) {
if (!hasOwnProperty$1(config, key)) {
throw new ValidationError(`Unexpected property`, [key]);
}
}
}
for (const key of Object.keys(knownGoodValue)) {
if (!hasOwnProperty$1(newValue, key)) {
isDifferent = true;
break;
}
}
return isDifferent ? newValue : knownGoodValue;
}
);
this.config = config;
this.shouldAllowUnknownProperties = shouldAllowUnknownProperties;
}
allowUnknownProperties() {
return new ObjectValidator(this.config, true);
}
/**
* Extend an object validator by adding additional properties.
*
* @example
*
* ```ts
* const animalValidator = T.object({
* name: T.string,
* })
* const catValidator = animalValidator.extend({
* meowVolume: T.number,
* })
* ```
*/
extend(extension) {
return new ObjectValidator({ ...this.config, ...extension });
}
}
class UnionValidator extends Validator {
constructor(key, config, unknownValueValidation, useNumberKeys) {
super(
(input) => {
this.expectObject(input);
const { matchingSchema, variant } = this.getMatchingSchemaAndVariant(input);
if (matchingSchema === void 0) {
return this.unknownValueValidation(input, variant);
}
return prefixError(`(${key} = ${variant})`, () => matchingSchema.validate(input));
},
(prevValue, newValue) => {
this.expectObject(newValue);
this.expectObject(prevValue);
const { matchingSchema, variant } = this.getMatchingSchemaAndVariant(newValue);
if (matchingSchema === void 0) {
return this.unknownValueValidation(newValue, variant);
}
if (getOwnProperty(prevValue, key) !== getOwnProperty(newValue, key)) {
return prefixError(`(${key} = ${variant})`, () => matchingSchema.validate(newValue));
}
return prefixError(`(${key} = ${variant})`, () => {
if (matchingSchema.validateUsingKnownGoodVersion) {
return matchingSchema.validateUsingKnownGoodVersion(prevValue, newValue);
} else {
return matchingSchema.validate(newValue);
}
});
}
);
this.key = key;
this.config = config;
this.unknownValueValidation = unknownValueValidation;
this.useNumberKeys = useNumberKeys;
}
expectObject(value) {
if (typeof value !== "object" || value === null) {
throw new ValidationError(`Expected an object, got ${typeToString(value)}`, []);
}
}
getMatchingSchemaAndVariant(object2) {
const variant = getOwnProperty(object2, this.key);
if (!this.useNumberKeys && typeof variant !== "string") {
throw new ValidationError(
`Expected a string for key "${this.key}", got ${typeToString(variant)}`
);
} else if (this.useNumberKeys && !Number.isFinite(Number(variant))) {
throw new ValidationError(`Expected a number for key "${this.key}", got "${variant}"`);
}
const matchingSchema = hasOwnProperty$1(this.config, variant) ? this.config[variant] : void 0;
return { matchingSchema, variant };
}
validateUnknownVariants(unknownValueValidation) {
return new UnionValidator(this.key, this.config, unknownValueValidation, this.useNumberKeys);
}
}
class DictValidator extends Validator {
constructor(keyValidator, valueValidator) {
super(
(object2) => {
if (typeof object2 !== "object" || object2 === null) {
throw new ValidationError(`Expected object, got ${typeToString(object2)}`);
}
for (const [key, value] of Object.entries(object2)) {
prefixError(key, () => {
keyValidator.validate(key);
valueValidator.validate(value);
});
}
return object2;
},
(knownGoodValue, newValue) => {
if (typeof newValue !== "object" || newValue === null) {
throw new ValidationError(`Expected object, got ${typeToString(newValue)}`);
}
let isDifferent = false;
for (const [key, value] of Object.entries(newValue)) {
if (!hasOwnProperty$1(knownGoodValue, key)) {
isDifferent = true;
prefixError(key, () => {
keyValidator.validate(key);
valueValidator.validate(value);
});
continue;
}
const prev = getOwnProperty(knownGoodValue, key);
const next = value;
if (Object.is(prev, next)) {
continue;
}
const checked = prefixError(key, () => {
if (valueValidator.validateUsingKnownGoodVersion) {
return valueValidator.validateUsingKnownGoodVersion(prev, next);
} else {
return valueValidator.validate(next);
}
});
if (!Object.is(checked, prev)) {
isDifferent = true;
}
}
for (const key of Object.keys(knownGoodValue)) {
if (!hasOwnProperty$1(newValue, key)) {
isDifferent = true;
break;
}
}
return isDifferent ? newValue : knownGoodValue;
}
);
this.keyValidator = keyValidator;
this.valueValidator = valueValidator;
}
}
function typeofValidator(type) {
return new Validator((value) => {
if (typeof value !== type) {
throw new ValidationError(`Expected ${type}, got ${typeToString(value)}`);
}
return value;
});
}
const any = new Validator((value) => value);
const string = typeofValidator("string");
const number = typeofValidator("number").check((number2) => {
if (Number.isNaN(number2)) {
throw new ValidationError("Expected a number, got NaN");
}
if (!Number.isFinite(number2)) {
throw new ValidationError(`Expected a finite number, got ${number2}`);
}
});
const positiveNumber = number.check((value) => {
if (value < 0) throw new ValidationError(`Expected a positive number, got ${value}`);
});
const nonZeroNumber = number.check((value) => {
if (value <= 0) throw new ValidationError(`Expected a non-zero positive number, got ${value}`);
});
const integer = number.check((value) => {
if (!Number.isInteger(value)) throw new ValidationError(`Expected an integer, got ${value}`);
});
const positiveInteger = integer.check((value) => {
if (value < 0) throw new ValidationError(`Expected a positive integer, got ${value}`);
});
const nonZeroInteger = integer.check((value) => {
if (value <= 0) throw new ValidationError(`Expected a non-zero positive integer, got ${value}`);
});
const boolean = typeofValidator("boolean");
function literal(expectedValue) {
return new Validator((actualValue) => {
if (actualValue !== expectedValue) {
throw new ValidationError(`Expected ${expectedValue}, got ${JSON.stringify(actualValue)}`);
}
return expectedValue;
});
}
const array = new Validator((value) => {
if (!Array.isArray(value)) {
throw new ValidationError(`Expected an array, got ${typeToString(value)}`);
}
return value;
});
function arrayOf(itemValidator) {
return new ArrayOfValidator(itemValidator);
}
function object(config) {
return new ObjectValidator(config);
}
function isPlainObject(value) {
return typeof value === "object" && value !== null && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null || Object.getPrototypeOf(value) === STRUCTURED_CLONE_OBJECT_PROTOTYPE);
}
function isValidJson(value) {
if (value === null || typeof value === "number" || typeof value === "string" || typeof value === "boolean") {
return true;
}
if (Array.isArray(value)) {
return value.every(isValidJson);
}
if (isPlainObject(value)) {
return Object.values(value).every(isValidJson);
}
return false;
}
const jsonValue = new Validator(
(value) => {
if (isValidJson(value)) {
return value;
}
throw new ValidationError(`Expected json serializable value, got ${typeof value}`);
},
(knownGoodValue, newValue) => {
if (Array.isArray(knownGoodValue) && Array.isArray(newValue)) {
let isDifferent = knownGoodValue.length !== newValue.length;
for (let i = 0; i < newValue.length; i++) {
if (i >= knownGoodValue.length) {
isDifferent = true;
jsonValue.validate(newValue[i]);
continue;
}
const prev = knownGoodValue[i];
const next = newValue[i];
if (Object.is(prev, next)) {
continue;
}
const checked = jsonValue.validateUsingKnownGoodVersion(prev, next);
if (!Object.is(checked, prev)) {
isDifferent = true;
}
}
return isDifferent ? newValue : knownGoodValue;
} else if (isPlainObject(knownGoodValue) && isPlainObject(newValue)) {
let isDifferent = false;
for (const key of Object.keys(newValue)) {
if (!hasOwnProperty$1(knownGoodValue, key)) {
isDifferent = true;
jsonValue.validate(newValue[key]);
continue;
}
const prev = knownGoodValue[key];
const next = newValue[key];
if (Object.is(prev, next)) {
continue;
}
const checked = jsonValue.validateUsingKnownGoodVersion(prev, next);
if (!Object.is(checked, prev)) {
isDifferent = true;
}
}
for (const key of Object.keys(knownGoodValue)) {
if (!hasOwnProperty$1(newValue, key)) {
isDifferent = true;
break;
}
}
return isDifferent ? newValue : knownGoodValue;
} else {
return jsonValue.validate(newValue);
}
}
);
function dict(keyValidator, valueValidator) {
return new DictValidator(keyValidator, valueValidator);
}
function union(key, config) {
return new UnionValidator(
key,
config,
(_unknownValue, unknownVariant) => {
throw new ValidationError(
`Expected one of ${Object.keys(config).map((key2) => JSON.stringify(key2)).join(" or ")}, got ${JSON.stringify(unknownVariant)}`,
[key]
);
},
false
);
}
function numberUnion(key, config) {
return new UnionValidator(
key,
config,
(unknownValue, unknownVariant) => {
throw new ValidationError(
`Expected one of ${Object.keys(config).map((key2) => JSON.stringify(key2)).join(" or ")}, got ${JSON.stringify(unknownVariant)}`,
[key]
);
},
true
);
}
function model(name, validator) {
return new Validator(
(value) => {
return prefixError(name, () => validator.validate(value));
},
(prevValue, newValue) => {
return prefixError(name, () => {
if (validator.validateUsingKnownGoodVersion) {
return validator.validateUsingKnownGoodVersion(prevValue, newValue);
} else {
return validator.validate(newValue);
}
});
}
);
}
function setEnum(values) {
return new Validator((value) => {
if (!values.has(value)) {
const valuesString = Array.from(values, (value2) => JSON.stringify(value2)).join(" or ");
throw new ValidationError(`Expected ${valuesString}, got ${value}`);
}
return value;
});
}
function optional(validator) {
return new Validator(
(value) => {
if (value === void 0) return void 0;
return validator.validate(value);
},
(knownGoodValue, newValue) => {
if (knownGoodValue === void 0 && newValue === void 0) return void 0;
if (newValue === void 0) return void 0;
if (validator.validateUsingKnownGoodVersion && knownGoodValue !== void 0) {
return validator.validateUsingKnownGoodVersion(knownGoodValue, newValue);
}
return validator.validate(newValue);
}
);
}
function nullable(validator) {
return new Validator(
(value) => {
if (value === null) return null;
return validator.validate(value);
},
(knownGoodValue, newValue) => {
if (newValue === null) return null;
if (validator.validateUsingKnownGoodVersion && knownGoodValue !== null) {
return validator.validateUsingKnownGoodVersion(knownGoodValue, newValue);
}
return validator.validate(newValue);
}
);
}
function literalEnum(...values) {
return setEnum(new Set(values));
}
function parseUrl(str) {
try {
return new URL(str);
} catch {
if (str.startsWith("/") || str.startsWith("./")) {
try {
return new URL(str, "http://example.com");
} catch {
throw new ValidationError(`Expected a valid url, got ${JSON.stringify(str)}`);
}
}
throw new ValidationError(`Expected a valid url, got ${JSON.stringify(str)}`);
}
}
const validLinkProtocols = /* @__PURE__ */ new Set(["http:", "https:", "mailto:"]);
const linkUrl = string.check((value) => {
if (value === "") return;
const url = parseUrl(value);
if (!validLinkProtocols.has(url.protocol.toLowerCase())) {
throw new ValidationError(
`Expected a valid url, got ${JSON.stringify(value)} (invalid protocol)`
);
}
});
const validSrcProtocols = /* @__PURE__ */ new Set(["http:", "https:", "data:", "asset:"]);
const srcUrl = string.check((value) => {
if (value === "") return;
const url = parseUrl(value);
if (!validSrcProtocols.has(url.protocol.toLowerCase())) {
throw new ValidationError(
`Expected a valid url, got ${JSON.stringify(value)} (invalid protocol)`
);
}
});
string.check((value) => {
if (value === "") return;
const url = parseUrl(value);
if (!url.protocol.toLowerCase().match(/^https?:$/)) {
throw new ValidationError(
`Expected a valid url, got ${JSON.stringify(value)} (invalid protocol)`
);
}
});
const indexKey = string.refine((key) => {
try {
validateIndexKey(key);
return key;
} catch {
throw new ValidationError(`Expected an index key, got ${JSON.stringify(key)}`);
}
});
registerTldrawLibraryVersion(
"@tldraw/validate",
"3.6.1",
"esm"
);
function idValidator(prefix) {
return string.refine((id) => {
if (!id.startsWith(`${prefix}:`)) {
throw new Error(`${prefix} ID must start with "${prefix}:"`);
}
return id;
});
}
const assetIdValidator = idValidator("asset");
function createAssetValidator(type, props) {
return object({
id: assetIdValidator,
typeName: literal("asset"),
type: literal(type),
props,
meta: jsonValue
});
}
const vecModelValidator = object({
x: number,
y: number,
z: number.optional()
});
const boxModelValidator = object({
x: number,
y: number,
w: number,
h: number
});
const opacityValidator = number.check((n) => {
if (n < 0 || n > 1) {
throw new ValidationError("Opacity must be between 0 and 1");
}
});
const parentIdValidator = string.refine((id) => {
if (!id.startsWith("page:") && !id.startsWith("shape:")) {
throw new Error('Parent ID must start with "page:" or "shape:"');
}
return id;
});
const shapeIdValidator = idValidator("shape");
function createShapeValidator(type, props, meta) {
return object({
id: shapeIdValidator,
typeName: literal("shape"),
x: number,
y: number,
rotation: number,
index: indexKey,
parentId: parentIdValidator,
type: literal(type),
isLocked: boolean,
opacity: opacityValidator,
props: props ? object(props) : jsonValue,
meta: meta ? object(meta) : jsonValue
});
}
const bindingIdValidator = idValidator("binding");
function createBindingValidator(type, props, meta) {
return object({
id: bindingIdValidator,
typeName: literal("binding"),
type: literal(type),
fromId: shapeIdValidator,
toId: shapeIdValidator,
props: props ? object(props) : jsonValue,
meta: meta ? object(meta) : jsonValue
});
}
createMigrationIds("com.tldraw.binding", {});
createRecordMigrationSequence({
sequenceId: "com.tldraw.binding",
recordType: "binding",
sequence: []
});
function createBindingId(id) {
return `binding:${uniqueId()}`;
}
function createBindingPropsMigrationSequence(migrations) {
return migrations;
}
function createBindingRecordType(bindings) {
return createRecordType("binding", {
scope: "document",
validator: model(
"binding",
union(
"type",
mapObjectMapValues(
bindings,
(type, { props, meta }) => createBindingValidator(type, props, meta)
)
)
)
}).withDefaultProperties(() => ({
meta: {}
}));
}
class StyleProp {
/** @internal */
constructor(id, defaultValue, type) {
this.id = id;
this.defaultValue = defaultValue;
this.type = type;
}
/**
* Define a new {@link StyleProp}.
*
* @param uniqueId - Each StyleProp must have a unique ID. We recommend you prefix this with
* your app/library name.
* @param options -
* - `defaultValue`: The default value for this style prop.
*
* - `type`: Optionally, describe what type of data you expect for this style prop.
*
* @example
* ```ts
* import {T} from '@tldraw/validate'
* import {StyleProp} from '@tldraw/tlschema'
*
* const MyLineWidthProp = StyleProp.define('myApp:lineWidth', {
* defaultValue: 1,
* type: T.number,
* })
* ```
* @public
*/
static define(uniqueId, options) {
const { defaultValue, type = any } = options;
return new StyleProp(uniqueId, defaultValue, type);
}
/**
* Define a new {@link StyleProp} as a list of possible values.
*
* @param uniqueId - Each StyleProp must have a unique ID. We recommend you prefix this with
* your app/library name.
* @param options -
* - `defaultValue`: The default value for this style prop.
*
* - `values`: An array of possible values of this style prop.
*
* @example
* ```ts
* import {StyleProp} from '@tldraw/tlschema'
*
* const MySizeProp = StyleProp.defineEnum('myApp:size', {
* defaultValue: 'medium',
* values: ['small', 'medium', 'large'],
* })
* ```
*/
static defineEnum(uniqueId, options) {
const { defaultValue, values } = options;
return new EnumStyleProp(uniqueId, defaultValue, values);
}
setDefaultValue(value) {
this.defaultValue = value;
}
validate(value) {
return this.type.validate(value);
}
validateUsingKnownGoodVersion(prevValue, newValue) {
if (this.type.validateUsingKnownGoodVersion) {
return this.type.validateUsingKnownGoodVersion(prevValue, newValue);
} else {
return this.validate(newValue);
}
}
}
class EnumStyleProp extends StyleProp {
/** @internal */
constructor(id, defaultValue, values) {
super(id, defaultValue, literalEnum(...values));
this.values = values;
}
}
const rootShapeVersions = createMigrationIds("com.tldraw.shape", {
AddIsLocked: 1,
HoistOpacity: 2,
AddMeta: 3,
AddWhite: 4
});
const rootShapeMigrations = createRecordMigrationSequence({
sequenceId: "com.tldraw.shape",
recordType: "shape",
sequence: [
{
id: rootShapeVersions.AddIsLocked,
up: (record) => {
record.isLocked = false;
},
down: (record) => {
delete record.isLocked;
}
},
{
id: rootShapeVersions.HoistOpacity,
up: (record) => {
record.opacity = Number(record.props.opacity ?? "1");
delete record.props.opacity;
},
down: (record) => {
const opacity = record.opacity;
delete record.opacity;
record.props.opacity = opacity < 0.175 ? "0.1" : opacity < 0.375 ? "0.25" : opacity < 0.625 ? "0.5" : opacity < 0.875 ? "0.75" : "1";
}
},
{
id: rootShapeVersions.AddMeta,
up: (record) => {
record.meta = {};
}
},
{
id: rootShapeVersions.AddWhite,
up: (_record) => {
},
down: (record) => {
if (record.props.color === "white") {
record.props.color = "black";
}
}
}
]
});
function isShape(record) {
if (!record) return false;
return record.typeName === "shape";
}
function isShapeId(id) {
if (!id) return false;
return id.startsWith("shape:");
}
function createShapeId(id) {
return `shape:${id ?? uniqueId()}`;
}
function getShapePropKeysByStyle(props) {
const propKeysByStyle = /* @__PURE__ */ new Map();
for (const [key, prop] of Object.entries(props)) {
if (prop instanceof StyleProp) {
if (propKeysByStyle.has(prop)) {
throw new Error(
`Duplicate style prop ${prop.id}. Each style prop can only be used once within a shape.`
);
}
propKeysByStyle.set(prop, key);
}
}
return propKeysByStyle;
}
function createShapePropsMigrationSequence(migrations) {
return migrations;
}
function createShapePropsMigrationIds(shapeType, ids) {
return mapObjectMapValues(ids, (_k, v) => `com.tldraw.shape.${shapeType}/${v}`);
}
function createShapeRecordType(shapes) {
return createRecordType("shape", {
scope: "document",
validator: model(
"shape",
union(
"type",
mapObjectMapValues(
shapes,
(type, { props, meta }) => createShapeValidator(type, props, meta)
)
)
)
}).withDefaultProperties(() => ({
x: 0,
y: 0,
rotation: 0,
isLocked: false,
opacity: 1,
meta: {}
}));
}
function processPropsMigrations(typeName, records) {
const result = [];
for (const [subType, { migrations }] of Object.entries(records)) {
const sequenceId = `com.tldraw.${typeName}.${subType}`;
if (!migrations) {
result.push(
createMigrationSequence({
sequenceId,
retroactive: false,
sequence: []
})
);
} else if ("sequenceId" in migrations) {
assert(
sequenceId === migrations.sequenceId,
`sequenceId mismatch for ${subType} ${RecordType} migrations. Expected '${sequenceId}', got '${migrations.sequenceId}'`
);
result.push(migrations);
} else if ("sequence" in migrations) {
result.push(
createMigrationSequence({
sequenceId,
retroactive: false,
sequence: migrations.sequence.map(
(m) => "id" in m ? createPropsMigration(typeName, subType, m) : m
)
})
);
} else {
result.push(
createMigrationSequence({
sequenceId,
retroactive: false,
sequence: Object.keys(migrations.migrators).map((k) => Number(k)).sort((a, b) => a - b).map(
(version) => ({
id: `${sequenceId}/${version}`,
scope: "record",
filter: (r) => r.typeName === typeName && r.type === subType,
up: (record) => {
const result2 = migrations.migrators[version].up(record);
if (result2) {
return result2;
}
},
down: (record) => {
const result2 = migrations.migrators[version].down(record);
if (result2) {
return result2;
}
}
})
)
})
);
}
}
return result;
}
function createPropsMigration(typeName, subType, m) {
return {
id: m.id,
dependsOn: m.dependsOn,
scope: "record",
filter: (r) => r.typeName === typeName && r.type === subType,
up: (record) => {
const result = m.up(record.props);
if (result) {
record.props = result;
}
},
down: typeof m.down === "function" ? (record) => {
const result = m.down(record.props);
if (result) {
record.props = result;
}
} : void 0
};
}
const defaultColorNames = [
"black",
"grey",
"light-violet",
"violet",
"blue",
"light-blue",
"yellow",
"orange",
"green",
"light-green",
"light-red",
"red",
"white"
];
const DefaultColorThemePalette = {
lightMode: {
id: "light",
text: "#000000",
background: "rgb(249, 250, 251)",
solid: "#fcfffe",
black: {
solid: "#1d1d1d",
fill: "#1d1d1d",
note: {
fill: "#FCE19C",
text: "#000000"
},
semi: "#e8e8e8",
pattern: "#494949",
highlight: {
srgb: "#fddd00",
p3: "color(display-p3 0.972 0.8705 0.05)"
}
},
blue: {
solid: "#4465e9",
fill: "#4465e9",
note: {
fill: "#8AA3FF",
text: "#000000"
},
semi: "#dce1f8",
pattern: "#6681ee",
highlight: {
srgb: "#10acff",
p3: "color(display-p3 0.308 0.6632 0.9996)"
}
},
green: {
solid: "#099268",
fill: "#099268",
note: {
fill: "#6FC896",
text: "#000000"
},
semi: "#d3e9e3",
pattern: "#39a785",
highlight: {
srgb: "#00ffc8",
p3: "color(display-p3 0.2536 0.984 0.7981)"
}
},
grey: {
solid: "#9fa8b2",
fill: "#9fa8b2",
note: {
fill: "#C0CAD3",
text: "#000000"
},
semi: "#eceef0",
pattern: "#bcc3c9",
highlight: {
srgb: "#cbe7f1",
p3: "color(display-p3 0.8163 0.9023 0.9416)"
}
},
"light-blue": {
solid: "#4ba1f1",
fill: "#4ba1f1",
note: {
fill: "#9BC4FD",
text: "#000000"
},
semi: "#ddedfa",
pattern: "#6fbbf8",
highlight: {
srgb: "#00f4ff",
p3: "color(display-p3 0.1512 0.9414 0.9996)"
}
},
"light-green": {
solid: "#4cb05e",
fill: "#4cb05e",
note: {
fill: "#98D08A",
text: "#000000"
},
semi: "#dbf0e0",
pattern: "#65cb78",
highlight: {
srgb: "#65f641",
p3: "color(display-p3 0.563 0.9495 0.3857)"
}
},
"light-red": {
solid: "#f87777",
fill: "#f87777",
note: {
fill: "#F7A5A1",
text: "#000000"
},
semi: "#f4dadb",
pattern: "#fe9e9e",
highlight: {
srgb: "#ff7fa3",
p3: "color(display-p3 0.9988 0.5301 0.6397)"
}
},
"light-violet": {
solid: "#e085f4",
fill: "#e085f4",
note: {
fill: "#DFB0F9",
text: "#000000"
},
semi: "#f5eafa",
pattern: "#e9acf8",
highlight: {
srgb: "#ff88ff",
p3: "color(display-p3 0.9676 0.5652 0.9999)"
}
},
orange: {
solid: "#e16919",
fill: "#e16919",
note: {
fill: "#FAA475",
text: "#000000"
},
semi: "#f8e2d4",
pattern: "#f78438",
highlight: {
srgb: "#ffa500",
p3: "color(display-p3 0.9988 0.6905 0.266)"
}
},
red: {
solid: "#e03131",
fill: "#e03131",
note: {
fill: "#FC8282",
text: "#000000"
},
semi: "#f4dadb",
pattern: "#e55959",
highlight: {
srgb: "#ff636e",
p3: "color(display-p3 0.9992 0.4376 0.45)"
}
},
violet: {
solid: "#ae3ec9",
fill: "#ae3ec9",
note: {
fill: "#DB91FD",
text: "#000000"
},
semi: "#ecdcf2",
pattern: "#bd63d3",
highlight: {
srgb: "#c77cff",
p3: "color(display-p3 0.7469 0.5089 0.9995)"
}
},
yellow: {
solid: "#f1ac4b",
fill: "#f1ac4b",
note: {
fill: "#FED49A",
text: "#000000"
},
semi: "#f9f0e6",
pattern: "#fecb92",
highlight: {
srgb: "#fddd00",
p3: "color(display-p3 0.972 0.8705 0.05)"
}
},
white: {
solid: "#FFFFFF",
fill: "#FFFFFF",
semi: "#f5f5f5",
pattern: "#f9f9f9",
note: {
fill: "#FFFFFF",
text: "#000000"
},
highlight: {
srgb: "#ffffff",
p3: "color(display-p3 1 1 1)"
}
}
},
darkMode: {
id: "dark",
text: "hsl(210, 17%, 98%)",
background: "hsl(240, 5%, 6.5%)",
solid: "#010403",
black: {
solid: "#f2f2f2",
fill: "#f2f2f2",
note: {
fill: "#2c2c2c",
text: "#f2f2f2"
},
semi: "#2c3036",
pattern: "#989898",
highlight: {
srgb: "#d2b700",
p3: "color(display-p3 0.8078 0.7225 0.0312)"
}
},
blue: {
solid: "#4f72fc",
// 3c60f0
fill: "#4f72fc",
note: {
fill: "#2A3F98",
text: "#f2f2f2"
},
semi: "#262d40",
pattern: "#3a4b9e",
highlight: {
srgb: "#0079d2",
p3: "color(display-p3 0.0032 0.4655 0.7991)"
}
},
green: {
solid: "#099268",
fill: "#099268",
note: {
fill: "#014429",
text: "#f2f2f2"
},
semi: "#253231",
pattern: "#366a53",
highlight: {
srgb: "#009774",
p3: "color(display-p3 0.0085 0.582 0.4604)"
}
},
grey: {
solid: "#9398b0",
fill: "#9398b0",
note: {
fill: "#56595F",
text: "#f2f2f2"
},
semi: "#33373c",
pattern: "#7c8187",
highlight: {
srgb: "#9cb4cb",
p3: "color(display-p3 0.6299 0.7012 0.7856)"
}
},
"light-blue": {
solid: "#4dabf7",
fill: "#4dabf7",
note: {
fill: "#1F5495",
text: "#f2f2f2"
},
semi: "#2a3642",
pattern: "#4d7aa9",
highlight: {
srgb: "#00bdc8",
p3: "color(display-p3 0.0023 0.7259 0.7735)"
}
},
"light-green": {
solid: "#40c057",
fill: "#40c057",
note: {
fill: "#21581D",
text: "#f2f2f2"
},
semi: "#2a3830",
pattern: "#4e874e",
highlight: {
srgb: "#00a000",
p3: "color(display-p3 0.2711 0.6172 0.0195)"
}
},
"light-red": {
solid: "#ff8787",
fill: "#ff8787",
note: {
fill: "#923632",
text: "#f2f2f2"
},
semi: "#3b3235",
pattern: "#a56767",
highlight: {
srgb: "#db005b",
p3: "color(display-p3 0.7849 0.0585 0.3589)"
}
},
"light-violet": {
solid: "#e599f7",
fill: "#e599f7",
note: {
fill: "#762F8E",
text: "#f2f2f2"
},
semi: "#383442",
pattern: "#9770a9",
highlight: {
srgb: "#c400c7",
p3: "color(display-p3 0.7024 0.0403 0.753)"
}
},
orange: {
solid: "#f76707",
fill: "#f76707",
note: {
fill: "#843906",
text: "#f2f2f2"
},
semi: "#3a2e2a",
pattern: "#9f552d",
highlight: {
srgb: "#d07a00",
p3: "color(display-p3 0.7699 0.4937 0.0085)"
}
},
red: {
solid: "#e03131",
fill: "#e03131",
note: {
fill: "#89231A",
text: "#f2f2f2"
},
semi: "#36292b",
pattern: "#8f3734",
highlight: {
srgb: "#de002c",
p3: "color(display-p3 0.7978 0.0509 0.2035)"
}
},
violet: {
solid: "#ae3ec9",
fill: "#ae3ec9",
note: {
fill: "#681683",
text: "#f2f2f2"
},
semi: "#31293c",
pattern: "#763a8b",
highlight: {
srgb: "#9e00ee",
p3: "color(display-p3 0.5651 0.0079 0.8986)"
}
},
yellow: {
solid: "#ffc034",
fill: "#ffc034",
note: {
fill: "#98571B",
text: "#f2f2f2"
},
semi: "#3c3934",
pattern: "#fecb92",
highlight: {
srgb: "#d2b700",
p3: "color(display-p3 0.8078 0.7225 0.0312)"
}
},
white: {
solid: "#f3f3f3",
fill: "#f3f3f3",
semi: "#f5f5f5",
pattern: "#f9f9f9",
note: {
fill: "#eaeaea",
text: "#1d1d1d"
},
highlight: {
srgb: "#ffffff",
p3: "color(display-p3 1 1 1)"
}
}
}
};
function getDefaultColorTheme(opts) {
return opts.isDarkMode ? DefaultColorThemePalette.darkMode : DefaultColorThemePalette.lightMode;
}
const DefaultColorStyle = StyleProp.defineEnum("tldraw:color", {
defaultValue: "black",
values: defaultColorNames
});
const DefaultLabelColorStyle = StyleProp.defineEnum("tldraw:labelColor", {
defaultValue: "black",
values: defaultColorNames
});
const DefaultDashStyle = StyleProp.defineEnum("tldraw:dash", {
defaultValue: "draw",
values: ["draw", "solid", "dashed", "dotted"]
});
const DefaultFillStyle = StyleProp.defineEnum("tldraw:fill", {
defaultValue: "none",
values: ["none", "semi", "solid", "pattern", "fill"]
});
const DefaultFontStyle = StyleProp.defineEnum("tldraw:font", {
defaultValue: "draw",
values: ["draw", "sans", "serif", "mono"]
});
const DefaultFontFamilies = {
draw: "'tldraw_draw', sans-serif",
sans: "'tldraw_sans', sans-serif",
serif: "'tldraw_serif', serif",
mono: "'tldraw_mono', monospace"
};
const DefaultSizeStyle = StyleProp.defineEnum("tldraw:size", {
defaultValue: "m",
values: ["s", "m", "l", "xl"]
});
const arrowheadTypes = [
"arrow",
"triangle",
"square",
"dot",
"pipe",
"diamond",
"inverted",
"bar",
"none"
];
const ArrowShapeArrowheadStartStyle = StyleProp.defineEnum("tldraw:arrowheadStart", {
defaultValue: "none",
values: arrowheadTypes
});
const ArrowShapeArrowheadEndStyle = StyleProp.defineEnum("tldraw:arrowheadEnd", {
defaultValue: "arrow",
values: arrowheadTypes
});
const arrowShapeProps = {
labelColor: DefaultLabelColorStyle,
color: DefaultColorStyle,
fill: DefaultFillStyle,
dash: DefaultDashStyle,
size: DefaultSizeStyle,
arrowheadStart: ArrowShapeArrowheadStartStyle,
arrowheadEnd: ArrowShapeArrowheadEndStyle,
font: DefaultFontStyle,
start: vecModelValidator,
end: vecModelValidator,
bend: number,
text: string,
labelPosition: number,
scale: nonZeroNumber
};
const arrowShapeVersions = createShapePropsMigrationIds("arrow", {
AddLabelColor: 1,
AddIsPrecise: 2,
AddLabelPosition: 3,
ExtractBindings: 4,
AddScale: 5
});
function propsMigration(migration) {
return createPropsMigration("shape", "arrow", migration);
}
const arrowShapeMigrations = createMigrationSequence({
sequenceId: "com.tldraw.shape.arrow",
retroactive: false,
sequence: [
propsMigration({
id: arrowShapeVersions.AddLabelColor,
up: (props) => {
props.labelColor = "black";
},
down: "retired"
}),
propsMigration({
id: arrowShapeVersions.AddIsPrecise,
up: ({ start, end }) => {
if (start.type === "binding") {
start.isPrecise = !(start.normalizedAnchor.x === 0.5 && start.normalizedAnchor.y === 0.5);
}
if (end.type === "binding") {
end.isPrecise = !(end.normalizedAnchor.x === 0.5 && end.normalizedAnchor.y === 0.5);
}
},
down: ({ start, end }) => {
if (start.type === "binding") {
if (!start.isPrecise) {
start.normalizedAnchor = { x: 0.5, y: 0.5 };
}
delete start.isPrecise;
}
if (end.type === "binding") {
if (!end.isPrecise) {
end.normalizedAnchor = { x: 0.5, y: 0.5 };
}
delete end.isPrecise;
}
}
}),
propsMigration({
id: arrowShapeVersions.AddLabelPosition,
up: (props) => {
props.labelPosition = 0.5;
},
down: (props) => {
delete props.labelPosition;
}
}),
{
id: arrowShapeVersions.ExtractBindings,
scope: "store",
up: (oldStore) => {
const arrows = Object.values(oldStore).filter(
(r) => r.typeName === "shape" && r.type === "arrow"
);
for (const arrow of arrows) {
const { start, end } = arrow.props;
if (start.type === "binding") {
const id = createBindingId();
const binding = {
typeName: "binding",
id,
type: "arrow",
fromId: arrow.id,
toId: start.boundShapeId,
meta: {},
props: {
terminal: "start",
normalizedAnchor: start.normalizedAnchor,
isExact: start.isExact,
isPrecise: start.isPrecise
}
};
oldStore[id] = binding;
arrow.props.start = { x: 0, y: 0 };
} else {
delete arrow.props.start.type;
}
if (end.type === "binding") {
const id = createBindingId();
const binding = {
typeName: "binding",
id,
type: "arrow",
fromId: arrow.id,
toId: end.boundShapeId,
meta: {},
props: {
terminal: "end",
normalizedAnchor: end.normalizedAnchor,
isExact: end.isExact,
isPrecise: end.isPrecise
}
};
oldStore[id] = binding;
arrow.props.end = { x: 0, y: 0 };
} else {
delete arrow.props.end.type;
}
}
}
},
propsMigration({
id: arrowShapeVersions.AddScale,
up: (props) => {
props.scale = 1;
},
down: (props) => {
delete props.scale;
}
})
]
});
const arrowBindingProps = {
terminal: literalEnum("start", "end"),
normalizedAnchor: vecModelValidator,
isExact: boolean,
isPrecise: boolean
};
const arrowBindingMigrations = createBindingPropsMigrationSequence({
sequence: [{ dependsOn: [arrowShapeVersions.ExtractBindings] }]
});
const cameraValidator = model(
"camera",
object({
typeName: literal("camera"),
id: idValidator("camera"),
x: number,
y: number,
z: number,
meta: jsonValue
})
);
const cameraVersions = createMigrationIds("com.tldraw.camera", {
AddMeta: 1
});
const cameraMigrations = createRecordMigrationSequence({
sequenceId: "com.tldraw.camera",
recordType: "camera",
sequence: [
{
id: cameraVersions.AddMeta,
up: (record) => {
record.meta = {};
}
}
]
});
const CameraRecordType = createRecordType("camera", {
validator: cameraValidator,
scope: "session"
}).withDefaultProperties(
() => ({
x: 0,
y: 0,
z: 1,
meta: {}
})
);
const TL_CURSOR_TYPES = /* @__PURE__ */ new Set([
"none",
"default",
"pointer",
"cross",
"grab",
"rotate",
"grabbing",
"resize-edge",
"resize-corner",
"text",
"move",
"ew-resize",
"ns-resize",
"nesw-resize",
"nwse-resize",
"nesw-rotate",
"nwse-rotate",
"swne-rotate",
"senw-rotate",
"zoom-in",
"zoom-out"
]);
const cursorTypeValidator = setEnum(TL_CURSOR_TYPES);
const cursorValidator = object({
type: cursorTypeValidator,
rotation: number
});
const TL_CANVAS_UI_COLOR_TYPES = /* @__PURE__ */ new Set([
"accent",
"white",
"black",
"selection-stroke",
"selection-fill",
"laser",
"muted-1"
]);
const canvasUiColorTypeValidator = setEnum(TL_CANVAS_UI_COLOR_TYPES);
const TL_SCRIBBLE_STATES = /* @__PURE__ */ new Set(["starting", "paused", "active", "stopping"]);
const scribbleValidator = object({
id: string,
points: arrayOf(vecModelValidator),
size: positiveNumber,
color: canvasUiColorTypeValidator,
opacity: number,
state: setEnum(TL_SCRIBBLE_STATES),
delay: number,
shrink: number,
taper: boolean
});
const pageIdValidator = idValidator("page");
const pageValidator = model(
"page",
object({
typeName: literal("page"),
id: pageIdValidator,
name: string,
index: indexKey,
meta: jsonValue
})
);
const pageVersions = createMigrationIds("com.tldraw.page", {
AddMeta: 1
});
const pageMigrations = createRecordMigrationSequence({
sequenceId: "com.tldraw.page",
recordType: "page",
sequence: [
{
id: pageVersions.AddMeta,
up: (record) => {
record.meta = {};
}
}
]
});
const PageRecordType = createRecordType("page", {
validator: pageValidator,
scope: "document"
}).withDefaultProperties(() => ({
meta: {}
}));
function isPageId(id) {
return PageRecordType.isId(id);
}
const shouldKeyBePreservedBetweenSessions = {
// This object defines keys that should be preserved across calls to loadSnapshot()
id: false,
// meta
typeName: false,
// meta
currentPageId: false,
// does not preserve because who knows if the page still exists
opacityForNextShape: false,
// does not preserve because it's a temporary state
stylesForNextShape: false,
// does not preserve because it's a temporary state
followingUserId: false,
// does not preserve because it's a temporary state
highlightedUserIds: false,
// does not preserve because it's a temporary state
brush: false,
// does not preserve because it's a temporary state
cursor: false,
// does not preserve because it's a temporary state
scribbles: false,
// does not preserve because it's a temporary state
isFocusMode: true,
// preserves because it's a user preference
isDebugMode: true,
// preserves because it's a user preference
isToolLocked: true,
// preserves because it's a user preference
exportBackground: true,
// preserves because it's a user preference
screenBounds: true,
// preserves because it's capturing the user's screen state
insets: true,
// preserves because it's capturing the user's screen state
zoomBrush: false,
// does not preserve because it's a temporary state
chatMessage: false,
// does not preserve because it's a temporary state
isChatting: false,
// does not preserve because it's a temporary state
isPenMode: false,
// does not preserve because it's a temporary state
isGridMode: true,
// preserves because it's a user preference
isFocused: true,
// preserves because obviously
devicePixelRatio: true,
// preserves because it captures the user's screen state
isCoarsePointer: true,
// preserves because it captures the user's screen state
isHoveringCanvas: false,
// does not preserve because it's a temporary state
openMenus: false,
// does not preserve because it's a temporary state
isChangingStyle: false,
// does not preserve because it's a temporary state
isReadonly: true,
// preserves because it's a config option
meta: false,
// does not preserve because who knows what's in there, leave it up to sdk users to save and reinstate
duplicateProps: false
//
};
function pluckPreservingValues(val) {
return val ? filterEntries(val, (key) => {
return shouldKeyBePreservedBetweenSessions[key];
}) : null;
}
idValidator("instance");
function createInstanceRecordType(stylesById) {
const stylesForNextShapeValidators = {};
for (const [id, style] of stylesById) {
stylesForNextShapeValidators[id] = optional(style);
}
const instanceTypeValidator = model(
"instance",
object({
typeName: literal("instance"),
id: idValidator("instance"),
currentPageId: pageIdValidator,
followingUserId: string.nullable(),
brush: boxModelValidator.nullable(),
opacityForNextShape: opacityValidator,
stylesForNextShape: object(stylesForNextShapeValidators),
cursor: cursorValidator,
scribbles: arrayOf(scribbleValidator),
isFocusMode: boolean,
isDebugMode: boolean,
isToolLocked: boolean,
exportBackground: boolean,
screenBounds: boxModelValidator,
insets: arrayOf(boolean),
zoomBrush: boxModelValidator.nullable(),
isPenMode: boolean,
isGridMode: boolean,
chatMessage: string,
isChatting: boolean,
highlightedUserIds: arrayOf(string),
isFocused: boolean,
devicePixelRatio: number,
isCoarsePointer: boolean,
isHoveringCanvas: boolean.nullable(),
openMenus: arrayOf(string),
isChangingStyle: boolean,
isReadonly: boolean,
meta: jsonValue,
duplicateProps: object({
shapeIds: arrayOf(idValidator("shape")),
offset: object({
x: number,
y: number
})
}).nullable()
})
);
return createRecordType("instance", {
validator: instanceTypeValidator,
scope: "session",
ephemeralKeys: {
currentPageId: false,
meta: false,
followingUserId: true,
opacityForNextShape: true,
stylesForNextShape: true,
brush: true,
cursor: true,
scribbles: true,
isFocusMode: true,
isDebugMode: true,
isToolLocked: true,
exportBackground: true,
screenBounds: true,
insets: true,
zoomBrush: true,
isPenMode: true,
isGridMode: true,
chatMessage: true,
isChatting: true,
highlightedUserIds: true,
isFocused: true,
devicePixelRatio: true,
isCoarsePointer: true,
isHoveringCanvas: true,
openMenus: true,
isChangingStyle: true,
isReadonly: true,
duplicateProps: true
}
}).withDefaultProperties(
() => ({
followingUserId: null,
opacityForNextShape: 1,
stylesForNextShape: {},
brush: null,
scribbles: [],
cursor: {
type: "default",
rotation: 0
},
isFocusMode: false,
exportBackground: false,
isDebugMode: false,
isToolLocked: false,
screenBounds: { x: 0, y: 0, w: 1080, h: 720 },
insets: [false, false, false, false],
zoomBrush: null,
isGridMode: false,
isPenMode: false,
chatMessage: "",
isChatting: false,
highlightedUserIds: [],
isFocused: false,
devicePixelRatio: typeof window === "undefined" ? 1 : window.devicePixelRatio,
isCoarsePointer: false,
isHoveringCanvas: null,
openMenus: [],
isChangingStyle: false,
isReadonly: false,
meta: {},
duplicateProps: null
})
);
}
const instanceVersions = createMigrationIds("com.tldraw.instance", {
AddTransparentExportBgs: 1,
RemoveDialog: 2,
AddToolLockMode: 3,
RemoveExtraPropsForNextShape: 4,
AddLabelColor: 5,
AddFollowingUserId: 6,
RemoveAlignJustify: 7,
AddZoom: 8,
AddVerticalAlign: 9,
AddScribbleDelay: 10,
RemoveUserId: 11,
AddIsPenModeAndIsGridMode: 12,
HoistOpacity: 13,
AddChat: 14,
AddHighlightedUserIds: 15,
ReplacePropsForNextShapeWithStylesForNextShape: 16,
AddMeta: 17,
RemoveCursorColor: 18,
AddLonelyProperties: 19,
ReadOnlyReadonly: 20,
AddHoveringCanvas: 21,
AddScribbles: 22,
AddInset: 23,
AddDuplicateProps: 24,
RemoveCanMoveCamera: 25
});
const instanceMigrations = createRecordMigrationSequence({
sequenceId: "com.tldraw.instance",
recordType: "instance",
sequence: [
{
id: instanceVersions.AddTransparentExportBgs,
up: (instance) => {
return { ...instance, exportBackground: true };
}
},
{
id: instanceVersions.RemoveDialog,
up: ({ dialog: _, ...instance }) => {
return instance;
}
},
{
id: instanceVersions.AddToolLockMode,
up: (instance) => {
return { ...instance, isToolLocked: false };
}
},
{
id: instanceVersions.RemoveExtraPropsForNextShape,
up: ({ propsForNextShape, ...instance }) => {
return {
...instance,
propsForNextShape: Object.fromEntries(
Object.entries(propsForNextShape).filter(
([key]) => [
"color",
"labelColor",
"dash",
"fill",
"size",
"font",
"align",
"verticalAlign",
"icon",
"geo",
"arrowheadStart",
"arrowheadEnd",
"spline"
].includes(key)
)
)
};
}
},
{
id: instanceVersions.AddLabelColor,
up: ({ propsForNextShape, ...instance }) => {
return {
...instance,
propsForNextShape: {
...propsForNextShape,
labelColor: "black"
}
};
}
},
{
id: instanceVersions.AddFollowingUserId,
up: (instance) => {
return { ...instance, followingUserId: null };
}
},
{
id: instanceVersions.RemoveAlignJustify,
up: (instance) => {
let newAlign = instance.propsForNextShape.align;
if (newAlign === "justify") {
newAlign = "start";
}
return {
...instance,
propsForNextShape: {
...instance.propsForNextShape,
align: newAlign
}
};
}
},
{
id: instanceVersions.AddZoom,
up: (instance) => {
return { ...instance, zoomBrush: null };
}
},
{
id: instanceVersions.AddVerticalAlign,
up: (instance) => {
return {
...instance,
propsForNextShape: {
...instance.propsForNextShape,
verticalAlign: "middle"
}
};
}
},
{
id: instanceVersions.AddScribbleDelay,
up: (instance) => {
if (instance.scribble !== null) {
return { ...instance, scribble: { ...instance.scribble, delay: 0 } };
}
return { ...instance };
}
},
{
id: instanceVersions.RemoveUserId,
up: ({ userId: _, ...instance }) => {
return instance;
}
},
{
id: instanceVersions.AddIsPenModeAndIsGridMode,
up: (instance) => {
return { ...instance, isPenMode: false, isGridMode: false };
}
},
{
id: instanceVersions.HoistOpacity,
up: ({ propsForNextShape: { opacity, ...propsForNextShape }, ...instance }) => {
return { ...instance, opacityForNextShape: Number(opacity ?? "1"), propsForNextShape };
}
},
{
id: instanceVersions.AddChat,
up: (instance) => {
return { ...instance, chatMessage: "", isChatting: false };
}
},
{
id: instanceVersions.AddHighlightedUserIds,
up: (instance) => {
return { ...instance, highlightedUserIds: [] };
}
},
{
id: instanceVersions.ReplacePropsForNextShapeWithStylesForNextShape,
up: ({ propsForNextShape: _, ...instance }) => {
return { ...instance, stylesForNextShape: {} };
}
},
{
id: instanceVersions.AddMeta,
up: (record) => {
return {
...record,
meta: {}
};
}
},
{
id: instanceVersions.RemoveCursorColor,
up: (record) => {
const { color: _, ...cursor } = record.cursor;
return {
...record,
cursor
};
}
},
{
id: instanceVersions.AddLonelyProperties,
up: (record) => {
return {
...record,
canMoveCamera: true,
isFocused: false,
devicePixelRatio: 1,
isCoarsePointer: false,
openMenus: [],
isChangingStyle: false,
isReadOnly: false
};
}
},
{
id: instanceVersions.ReadOnlyReadonly,
up: ({ isReadOnly: _isReadOnly, ...record }) => {
return {
...record,
isReadonly: _isReadOnly
};
}
},
{
id: instanceVersions.AddHoveringCanvas,
up: (record) => {
return {
...record,
isHoveringCanvas: null
};
}
},
{
id: instanceVersions.AddScribbles,
up: ({ scribble: _, ...record }) => {
return {
...record,
scribbles: []
};
}
},
{
id: instanceVersions.AddInset,
up: (record) => {
return {
...record,
insets: [false, false, false, false]
};
},
down: ({ insets: _, ...record }) => {
return {
...record
};
}
},
{
id: instanceVersions.AddDuplicateProps,
up: (record) => {
return {
...record,
duplicateProps: null
};
},
down: ({ duplicateProps: _, ...record }) => {
return {
...record
};
}
},
{
id: instanceVersions.RemoveCanMoveCamera,
up: ({ canMoveCamera: _, ...record }) => {
return {
...record
};
},
down: (instance) => {
return { ...instance, canMoveCamera: true };
}
}
]
});
const TLINSTANCE_ID = "instance:instance";
const instancePageStateValidator = model(
"instance_page_state",
object({
typeName: literal("instance_page_state"),
id: idValidator("instance_page_state"),
pageId: pageIdValidator,
selectedShapeIds: arrayOf(shapeIdValidator),
hintingShapeIds: arrayOf(shapeIdValidator),
erasingShapeIds: arrayOf(shapeIdValidator),
hoveredShapeId: shapeIdValidator.nullable(),
editingShapeId: shapeIdValidator.nullable(),
croppingShapeId: shapeIdValidator.nullable(),
focusedGroupId: shapeIdValidator.nullable(),
meta: jsonValue
})
);
const instancePageStateVersions = createMigrationIds("com.tldraw.instance_page_state", {
AddCroppingId: 1,
RemoveInstanceIdAndCameraId: 2,
AddMeta: 3,
RenameProperties: 4,
RenamePropertiesAgain: 5
});
const instancePageStateMigrations = createRecordMigrationSequence({
sequenceId: "com.tldraw.instance_page_state",
recordType: "instance_page_state",
sequence: [
{
id: instancePageStateVersions.AddCroppingId,
up(instance) {
instance.croppingShapeId = null;
}
},
{
id: instancePageStateVersions.RemoveInstanceIdAndCameraId,
up(instance) {
delete instance.instanceId;
delete instance.cameraId;
}
},
{
id: instancePageStateVersions.AddMeta,
up: (record) => {
record.meta = {};
}
},
{
id: instancePageStateVersions.RenameProperties,
// this migration is cursed: it was written wrong and doesn't do anything.
// rather than replace it, I've added another migration below that fixes it.
up: (_record) => {
},
down: (_record) => {
}
},
{
id: instancePageStateVersions.RenamePropertiesAgain,
up: (record) => {
record.selectedShapeIds = record.selectedIds;
delete record.selectedIds;
record.hintingShapeIds = record.hintingIds;
delete record.hintingIds;
record.erasingShapeIds = record.erasingIds;
delete record.erasingIds;
record.hoveredShapeId = record.hoveredId;
delete record.hoveredId;
record.editingShapeId = record.editingId;
delete record.editingId;
record.croppingShapeId = record.croppingShapeId ?? record.croppingId ?? null;
delete record.croppingId;
record.focusedGroupId = record.focusLayerId;
delete record.focusLayerId;
},
down: (record) => {
record.selectedIds = record.selectedShapeIds;
delete record.selectedShapeIds;
record.hintingIds = record.hintingShapeIds;
delete record.hintingShapeIds;
record.erasingIds = record.erasingShapeIds;
delete record.erasingShapeIds;
record.hoveredId = record.hoveredShapeId;
delete record.hoveredShapeId;
record.editingId = record.editingShapeId;
delete record.editingShapeId;
record.croppingId = record.croppingShapeId;
delete record.croppingShapeId;
record.focusLayerId = record.focusedGroupId;
delete record.focusedGroupId;
}
}
]
});
const InstancePageStateRecordType = createRecordType(
"instance_page_state",
{
validator: instancePageStateValidator,
scope: "session",
ephemeralKeys: {
pageId: false,
selectedShapeIds: false,
editingShapeId: false,
croppingShapeId: false,
meta: false,
hintingShapeIds: true,
erasingShapeIds: true,
hoveredShapeId: true,
focusedGroupId: true
}
}
).withDefaultProperties(
() => ({
editingShapeId: null,
croppingShapeId: null,
selectedShapeIds: [],
hoveredShapeId: null,
erasingShapeIds: [],
hintingShapeIds: [],
focusedGroupId: null,
meta: {}
})
);
const pointerValidator = model(
"pointer",
object({
typeName: literal("pointer"),
id: idValidator("pointer"),
x: number,
y: number,
lastActivityTimestamp: number,
meta: jsonValue
})
);
const pointerVersions = createMigrationIds("com.tldraw.pointer", {
AddMeta: 1
});
const pointerMigrations = createRecordMigrationSequence({
sequenceId: "com.tldraw.pointer",
recordType: "pointer",
sequence: [
{
id: pointerVersions.AddMeta,
up: (record) => {
record.meta = {};
}
}
]
});
const PointerRecordType = createRecordType("pointer", {
validator: pointerValidator,
scope: "session"
}).withDefaultProperties(
() => ({
x: 0,
y: 0,
lastActivityTimestamp: 0,
meta: {}
})
);
const TLPOINTER_ID = PointerRecordType.createId("pointer");
const instancePresenceValidator = model(
"instance_presence",
object({
typeName: literal("instance_presence"),
id: idValidator("instance_presence"),
userId: string,
userName: string,
lastActivityTimestamp: number,
followingUserId: string.nullable(),
cursor: object({
x: number,
y: number,
type: cursorTypeValidator,
rotation: number
}),
color: string,
camera: object({
x: number,
y: number,
z: number
}),
screenBounds: boxModelValidator,
selectedShapeIds: arrayOf(idValidator("shape")),
currentPageId: idValidator("page"),
brush: boxModelValidator.nullable(),
scribbles: arrayOf(scribbleValidator),
chatMessage: string,
meta: jsonValue
})
);
const instancePresenceVersions = createMigrationIds("com.tldraw.instance_presence", {
AddScribbleDelay: 1,
RemoveInstanceId: 2,
AddChatMessage: 3,
AddMeta: 4,
RenameSelectedShapeIds: 5
});
const instancePresenceMigrations = createRecordMigrationSequence({
sequenceId: "com.tldraw.instance_presence",
recordType: "instance_presence",
sequence: [
{
id: instancePresenceVersions.AddScribbleDelay,
up: (instance) => {
if (instance.scribble !== null) {
instance.scribble.delay = 0;
}
}
},
{
id: instancePresenceVersions.RemoveInstanceId,
up: (instance) => {
delete instance.instanceId;
}
},
{
id: instancePresenceVersions.AddChatMessage,
up: (instance) => {
instance.chatMessage = "";
}
},
{
id: instancePresenceVersions.AddMeta,
up: (record) => {
record.meta = {};
}
},
{
id: instancePresenceVersions.RenameSelectedShapeIds,
up: (_record) => {
}
}
]
});
const InstancePresenceRecordType = createRecordType(
"instance_presence",
{
validator: instancePresenceValidator,
scope: "presence"
}
).withDefaultProperties(() => ({
lastActivityTimestamp: 0,
followingUserId: null,
color: "#FF0000",
camera: {
x: 0,
y: 0,
z: 1
},
cursor: {
x: 0,
y: 0,
type: "default",
rotation: 0
},
screenBounds: {
x: 0,
y: 0,
w: 1,
h: 1
},
selectedShapeIds: [],
brush: null,
scribbles: [],
chatMessage: "",
meta: {}
}));
function createPresenceStateDerivation($user, instanceId) {
return (store) => {
return computed("instancePresence", () => {
const instance = store.get(TLINSTANCE_ID);
const pageState = store.get(InstancePageStateRecordType.createId(instance?.currentPageId));
const camera = store.get(CameraRecordType.createId(instance?.currentPageId));
const pointer = store.get(TLPOINTER_ID);
const user = $user.get();
if (!pageState || !instance || !camera || !pointer || !user) {
return null;
}
return InstancePresenceRecordType.create({
id: InstancePresenceRecordType.createId(store.id),
selectedShapeIds: pageState.selectedShapeIds,
brush: instance.brush,
scribbles: instance.scribbles,
userId: user.id,
userName: user.name,
followingUserId: instance.followingUserId,
camera: {
x: camera.x,
y: camera.y,
z: camera.z
},
color: user.color,
currentPageId: instance.currentPageId,
cursor: {
x: pointer.x,
y: pointer.y,
rotation: instance.cursor.rotation,
type: instance.cursor.type
},
lastActivityTimestamp: pointer.lastActivityTimestamp,
screenBounds: instance.screenBounds,
chatMessage: instance.chatMessage,
meta: {}
});
});
};
}
const documentValidator = model(
"document",
object({
typeName: literal("document"),
id: literal("document:document"),
gridSize: number,
name: string,
meta: jsonValue
})
);
const documentVersions = createMigrationIds("com.tldraw.document", {
AddName: 1,
AddMeta: 2
});
const documentMigrations = createRecordMigrationSequence({
sequenceId: "com.tldraw.document",
recordType: "document",
sequence: [
{
id: documentVersions.AddName,
up: (document) => {
document.name = "";
},
down: (document) => {
delete document.name;
}
},
{
id: documentVersions.AddMeta,
up: (record) => {
record.meta = {};
}
}
]
});
const DocumentRecordType = createRecordType("document", {
validator: documentValidator,
scope: "document"
}).withDefaultProperties(
() => ({
gridSize: 10,
name: "",
meta: {}
})
);
const TLDOCUMENT_ID = DocumentRecordType.createId("document");
function sortByIndex(a, b) {
if (a.index < b.index) {
return -1;
} else if (a.index > b.index) {
return 1;
}
return 0;
}
function redactRecordForErrorReporting(record) {
if (record.typeName === "asset") {
if ("src" in record) {
record.src = "";
}
if ("src" in record.props) {
record.props.src = "";
}
}
}
function onValidationFailure({
error,
phase,
record,
recordBefore
}) {
const isExistingValidationIssue = (
// if we're initializing the store for the first time, we should
// allow invalid records so people can load old buggy data:
(phase === "initialize")
);
annotateError(error, {
tags: {
origin: "store.validateRecord",
storePhase: phase,
isExistingValidationIssue
},
extras: {
recordBefore: recordBefore ? redactRecordForErrorReporting(structuredClone(recordBefore)) : void 0,
recordAfter: redactRecordForErrorReporting(structuredClone(record))
}
});
throw error;
}
function getDefaultPages() {
return [
PageRecordType.create({
id: "page:page",
name: "Page 1",
index: "a1",
meta: {}
})
];
}
function createIntegrityChecker(store) {
const $pageIds = store.query.ids("page");
const $pageStates = store.query.records("instance_page_state");
const ensureStoreIsUsable = () => {
if (!store.has(TLDOCUMENT_ID)) {
store.put([DocumentRecordType.create({ id: TLDOCUMENT_ID, name: store.props.defaultName })]);
return ensureStoreIsUsable();
}
if (!store.has(TLPOINTER_ID)) {
store.put([PointerRecordType.create({ id: TLPOINTER_ID })]);
return ensureStoreIsUsable();
}
const pageIds = $pageIds.get();
if (pageIds.size === 0) {
store.put(getDefaultPages());
return ensureStoreIsUsable();
}
const getFirstPageId = () => [...pageIds].map((id) => store.get(id)).sort(sortByIndex)[0].id;
const instanceState = store.get(TLINSTANCE_ID);
if (!instanceState) {
store.put([
store.schema.types.instance.create({
id: TLINSTANCE_ID,
currentPageId: getFirstPageId(),
exportBackground: true
})
]);
return ensureStoreIsUsable();
} else if (!pageIds.has(instanceState.currentPageId)) {
store.put([{ ...instanceState, currentPageId: getFirstPageId() }]);
return ensureStoreIsUsable();
}
const missingPageStateIds = /* @__PURE__ */ new Set();
const missingCameraIds = /* @__PURE__ */ new Set();
for (const id of pageIds) {
const pageStateId = InstancePageStateRecordType.createId(id);
const pageState = store.get(pageStateId);
if (!pageState) {
missingPageStateIds.add(pageStateId);
}
const cameraId = CameraRecordType.createId(id);
if (!store.has(cameraId)) {
missingCameraIds.add(cameraId);
}
}
if (missingPageStateIds.size > 0) {
store.put(
[...missingPageStateIds].map(
(id) => InstancePageStateRecordType.create({
id,
pageId: InstancePageStateRecordType.parseId(id)
})
)
);
}
if (missingCameraIds.size > 0) {
store.put([...missingCameraIds].map((id) => CameraRecordType.create({ id })));
}
const pageStates = $pageStates.get();
for (const pageState of pageStates) {
if (!pageIds.has(pageState.pageId)) {
store.remove([pageState.id]);
continue;
}
if (pageState.croppingShapeId && !store.has(pageState.croppingShapeId)) {
store.put([{ ...pageState, croppingShapeId: null }]);
return ensureStoreIsUsable();
}
if (pageState.focusedGroupId && !store.has(pageState.focusedGroupId)) {
store.put([{ ...pageState, focusedGroupId: null }]);
return ensureStoreIsUsable();
}
if (pageState.hoveredShapeId && !store.has(pageState.hoveredShapeId)) {
store.put([{ ...pageState, hoveredShapeId: null }]);
return ensureStoreIsUsable();
}
const filteredSelectedIds = pageState.selectedShapeIds.filter((id) => store.has(id));
if (filteredSelectedIds.length !== pageState.selectedShapeIds.length) {
store.put([{ ...pageState, selectedShapeIds: filteredSelectedIds }]);
return ensureStoreIsUsable();
}
const filteredHintingIds = pageState.hintingShapeIds.filter((id) => store.has(id));
if (filteredHintingIds.length !== pageState.hintingShapeIds.length) {
store.put([{ ...pageState, hintingShapeIds: filteredHintingIds }]);
return ensureStoreIsUsable();
}
const filteredErasingIds = pageState.erasingShapeIds.filter((id) => store.has(id));
if (filteredErasingIds.length !== pageState.erasingShapeIds.length) {
store.put([{ ...pageState, erasingShapeIds: filteredErasingIds }]);
return ensureStoreIsUsable();
}
}
};
return ensureStoreIsUsable;
}
const bookmarkAssetValidator = createAssetValidator(
"bookmark",
object({
title: string,
description: string,
image: string,
favicon: string,
src: srcUrl.nullable()
})
);
const Versions$d = createMigrationIds("com.tldraw.asset.bookmark", {
MakeUrlsValid: 1,
AddFavicon: 2
});
const bookmarkAssetMigrations = createRecordMigrationSequence({
sequenceId: "com.tldraw.asset.bookmark",
recordType: "asset",
filter: (asset) => asset.type === "bookmark",
sequence: [
{
id: Versions$d.MakeUrlsValid,
up: (asset) => {
if (!srcUrl.isValid(asset.props.src)) {
asset.props.src = "";
}
},
down: (_asset) => {
}
},
{
id: Versions$d.AddFavicon,
up: (asset) => {
if (!srcUrl.isValid(asset.props.favicon)) {
asset.props.favicon = "";
}
},
down: (asset) => {
delete asset.props.favicon;
}
}
]
});
const imageAssetValidator = createAssetValidator(
"image",
object({
w: number,
h: number,
name: string,
isAnimated: boolean,
mimeType: string.nullable(),
src: srcUrl.nullable(),
fileSize: nonZeroNumber.optional()
})
);
const Versions$c = createMigrationIds("com.tldraw.asset.image", {
AddIsAnimated: 1,
RenameWidthHeight: 2,
MakeUrlsValid: 3,
AddFileSize: 4,
MakeFileSizeOptional: 5
});
const imageAssetMigrations = createRecordMigrationSequence({
sequenceId: "com.tldraw.asset.image",
recordType: "asset",
filter: (asset) => asset.type === "image",
sequence: [
{
id: Versions$c.AddIsAnimated,
up: (asset) => {
asset.props.isAnimated = false;
},
down: (asset) => {
delete asset.props.isAnimated;
}
},
{
id: Versions$c.RenameWidthHeight,
up: (asset) => {
asset.props.w = asset.props.width;
asset.props.h = asset.props.height;
delete asset.props.width;
delete asset.props.height;
},
down: (asset) => {
asset.props.width = asset.props.w;
asset.props.height = asset.props.h;
delete asset.props.w;
delete asset.props.h;
}
},
{
id: Versions$c.MakeUrlsValid,
up: (asset) => {
if (!srcUrl.isValid(asset.props.src)) {
asset.props.src = "";
}
},
down: (_asset) => {
}
},
{
id: Versions$c.AddFileSize,
up: (asset) => {
asset.props.fileSize = -1;
},
down: (asset) => {
delete asset.props.fileSize;
}
},
{
id: Versions$c.MakeFileSizeOptional,
up: (asset) => {
if (asset.props.fileSize === -1) {
asset.props.fileSize = void 0;
}
},
down: (asset) => {
if (asset.props.fileSize === void 0) {
asset.props.fileSize = -1;
}
}
}
]
});
const videoAssetValidator = createAssetValidator(
"video",
object({
w: number,
h: number,
name: string,
isAnimated: boolean,
mimeType: string.nullable(),
src: srcUrl.nullable(),
fileSize: number.optional()
})
);
const Versions$b = createMigrationIds("com.tldraw.asset.video", {
AddIsAnimated: 1,
RenameWidthHeight: 2,
MakeUrlsValid: 3,
AddFileSize: 4,
MakeFileSizeOptional: 5
});
const videoAssetMigrations = createRecordMigrationSequence({
sequenceId: "com.tldraw.asset.video",
recordType: "asset",
filter: (asset) => asset.type === "video",
sequence: [
{
id: Versions$b.AddIsAnimated,
up: (asset) => {
asset.props.isAnimated = false;
},
down: (asset) => {
delete asset.props.isAnimated;
}
},
{
id: Versions$b.RenameWidthHeight,
up: (asset) => {
asset.props.w = asset.props.width;
asset.props.h = asset.props.height;
delete asset.props.width;
delete asset.props.height;
},
down: (asset) => {
asset.props.width = asset.props.w;
asset.props.height = asset.props.h;
delete asset.props.w;
delete asset.props.h;
}
},
{
id: Versions$b.MakeUrlsValid,
up: (asset) => {
if (!srcUrl.isValid(asset.props.src)) {
asset.props.src = "";
}
},
down: (_asset) => {
}
},
{
id: Versions$b.AddFileSize,
up: (asset) => {
asset.props.fileSize = -1;
},
down: (asset) => {
delete asset.props.fileSize;
}
},
{
id: Versions$b.MakeFileSizeOptional,
up: (asset) => {
if (asset.props.fileSize === -1) {
asset.props.fileSize = void 0;
}
},
down: (asset) => {
if (asset.props.fileSize === void 0) {
asset.props.fileSize = -1;
}
}
}
]
});
const assetValidator = model(
"asset",
union("type", {
image: imageAssetValidator,
video: videoAssetValidator,
bookmark: bookmarkAssetValidator
})
);
const assetVersions = createMigrationIds("com.tldraw.asset", {
AddMeta: 1
});
const assetMigrations = createRecordMigrationSequence({
sequenceId: "com.tldraw.asset",
recordType: "asset",
sequence: [
{
id: assetVersions.AddMeta,
up: (record) => {
record.meta = {};
}
}
]
});
const AssetRecordType = createRecordType("asset", {
validator: assetValidator,
scope: "document"
}).withDefaultProperties(() => ({
meta: {}
}));
const bookmarkShapeProps = {
w: nonZeroNumber,
h: nonZeroNumber,
assetId: assetIdValidator.nullable(),
url: linkUrl
};
const Versions$a = createShapePropsMigrationIds("bookmark", {
NullAssetId: 1,
MakeUrlsValid: 2
});
const bookmarkShapeMigrations = createShapePropsMigrationSequence({
sequence: [
{
id: Versions$a.NullAssetId,
up: (props) => {
if (props.assetId === void 0) {
props.assetId = null;
}
},
down: "retired"
},
{
id: Versions$a.MakeUrlsValid,
up: (props) => {
if (!linkUrl.isValid(props.url)) {
props.url = "";
}
},
down: (_props) => {
}
}
]
});
const DrawShapeSegment = object({
type: literalEnum("free", "straight"),
points: arrayOf(vecModelValidator)
});
const drawShapeProps = {
color: DefaultColorStyle,
fill: DefaultFillStyle,
dash: DefaultDashStyle,
size: DefaultSizeStyle,
segments: arrayOf(DrawShapeSegment),
isComplete: boolean,
isClosed: boolean,
isPen: boolean,
scale: nonZeroNumber
};
const Versions$9 = createShapePropsMigrationIds("draw", {
AddInPen: 1,
AddScale: 2
});
const drawShapeMigrations = createShapePropsMigrationSequence({
sequence: [
{
id: Versions$9.AddInPen,
up: (props) => {
const { points } = props.segments[0];
if (points.length === 0) {
props.isPen = false;
return;
}
let isPen = !(points[0].z === 0 || points[0].z === 0.5);
if (points[1]) {
isPen = isPen && !(points[1].z === 0 || points[1].z === 0.5);
}
props.isPen = isPen;
},
down: "retired"
},
{
id: Versions$9.AddScale,
up: (props) => {
props.scale = 1;
},
down: (props) => {
delete props.scale;
}
}
]
});
const TLDRAW_APP_RE$1 = /(^\/r\/[^/]+\/?$)/;
const EMBED_DEFINITIONS = [
{
hostnames: ["beta.tldraw.com", "tldraw.com", "localhost:3000"],
fromEmbedUrl: (url) => {
const urlObj = safeParseUrl(url);
if (urlObj && urlObj.pathname.match(TLDRAW_APP_RE$1)) {
return url;
}
return;
}
},
{
hostnames: ["figma.com"],
fromEmbedUrl: (url) => {
const urlObj = safeParseUrl(url);
if (urlObj && urlObj.pathname.match(/^\/embed\/?$/)) {
const outUrl = urlObj.searchParams.get("url");
if (outUrl) {
return outUrl;
}
}
return;
}
},
{
hostnames: ["google.*"],
fromEmbedUrl: (url) => {
const urlObj = safeParseUrl(url);
if (!urlObj) return;
const matches = urlObj.pathname.match(/^\/maps\/embed\/v1\/view\/?$/);
if (matches && urlObj.searchParams.has("center") && urlObj.searchParams.get("zoom")) {
const zoom = urlObj.searchParams.get("zoom");
const [lat, lon] = urlObj.searchParams.get("center").split(",");
return `https://www.google.com/maps/@${lat},${lon},${zoom}z`;
}
return;
}
},
{
hostnames: ["val.town"],
fromEmbedUrl: (url) => {
const urlObj = safeParseUrl(url);
const matches = urlObj && urlObj.pathname.match(/\/embed\/(.+)\/?/);
if (matches) {
return `https://www.val.town/v/${matches[1]}`;
}
return;
}
},
{
hostnames: ["codesandbox.io"],
fromEmbedUrl: (url) => {
const urlObj = safeParseUrl(url);
const matches = urlObj && urlObj.pathname.match(/\/embed\/([^/]+)\/?/);
if (matches) {
return `https://codesandbox.io/s/${matches[1]}`;
}
return;
}
},
{
hostnames: ["codepen.io"],
fromEmbedUrl: (url) => {
const CODEPEN_EMBED_REGEXP = /https:\/\/codepen.io\/([^/]+)\/embed\/([^/]+)/;
const matches = url.match(CODEPEN_EMBED_REGEXP);
if (matches) {
const [_, user, id] = matches;
return `https://codepen.io/${user}/pen/${id}`;
}
return;
}
},
{
hostnames: ["scratch.mit.edu"],
fromEmbedUrl: (url) => {
const SCRATCH_EMBED_REGEXP = /https:\/\/scratch.mit.edu\/projects\/embed\/([^/]+)/;
const matches = url.match(SCRATCH_EMBED_REGEXP);
if (matches) {
const [_, id] = matches;
return `https://scratch.mit.edu/projects/${id}`;
}
return;
}
},
{
hostnames: ["*.youtube.com", "youtube.com", "youtu.be"],
fromEmbedUrl: (url) => {
const urlObj = safeParseUrl(url);
if (!urlObj) return;
const hostname = urlObj.hostname.replace(/^www./, "");
if (hostname === "youtube.com") {
const matches = urlObj.pathname.match(/^\/embed\/([^/]+)\/?/);
if (matches) {
return `https://www.youtube.com/watch?v=${matches[1]}`;
}
}
return;
}
},
{
hostnames: ["calendar.google.*"],
fromEmbedUrl: (url) => {
const urlObj = safeParseUrl(url);
const srcQs = urlObj?.searchParams.get("src");
if (urlObj?.pathname.match(/\/calendar\/embed/) && srcQs) {
urlObj.pathname = "/calendar/u/0";
const keys = Array.from(urlObj.searchParams.keys());
for (const key of keys) {
urlObj.searchParams.delete(key);
}
urlObj.searchParams.set("cid", srcQs);
return urlObj.href;
}
return;
}
},
{
hostnames: ["docs.google.*"],
fromEmbedUrl: (url) => {
const urlObj = safeParseUrl(url);
if (urlObj?.pathname.match(/^\/presentation/) && urlObj?.pathname.match(/\/embed\/?$/)) {
urlObj.pathname = urlObj.pathname.replace(/\/embed$/, "/pub");
const keys = Array.from(urlObj.searchParams.keys());
for (const key of keys) {
urlObj.searchParams.delete(key);
}
return urlObj.href;
}
return;
}
},
{
hostnames: ["gist.github.com"],
fromEmbedUrl: (url) => {
const urlObj = safeParseUrl(url);
if (urlObj && urlObj.pathname.match(/\/([^/]+)\/([^/]+)/)) {
if (!url.split("/").pop()) return;
return url;
}
return;
}
},
{
hostnames: ["replit.com"],
fromEmbedUrl: (url) => {
const urlObj = safeParseUrl(url);
if (urlObj && urlObj.pathname.match(/\/@([^/]+)\/([^/]+)/) && urlObj.searchParams.has("embed")) {
urlObj.searchParams.delete("embed");
return urlObj.href;
}
return;
}
},
{
hostnames: ["felt.com"],
fromEmbedUrl: (url) => {
const urlObj = safeParseUrl(url);
if (urlObj && urlObj.pathname.match(/^\/embed\/map\//)) {
urlObj.pathname = urlObj.pathname.replace(/^\/embed/, "");
return urlObj.href;
}
return;
}
},
{
hostnames: ["open.spotify.com"],
fromEmbedUrl: (url) => {
const urlObj = safeParseUrl(url);
if (urlObj && urlObj.pathname.match(/^\/embed\/(artist|album)\//)) {
return urlObj.origin + urlObj.pathname.replace(/^\/embed/, "");
}
return;
}
},
{
hostnames: ["vimeo.com", "player.vimeo.com"],
fromEmbedUrl: (url) => {
const urlObj = safeParseUrl(url);
if (urlObj && urlObj.hostname === "player.vimeo.com") {
const matches = urlObj.pathname.match(/^\/video\/([^/]+)\/?$/);
if (matches) {
return "https://vimeo.com/" + matches[1];
}
}
return;
}
},
{
hostnames: ["excalidraw.com"],
fromEmbedUrl: (url) => {
const urlObj = safeParseUrl(url);
if (urlObj && urlObj.hash.match(/#room=/)) {
return url;
}
return;
}
},
{
hostnames: ["observablehq.com"],
fromEmbedUrl: (url) => {
const urlObj = safeParseUrl(url);
if (urlObj && urlObj.pathname.match(/^\/embed\/@([^/]+)\/([^/]+)\/?$/)) {
return `${urlObj.origin}${urlObj.pathname.replace("/embed", "")}#cell-*`;
}
if (urlObj && urlObj.pathname.match(/^\/embed\/([^/]+)\/?$/)) {
return `${urlObj.origin}${urlObj.pathname.replace("/embed", "/d")}#cell-*`;
}
return;
}
},
{
hostnames: ["desmos.com"],
fromEmbedUrl: (url) => {
const urlObj = safeParseUrl(url);
if (urlObj && urlObj.hostname === "www.desmos.com" && urlObj.pathname.match(/^\/calculator\/([^/]+)\/?$/) && urlObj.search === "?embed" && urlObj.hash === "") {
return url.replace("?embed", "");
}
return;
}
}
];
const embedShapeProps = {
w: nonZeroNumber,
h: nonZeroNumber,
url: string
};
const Versions$8 = createShapePropsMigrationIds("embed", {
GenOriginalUrlInEmbed: 1,
RemoveDoesResize: 2,
RemoveTmpOldUrl: 3,
RemovePermissionOverrides: 4
});
const embedShapeMigrations = createShapePropsMigrationSequence({
sequence: [
{
id: Versions$8.GenOriginalUrlInEmbed,
// add tmpOldUrl property
up: (props) => {
try {
const url = props.url;
const host = new URL(url).host.replace("www.", "");
let originalUrl;
for (const localEmbedDef of EMBED_DEFINITIONS) {
if (localEmbedDef.hostnames.includes(host)) {
try {
originalUrl = localEmbedDef.fromEmbedUrl(url);
} catch (err) {
console.warn(err);
}
}
}
props.tmpOldUrl = props.url;
props.url = originalUrl ?? "";
} catch {
props.url = "";
props.tmpOldUrl = props.url;
}
},
down: "retired"
},
{
id: Versions$8.RemoveDoesResize,
up: (props) => {
delete props.doesResize;
},
down: "retired"
},
{
id: Versions$8.RemoveTmpOldUrl,
up: (props) => {
delete props.tmpOldUrl;
},
down: "retired"
},
{
id: Versions$8.RemovePermissionOverrides,
up: (props) => {
delete props.overridePermissions;
},
down: "retired"
}
]
});
const frameShapeProps = {
w: nonZeroNumber,
h: nonZeroNumber,
name: string
};
const frameShapeMigrations = createShapePropsMigrationSequence({
sequence: []
});
const DefaultHorizontalAlignStyle = StyleProp.defineEnum("tldraw:horizontalAlign", {
defaultValue: "middle",
values: ["start", "middle", "end", "start-legacy", "end-legacy", "middle-legacy"]
});
const DefaultVerticalAlignStyle = StyleProp.defineEnum("tldraw:verticalAlign", {
defaultValue: "middle",
values: ["start", "middle", "end"]
});
const GeoShapeGeoStyle = StyleProp.defineEnum("tldraw:geo", {
defaultValue: "rectangle",
values: [
"cloud",
"rectangle",
"ellipse",
"triangle",
"diamond",
"pentagon",
"hexagon",
"octagon",
"star",
"rhombus",
"rhombus-2",
"oval",
"trapezoid",
"arrow-right",
"arrow-left",
"arrow-up",
"arrow-down",
"x-box",
"check-box",
"heart"
]
});
const geoShapeProps = {
geo: GeoShapeGeoStyle,
labelColor: DefaultLabelColorStyle,
color: DefaultColorStyle,
fill: DefaultFillStyle,
dash: DefaultDashStyle,
size: DefaultSizeStyle,
font: DefaultFontStyle,
align: DefaultHorizontalAlignStyle,
verticalAlign: DefaultVerticalAlignStyle,
url: linkUrl,
w: nonZeroNumber,
h: nonZeroNumber,
growY: positiveNumber,
text: string,
scale: nonZeroNumber
};
const geoShapeVersions = createShapePropsMigrationIds("geo", {
AddUrlProp: 1,
AddLabelColor: 2,
RemoveJustify: 3,
AddCheckBox: 4,
AddVerticalAlign: 5,
MigrateLegacyAlign: 6,
AddCloud: 7,
MakeUrlsValid: 8,
AddScale: 9
});
const geoShapeMigrations = createShapePropsMigrationSequence({
sequence: [
{
id: geoShapeVersions.AddUrlProp,
up: (props) => {
props.url = "";
},
down: "retired"
},
{
id: geoShapeVersions.AddLabelColor,
up: (props) => {
props.labelColor = "black";
},
down: "retired"
},
{
id: geoShapeVersions.RemoveJustify,
up: (props) => {
if (props.align === "justify") {
props.align = "start";
}
},
down: "retired"
},
{
id: geoShapeVersions.AddCheckBox,
up: (_props) => {
},
down: "retired"
},
{
id: geoShapeVersions.AddVerticalAlign,
up: (props) => {
props.verticalAlign = "middle";
},
down: "retired"
},
{
id: geoShapeVersions.MigrateLegacyAlign,
up: (props) => {
let newAlign;
switch (props.align) {
case "start":
newAlign = "start-legacy";
break;
case "end":
newAlign = "end-legacy";
break;
default:
newAlign = "middle-legacy";
break;
}
props.align = newAlign;
},
down: "retired"
},
{
id: geoShapeVersions.AddCloud,
up: (_props) => {
},
down: "retired"
},
{
id: geoShapeVersions.MakeUrlsValid,
up: (props) => {
if (!linkUrl.isValid(props.url)) {
props.url = "";
}
},
down: (_props) => {
}
},
{
id: geoShapeVersions.AddScale,
up: (props) => {
props.scale = 1;
},
down: (props) => {
delete props.scale;
}
}
]
});
const groupShapeProps = {};
const groupShapeMigrations = createShapePropsMigrationSequence({ sequence: [] });
const highlightShapeProps = {
color: DefaultColorStyle,
size: DefaultSizeStyle,
segments: arrayOf(DrawShapeSegment),
isComplete: boolean,
isPen: boolean,
scale: nonZeroNumber
};
const Versions$7 = createShapePropsMigrationIds("highlight", {
AddScale: 1
});
const highlightShapeMigrations = createShapePropsMigrationSequence({
sequence: [
{
id: Versions$7.AddScale,
up: (props) => {
props.scale = 1;
},
down: (props) => {
delete props.scale;
}
}
]
});
const ImageShapeCrop = object({
topLeft: vecModelValidator,
bottomRight: vecModelValidator
});
const imageShapeProps = {
w: nonZeroNumber,
h: nonZeroNumber,
playing: boolean,
url: linkUrl,
assetId: assetIdValidator.nullable(),
crop: ImageShapeCrop.nullable(),
flipX: boolean,
flipY: boolean
};
const Versions$6 = createShapePropsMigrationIds("image", {
AddUrlProp: 1,
AddCropProp: 2,
MakeUrlsValid: 3,
AddFlipProps: 4
});
const imageShapeMigrations = createShapePropsMigrationSequence({
sequence: [
{
id: Versions$6.AddUrlProp,
up: (props) => {
props.url = "";
},
down: "retired"
},
{
id: Versions$6.AddCropProp,
up: (props) => {
props.crop = null;
},
down: (props) => {
delete props.crop;
}
},
{
id: Versions$6.MakeUrlsValid,
up: (props) => {
if (!linkUrl.isValid(props.url)) {
props.url = "";
}
},
down: (_props) => {
}
},
{
id: Versions$6.AddFlipProps,
up: (props) => {
props.flipX = false;
props.flipY = false;
},
down: (props) => {
delete props.flipX;
delete props.flipY;
}
}
]
});
const LineShapeSplineStyle = StyleProp.defineEnum("tldraw:spline", {
defaultValue: "line",
values: ["cubic", "line"]
});
const lineShapePointValidator = object({
id: string,
index: indexKey,
x: number,
y: number
});
const lineShapeProps = {
color: DefaultColorStyle,
dash: DefaultDashStyle,
size: DefaultSizeStyle,
spline: LineShapeSplineStyle,
points: dict(string, lineShapePointValidator),
scale: nonZeroNumber
};
const lineShapeVersions = createShapePropsMigrationIds("line", {
AddSnapHandles: 1,
RemoveExtraHandleProps: 2,
HandlesToPoints: 3,
PointIndexIds: 4,
AddScale: 5
});
const lineShapeMigrations = createShapePropsMigrationSequence({
sequence: [
{
id: lineShapeVersions.AddSnapHandles,
up: (props) => {
for (const handle of Object.values(props.handles)) {
handle.canSnap = true;
}
},
down: "retired"
},
{
id: lineShapeVersions.RemoveExtraHandleProps,
up: (props) => {
props.handles = objectMapFromEntries(
Object.values(props.handles).map((handle) => [
handle.index,
{
x: handle.x,
y: handle.y
}
])
);
},
down: (props) => {
const handles = Object.entries(props.handles).map(([index, handle]) => ({ index, ...handle })).sort(sortByIndex$1);
props.handles = Object.fromEntries(
handles.map((handle, i) => {
const id = i === 0 ? "start" : i === handles.length - 1 ? "end" : `handle:${handle.index}`;
return [
id,
{
id,
type: "vertex",
canBind: false,
canSnap: true,
index: handle.index,
x: handle.x,
y: handle.y
}
];
})
);
}
},
{
id: lineShapeVersions.HandlesToPoints,
up: (props) => {
const sortedHandles = Object.entries(props.handles).map(([index, { x, y }]) => ({ x, y, index })).sort(sortByIndex$1);
props.points = sortedHandles.map(({ x, y }) => ({ x, y }));
delete props.handles;
},
down: (props) => {
const indices = getIndices(props.points.length);
props.handles = Object.fromEntries(
props.points.map((handle, i) => {
const index = indices[i];
return [
index,
{
x: handle.x,
y: handle.y
}
];
})
);
delete props.points;
}
},
{
id: lineShapeVersions.PointIndexIds,
up: (props) => {
const indices = getIndices(props.points.length);
props.points = Object.fromEntries(
props.points.map((point, i) => {
const id = indices[i];
return [
id,
{
id,
index: id,
x: point.x,
y: point.y
}
];
})
);
},
down: (props) => {
const sortedHandles = Object.values(props.points).sort(sortByIndex$1);
props.points = sortedHandles.map(({ x, y }) => ({ x, y }));
}
},
{
id: lineShapeVersions.AddScale,
up: (props) => {
props.scale = 1;
},
down: (props) => {
delete props.scale;
}
}
]
});
const noteShapeProps = {
color: DefaultColorStyle,
labelColor: DefaultLabelColorStyle,
size: DefaultSizeStyle,
font: DefaultFontStyle,
fontSizeAdjustment: positiveNumber,
align: DefaultHorizontalAlignStyle,
verticalAlign: DefaultVerticalAlignStyle,
growY: positiveNumber,
url: linkUrl,
text: string,
scale: nonZeroNumber
};
const Versions$5 = createShapePropsMigrationIds("note", {
AddUrlProp: 1,
RemoveJustify: 2,
MigrateLegacyAlign: 3,
AddVerticalAlign: 4,
MakeUrlsValid: 5,
AddFontSizeAdjustment: 6,
AddScale: 7,
AddLabelColor: 8
});
const noteShapeMigrations = createShapePropsMigrationSequence({
sequence: [
{
id: Versions$5.AddUrlProp,
up: (props) => {
props.url = "";
},
down: "retired"
},
{
id: Versions$5.RemoveJustify,
up: (props) => {
if (props.align === "justify") {
props.align = "start";
}
},
down: "retired"
},
{
id: Versions$5.MigrateLegacyAlign,
up: (props) => {
switch (props.align) {
case "start":
props.align = "start-legacy";
return;
case "end":
props.align = "end-legacy";
return;
default:
props.align = "middle-legacy";
return;
}
},
down: "retired"
},
{
id: Versions$5.AddVerticalAlign,
up: (props) => {
props.verticalAlign = "middle";
},
down: "retired"
},
{
id: Versions$5.MakeUrlsValid,
up: (props) => {
if (!linkUrl.isValid(props.url)) {
props.url = "";
}
},
down: (_props) => {
}
},
{
id: Versions$5.AddFontSizeAdjustment,
up: (props) => {
props.fontSizeAdjustment = 0;
},
down: (props) => {
delete props.fontSizeAdjustment;
}
},
{
id: Versions$5.AddScale,
up: (props) => {
props.scale = 1;
},
down: (props) => {
delete props.scale;
}
},
{
id: Versions$5.AddLabelColor,
up: (props) => {
props.labelColor = "black";
},
down: (props) => {
delete props.labelColor;
}
}
]
});
const DefaultTextAlignStyle = StyleProp.defineEnum("tldraw:textAlign", {
defaultValue: "start",
values: ["start", "middle", "end"]
});
const textShapeProps = {
color: DefaultColorStyle,
size: DefaultSizeStyle,
font: DefaultFontStyle,
textAlign: DefaultTextAlignStyle,
w: nonZeroNumber,
text: string,
scale: nonZeroNumber,
autoSize: boolean
};
const Versions$4 = createShapePropsMigrationIds("text", {
RemoveJustify: 1,
AddTextAlign: 2
});
const textShapeMigrations = createShapePropsMigrationSequence({
sequence: [
{
id: Versions$4.RemoveJustify,
up: (props) => {
if (props.align === "justify") {
props.align = "start";
}
},
down: "retired"
},
{
id: Versions$4.AddTextAlign,
up: (props) => {
props.textAlign = props.align;
delete props.align;
},
down: (props) => {
props.align = props.textAlign;
delete props.textAlign;
}
}
]
});
const videoShapeProps = {
w: nonZeroNumber,
h: nonZeroNumber,
time: number,
playing: boolean,
url: linkUrl,
assetId: assetIdValidator.nullable()
};
const Versions$3 = createShapePropsMigrationIds("video", {
AddUrlProp: 1,
MakeUrlsValid: 2
});
const videoShapeMigrations = createShapePropsMigrationSequence({
sequence: [
{
id: Versions$3.AddUrlProp,
up: (props) => {
props.url = "";
},
down: "retired"
},
{
id: Versions$3.MakeUrlsValid,
up: (props) => {
if (!linkUrl.isValid(props.url)) {
props.url = "";
}
},
down: (_props) => {
}
}
]
});
const Versions$2 = createMigrationIds("com.tldraw.store", {
RemoveCodeAndIconShapeTypes: 1,
AddInstancePresenceType: 2,
RemoveTLUserAndPresenceAndAddPointer: 3,
RemoveUserDocument: 4
});
const storeMigrations = createMigrationSequence({
sequenceId: "com.tldraw.store",
retroactive: false,
sequence: [
{
id: Versions$2.RemoveCodeAndIconShapeTypes,
scope: "store",
up: (store) => {
for (const [id, record] of objectMapEntries(store)) {
if (record.typeName === "shape" && (record.type === "icon" || record.type === "code")) {
delete store[id];
}
}
}
},
{
id: Versions$2.AddInstancePresenceType,
scope: "store",
up(_store) {
}
},
{
// remove user and presence records and add pointer records
id: Versions$2.RemoveTLUserAndPresenceAndAddPointer,
scope: "store",
up: (store) => {
for (const [id, record] of objectMapEntries(store)) {
if (record.typeName.match(/^(user|user_presence)$/)) {
delete store[id];
}
}
}
},
{
// remove user document records
id: Versions$2.RemoveUserDocument,
scope: "store",
up: (store) => {
for (const [id, record] of objectMapEntries(store)) {
if (record.typeName.match("user_document")) {
delete store[id];
}
}
}
}
]
});
const defaultShapeSchemas = {
arrow: { migrations: arrowShapeMigrations, props: arrowShapeProps },
bookmark: { migrations: bookmarkShapeMigrations, props: bookmarkShapeProps },
draw: { migrations: drawShapeMigrations, props: drawShapeProps },
embed: { migrations: embedShapeMigrations, props: embedShapeProps },
frame: { migrations: frameShapeMigrations, props: frameShapeProps },
geo: { migrations: geoShapeMigrations, props: geoShapeProps },
group: { migrations: groupShapeMigrations, props: groupShapeProps },
highlight: { migrations: highlightShapeMigrations, props: highlightShapeProps },
image: { migrations: imageShapeMigrations, props: imageShapeProps },
line: { migrations: lineShapeMigrations, props: lineShapeProps },
note: { migrations: noteShapeMigrations, props: noteShapeProps },
text: { migrations: textShapeMigrations, props: textShapeProps },
video: { migrations: videoShapeMigrations, props: videoShapeProps }
};
const defaultBindingSchemas = {
arrow: { migrations: arrowBindingMigrations, props: arrowBindingProps }
};
function createTLSchema({
shapes = defaultShapeSchemas,
bindings = defaultBindingSchemas,
migrations
} = {}) {
const stylesById = /* @__PURE__ */ new Map();
for (const shape of objectMapValues(shapes)) {
for (const style of getShapePropKeysByStyle(shape.props ?? {}).keys()) {
if (stylesById.has(style.id) && stylesById.get(style.id) !== style) {
throw new Error(`Multiple StyleProp instances with the same id: ${style.id}`);
}
stylesById.set(style.id, style);
}
}
const ShapeRecordType = createShapeRecordType(shapes);
const BindingRecordType = createBindingRecordType(bindings);
const InstanceRecordType = createInstanceRecordType(stylesById);
return StoreSchema.create(
{
asset: AssetRecordType,
binding: BindingRecordType,
camera: CameraRecordType,
document: DocumentRecordType,
instance: InstanceRecordType,
instance_page_state: InstancePageStateRecordType,
page: PageRecordType,
instance_presence: InstancePresenceRecordType,
pointer: PointerRecordType,
shape: ShapeRecordType
},
{
migrations: [
storeMigrations,
assetMigrations,
cameraMigrations,
documentMigrations,
instanceMigrations,
instancePageStateMigrations,
pageMigrations,
instancePresenceMigrations,
pointerMigrations,
rootShapeMigrations,
bookmarkAssetMigrations,
imageAssetMigrations,
videoAssetMigrations,
...processPropsMigrations("shape", shapes),
...processPropsMigrations("binding", bindings),
...(migrations ?? [])
],
onValidationFailure,
createIntegrityChecker
}
);
}
const LANGUAGES = [
{ locale: "id", label: "Bahasa Indonesia" },
{ locale: "ca", label: "Catal\xE0" },
{ locale: "cs", label: "\u010Ce\u0161tina" },
{ locale: "da", label: "Danish" },
{ locale: "de", label: "Deutsch" },
{ locale: "en", label: "English" },
{ locale: "es", label: "Espa\xF1ol" },
{ locale: "fr", label: "Fran\xE7ais" },
{ locale: "gl", label: "Galego" },
{ locale: "hr", label: "Hrvatski" },
{ locale: "it", label: "Italiano" },
{ locale: "hu", label: "Magyar" },
{ locale: "no", label: "Norwegian" },
{ locale: "pl", label: "Polski" },
{ locale: "pt-br", label: "Portugu\xEAs - Brasil" },
{ locale: "pt-pt", label: "Portugu\xEAs - Europeu" },
{ locale: "ro", label: "Rom\xE2n\u0103" },
{ locale: "ru", label: "Russian" },
{ locale: "sl", label: "Sloven\u0161\u010Dina" },
{ locale: "so", label: "Somali" },
{ locale: "fi", label: "Suomi" },
{ locale: "sv", label: "Svenska" },
{ locale: "vi", label: "Ti\u1EBFng Vi\u1EC7t" },
{ locale: "tr", label: "T\xFCrk\xE7e" },
{ locale: "uk", label: "Ukrainian" },
{ locale: "he", label: "\u05E2\u05D1\u05E8\u05D9\u05EA" },
{ locale: "ar", label: "\u0639\u0631\u0628\u064A" },
{ locale: "fa", label: "\u0641\u0627\u0631\u0633\u06CC" },
{ locale: "ku", label: "\u06A9\u0648\u0631\u062F\u06CC" },
{ locale: "ne", label: "\u0928\u0947\u092A\u093E\u0932\u0940" },
{ locale: "hi-in", label: "\u0939\u093F\u0928\u094D\u0926\u0940" },
{ locale: "te", label: "\u0C24\u0C46\u0C32\u0C41\u0C17\u0C41" },
{ locale: "th", label: "\u0E20\u0E32\u0E29\u0E32\u0E44\u0E17\u0E22" },
{ locale: "my", label: "\u1019\u103C\u1014\u103A\u1019\u102C\u1005\u102C" },
{ locale: "ko-kr", label: "\uD55C\uAD6D\uC5B4" },
{ locale: "ja", label: "\u65E5\u672C\u8A9E" },
{ locale: "zh-cn", label: "\u7B80\u4F53\u4E2D\u6587" },
{ locale: "zh-tw", label: "\u7E41\u9AD4\u4E2D\u6587 (\u53F0\u7063)" }
];
function getDefaultTranslationLocale() {
const locales = typeof window !== "undefined" ? window.navigator.languages ?? ["en"] : ["en"];
return _getDefaultTranslationLocale(locales);
}
function _getDefaultTranslationLocale(locales) {
for (const locale of locales) {
const supportedLocale = getSupportedLocale(locale);
if (supportedLocale) {
return supportedLocale;
}
}
return "en";
}
const DEFAULT_LOCALE_REGIONS = {
zh: "zh-cn",
pt: "pt-br",
ko: "ko-kr",
hi: "hi-in"
};
function getSupportedLocale(locale) {
const exactMatch = LANGUAGES.find((t) => t.locale === locale.toLowerCase());
if (exactMatch) {
return exactMatch.locale;
}
const [language, region] = locale.split(/[-_]/).map((s) => s.toLowerCase());
if (region) {
const languageMatch = LANGUAGES.find((t) => t.locale === language);
if (languageMatch) {
return languageMatch.locale;
}
}
if (language in DEFAULT_LOCALE_REGIONS) {
return DEFAULT_LOCALE_REGIONS[language];
}
return null;
}
registerTldrawLibraryVersion(
"@tldraw/tlschema",
"3.6.1",
"esm"
);
var classnames = {exports: {}};
/*!
Copyright (c) 2018 Jed Watson.
Licensed under the MIT License (MIT), see
http://jedwatson.github.io/classnames
*/
(function (module) {
/* global define */
(function () {
var hasOwn = {}.hasOwnProperty;
function classNames () {
var classes = '';
for (var i = 0; i < arguments.length; i++) {
var arg = arguments[i];
if (arg) {
classes = appendClass(classes, parseValue(arg));
}
}
return classes;
}
function parseValue (arg) {
if (typeof arg === 'string' || typeof arg === 'number') {
return arg;
}
if (typeof arg !== 'object') {
return '';
}
if (Array.isArray(arg)) {
return classNames.apply(null, arg);
}
if (arg.toString !== Object.prototype.toString && !arg.toString.toString().includes('[native code]')) {
return arg.toString();
}
var classes = '';
for (var key in arg) {
if (hasOwn.call(arg, key) && arg[key]) {
classes = appendClass(classes, key);
}
}
return classes;
}
function appendClass (value, newClass) {
if (!newClass) {
return value;
}
if (value) {
return value + ' ' + newClass;
}
return value + newClass;
}
if (module.exports) {
classNames.default = classNames;
module.exports = classNames;
} else {
window.classNames = classNames;
}
}());
} (classnames));
var classnamesExports = classnames.exports;
const classNames = /*@__PURE__*/getDefaultExportFromCjs(classnamesExports);
const version = "3.6.1";
const publishDates = {
major: "2024-09-13T14:36:29.063Z",
minor: "2024-12-04T15:00:05.881Z"};
const initialState = { error: null };
class ErrorBoundary extends reactExports.Component {
static getDerivedStateFromError(error) {
return { error };
}
state = initialState;
componentDidCatch(error) {
this.props.onError?.(error);
}
render() {
const { error } = this.state;
if (error !== null) {
const { fallback: Fallback } = this.props;
return /* @__PURE__ */ jsxRuntimeExports.jsx(Fallback, { error });
}
return this.props.children;
}
}
function OptionalErrorBoundary({
children,
fallback,
...props
}) {
if (fallback === null) {
return children;
}
return /* @__PURE__ */ jsxRuntimeExports.jsx(ErrorBoundary, { fallback, ...props, children });
}
function suffixSafeId(id, suffix) {
return sanitizeId(`${id}_${suffix}`);
}
function useUniqueSafeId(suffix) {
return sanitizeId(`${reactExports.useId()}${suffix ?? ""}`);
}
function useSharedSafeId(id) {
const idScope = assertExists(reactExports.useContext(IdContext));
return sanitizeId(`${idScope}_${id}`);
}
function sanitizeId(id) {
return id.replace(/:/g, "_");
}
const IdContext = reactExports.createContext(null);
function IdProvider({ children }) {
const id = useUniqueSafeId();
return /* @__PURE__ */ jsxRuntimeExports.jsx(IdContext.Provider, { value: id, children });
}
const EditorContext = reactExports.createContext(null);
function useEditor() {
const editor = React.useContext(EditorContext);
if (!editor) {
throw new Error(
"useEditor must be used inside of the or components"
);
}
return editor;
}
function useMaybeEditor() {
return React.useContext(EditorContext);
}
function EditorProvider({
editor,
children
}) {
return /* @__PURE__ */ jsxRuntimeExports.jsx(EditorContext.Provider, { value: editor, children: /* @__PURE__ */ jsxRuntimeExports.jsx(IdProvider, { children }) });
}
function DefaultBackground() {
return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "tl-background" });
}
function useTransform(ref, x, y, scale, rotate, additionalOffset) {
reactExports.useLayoutEffect(() => {
const elm = ref.current;
if (!elm) return;
if (x === void 0) return;
let trans = `translate(${x}px, ${y}px)`;
if (scale !== void 0) {
trans += ` scale(${scale})`;
}
if (rotate !== void 0) {
trans += ` rotate(${rotate}rad)`;
}
if (additionalOffset) {
trans += ` translate(${additionalOffset.x}px, ${additionalOffset.y}px)`;
}
elm.style.transform = trans;
});
}
const EASINGS = {
linear: (t) => t,
easeInQuad: (t) => t * t,
easeOutQuad: (t) => t * (2 - t),
easeInOutQuad: (t) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t,
easeInCubic: (t) => t * t * t,
easeOutCubic: (t) => --t * t * t + 1,
easeInOutCubic: (t) => t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1,
easeInQuart: (t) => t * t * t * t,
easeOutQuart: (t) => 1 - --t * t * t * t,
easeInOutQuart: (t) => t < 0.5 ? 8 * t * t * t * t : 1 - 8 * --t * t * t * t,
easeInQuint: (t) => t * t * t * t * t,
easeOutQuint: (t) => 1 + --t * t * t * t * t,
easeInOutQuint: (t) => t < 0.5 ? 16 * t * t * t * t * t : 1 + 16 * --t * t * t * t * t,
easeInSine: (t) => 1 - Math.cos(t * Math.PI / 2),
easeOutSine: (t) => Math.sin(t * Math.PI / 2),
easeInOutSine: (t) => -(Math.cos(Math.PI * t) - 1) / 2,
easeInExpo: (t) => t <= 0 ? 0 : Math.pow(2, 10 * t - 10),
easeOutExpo: (t) => t >= 1 ? 1 : 1 - Math.pow(2, -10 * t),
easeInOutExpo: (t) => t <= 0 ? 0 : t >= 1 ? 1 : t < 0.5 ? Math.pow(2, 20 * t - 10) / 2 : (2 - Math.pow(2, -20 * t + 10)) / 2
};
class Vec {
constructor(x = 0, y = 0, z = 1) {
this.x = x;
this.y = y;
this.z = z;
}
// eslint-disable-next-line no-restricted-syntax
get pressure() {
return this.z;
}
set(x = this.x, y = this.y, z = this.z) {
this.x = x;
this.y = y;
this.z = z;
return this;
}
setTo({ x = 0, y = 0, z = 1 }) {
this.x = x;
this.y = y;
this.z = z;
return this;
}
rot(r) {
if (r === 0) return this;
const { x, y } = this;
const s = Math.sin(r);
const c = Math.cos(r);
this.x = x * c - y * s;
this.y = x * s + y * c;
return this;
}
rotWith(C, r) {
if (r === 0) return this;
const x = this.x - C.x;
const y = this.y - C.y;
const s = Math.sin(r);
const c = Math.cos(r);
this.x = C.x + (x * c - y * s);
this.y = C.y + (x * s + y * c);
return this;
}
clone() {
const { x, y, z } = this;
return new Vec(x, y, z);
}
sub(V) {
this.x -= V.x;
this.y -= V.y;
return this;
}
subXY(x, y) {
this.x -= x;
this.y -= y;
return this;
}
subScalar(n) {
this.x -= n;
this.y -= n;
return this;
}
add(V) {
this.x += V.x;
this.y += V.y;
return this;
}
addXY(x, y) {
this.x += x;
this.y += y;
return this;
}
addScalar(n) {
this.x += n;
this.y += n;
return this;
}
clamp(min, max) {
this.x = Math.max(this.x, min);
this.y = Math.max(this.y, min);
if (max !== void 0) {
this.x = Math.min(this.x, max);
this.y = Math.min(this.y, max);
}
return this;
}
div(t) {
this.x /= t;
this.y /= t;
return this;
}
divV(V) {
this.x /= V.x;
this.y /= V.y;
return this;
}
mul(t) {
this.x *= t;
this.y *= t;
return this;
}
mulV(V) {
this.x *= V.x;
this.y *= V.y;
return this;
}
abs() {
this.x = Math.abs(this.x);
this.y = Math.abs(this.y);
return this;
}
nudge(B, distance) {
const tan = Vec.Tan(B, this);
return this.add(tan.mul(distance));
}
neg() {
this.x *= -1;
this.y *= -1;
return this;
}
cross(V) {
this.x = this.y * V.z - this.z * V.y;
this.y = this.z * V.x - this.x * V.z;
return this;
}
dpr(V) {
return Vec.Dpr(this, V);
}
cpr(V) {
return Vec.Cpr(this, V);
}
len2() {
return Vec.Len2(this);
}
len() {
return Vec.Len(this);
}
pry(V) {
return Vec.Pry(this, V);
}
per() {
const { x, y } = this;
this.x = y;
this.y = -x;
return this;
}
uni() {
return Vec.Uni(this);
}
tan(V) {
return Vec.Tan(this, V);
}
dist(V) {
return Vec.Dist(this, V);
}
distanceToLineSegment(A, B) {
return Vec.DistanceToLineSegment(A, B, this);
}
slope(B) {
return Vec.Slope(this, B);
}
snapToGrid(gridSize) {
this.x = Math.round(this.x / gridSize) * gridSize;
this.y = Math.round(this.y / gridSize) * gridSize;
return this;
}
angle(B) {
return Vec.Angle(this, B);
}
toAngle() {
return Vec.ToAngle(this);
}
lrp(B, t) {
this.x = this.x + (B.x - this.x) * t;
this.y = this.y + (B.y - this.y) * t;
return this;
}
equals(B) {
return Vec.Equals(this, B);
}
equalsXY(x, y) {
return Vec.EqualsXY(this, x, y);
}
norm() {
const l = this.len();
this.x = l === 0 ? 0 : this.x / l;
this.y = l === 0 ? 0 : this.y / l;
return this;
}
toFixed() {
return Vec.ToFixed(this);
}
toString() {
return Vec.ToString(Vec.ToFixed(this));
}
toJson() {
return Vec.ToJson(this);
}
toArray() {
return Vec.ToArray(this);
}
static Add(A, B) {
return new Vec(A.x + B.x, A.y + B.y);
}
static AddXY(A, x, y) {
return new Vec(A.x + x, A.y + y);
}
static Sub(A, B) {
return new Vec(A.x - B.x, A.y - B.y);
}
static SubXY(A, x, y) {
return new Vec(A.x - x, A.y - y);
}
static AddScalar(A, n) {
return new Vec(A.x + n, A.y + n);
}
static SubScalar(A, n) {
return new Vec(A.x - n, A.y - n);
}
static Div(A, t) {
return new Vec(A.x / t, A.y / t);
}
static Mul(A, t) {
return new Vec(A.x * t, A.y * t);
}
static DivV(A, B) {
return new Vec(A.x / B.x, A.y / B.y);
}
static MulV(A, B) {
return new Vec(A.x * B.x, A.y * B.y);
}
static Neg(A) {
return new Vec(-A.x, -A.y);
}
/**
* Get the perpendicular vector to A.
*/
static Per(A) {
return new Vec(A.y, -A.x);
}
static Abs(A) {
return new Vec(Math.abs(A.x), Math.abs(A.y));
}
// Get the distance between two points.
static Dist(A, B) {
return ((A.y - B.y) ** 2 + (A.x - B.x) ** 2) ** 0.5;
}
// Get whether a distance between two points is less than a number. This is faster to calulate than using `Vec.Dist(a, b) < n`.
static DistMin(A, B, n) {
return (A.x - B.x) * (A.x - B.x) + (A.y - B.y) * (A.y - B.y) < n ** 2;
}
// Get the squared distance between two points. This is faster to calculate (no square root) so useful for "minimum distance" checks where the actual measurement does not matter.
static Dist2(A, B) {
return (A.x - B.x) * (A.x - B.x) + (A.y - B.y) * (A.y - B.y);
}
/**
* Dot product of two vectors which is used to calculate the angle between them.
*/
static Dpr(A, B) {
return A.x * B.x + A.y * B.y;
}
static Cross(A, V) {
return new Vec(
A.y * V.z - A.z * V.y,
A.z * V.x - A.x * V.z
// A.z = A.x * V.y - A.y * V.x
);
}
/**
* Cross product of two vectors which is used to calculate the area of a parallelogram.
*/
static Cpr(A, B) {
return A.x * B.y - B.x * A.y;
}
static Len2(A) {
return A.x * A.x + A.y * A.y;
}
static Len(A) {
return (A.x * A.x + A.y * A.y) ** 0.5;
}
/**
* Get the projection of A onto B.
*/
static Pry(A, B) {
return Vec.Dpr(A, B) / Vec.Len(B);
}
/**
* Get the unit vector of A.
*/
static Uni(A) {
return Vec.Div(A, Vec.Len(A));
}
static Tan(A, B) {
return Vec.Uni(Vec.Sub(A, B));
}
static Min(A, B) {
return new Vec(Math.min(A.x, B.x), Math.min(A.y, B.y));
}
static Max(A, B) {
return new Vec(Math.max(A.x, B.x), Math.max(A.y, B.y));
}
static From({ x, y, z = 1 }) {
return new Vec(x, y, z);
}
static FromArray(v) {
return new Vec(v[0], v[1]);
}
static Rot(A, r = 0) {
const s = Math.sin(r);
const c = Math.cos(r);
return new Vec(A.x * c - A.y * s, A.x * s + A.y * c);
}
static RotWith(A, C, r) {
const x = A.x - C.x;
const y = A.y - C.y;
const s = Math.sin(r);
const c = Math.cos(r);
return new Vec(C.x + (x * c - y * s), C.y + (x * s + y * c));
}
/**
* Get the nearest point on a line with a known unit vector that passes through point A
*
* ```ts
* Vec.nearestPointOnLineThroughPoint(A, u, Point)
* ```
*
* @param A - Any point on the line
* @param u - The unit vector for the line.
* @param P - A point not on the line to test.
*/
static NearestPointOnLineThroughPoint(A, u, P) {
return Vec.Mul(u, Vec.Sub(P, A).pry(u)).add(A);
}
static NearestPointOnLineSegment(A, B, P, clamp = true) {
if (Vec.Equals(A, P)) return Vec.From(P);
if (Vec.Equals(B, P)) return Vec.From(P);
const u = Vec.Tan(B, A);
const C = Vec.Add(A, Vec.Mul(u, Vec.Sub(P, A).pry(u)));
if (clamp) {
if (C.x < Math.min(A.x, B.x)) return Vec.Cast(A.x < B.x ? A : B);
if (C.x > Math.max(A.x, B.x)) return Vec.Cast(A.x > B.x ? A : B);
if (C.y < Math.min(A.y, B.y)) return Vec.Cast(A.y < B.y ? A : B);
if (C.y > Math.max(A.y, B.y)) return Vec.Cast(A.y > B.y ? A : B);
}
return C;
}
static DistanceToLineThroughPoint(A, u, P) {
return Vec.Dist(P, Vec.NearestPointOnLineThroughPoint(A, u, P));
}
static DistanceToLineSegment(A, B, P, clamp = true) {
return Vec.Dist(P, Vec.NearestPointOnLineSegment(A, B, P, clamp));
}
static Snap(A, step = 1) {
return new Vec(Math.round(A.x / step) * step, Math.round(A.y / step) * step);
}
static Cast(A) {
if (A instanceof Vec) return A;
return Vec.From(A);
}
static Slope(A, B) {
if (A.x === B.y) return NaN;
return (A.y - B.y) / (A.x - B.x);
}
static IsNaN(A) {
return isNaN(A.x) || isNaN(A.y);
}
static Angle(A, B) {
return Math.atan2(B.y - A.y, B.x - A.x);
}
/**
* Linearly interpolate between two points.
* @param A - The first point.
* @param B - The second point.
* @param t - The interpolation value between 0 and 1.
* @returns The interpolated point.
*/
static Lrp(A, B, t) {
return Vec.Sub(B, A).mul(t).add(A);
}
static Med(A, B) {
return new Vec((A.x + B.x) / 2, (A.y + B.y) / 2);
}
static Equals(A, B) {
return Math.abs(A.x - B.x) < 1e-4 && Math.abs(A.y - B.y) < 1e-4;
}
static EqualsXY(A, x, y) {
return A.x === x && A.y === y;
}
static Clockwise(A, B, C) {
return (C.x - A.x) * (B.y - A.y) - (B.x - A.x) * (C.y - A.y) < 0;
}
static Rescale(A, n) {
const l = Vec.Len(A);
return new Vec(n * A.x / l, n * A.y / l);
}
static ScaleWithOrigin(A, scale, origin) {
return Vec.Sub(A, origin).mul(scale).add(origin);
}
static ToFixed(A) {
return new Vec(toFixed(A.x), toFixed(A.y));
}
static ToInt(A) {
return new Vec(
parseInt(A.x.toFixed(0)),
parseInt(A.y.toFixed(0)),
parseInt((A.z ?? 0).toFixed(0))
);
}
static ToCss(A) {
return `${A.x},${A.y}`;
}
static Nudge(A, B, distance) {
return Vec.Add(A, Vec.Tan(B, A).mul(distance));
}
static ToString(A) {
return `${A.x}, ${A.y}`;
}
static ToAngle(A) {
let r = Math.atan2(A.y, A.x);
if (r < 0) r += Math.PI * 2;
return r;
}
static FromAngle(r, length = 1) {
return new Vec(Math.cos(r) * length, Math.sin(r) * length);
}
static ToArray(A) {
return [A.x, A.y, A.z];
}
static ToJson(A) {
const { x, y, z } = A;
return { x, y, z };
}
static Average(arr) {
const len = arr.length;
const avg = new Vec(0, 0);
if (len === 0) {
return avg;
}
for (let i = 0; i < len; i++) {
avg.add(arr[i]);
}
return avg.div(len);
}
static Clamp(A, min, max) {
if (max === void 0) {
return new Vec(Math.min(Math.max(A.x, min)), Math.min(Math.max(A.y, min)));
}
return new Vec(Math.min(Math.max(A.x, min), max), Math.min(Math.max(A.y, min), max));
}
/**
* Get an array of points (with simulated pressure) between two points.
*
* @param A - The first point.
* @param B - The second point.
* @param steps - The number of points to return.
*/
static PointsBetween(A, B, steps = 6) {
const results = [];
for (let i = 0; i < steps; i++) {
const t = EASINGS.easeInQuad(i / (steps - 1));
const point = Vec.Lrp(A, B, t);
point.z = Math.min(1, 0.5 + Math.abs(0.5 - ease(t)) * 0.65);
results.push(point);
}
return results;
}
static SnapToGrid(A, gridSize = 8) {
return new Vec(Math.round(A.x / gridSize) * gridSize, Math.round(A.y / gridSize) * gridSize);
}
}
const ease = (t) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
function precise(A) {
return `${toDomPrecision(A.x)},${toDomPrecision(A.y)} `;
}
function average(A, B) {
return `${toDomPrecision((A.x + B.x) / 2)},${toDomPrecision((A.y + B.y) / 2)} `;
}
const PI$1 = Math.PI;
const HALF_PI = PI$1 / 2;
const PI2 = PI$1 * 2;
const SIN = Math.sin;
function clamp$2(n, min, max) {
return Math.max(min, typeof max !== "undefined" ? Math.min(n, max) : n);
}
function toPrecision(n, precision = 1e10) {
if (!n) return 0;
return Math.round(n * precision) / precision;
}
function approximately(a, b, precision = 1e-6) {
return Math.abs(a - b) <= precision;
}
function perimeterOfEllipse(rx, ry) {
const h = Math.pow(rx - ry, 2) / Math.pow(rx + ry, 2);
return PI$1 * (rx + ry) * (1 + 3 * h / (10 + Math.sqrt(4 - 3 * h)));
}
function canonicalizeRotation(a) {
a = a % PI2;
if (a < 0) {
a = a + PI2;
} else if (a === 0) {
a = 0;
}
return a;
}
function clockwiseAngleDist(a0, a1) {
a0 = canonicalizeRotation(a0);
a1 = canonicalizeRotation(a1);
if (a0 > a1) {
a1 += PI2;
}
return a1 - a0;
}
function counterClockwiseAngleDist(a0, a1) {
return PI2 - clockwiseAngleDist(a0, a1);
}
function shortAngleDist(a0, a1) {
const da = (a1 - a0) % PI2;
return 2 * da % PI2 - da;
}
function clampRadians(r) {
return (PI2 + r) % PI2;
}
function snapAngle(r, segments) {
const seg = PI2 / segments;
let ang = Math.floor((clampRadians(r) + seg / 2) / seg) * seg % PI2;
if (ang < PI$1) ang += PI2;
if (ang > PI$1) ang -= PI2;
return ang;
}
function areAnglesCompatible(a, b) {
return a === b || approximately(a % (Math.PI / 2) - b % (Math.PI / 2), 0);
}
function degreesToRadians(d) {
return d * PI$1 / 180;
}
function radiansToDegrees(r) {
return r * 180 / PI$1;
}
function getPointOnCircle(center, r, a) {
return new Vec(center.x, center.y).add(Vec.FromAngle(a, r));
}
function getPolygonVertices(width, height, sides) {
const cx = width / 2;
const cy = height / 2;
const pointsOnPerimeter = [];
let minX = Infinity;
let maxX = -Infinity;
let minY = Infinity;
let maxY = -Infinity;
for (let i = 0; i < sides; i++) {
const step = PI2 / sides;
const t = -HALF_PI + i * step;
const x = cx + cx * Math.cos(t);
const y = cy + cy * Math.sin(t);
if (x < minX) minX = x;
if (y < minY) minY = y;
if (x > maxX) maxX = x;
if (y > maxY) maxY = y;
pointsOnPerimeter.push(new Vec(x, y));
}
const w = maxX - minX;
const h = maxY - minY;
const dx = width - w;
const dy = height - h;
if (dx !== 0 || dy !== 0) {
for (let i = 0; i < pointsOnPerimeter.length; i++) {
const pt = pointsOnPerimeter[i];
pt.x = (pt.x - minX) / w * width;
pt.y = (pt.y - minY) / h * height;
}
}
return pointsOnPerimeter;
}
function rangesOverlap(a0, a1, b0, b1) {
return a0 < b1 && b0 < a1;
}
function rangeIntersection(a0, a1, b0, b1) {
const min = Math.max(a0, b0);
const max = Math.min(a1, b1);
if (min <= max) {
return [min, max];
}
return null;
}
function cross(x, y, z) {
return (y.x - x.x) * (z.y - x.y) - (z.x - x.x) * (y.y - x.y);
}
function pointInPolygon(A, points) {
let windingNumber = 0;
let a;
let b;
for (let i = 0; i < points.length; i++) {
a = points[i];
if (a.x === A.x && a.y === A.y) return true;
b = points[(i + 1) % points.length];
if (Vec.Dist(A, a) + Vec.Dist(A, b) === Vec.Dist(a, b)) return true;
if (a.y <= A.y) {
if (b.y > A.y && cross(a, b, A) > 0) {
windingNumber += 1;
}
} else if (b.y <= A.y && cross(a, b, A) < 0) {
windingNumber -= 1;
}
}
return windingNumber !== 0;
}
function toDomPrecision(v) {
return Math.round(v * 1e4) / 1e4;
}
function toFixed(v) {
return Math.round(v * 100) / 100;
}
const isSafeFloat = (n) => {
return Math.abs(n) < Number.MAX_SAFE_INTEGER;
};
function angleDistance(fromAngle, toAngle, direction) {
const dist = direction < 0 ? clockwiseAngleDist(fromAngle, toAngle) : counterClockwiseAngleDist(fromAngle, toAngle);
return dist;
}
function getPointInArcT(mAB, A, B, P) {
let mAP;
if (Math.abs(mAB) > PI$1) {
mAP = shortAngleDist(A, P);
const mPB = shortAngleDist(P, B);
if (Math.abs(mAP) < Math.abs(mPB)) {
return mAP / mAB;
} else {
return (mAB - mPB) / mAB;
}
} else {
mAP = shortAngleDist(A, P);
const t = mAP / mAB;
if (Math.sign(mAP) !== Math.sign(mAB)) {
return Math.abs(t) > 0.5 ? 1 : 0;
}
return t;
}
}
function getArcMeasure(A, B, sweepFlag, largeArcFlag) {
const m = 2 * ((B - A) % PI2) % PI2 - (B - A) % PI2;
if (!largeArcFlag) return m;
return (PI2 - Math.abs(m)) * (sweepFlag ? 1 : -1);
}
function centerOfCircleFromThreePoints(a, b, c) {
const u = -2 * (a.x * (b.y - c.y) - a.y * (b.x - c.x) + b.x * c.y - c.x * b.y);
const x = ((a.x * a.x + a.y * a.y) * (c.y - b.y) + (b.x * b.x + b.y * b.y) * (a.y - c.y) + (c.x * c.x + c.y * c.y) * (b.y - a.y)) / u;
const y = ((a.x * a.x + a.y * a.y) * (b.x - c.x) + (b.x * b.x + b.y * b.y) * (c.x - a.x) + (c.x * c.x + c.y * c.y) * (a.x - b.x)) / u;
if (!Number.isFinite(x) || !Number.isFinite(y)) {
return null;
}
return new Vec(x, y);
}
function getPointsOnArc(startPoint, endPoint, center, radius, numPoints) {
if (center === null) {
return [Vec.From(startPoint), Vec.From(endPoint)];
}
const results = [];
const startAngle = Vec.Angle(center, startPoint);
const endAngle = Vec.Angle(center, endPoint);
const l = clockwiseAngleDist(startAngle, endAngle);
for (let i = 0; i < numPoints; i++) {
const t = i / (numPoints - 1);
const angle = startAngle + l * t;
const point = getPointOnCircle(center, radius, angle);
results.push(point);
}
return results;
}
const DefaultBrush = ({ brush, color, opacity, className }) => {
const rSvg = reactExports.useRef(null);
useTransform(rSvg, brush.x, brush.y);
const w = toDomPrecision(Math.max(1, brush.w));
const h = toDomPrecision(Math.max(1, brush.h));
return /* @__PURE__ */ jsxRuntimeExports.jsx("svg", { className: "tl-overlays__item", ref: rSvg, children: color ? /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { className: "tl-brush", opacity, children: [
/* @__PURE__ */ jsxRuntimeExports.jsx("rect", { width: w, height: h, fill: color, opacity: 0.75 }),
/* @__PURE__ */ jsxRuntimeExports.jsx("rect", { width: w, height: h, fill: "none", stroke: color, opacity: 0.1 })
] }) : /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { className: `tl-brush tl-brush__default ${className}`, width: w, height: h }) });
};
const tlenv = {
isSafari: false,
isIos: false,
isChromeForIos: false,
isFirefox: false,
isAndroid: false,
isWebview: false,
isDarwin: false
};
if (typeof window !== "undefined" && "navigator" in window) {
tlenv.isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
tlenv.isIos = !!navigator.userAgent.match(/iPad/i) || !!navigator.userAgent.match(/iPhone/i);
tlenv.isChromeForIos = /crios.*safari/i.test(navigator.userAgent);
tlenv.isFirefox = /firefox/i.test(navigator.userAgent);
tlenv.isAndroid = /android/i.test(navigator.userAgent);
tlenv.isDarwin = window.navigator.userAgent.toLowerCase().indexOf("mac") > -1;
}
const DEFAULT_CAMERA_OPTIONS = {
isLocked: false,
wheelBehavior: "pan",
panSpeed: 1,
zoomSpeed: 1,
zoomSteps: [0.1, 0.25, 0.5, 1, 2, 4, 8]
};
const DEFAULT_ANIMATION_OPTIONS = {
duration: 0,
easing: EASINGS.easeInOutCubic
};
const INTERNAL_POINTER_IDS = {
CAMERA_MOVE: -10
};
const SIDES = ["top", "right", "bottom", "left"];
const LEFT_MOUSE_BUTTON = 0;
const RIGHT_MOUSE_BUTTON = 2;
const MIDDLE_MOUSE_BUTTON = 1;
const STYLUS_ERASER_BUTTON = 5;
const ZOOM_TO_FIT_PADDING = 128;
var define_process_env_default$1 = { VITE_APP_NAME: "Classroom Copilot", VITE_SUPER_ADMIN_EMAIL: "admin@classroomcopilot.ai", VITE_DEV: "true", VITE_FRONTEND_SITE_URL: "http://localhost:5173", VITE_SUPABASE_URL: "http://localhost:8000", VITE_SUPABASE_ANON_KEY: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0", VITE_APP_API_URL: "http://localhost:8001" };
const featureFlags = {};
const pointerCaptureTrackingObject = createDebugValue(
"pointerCaptureTrackingObject",
// ideally we wouldn't store this mutable value in an atom but it's not
// a big deal for debug values
{
defaults: { all: /* @__PURE__ */ new Map() },
shouldStoreForSession: false
}
);
const debugFlags = {
// --- DEBUG VALUES ---
logPreventDefaults: createDebugValue("logPreventDefaults", {
defaults: { all: false }
}),
logPointerCaptures: createDebugValue("logPointerCaptures", {
defaults: { all: false }
}),
logElementRemoves: createDebugValue("logElementRemoves", {
defaults: { all: false }
}),
debugSvg: createDebugValue("debugSvg", {
defaults: { all: false }
}),
showFps: createDebugValue("showFps", {
defaults: { all: false }
}),
measurePerformance: createDebugValue("measurePerformance", { defaults: { all: false } }),
throwToBlob: createDebugValue("throwToBlob", {
defaults: { all: false }
}),
reconnectOnPing: createDebugValue("reconnectOnPing", {
defaults: { all: false }
}),
debugCursors: createDebugValue("debugCursors", {
defaults: { all: false }
}),
forceSrgb: createDebugValue("forceSrgbColors", { defaults: { all: false } }),
debugGeometry: createDebugValue("debugGeometry", { defaults: { all: false } }),
hideShapes: createDebugValue("hideShapes", { defaults: { all: false } }),
editOnType: createDebugValue("editOnType", { defaults: { all: false } })
};
if (typeof Element !== "undefined") {
const nativeElementRemoveChild = Element.prototype.removeChild;
react("element removal logging", () => {
if (debugFlags.logElementRemoves.get()) {
Element.prototype.removeChild = function(child) {
console.warn("[tldraw] removing child:", child);
return nativeElementRemoveChild.call(this, child);
};
} else {
Element.prototype.removeChild = nativeElementRemoveChild;
}
});
}
function createDebugValue(name, {
defaults,
shouldStoreForSession = true
}) {
return createDebugValueBase({
name,
defaults,
shouldStoreForSession
});
}
function createDebugValueBase(def) {
const defaultValue = getDefaultValue(def);
const storedValue = def.shouldStoreForSession ? getStoredInitialValue(def.name) : null;
const valueAtom = atom(`debug:${def.name}`, storedValue ?? defaultValue);
if (typeof window !== "undefined") {
if (def.shouldStoreForSession) {
react(`debug:${def.name}`, () => {
const currentValue = valueAtom.get();
if (currentValue === defaultValue) {
deleteFromSessionStorage(`tldraw_debug:${def.name}`);
} else {
setInSessionStorage(`tldraw_debug:${def.name}`, JSON.stringify(currentValue));
}
});
}
Object.defineProperty(window, `tldraw${def.name.replace(/^[a-z]/, (l) => l.toUpperCase())}`, {
get() {
return valueAtom.get();
},
set(newValue) {
valueAtom.set(newValue);
},
configurable: true
});
}
return Object.assign(valueAtom, def);
}
function getStoredInitialValue(name) {
try {
return JSON.parse(getFromSessionStorage(`tldraw_debug:${name}`) ?? "null");
} catch {
return null;
}
}
function readEnv(fn) {
try {
return fn();
} catch {
return null;
}
}
function getDefaultValue(def) {
const env = readEnv(() => define_process_env_default$1.TLDRAW_ENV) ?? readEnv(() => define_process_env_default$1.VERCEL_PUBLIC_TLDRAW_ENV) ?? readEnv(() => define_process_env_default$1.NEXT_PUBLIC_TLDRAW_ENV) ?? // default to production because if we don't have one of these, this is probably a library use
"production";
switch (env) {
case "production":
return def.defaults.production ?? def.defaults.all;
case "preview":
case "staging":
return def.defaults.staging ?? def.defaults.all;
default:
return def.defaults.development ?? def.defaults.all;
}
}
function loopToHtmlElement(elm) {
if (elm instanceof HTMLElement) return elm;
if (elm.parentElement) return loopToHtmlElement(elm.parentElement);
else throw Error("Could not find a parent element of an HTML type!");
}
function preventDefault(event) {
event.preventDefault();
if (debugFlags.logPreventDefaults.get()) {
console.warn("preventDefault called on event:", event);
}
}
function setPointerCapture(element, event) {
element.setPointerCapture(event.pointerId);
if (debugFlags.logPointerCaptures.get()) {
const trackingObj = pointerCaptureTrackingObject.get();
trackingObj.set(element, (trackingObj.get(element) ?? 0) + 1);
console.warn("setPointerCapture called on element:", element, event);
}
}
function releasePointerCapture(element, event) {
if (!element.hasPointerCapture(event.pointerId)) {
return;
}
element.releasePointerCapture(event.pointerId);
if (debugFlags.logPointerCaptures.get()) {
const trackingObj = pointerCaptureTrackingObject.get();
if (trackingObj.get(element) === 1) {
trackingObj.delete(element);
} else if (trackingObj.has(element)) {
trackingObj.set(element, trackingObj.get(element) - 1);
} else {
console.warn("Release without capture");
}
console.warn("releasePointerCapture called on element:", element, event);
}
}
const stopEventPropagation = (e) => e.stopPropagation();
const setStyleProperty = (elm, property, value) => {
if (!elm) return;
elm.style.setProperty(property, value);
};
const isAccelKey = (e) => {
return tlenv.isDarwin ? e.metaKey : e.ctrlKey || e.metaKey;
};
function getPointerInfo(e) {
e.isKilled = true;
return {
point: {
x: e.clientX,
y: e.clientY,
z: e.pressure
},
shiftKey: e.shiftKey,
altKey: e.altKey,
ctrlKey: e.metaKey || e.ctrlKey,
metaKey: e.metaKey,
accelKey: isAccelKey(e),
pointerId: e.pointerId,
button: e.button,
isPen: e.pointerType === "pen"
};
}
function useCanvasEvents() {
const editor = useEditor();
const events = reactExports.useMemo(
function canvasEvents() {
let lastX, lastY;
function onPointerDown(e) {
if (e.isKilled) return;
if (e.button === RIGHT_MOUSE_BUTTON) {
editor.dispatch({
type: "pointer",
target: "canvas",
name: "right_click",
...getPointerInfo(e)
});
return;
}
if (e.button !== 0 && e.button !== 1 && e.button !== 5) return;
setPointerCapture(e.currentTarget, e);
editor.dispatch({
type: "pointer",
target: "canvas",
name: "pointer_down",
...getPointerInfo(e)
});
}
function onPointerMove(e) {
if (e.isKilled) return;
if (e.clientX === lastX && e.clientY === lastY) return;
lastX = e.clientX;
lastY = e.clientY;
editor.dispatch({
type: "pointer",
target: "canvas",
name: "pointer_move",
...getPointerInfo(e)
});
}
function onPointerUp(e) {
if (e.isKilled) return;
if (e.button !== 0 && e.button !== 1 && e.button !== 2 && e.button !== 5) return;
lastX = e.clientX;
lastY = e.clientY;
releasePointerCapture(e.currentTarget, e);
editor.dispatch({
type: "pointer",
target: "canvas",
name: "pointer_up",
...getPointerInfo(e)
});
}
function onPointerEnter(e) {
if (e.isKilled) return;
if (editor.getInstanceState().isPenMode && e.pointerType !== "pen") return;
const canHover = e.pointerType === "mouse" || e.pointerType === "pen";
editor.updateInstanceState({ isHoveringCanvas: canHover ? true : null });
}
function onPointerLeave(e) {
if (e.isKilled) return;
if (editor.getInstanceState().isPenMode && e.pointerType !== "pen") return;
const canHover = e.pointerType === "mouse" || e.pointerType === "pen";
editor.updateInstanceState({ isHoveringCanvas: canHover ? false : null });
}
function onTouchStart(e) {
e.isKilled = true;
preventDefault(e);
}
function onTouchEnd(e) {
e.isKilled = true;
if (!(e.target instanceof HTMLElement)) return;
if (e.target.tagName !== "A" && e.target.tagName !== "TEXTAREA" && // When in EditingShape state, we are actually clicking on a 'DIV'
// not A/TEXTAREA element yet. So, to preserve cursor position
// for edit mode on mobile we need to not preventDefault.
// TODO: Find out if we still need this preventDefault in general though.
!(editor.getEditingShape() && e.target.className.includes("tl-text-content"))) {
preventDefault(e);
}
}
function onDragOver(e) {
preventDefault(e);
}
async function onDrop(e) {
preventDefault(e);
stopEventPropagation(e);
if (!e.dataTransfer?.files?.length) return;
const files = Array.from(e.dataTransfer.files);
await editor.putExternalContent({
type: "files",
files,
point: editor.screenToPage({ x: e.clientX, y: e.clientY }),
ignoreParent: false
});
}
function onClick(e) {
stopEventPropagation(e);
}
return {
onPointerDown,
onPointerMove,
onPointerUp,
onPointerEnter,
onPointerLeave,
onDragOver,
onDrop,
onTouchStart,
onTouchEnd,
onClick
};
},
[editor]
);
return events;
}
function useCoarsePointer() {
const editor = useEditor();
reactExports.useEffect(() => {
let isCoarse = editor.getInstanceState().isCoarsePointer;
const handlePointerDown = (e) => {
const isCoarseEvent = e.pointerType !== "mouse";
if (isCoarse === isCoarseEvent) return;
isCoarse = isCoarseEvent;
editor.updateInstanceState({ isCoarsePointer: isCoarseEvent });
};
window.addEventListener("pointerdown", handlePointerDown, { capture: true });
const mql = window.matchMedia && window.matchMedia("(any-pointer: coarse)");
const isForcedFinePointer = tlenv.isFirefox && !tlenv.isAndroid && !tlenv.isIos;
const handleMediaQueryChange = () => {
const next = isForcedFinePointer ? false : mql.matches;
if (isCoarse !== next) return;
isCoarse = next;
editor.updateInstanceState({ isCoarsePointer: next });
};
if (mql) {
mql.addEventListener("change", handleMediaQueryChange);
handleMediaQueryChange();
}
return () => {
window.removeEventListener("pointerdown", handlePointerDown, { capture: true });
if (mql) {
mql.removeEventListener("change", handleMediaQueryChange);
}
};
}, [editor]);
}
const ContainerContext = reactExports.createContext(null);
function ContainerProvider({ container, children }) {
return /* @__PURE__ */ jsxRuntimeExports.jsx(ContainerContext.Provider, { value: container, children });
}
function useContainer() {
return assertExists(reactExports.useContext(ContainerContext), "useContainer used outside of ");
}
function useDocumentEvents() {
const editor = useEditor();
const container = useContainer();
const isAppFocused = useValue("isFocused", () => editor.getIsFocused(), [editor]);
reactExports.useEffect(() => {
if (!container) return;
function onDrop(e) {
if (e.isSpecialRedispatchedEvent) return;
preventDefault(e);
stopEventPropagation(e);
const cvs = container.querySelector(".tl-canvas");
if (!cvs) return;
const newEvent = new DragEvent(e.type, e);
newEvent.isSpecialRedispatchedEvent = true;
cvs.dispatchEvent(newEvent);
}
container.addEventListener("dragover", onDrop);
container.addEventListener("drop", onDrop);
return () => {
container.removeEventListener("dragover", onDrop);
container.removeEventListener("drop", onDrop);
};
}, [container]);
reactExports.useEffect(() => {
if (typeof window === "undefined" || !("matchMedia" in window)) return;
let remove = null;
const updatePixelRatio = () => {
if (remove != null) {
remove();
}
const mqString = `(resolution: ${window.devicePixelRatio}dppx)`;
const media = matchMedia(mqString);
const safariCb = (ev) => {
if (ev.type === "change") {
updatePixelRatio();
}
};
if (media.addEventListener) {
media.addEventListener("change", updatePixelRatio);
} else if (media.addListener) {
media.addListener(safariCb);
}
remove = () => {
if (media.removeEventListener) {
media.removeEventListener("change", updatePixelRatio);
} else if (media.removeListener) {
media.removeListener(safariCb);
}
};
editor.updateInstanceState({ devicePixelRatio: window.devicePixelRatio });
};
updatePixelRatio();
return () => {
remove?.();
};
}, [editor]);
reactExports.useEffect(() => {
if (!isAppFocused) return;
const handleKeyDown = (e) => {
if (e.altKey && // todo: When should we allow the alt key to be used? Perhaps states should declare which keys matter to them?
(editor.isIn("zoom") || !editor.getPath().endsWith(".idle")) && !areShortcutsDisabled$2(editor)) {
preventDefault(e);
}
if (e.isKilled) return;
e.isKilled = true;
switch (e.key) {
case "=":
case "-":
case "0": {
if (e.metaKey || e.ctrlKey) {
preventDefault(e);
return;
}
break;
}
case "Tab": {
if (areShortcutsDisabled$2(editor)) {
return;
}
break;
}
case ",": {
return;
}
case "Escape": {
if (editor.getEditingShape() || editor.getSelectedShapeIds().length > 0) {
preventDefault(e);
}
if (editor.menus.getOpenMenus().length > 0) return;
if (editor.inputs.keys.has("Escape")) ; else {
editor.inputs.keys.add("Escape");
editor.cancel();
container.focus();
}
return;
}
default: {
if (areShortcutsDisabled$2(editor)) {
return;
}
}
}
const info = {
type: "keyboard",
name: e.repeat ? "key_repeat" : "key_down",
key: e.key,
code: e.code,
shiftKey: e.shiftKey,
altKey: e.altKey,
ctrlKey: e.metaKey || e.ctrlKey,
metaKey: e.metaKey,
accelKey: isAccelKey(e)
};
editor.dispatch(info);
};
const handleKeyUp = (e) => {
if (e.isKilled) return;
e.isKilled = true;
if (areShortcutsDisabled$2(editor)) {
return;
}
if (e.key === ",") {
return;
}
const info = {
type: "keyboard",
name: "key_up",
key: e.key,
code: e.code,
shiftKey: e.shiftKey,
altKey: e.altKey,
ctrlKey: e.metaKey || e.ctrlKey,
metaKey: e.metaKey,
accelKey: isAccelKey(e)
};
editor.dispatch(info);
};
function handleTouchStart(e) {
if (container.contains(e.target)) {
const touchXPosition = e.touches[0].pageX;
const touchXRadius = e.touches[0].radiusX || 0;
if (touchXPosition - touchXRadius < 10 || touchXPosition + touchXRadius > editor.getViewportScreenBounds().width - 10) {
if (e.target?.tagName === "BUTTON") {
e.target?.click();
}
preventDefault(e);
}
}
}
const handleWheel = (e) => {
if (container.contains(e.target) && (e.ctrlKey || e.metaKey)) {
preventDefault(e);
}
};
container.addEventListener("touchstart", handleTouchStart, { passive: false });
container.addEventListener("wheel", handleWheel, { passive: false });
document.addEventListener("gesturestart", preventDefault);
document.addEventListener("gesturechange", preventDefault);
document.addEventListener("gestureend", preventDefault);
container.addEventListener("keydown", handleKeyDown);
container.addEventListener("keyup", handleKeyUp);
return () => {
container.removeEventListener("touchstart", handleTouchStart);
container.removeEventListener("wheel", handleWheel);
document.removeEventListener("gesturestart", preventDefault);
document.removeEventListener("gesturechange", preventDefault);
document.removeEventListener("gestureend", preventDefault);
container.removeEventListener("keydown", handleKeyDown);
container.removeEventListener("keyup", handleKeyUp);
};
}, [editor, container, isAppFocused]);
}
const INPUTS$1 = ["input", "select", "button", "textarea"];
function areShortcutsDisabled$2(editor) {
const { activeElement } = document;
return editor.menus.hasOpenMenus() || activeElement && (activeElement.getAttribute("contenteditable") || INPUTS$1.indexOf(activeElement.tagName.toLowerCase()) > -1);
}
const IGNORED_TAGS = ["textarea", "input"];
function useFixSafariDoubleTapZoomPencilEvents(ref) {
const editor = useEditor();
reactExports.useEffect(() => {
const elm = ref.current;
if (!elm) return;
const handleEvent = (e) => {
if (e instanceof PointerEvent && e.pointerType === "pen") {
e.isKilled = true;
const { target } = e;
if (IGNORED_TAGS.includes(target.tagName?.toLocaleLowerCase()) || editor.isIn("select.editing_shape")) {
return;
}
preventDefault(e);
}
};
elm.addEventListener("touchstart", handleEvent);
elm.addEventListener("touchend", handleEvent);
return () => {
elm.removeEventListener("touchstart", handleEvent);
elm.removeEventListener("touchend", handleEvent);
};
}, [editor, ref]);
}
function clamp$1(v, min, max) {
return Math.max(min, Math.min(v, max));
}
const V = {
toVector(v, fallback) {
if (v === undefined) v = fallback;
return Array.isArray(v) ? v : [v, v];
},
add(v1, v2) {
return [v1[0] + v2[0], v1[1] + v2[1]];
},
sub(v1, v2) {
return [v1[0] - v2[0], v1[1] - v2[1]];
},
addTo(v1, v2) {
v1[0] += v2[0];
v1[1] += v2[1];
},
subTo(v1, v2) {
v1[0] -= v2[0];
v1[1] -= v2[1];
}
};
function rubberband(distance, dimension, constant) {
if (dimension === 0 || Math.abs(dimension) === Infinity) return Math.pow(distance, constant * 5);
return distance * dimension * constant / (dimension + constant * distance);
}
function rubberbandIfOutOfBounds(position, min, max, constant = 0.15) {
if (constant === 0) return clamp$1(position, min, max);
if (position < min) return -rubberband(min - position, max - min, constant) + min;
if (position > max) return +rubberband(position - max, max - min, constant) + max;
return position;
}
function computeRubberband(bounds, [Vx, Vy], [Rx, Ry]) {
const [[X0, X1], [Y0, Y1]] = bounds;
return [rubberbandIfOutOfBounds(Vx, X0, X1, Rx), rubberbandIfOutOfBounds(Vy, Y0, Y1, Ry)];
}
function _toPrimitive$1(input, hint) {
if (typeof input !== "object" || input === null) return input;
var prim = input[Symbol.toPrimitive];
if (prim !== void 0) {
var res = prim.call(input, hint);
if (typeof res !== "object") return res;
throw new TypeError("@@toPrimitive must return a primitive value.");
}
return (hint === "string" ? String : Number)(input);
}
function _toPropertyKey$1(arg) {
var key = _toPrimitive$1(arg, "string");
return typeof key === "symbol" ? key : String(key);
}
function _defineProperty$1(obj, key, value) {
key = _toPropertyKey$1(key);
if (key in obj) {
Object.defineProperty(obj, key, {
value,
enumerable: true,
configurable: true,
writable: true
});
} else {
obj[key] = value;
}
return obj;
}
function ownKeys$1(e, r) {
var t = Object.keys(e);
if (Object.getOwnPropertySymbols) {
var o = Object.getOwnPropertySymbols(e);
r && (o = o.filter(function(r2) {
return Object.getOwnPropertyDescriptor(e, r2).enumerable;
})), t.push.apply(t, o);
}
return t;
}
function _objectSpread2$1(e) {
for (var r = 1; r < arguments.length; r++) {
var t = null != arguments[r] ? arguments[r] : {};
r % 2 ? ownKeys$1(Object(t), true).forEach(function(r2) {
_defineProperty$1(e, r2, t[r2]);
}) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys$1(Object(t)).forEach(function(r2) {
Object.defineProperty(e, r2, Object.getOwnPropertyDescriptor(t, r2));
});
}
return e;
}
const EVENT_TYPE_MAP = {
pointer: {
start: "down",
change: "move",
end: "up"
},
mouse: {
start: "down",
change: "move",
end: "up"
},
touch: {
start: "start",
change: "move",
end: "end"
},
gesture: {
start: "start",
change: "change",
end: "end"
}
};
function capitalize(string) {
if (!string) return "";
return string[0].toUpperCase() + string.slice(1);
}
const actionsWithoutCaptureSupported = ["enter", "leave"];
function hasCapture(capture = false, actionKey) {
return capture && !actionsWithoutCaptureSupported.includes(actionKey);
}
function toHandlerProp(device, action = "", capture = false) {
const deviceProps = EVENT_TYPE_MAP[device];
const actionKey = deviceProps ? deviceProps[action] || action : action;
return "on" + capitalize(device) + capitalize(actionKey) + (hasCapture(capture, actionKey) ? "Capture" : "");
}
const pointerCaptureEvents = ["gotpointercapture", "lostpointercapture"];
function parseProp(prop) {
let eventKey = prop.substring(2).toLowerCase();
const passive = !!~eventKey.indexOf("passive");
if (passive) eventKey = eventKey.replace("passive", "");
const captureKey = pointerCaptureEvents.includes(eventKey) ? "capturecapture" : "capture";
const capture = !!~eventKey.indexOf(captureKey);
if (capture) eventKey = eventKey.replace("capture", "");
return {
device: eventKey,
capture,
passive
};
}
function toDomEventType(device, action = "") {
const deviceProps = EVENT_TYPE_MAP[device];
const actionKey = deviceProps ? deviceProps[action] || action : action;
return device + actionKey;
}
function isTouch(event) {
return "touches" in event;
}
function getPointerType(event) {
if (isTouch(event)) return "touch";
if ("pointerType" in event) return event.pointerType;
return "mouse";
}
function getCurrentTargetTouchList(event) {
return Array.from(event.touches).filter((e) => {
var _event$currentTarget, _event$currentTarget$;
return e.target === event.currentTarget || ((_event$currentTarget = event.currentTarget) === null || _event$currentTarget === void 0 || (_event$currentTarget$ = _event$currentTarget.contains) === null || _event$currentTarget$ === void 0 ? void 0 : _event$currentTarget$.call(_event$currentTarget, e.target));
});
}
function distanceAngle(P1, P2) {
try {
const dx = P2.clientX - P1.clientX;
const dy = P2.clientY - P1.clientY;
const cx = (P2.clientX + P1.clientX) / 2;
const cy = (P2.clientY + P1.clientY) / 2;
const distance = Math.hypot(dx, dy);
const angle = -(Math.atan2(dx, dy) * 180) / Math.PI;
const origin = [cx, cy];
return {
angle,
distance,
origin
};
} catch (_unused) {
}
return null;
}
function touchIds(event) {
return getCurrentTargetTouchList(event).map((touch) => touch.identifier);
}
function touchDistanceAngle(event, ids) {
const [P1, P2] = Array.from(event.touches).filter((touch) => ids.includes(touch.identifier));
return distanceAngle(P1, P2);
}
const LINE_HEIGHT = 40;
const PAGE_HEIGHT = 800;
function wheelValues(event) {
let {
deltaX,
deltaY,
deltaMode
} = event;
if (deltaMode === 1) {
deltaX *= LINE_HEIGHT;
deltaY *= LINE_HEIGHT;
} else if (deltaMode === 2) {
deltaX *= PAGE_HEIGHT;
deltaY *= PAGE_HEIGHT;
}
return [deltaX, deltaY];
}
function getEventDetails(event) {
const payload = {};
if ("buttons" in event) payload.buttons = event.buttons;
if ("shiftKey" in event) {
const {
shiftKey,
altKey,
metaKey,
ctrlKey
} = event;
Object.assign(payload, {
shiftKey,
altKey,
metaKey,
ctrlKey
});
}
return payload;
}
function call(v, ...args) {
if (typeof v === "function") {
return v(...args);
} else {
return v;
}
}
function noop() {
}
function chain(...fns) {
if (fns.length === 0) return noop;
if (fns.length === 1) return fns[0];
return function() {
let result;
for (const fn of fns) {
result = fn.apply(this, arguments) || result;
}
return result;
};
}
function assignDefault(value, fallback) {
return Object.assign({}, fallback, value || {});
}
const BEFORE_LAST_KINEMATICS_DELAY = 32;
class Engine {
constructor(ctrl, args, key) {
this.ctrl = ctrl;
this.args = args;
this.key = key;
if (!this.state) {
this.state = {};
this.computeValues([0, 0]);
this.computeInitial();
if (this.init) this.init();
this.reset();
}
}
get state() {
return this.ctrl.state[this.key];
}
set state(state) {
this.ctrl.state[this.key] = state;
}
get shared() {
return this.ctrl.state.shared;
}
get eventStore() {
return this.ctrl.gestureEventStores[this.key];
}
get timeoutStore() {
return this.ctrl.gestureTimeoutStores[this.key];
}
get config() {
return this.ctrl.config[this.key];
}
get sharedConfig() {
return this.ctrl.config.shared;
}
get handler() {
return this.ctrl.handlers[this.key];
}
reset() {
const {
state,
shared,
ingKey,
args
} = this;
shared[ingKey] = state._active = state.active = state._blocked = state._force = false;
state._step = [false, false];
state.intentional = false;
state._movement = [0, 0];
state._distance = [0, 0];
state._direction = [0, 0];
state._delta = [0, 0];
state._bounds = [[-Infinity, Infinity], [-Infinity, Infinity]];
state.args = args;
state.axis = void 0;
state.memo = void 0;
state.elapsedTime = state.timeDelta = 0;
state.direction = [0, 0];
state.distance = [0, 0];
state.overflow = [0, 0];
state._movementBound = [false, false];
state.velocity = [0, 0];
state.movement = [0, 0];
state.delta = [0, 0];
state.timeStamp = 0;
}
start(event) {
const state = this.state;
const config = this.config;
if (!state._active) {
this.reset();
this.computeInitial();
state._active = true;
state.target = event.target;
state.currentTarget = event.currentTarget;
state.lastOffset = config.from ? call(config.from, state) : state.offset;
state.offset = state.lastOffset;
state.startTime = state.timeStamp = event.timeStamp;
}
}
computeValues(values) {
const state = this.state;
state._values = values;
state.values = this.config.transform(values);
}
computeInitial() {
const state = this.state;
state._initial = state._values;
state.initial = state.values;
}
compute(event) {
const {
state,
config,
shared
} = this;
state.args = this.args;
let dt = 0;
if (event) {
state.event = event;
if (config.preventDefault && event.cancelable) state.event.preventDefault();
state.type = event.type;
shared.touches = this.ctrl.pointerIds.size || this.ctrl.touchIds.size;
shared.locked = !!document.pointerLockElement;
Object.assign(shared, getEventDetails(event));
shared.down = shared.pressed = shared.buttons % 2 === 1 || shared.touches > 0;
dt = event.timeStamp - state.timeStamp;
state.timeStamp = event.timeStamp;
state.elapsedTime = state.timeStamp - state.startTime;
}
if (state._active) {
const _absoluteDelta = state._delta.map(Math.abs);
V.addTo(state._distance, _absoluteDelta);
}
if (this.axisIntent) this.axisIntent(event);
const [_m0, _m1] = state._movement;
const [t0, t1] = config.threshold;
const {
_step,
values
} = state;
if (config.hasCustomTransform) {
if (_step[0] === false) _step[0] = Math.abs(_m0) >= t0 && values[0];
if (_step[1] === false) _step[1] = Math.abs(_m1) >= t1 && values[1];
} else {
if (_step[0] === false) _step[0] = Math.abs(_m0) >= t0 && Math.sign(_m0) * t0;
if (_step[1] === false) _step[1] = Math.abs(_m1) >= t1 && Math.sign(_m1) * t1;
}
state.intentional = _step[0] !== false || _step[1] !== false;
if (!state.intentional) return;
const movement = [0, 0];
if (config.hasCustomTransform) {
const [v0, v1] = values;
movement[0] = _step[0] !== false ? v0 - _step[0] : 0;
movement[1] = _step[1] !== false ? v1 - _step[1] : 0;
} else {
movement[0] = _step[0] !== false ? _m0 - _step[0] : 0;
movement[1] = _step[1] !== false ? _m1 - _step[1] : 0;
}
if (this.restrictToAxis && !state._blocked) this.restrictToAxis(movement);
const previousOffset = state.offset;
const gestureIsActive = state._active && !state._blocked || state.active;
if (gestureIsActive) {
state.first = state._active && !state.active;
state.last = !state._active && state.active;
state.active = shared[this.ingKey] = state._active;
if (event) {
if (state.first) {
if ("bounds" in config) state._bounds = call(config.bounds, state);
if (this.setup) this.setup();
}
state.movement = movement;
this.computeOffset();
}
}
const [ox, oy] = state.offset;
const [[x0, x1], [y0, y1]] = state._bounds;
state.overflow = [ox < x0 ? -1 : ox > x1 ? 1 : 0, oy < y0 ? -1 : oy > y1 ? 1 : 0];
state._movementBound[0] = state.overflow[0] ? state._movementBound[0] === false ? state._movement[0] : state._movementBound[0] : false;
state._movementBound[1] = state.overflow[1] ? state._movementBound[1] === false ? state._movement[1] : state._movementBound[1] : false;
const rubberband = state._active ? config.rubberband || [0, 0] : [0, 0];
state.offset = computeRubberband(state._bounds, state.offset, rubberband);
state.delta = V.sub(state.offset, previousOffset);
this.computeMovement();
if (gestureIsActive && (!state.last || dt > BEFORE_LAST_KINEMATICS_DELAY)) {
state.delta = V.sub(state.offset, previousOffset);
const absoluteDelta = state.delta.map(Math.abs);
V.addTo(state.distance, absoluteDelta);
state.direction = state.delta.map(Math.sign);
state._direction = state._delta.map(Math.sign);
if (!state.first && dt > 0) {
state.velocity = [absoluteDelta[0] / dt, absoluteDelta[1] / dt];
state.timeDelta = dt;
}
}
}
emit() {
const state = this.state;
const shared = this.shared;
const config = this.config;
if (!state._active) this.clean();
if ((state._blocked || !state.intentional) && !state._force && !config.triggerAllEvents) return;
const memo = this.handler(_objectSpread2$1(_objectSpread2$1(_objectSpread2$1({}, shared), state), {}, {
[this.aliasKey]: state.values
}));
if (memo !== void 0) state.memo = memo;
}
clean() {
this.eventStore.clean();
this.timeoutStore.clean();
}
}
function selectAxis([dx, dy], threshold) {
const absDx = Math.abs(dx);
const absDy = Math.abs(dy);
if (absDx > absDy && absDx > threshold) {
return "x";
}
if (absDy > absDx && absDy > threshold) {
return "y";
}
return void 0;
}
class CoordinatesEngine extends Engine {
constructor(...args) {
super(...args);
_defineProperty$1(this, "aliasKey", "xy");
}
reset() {
super.reset();
this.state.axis = void 0;
}
init() {
this.state.offset = [0, 0];
this.state.lastOffset = [0, 0];
}
computeOffset() {
this.state.offset = V.add(this.state.lastOffset, this.state.movement);
}
computeMovement() {
this.state.movement = V.sub(this.state.offset, this.state.lastOffset);
}
axisIntent(event) {
const state = this.state;
const config = this.config;
if (!state.axis && event) {
const threshold = typeof config.axisThreshold === "object" ? config.axisThreshold[getPointerType(event)] : config.axisThreshold;
state.axis = selectAxis(state._movement, threshold);
}
state._blocked = (config.lockDirection || !!config.axis) && !state.axis || !!config.axis && config.axis !== state.axis;
}
restrictToAxis(v) {
if (this.config.axis || this.config.lockDirection) {
switch (this.state.axis) {
case "x":
v[1] = 0;
break;
case "y":
v[0] = 0;
break;
}
}
}
}
const identity = (v) => v;
const DEFAULT_RUBBERBAND = 0.15;
const commonConfigResolver = {
enabled(value = true) {
return value;
},
eventOptions(value, _k, config) {
return _objectSpread2$1(_objectSpread2$1({}, config.shared.eventOptions), value);
},
preventDefault(value = false) {
return value;
},
triggerAllEvents(value = false) {
return value;
},
rubberband(value = 0) {
switch (value) {
case true:
return [DEFAULT_RUBBERBAND, DEFAULT_RUBBERBAND];
case false:
return [0, 0];
default:
return V.toVector(value);
}
},
from(value) {
if (typeof value === "function") return value;
if (value != null) return V.toVector(value);
},
transform(value, _k, config) {
const transform = value || config.shared.transform;
this.hasCustomTransform = !!transform;
return transform || identity;
},
threshold(value) {
return V.toVector(value, 0);
}
};
const DEFAULT_AXIS_THRESHOLD = 0;
const coordinatesConfigResolver = _objectSpread2$1(_objectSpread2$1({}, commonConfigResolver), {}, {
axis(_v, _k, {
axis
}) {
this.lockDirection = axis === "lock";
if (!this.lockDirection) return axis;
},
axisThreshold(value = DEFAULT_AXIS_THRESHOLD) {
return value;
},
bounds(value = {}) {
if (typeof value === "function") {
return (state) => coordinatesConfigResolver.bounds(value(state));
}
if ("current" in value) {
return () => value.current;
}
if (typeof HTMLElement === "function" && value instanceof HTMLElement) {
return value;
}
const {
left = -Infinity,
right = Infinity,
top = -Infinity,
bottom = Infinity
} = value;
return [[left, right], [top, bottom]];
}
});
const isBrowser = typeof window !== "undefined" && window.document && window.document.createElement;
function supportsTouchEvents() {
return isBrowser && "ontouchstart" in window;
}
function isTouchScreen() {
return supportsTouchEvents() || isBrowser && window.navigator.maxTouchPoints > 1;
}
function supportsPointerEvents() {
return isBrowser && "onpointerdown" in window;
}
function supportsPointerLock() {
return isBrowser && "exitPointerLock" in window.document;
}
function supportsGestureEvents() {
try {
return "constructor" in GestureEvent;
} catch (e) {
return false;
}
}
const SUPPORT = {
isBrowser,
gesture: supportsGestureEvents(),
touch: supportsTouchEvents(),
touchscreen: isTouchScreen(),
pointer: supportsPointerEvents(),
pointerLock: supportsPointerLock()
};
const DEFAULT_PREVENT_SCROLL_DELAY = 250;
const DEFAULT_DRAG_DELAY = 180;
const DEFAULT_SWIPE_VELOCITY = 0.5;
const DEFAULT_SWIPE_DISTANCE = 50;
const DEFAULT_SWIPE_DURATION = 250;
const DEFAULT_KEYBOARD_DISPLACEMENT = 10;
const DEFAULT_DRAG_AXIS_THRESHOLD = {
mouse: 0,
touch: 0,
pen: 8
};
_objectSpread2$1(_objectSpread2$1({}, coordinatesConfigResolver), {}, {
device(_v, _k, {
pointer: {
touch = false,
lock = false,
mouse = false
} = {}
}) {
this.pointerLock = lock && SUPPORT.pointerLock;
if (SUPPORT.touch && touch) return "touch";
if (this.pointerLock) return "mouse";
if (SUPPORT.pointer && !mouse) return "pointer";
if (SUPPORT.touch) return "touch";
return "mouse";
},
preventScrollAxis(value, _k, {
preventScroll
}) {
this.preventScrollDelay = typeof preventScroll === "number" ? preventScroll : preventScroll || preventScroll === void 0 && value ? DEFAULT_PREVENT_SCROLL_DELAY : void 0;
if (!SUPPORT.touchscreen || preventScroll === false) return void 0;
return value ? value : preventScroll !== void 0 ? "y" : void 0;
},
pointerCapture(_v, _k, {
pointer: {
capture = true,
buttons = 1,
keys = true
} = {}
}) {
this.pointerButtons = buttons;
this.keys = keys;
return !this.pointerLock && this.device === "pointer" && capture;
},
threshold(value, _k, {
filterTaps = false,
tapsThreshold = 3,
axis = void 0
}) {
const threshold = V.toVector(value, filterTaps ? tapsThreshold : axis ? 1 : 0);
this.filterTaps = filterTaps;
this.tapsThreshold = tapsThreshold;
return threshold;
},
swipe({
velocity = DEFAULT_SWIPE_VELOCITY,
distance = DEFAULT_SWIPE_DISTANCE,
duration = DEFAULT_SWIPE_DURATION
} = {}) {
return {
velocity: this.transform(V.toVector(velocity)),
distance: this.transform(V.toVector(distance)),
duration
};
},
delay(value = 0) {
switch (value) {
case true:
return DEFAULT_DRAG_DELAY;
case false:
return 0;
default:
return value;
}
},
axisThreshold(value) {
if (!value) return DEFAULT_DRAG_AXIS_THRESHOLD;
return _objectSpread2$1(_objectSpread2$1({}, DEFAULT_DRAG_AXIS_THRESHOLD), value);
},
keyboardDisplacement(value = DEFAULT_KEYBOARD_DISPLACEMENT) {
return value;
}
});
function clampStateInternalMovementToBounds(state) {
const [ox, oy] = state.overflow;
const [dx, dy] = state._delta;
const [dirx, diry] = state._direction;
if (ox < 0 && dx > 0 && dirx < 0 || ox > 0 && dx < 0 && dirx > 0) {
state._movement[0] = state._movementBound[0];
}
if (oy < 0 && dy > 0 && diry < 0 || oy > 0 && dy < 0 && diry > 0) {
state._movement[1] = state._movementBound[1];
}
}
const SCALE_ANGLE_RATIO_INTENT_DEG = 30;
const PINCH_WHEEL_RATIO = 100;
class PinchEngine extends Engine {
constructor(...args) {
super(...args);
_defineProperty$1(this, "ingKey", "pinching");
_defineProperty$1(this, "aliasKey", "da");
}
init() {
this.state.offset = [1, 0];
this.state.lastOffset = [1, 0];
this.state._pointerEvents = /* @__PURE__ */ new Map();
}
reset() {
super.reset();
const state = this.state;
state._touchIds = [];
state.canceled = false;
state.cancel = this.cancel.bind(this);
state.turns = 0;
}
computeOffset() {
const {
type,
movement,
lastOffset
} = this.state;
if (type === "wheel") {
this.state.offset = V.add(movement, lastOffset);
} else {
this.state.offset = [(1 + movement[0]) * lastOffset[0], movement[1] + lastOffset[1]];
}
}
computeMovement() {
const {
offset,
lastOffset
} = this.state;
this.state.movement = [offset[0] / lastOffset[0], offset[1] - lastOffset[1]];
}
axisIntent() {
const state = this.state;
const [_m0, _m1] = state._movement;
if (!state.axis) {
const axisMovementDifference = Math.abs(_m0) * SCALE_ANGLE_RATIO_INTENT_DEG - Math.abs(_m1);
if (axisMovementDifference < 0) state.axis = "angle";
else if (axisMovementDifference > 0) state.axis = "scale";
}
}
restrictToAxis(v) {
if (this.config.lockDirection) {
if (this.state.axis === "scale") v[1] = 0;
else if (this.state.axis === "angle") v[0] = 0;
}
}
cancel() {
const state = this.state;
if (state.canceled) return;
setTimeout(() => {
state.canceled = true;
state._active = false;
this.compute();
this.emit();
}, 0);
}
touchStart(event) {
this.ctrl.setEventIds(event);
const state = this.state;
const ctrlTouchIds = this.ctrl.touchIds;
if (state._active) {
if (state._touchIds.every((id) => ctrlTouchIds.has(id))) return;
}
if (ctrlTouchIds.size < 2) return;
this.start(event);
state._touchIds = Array.from(ctrlTouchIds).slice(0, 2);
const payload = touchDistanceAngle(event, state._touchIds);
if (!payload) return;
this.pinchStart(event, payload);
}
pointerStart(event) {
if (event.buttons != null && event.buttons % 2 !== 1) return;
this.ctrl.setEventIds(event);
event.target.setPointerCapture(event.pointerId);
const state = this.state;
const _pointerEvents = state._pointerEvents;
const ctrlPointerIds = this.ctrl.pointerIds;
if (state._active) {
if (Array.from(_pointerEvents.keys()).every((id) => ctrlPointerIds.has(id))) return;
}
if (_pointerEvents.size < 2) {
_pointerEvents.set(event.pointerId, event);
}
if (state._pointerEvents.size < 2) return;
this.start(event);
const payload = distanceAngle(...Array.from(_pointerEvents.values()));
if (!payload) return;
this.pinchStart(event, payload);
}
pinchStart(event, payload) {
const state = this.state;
state.origin = payload.origin;
this.computeValues([payload.distance, payload.angle]);
this.computeInitial();
this.compute(event);
this.emit();
}
touchMove(event) {
if (!this.state._active) return;
const payload = touchDistanceAngle(event, this.state._touchIds);
if (!payload) return;
this.pinchMove(event, payload);
}
pointerMove(event) {
const _pointerEvents = this.state._pointerEvents;
if (_pointerEvents.has(event.pointerId)) {
_pointerEvents.set(event.pointerId, event);
}
if (!this.state._active) return;
const payload = distanceAngle(...Array.from(_pointerEvents.values()));
if (!payload) return;
this.pinchMove(event, payload);
}
pinchMove(event, payload) {
const state = this.state;
const prev_a = state._values[1];
const delta_a = payload.angle - prev_a;
let delta_turns = 0;
if (Math.abs(delta_a) > 270) delta_turns += Math.sign(delta_a);
this.computeValues([payload.distance, payload.angle - 360 * delta_turns]);
state.origin = payload.origin;
state.turns = delta_turns;
state._movement = [state._values[0] / state._initial[0] - 1, state._values[1] - state._initial[1]];
this.compute(event);
this.emit();
}
touchEnd(event) {
this.ctrl.setEventIds(event);
if (!this.state._active) return;
if (this.state._touchIds.some((id) => !this.ctrl.touchIds.has(id))) {
this.state._active = false;
this.compute(event);
this.emit();
}
}
pointerEnd(event) {
const state = this.state;
this.ctrl.setEventIds(event);
try {
event.target.releasePointerCapture(event.pointerId);
} catch (_unused) {
}
if (state._pointerEvents.has(event.pointerId)) {
state._pointerEvents.delete(event.pointerId);
}
if (!state._active) return;
if (state._pointerEvents.size < 2) {
state._active = false;
this.compute(event);
this.emit();
}
}
gestureStart(event) {
if (event.cancelable) event.preventDefault();
const state = this.state;
if (state._active) return;
this.start(event);
this.computeValues([event.scale, event.rotation]);
state.origin = [event.clientX, event.clientY];
this.compute(event);
this.emit();
}
gestureMove(event) {
if (event.cancelable) event.preventDefault();
if (!this.state._active) return;
const state = this.state;
this.computeValues([event.scale, event.rotation]);
state.origin = [event.clientX, event.clientY];
const _previousMovement = state._movement;
state._movement = [event.scale - 1, event.rotation];
state._delta = V.sub(state._movement, _previousMovement);
this.compute(event);
this.emit();
}
gestureEnd(event) {
if (!this.state._active) return;
this.state._active = false;
this.compute(event);
this.emit();
}
wheel(event) {
const modifierKey = this.config.modifierKey;
if (modifierKey && (Array.isArray(modifierKey) ? !modifierKey.find((k) => event[k]) : !event[modifierKey])) return;
if (!this.state._active) this.wheelStart(event);
else this.wheelChange(event);
this.timeoutStore.add("wheelEnd", this.wheelEnd.bind(this));
}
wheelStart(event) {
this.start(event);
this.wheelChange(event);
}
wheelChange(event) {
const isR3f = "uv" in event;
if (!isR3f) {
if (event.cancelable) {
event.preventDefault();
}
}
const state = this.state;
state._delta = [-wheelValues(event)[1] / PINCH_WHEEL_RATIO * state.offset[0], 0];
V.addTo(state._movement, state._delta);
clampStateInternalMovementToBounds(state);
this.state.origin = [event.clientX, event.clientY];
this.compute(event);
this.emit();
}
wheelEnd() {
if (!this.state._active) return;
this.state._active = false;
this.compute();
this.emit();
}
bind(bindFunction) {
const device = this.config.device;
if (!!device) {
bindFunction(device, "start", this[device + "Start"].bind(this));
bindFunction(device, "change", this[device + "Move"].bind(this));
bindFunction(device, "end", this[device + "End"].bind(this));
bindFunction(device, "cancel", this[device + "End"].bind(this));
bindFunction("lostPointerCapture", "", this[device + "End"].bind(this));
}
if (this.config.pinchOnWheel) {
bindFunction("wheel", "", this.wheel.bind(this), {
passive: false
});
}
}
}
const pinchConfigResolver = _objectSpread2$1(_objectSpread2$1({}, commonConfigResolver), {}, {
device(_v, _k, {
shared,
pointer: {
touch = false
} = {}
}) {
const sharedConfig = shared;
if (sharedConfig.target && !SUPPORT.touch && SUPPORT.gesture) return "gesture";
if (SUPPORT.touch && touch) return "touch";
if (SUPPORT.touchscreen) {
if (SUPPORT.pointer) return "pointer";
if (SUPPORT.touch) return "touch";
}
},
bounds(_v, _k, {
scaleBounds = {},
angleBounds = {}
}) {
const _scaleBounds = (state) => {
const D = assignDefault(call(scaleBounds, state), {
min: -Infinity,
max: Infinity
});
return [D.min, D.max];
};
const _angleBounds = (state) => {
const A = assignDefault(call(angleBounds, state), {
min: -Infinity,
max: Infinity
});
return [A.min, A.max];
};
if (typeof scaleBounds !== "function" && typeof angleBounds !== "function") return [_scaleBounds(), _angleBounds()];
return (state) => [_scaleBounds(state), _angleBounds(state)];
},
threshold(value, _k, config) {
this.lockDirection = config.axis === "lock";
const threshold = V.toVector(value, this.lockDirection ? [0.1, 3] : 0);
return threshold;
},
modifierKey(value) {
if (value === void 0) return "ctrlKey";
return value;
},
pinchOnWheel(value = true) {
return value;
}
});
_objectSpread2$1(_objectSpread2$1({}, coordinatesConfigResolver), {}, {
mouseOnly: (value = true) => value
});
class WheelEngine extends CoordinatesEngine {
constructor(...args) {
super(...args);
_defineProperty$1(this, "ingKey", "wheeling");
}
wheel(event) {
if (!this.state._active) this.start(event);
this.wheelChange(event);
this.timeoutStore.add("wheelEnd", this.wheelEnd.bind(this));
}
wheelChange(event) {
const state = this.state;
state._delta = wheelValues(event);
V.addTo(state._movement, state._delta);
clampStateInternalMovementToBounds(state);
this.compute(event);
this.emit();
}
wheelEnd() {
if (!this.state._active) return;
this.state._active = false;
this.compute();
this.emit();
}
bind(bindFunction) {
bindFunction("wheel", "", this.wheel.bind(this));
}
}
const wheelConfigResolver = coordinatesConfigResolver;
_objectSpread2$1(_objectSpread2$1({}, coordinatesConfigResolver), {}, {
mouseOnly: (value = true) => value
});
const EngineMap = /* @__PURE__ */ new Map();
const ConfigResolverMap = /* @__PURE__ */ new Map();
function registerAction(action) {
EngineMap.set(action.key, action.engine);
ConfigResolverMap.set(action.key, action.resolver);
}
const pinchAction = {
key: "pinch",
engine: PinchEngine,
resolver: pinchConfigResolver
};
const wheelAction = {
key: "wheel",
engine: WheelEngine,
resolver: wheelConfigResolver
};
function _objectWithoutPropertiesLoose$1(source, excluded) {
if (source == null) return {};
var target = {};
var sourceKeys = Object.keys(source);
var key, i;
for (i = 0; i < sourceKeys.length; i++) {
key = sourceKeys[i];
if (excluded.indexOf(key) >= 0) continue;
target[key] = source[key];
}
return target;
}
function _objectWithoutProperties$1(source, excluded) {
if (source == null) return {};
var target = _objectWithoutPropertiesLoose$1(source, excluded);
var key, i;
if (Object.getOwnPropertySymbols) {
var sourceSymbolKeys = Object.getOwnPropertySymbols(source);
for (i = 0; i < sourceSymbolKeys.length; i++) {
key = sourceSymbolKeys[i];
if (excluded.indexOf(key) >= 0) continue;
if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue;
target[key] = source[key];
}
}
return target;
}
const sharedConfigResolver = {
target(value) {
if (value) {
return () => "current" in value ? value.current : value;
}
return void 0;
},
enabled(value = true) {
return value;
},
window(value = SUPPORT.isBrowser ? window : void 0) {
return value;
},
eventOptions({
passive = true,
capture = false
} = {}) {
return {
passive,
capture
};
},
transform(value) {
return value;
}
};
const _excluded$1 = ["target", "eventOptions", "window", "enabled", "transform"];
function resolveWith(config = {}, resolvers) {
const result = {};
for (const [key, resolver] of Object.entries(resolvers)) {
switch (typeof resolver) {
case "function":
{
result[key] = resolver.call(result, config[key], key, config);
}
break;
case "object":
result[key] = resolveWith(config[key], resolver);
break;
case "boolean":
if (resolver) result[key] = config[key];
break;
}
}
return result;
}
function parse$1(newConfig, gestureKey, _config = {}) {
const _ref = newConfig, {
target,
eventOptions,
window: window2,
enabled,
transform
} = _ref, rest = _objectWithoutProperties$1(_ref, _excluded$1);
_config.shared = resolveWith({
target,
eventOptions,
window: window2,
enabled,
transform
}, sharedConfigResolver);
if (gestureKey) {
const resolver = ConfigResolverMap.get(gestureKey);
_config[gestureKey] = resolveWith(_objectSpread2$1({
shared: _config.shared
}, rest), resolver);
} else {
for (const key in rest) {
const resolver = ConfigResolverMap.get(key);
if (resolver) {
_config[key] = resolveWith(_objectSpread2$1({
shared: _config.shared
}, rest[key]), resolver);
}
}
}
return _config;
}
class EventStore {
constructor(ctrl, gestureKey) {
_defineProperty$1(this, "_listeners", /* @__PURE__ */ new Set());
this._ctrl = ctrl;
this._gestureKey = gestureKey;
}
add(element, device, action, handler, options) {
const listeners = this._listeners;
const type = toDomEventType(device, action);
const _options = this._gestureKey ? this._ctrl.config[this._gestureKey].eventOptions : {};
const eventOptions = _objectSpread2$1(_objectSpread2$1({}, _options), options);
element.addEventListener(type, handler, eventOptions);
const remove = () => {
element.removeEventListener(type, handler, eventOptions);
listeners.delete(remove);
};
listeners.add(remove);
return remove;
}
clean() {
this._listeners.forEach((remove) => remove());
this._listeners.clear();
}
}
class TimeoutStore {
constructor() {
_defineProperty$1(this, "_timeouts", /* @__PURE__ */ new Map());
}
add(key, callback, ms = 140, ...args) {
this.remove(key);
this._timeouts.set(key, window.setTimeout(callback, ms, ...args));
}
remove(key) {
const timeout = this._timeouts.get(key);
if (timeout) window.clearTimeout(timeout);
}
clean() {
this._timeouts.forEach((timeout) => void window.clearTimeout(timeout));
this._timeouts.clear();
}
}
class Controller {
constructor(handlers) {
_defineProperty$1(this, "gestures", /* @__PURE__ */ new Set());
_defineProperty$1(this, "_targetEventStore", new EventStore(this));
_defineProperty$1(this, "gestureEventStores", {});
_defineProperty$1(this, "gestureTimeoutStores", {});
_defineProperty$1(this, "handlers", {});
_defineProperty$1(this, "config", {});
_defineProperty$1(this, "pointerIds", /* @__PURE__ */ new Set());
_defineProperty$1(this, "touchIds", /* @__PURE__ */ new Set());
_defineProperty$1(this, "state", {
shared: {
shiftKey: false,
metaKey: false,
ctrlKey: false,
altKey: false
}
});
resolveGestures(this, handlers);
}
setEventIds(event) {
if (isTouch(event)) {
this.touchIds = new Set(touchIds(event));
return this.touchIds;
} else if ("pointerId" in event) {
if (event.type === "pointerup" || event.type === "pointercancel") this.pointerIds.delete(event.pointerId);
else if (event.type === "pointerdown") this.pointerIds.add(event.pointerId);
return this.pointerIds;
}
}
applyHandlers(handlers, nativeHandlers) {
this.handlers = handlers;
this.nativeHandlers = nativeHandlers;
}
applyConfig(config, gestureKey) {
this.config = parse$1(config, gestureKey, this.config);
}
clean() {
this._targetEventStore.clean();
for (const key of this.gestures) {
this.gestureEventStores[key].clean();
this.gestureTimeoutStores[key].clean();
}
}
effect() {
if (this.config.shared.target) this.bind();
return () => this._targetEventStore.clean();
}
bind(...args) {
const sharedConfig = this.config.shared;
const props = {};
let target;
if (sharedConfig.target) {
target = sharedConfig.target();
if (!target) return;
}
if (sharedConfig.enabled) {
for (const gestureKey of this.gestures) {
const gestureConfig = this.config[gestureKey];
const bindFunction = bindToProps(props, gestureConfig.eventOptions, !!target);
if (gestureConfig.enabled) {
const Engine = EngineMap.get(gestureKey);
new Engine(this, args, gestureKey).bind(bindFunction);
}
}
const nativeBindFunction = bindToProps(props, sharedConfig.eventOptions, !!target);
for (const eventKey in this.nativeHandlers) {
nativeBindFunction(eventKey, "", (event) => this.nativeHandlers[eventKey](_objectSpread2$1(_objectSpread2$1({}, this.state.shared), {}, {
event,
args
})), void 0, true);
}
}
for (const handlerProp in props) {
props[handlerProp] = chain(...props[handlerProp]);
}
if (!target) return props;
for (const handlerProp in props) {
const {
device,
capture,
passive
} = parseProp(handlerProp);
this._targetEventStore.add(target, device, "", props[handlerProp], {
capture,
passive
});
}
}
}
function setupGesture(ctrl, gestureKey) {
ctrl.gestures.add(gestureKey);
ctrl.gestureEventStores[gestureKey] = new EventStore(ctrl, gestureKey);
ctrl.gestureTimeoutStores[gestureKey] = new TimeoutStore();
}
function resolveGestures(ctrl, internalHandlers) {
if (internalHandlers.drag) setupGesture(ctrl, "drag");
if (internalHandlers.wheel) setupGesture(ctrl, "wheel");
if (internalHandlers.scroll) setupGesture(ctrl, "scroll");
if (internalHandlers.move) setupGesture(ctrl, "move");
if (internalHandlers.pinch) setupGesture(ctrl, "pinch");
if (internalHandlers.hover) setupGesture(ctrl, "hover");
}
const bindToProps = (props, eventOptions, withPassiveOption) => (device, action, handler, options = {}, isNative = false) => {
var _options$capture, _options$passive;
const capture = (_options$capture = options.capture) !== null && _options$capture !== void 0 ? _options$capture : eventOptions.capture;
const passive = (_options$passive = options.passive) !== null && _options$passive !== void 0 ? _options$passive : eventOptions.passive;
let handlerProp = isNative ? device : toHandlerProp(device, action, capture);
if (withPassiveOption && passive) handlerProp += "Passive";
props[handlerProp] = props[handlerProp] || [];
props[handlerProp].push(handler);
};
const RE_NOT_NATIVE = /^on(Drag|Wheel|Scroll|Move|Pinch|Hover)/;
function sortHandlers(_handlers) {
const native = {};
const handlers = {};
const actions = /* @__PURE__ */ new Set();
for (let key in _handlers) {
if (RE_NOT_NATIVE.test(key)) {
actions.add(RegExp.lastMatch);
handlers[key] = _handlers[key];
} else {
native[key] = _handlers[key];
}
}
return [handlers, native, actions];
}
function registerGesture(actions, handlers, handlerKey, key, internalHandlers, config) {
if (!actions.has(handlerKey)) return;
if (!EngineMap.has(key)) {
return;
}
const startKey = handlerKey + "Start";
const endKey = handlerKey + "End";
const fn = (state) => {
let memo = void 0;
if (state.first && startKey in handlers) handlers[startKey](state);
if (handlerKey in handlers) memo = handlers[handlerKey](state);
if (state.last && endKey in handlers) handlers[endKey](state);
return memo;
};
internalHandlers[key] = fn;
config[key] = config[key] || {};
}
function parseMergedHandlers(mergedHandlers, mergedConfig) {
const [handlers, nativeHandlers, actions] = sortHandlers(mergedHandlers);
const internalHandlers = {};
registerGesture(actions, handlers, "onDrag", "drag", internalHandlers, mergedConfig);
registerGesture(actions, handlers, "onWheel", "wheel", internalHandlers, mergedConfig);
registerGesture(actions, handlers, "onScroll", "scroll", internalHandlers, mergedConfig);
registerGesture(actions, handlers, "onPinch", "pinch", internalHandlers, mergedConfig);
registerGesture(actions, handlers, "onMove", "move", internalHandlers, mergedConfig);
registerGesture(actions, handlers, "onHover", "hover", internalHandlers, mergedConfig);
return {
handlers: internalHandlers,
config: mergedConfig,
nativeHandlers
};
}
function useRecognizers(handlers, config = {}, gestureKey, nativeHandlers) {
const ctrl = React.useMemo(() => new Controller(handlers), []);
ctrl.applyHandlers(handlers, nativeHandlers);
ctrl.applyConfig(config, gestureKey);
React.useEffect(ctrl.effect.bind(ctrl));
React.useEffect(() => {
return ctrl.clean.bind(ctrl);
}, []);
if (config.target === undefined) {
return ctrl.bind.bind(ctrl);
}
return undefined;
}
function createUseGesture(actions) {
actions.forEach(registerAction);
return function useGesture(_handlers, _config) {
const {
handlers,
nativeHandlers,
config
} = parseMergedHandlers(_handlers, _config || {});
return useRecognizers(handlers, config, undefined, nativeHandlers);
};
}
const MAX_ZOOM_STEP = 10;
const IS_DARWIN = /Mac|iPod|iPhone|iPad/.test(
// eslint-disable-next-line @typescript-eslint/no-deprecated
typeof window === "undefined" ? "node" : window.navigator.platform
);
function normalizeWheel(event) {
let { deltaY, deltaX } = event;
let deltaZ = 0;
if (event.ctrlKey || event.altKey || event.metaKey) {
deltaZ = (Math.abs(deltaY) > MAX_ZOOM_STEP ? MAX_ZOOM_STEP * Math.sign(deltaY) : deltaY) / 100;
} else {
if (event.shiftKey && !IS_DARWIN) {
deltaX = deltaY;
deltaY = 0;
}
}
return { x: -deltaX, y: -deltaY, z: -deltaZ };
}
const useGesture = createUseGesture([wheelAction, pinchAction]);
let lastWheelTime = void 0;
const isWheelEndEvent = (time) => {
if (lastWheelTime === void 0) {
lastWheelTime = time;
return false;
}
if (time - lastWheelTime > 120 && time - lastWheelTime < 160) {
lastWheelTime = time;
return true;
}
lastWheelTime = time;
return false;
};
function useGestureEvents(ref) {
const editor = useEditor();
const events = reactExports.useMemo(() => {
let pinchState = "not sure";
const onWheel = ({ event }) => {
if (!editor.getInstanceState().isFocused) {
return;
}
pinchState = "not sure";
if (isWheelEndEvent(Date.now())) {
return;
}
const editingShapeId = editor.getEditingShapeId();
if (editingShapeId) {
const shape = editor.getShape(editingShapeId);
if (shape) {
const util = editor.getShapeUtil(shape);
if (util.canScroll(shape)) {
const bounds = editor.getShapePageBounds(editingShapeId);
if (bounds?.containsPoint(editor.inputs.currentPagePoint)) {
return;
}
}
}
}
preventDefault(event);
stopEventPropagation(event);
const delta = normalizeWheel(event);
if (delta.x === 0 && delta.y === 0) return;
const info = {
type: "wheel",
name: "wheel",
delta,
point: new Vec(event.clientX, event.clientY),
shiftKey: event.shiftKey,
altKey: event.altKey,
ctrlKey: event.metaKey || event.ctrlKey,
metaKey: event.metaKey,
accelKey: isAccelKey(event)
};
editor.dispatch(info);
};
let initDistanceBetweenFingers = 1;
let initZoom = 1;
let currZoom = 1;
let currDistanceBetweenFingers = 0;
const initPointBetweenFingers = new Vec();
const prevPointBetweenFingers = new Vec();
const onPinchStart = (gesture) => {
const elm = ref.current;
pinchState = "not sure";
const { event, origin, da } = gesture;
if (event instanceof WheelEvent) return;
if (!(event.target === elm || elm?.contains(event.target))) return;
prevPointBetweenFingers.x = origin[0];
prevPointBetweenFingers.y = origin[1];
initPointBetweenFingers.x = origin[0];
initPointBetweenFingers.y = origin[1];
initDistanceBetweenFingers = da[0];
initZoom = editor.getZoomLevel();
editor.dispatch({
type: "pinch",
name: "pinch_start",
point: { x: origin[0], y: origin[1], z: editor.getZoomLevel() },
delta: { x: 0, y: 0 },
shiftKey: event.shiftKey,
altKey: event.altKey,
ctrlKey: event.metaKey || event.ctrlKey,
metaKey: event.metaKey,
accelKey: isAccelKey(event)
});
};
const updatePinchState = (isSafariTrackpadPinch) => {
if (isSafariTrackpadPinch) {
pinchState = "zooming";
}
if (pinchState === "zooming") {
return;
}
const touchDistance = Math.abs(currDistanceBetweenFingers - initDistanceBetweenFingers);
const originDistance = Vec.Dist(initPointBetweenFingers, prevPointBetweenFingers);
switch (pinchState) {
case "not sure": {
if (touchDistance > 24) {
pinchState = "zooming";
} else if (originDistance > 16) {
pinchState = "panning";
}
break;
}
case "panning": {
if (touchDistance > 64) {
pinchState = "zooming";
}
break;
}
}
};
const onPinch = (gesture) => {
const elm = ref.current;
const { event, origin, offset, da } = gesture;
if (event instanceof WheelEvent) return;
if (!(event.target === elm || elm?.contains(event.target))) return;
const isSafariTrackpadPinch = gesture.type === "gesturechange" || gesture.type === "gestureend";
currDistanceBetweenFingers = da[0];
const dx = origin[0] - prevPointBetweenFingers.x;
const dy = origin[1] - prevPointBetweenFingers.y;
prevPointBetweenFingers.x = origin[0];
prevPointBetweenFingers.y = origin[1];
updatePinchState(isSafariTrackpadPinch);
switch (pinchState) {
case "zooming": {
currZoom = offset[0];
editor.dispatch({
type: "pinch",
name: "pinch",
point: { x: origin[0], y: origin[1], z: currZoom },
delta: { x: dx, y: dy },
shiftKey: event.shiftKey,
altKey: event.altKey,
ctrlKey: event.metaKey || event.ctrlKey,
metaKey: event.metaKey,
accelKey: isAccelKey(event)
});
break;
}
case "panning": {
editor.dispatch({
type: "pinch",
name: "pinch",
point: { x: origin[0], y: origin[1], z: initZoom },
delta: { x: dx, y: dy },
shiftKey: event.shiftKey,
altKey: event.altKey,
ctrlKey: event.metaKey || event.ctrlKey,
metaKey: event.metaKey,
accelKey: isAccelKey(event)
});
break;
}
}
};
const onPinchEnd = (gesture) => {
const elm = ref.current;
const { event, origin, offset } = gesture;
if (event instanceof WheelEvent) return;
if (!(event.target === elm || elm?.contains(event.target))) return;
const scale = offset[0];
pinchState = "not sure";
editor.timers.requestAnimationFrame(() => {
editor.dispatch({
type: "pinch",
name: "pinch_end",
point: { x: origin[0], y: origin[1], z: scale },
delta: { x: origin[0], y: origin[1] },
shiftKey: event.shiftKey,
altKey: event.altKey,
ctrlKey: event.metaKey || event.ctrlKey,
metaKey: event.metaKey,
accelKey: isAccelKey(event)
});
});
};
return {
onWheel,
onPinchStart,
onPinchEnd,
onPinch
};
}, [editor, ref]);
useGesture(events, {
target: ref,
eventOptions: { passive: false },
pinch: {
from: () => [editor.getZoomLevel(), 0],
// Return the camera z to use when pinch starts
scaleBounds: () => {
const baseZoom = editor.getBaseZoom();
const zoomSteps = editor.getCameraOptions().zoomSteps;
const zoomMin = zoomSteps[0] * baseZoom;
const zoomMax = zoomSteps[zoomSteps.length - 1] * baseZoom;
return { from: editor.getZoomLevel(), max: zoomMax, min: zoomMin };
}
}
});
}
function getHandle(editor, id, handleId) {
const shape = editor.getShape(id);
const handles = editor.getShapeHandles(shape);
return { shape, handle: handles.find((h) => h.id === handleId) };
}
function useHandleEvents(id, handleId) {
const editor = useEditor();
return reactExports.useMemo(() => {
const onPointerDown = (e) => {
if (e.isKilled) return;
const target = loopToHtmlElement(e.currentTarget);
setPointerCapture(target, e);
const { shape, handle } = getHandle(editor, id, handleId);
if (!handle) return;
editor.dispatch({
type: "pointer",
target: "handle",
handle,
shape,
name: "pointer_down",
...getPointerInfo(e)
});
};
let lastX, lastY;
const onPointerMove = (e) => {
if (e.isKilled) return;
if (e.clientX === lastX && e.clientY === lastY) return;
lastX = e.clientX;
lastY = e.clientY;
const { shape, handle } = getHandle(editor, id, handleId);
if (!handle) return;
editor.dispatch({
type: "pointer",
target: "handle",
handle,
shape,
name: "pointer_move",
...getPointerInfo(e)
});
};
const onPointerUp = (e) => {
if (e.isKilled) return;
const target = loopToHtmlElement(e.currentTarget);
releasePointerCapture(target, e);
const { shape, handle } = getHandle(editor, id, handleId);
if (!handle) return;
editor.dispatch({
type: "pointer",
target: "handle",
handle,
shape,
name: "pointer_up",
...getPointerInfo(e)
});
};
return {
onPointerDown,
onPointerMove,
onPointerUp
};
}, [editor, id, handleId]);
}
function useScreenBounds(ref) {
const editor = useEditor();
reactExports.useLayoutEffect(() => {
const updateBounds = throttle$1(
() => {
if (!ref.current) return;
editor.updateViewportScreenBounds(ref.current);
},
200,
{
trailing: true
}
);
const interval = editor.timers.setInterval(updateBounds, 1e3);
window.addEventListener("resize", updateBounds);
const resizeObserver = new ResizeObserver((entries) => {
if (!entries[0].contentRect) return;
updateBounds();
});
const container = ref.current;
let scrollingParent = null;
if (container) {
resizeObserver.observe(container);
scrollingParent = getNearestScrollableContainer(container);
scrollingParent.addEventListener("scroll", updateBounds);
}
return () => {
clearInterval(interval);
window.removeEventListener("resize", updateBounds);
resizeObserver.disconnect();
scrollingParent?.removeEventListener("scroll", updateBounds);
updateBounds.cancel();
};
}, [editor, ref]);
}
/*!
* Author: excalidraw
* MIT License: https://github.com/excalidraw/excalidraw/blob/master/LICENSE
* https://github.com/excalidraw/excalidraw/blob/48c3465b19f10ec755b3eb84e21a01a468e96e43/packages/excalidraw/utils.ts#L600
*/
const getNearestScrollableContainer = (element) => {
let parent = element.parentElement;
while (parent) {
if (parent === document.body) {
return document;
}
const { overflowY } = window.getComputedStyle(parent);
const hasScrollableContent = parent.scrollHeight > parent.clientHeight;
if (hasScrollableContent && (overflowY === "auto" || overflowY === "scroll" || overflowY === "overlay")) {
return parent;
}
parent = parent.parentElement;
}
return document;
};
class Box {
constructor(x = 0, y = 0, w = 0, h = 0) {
this.x = x;
this.y = y;
this.w = w;
this.h = h;
}
x = 0;
y = 0;
w = 0;
h = 0;
// eslint-disable-next-line no-restricted-syntax
get point() {
return new Vec(this.x, this.y);
}
// eslint-disable-next-line no-restricted-syntax
set point(val) {
this.x = val.x;
this.y = val.y;
}
// eslint-disable-next-line no-restricted-syntax
get minX() {
return this.x;
}
// eslint-disable-next-line no-restricted-syntax
set minX(n) {
this.x = n;
}
// eslint-disable-next-line no-restricted-syntax
get midX() {
return this.x + this.w / 2;
}
// eslint-disable-next-line no-restricted-syntax
get maxX() {
return this.x + this.w;
}
// eslint-disable-next-line no-restricted-syntax
get minY() {
return this.y;
}
// eslint-disable-next-line no-restricted-syntax
set minY(n) {
this.y = n;
}
// eslint-disable-next-line no-restricted-syntax
get midY() {
return this.y + this.h / 2;
}
// eslint-disable-next-line no-restricted-syntax
get maxY() {
return this.y + this.h;
}
// eslint-disable-next-line no-restricted-syntax
get width() {
return this.w;
}
// eslint-disable-next-line no-restricted-syntax
set width(n) {
this.w = n;
}
// eslint-disable-next-line no-restricted-syntax
get height() {
return this.h;
}
// eslint-disable-next-line no-restricted-syntax
set height(n) {
this.h = n;
}
// eslint-disable-next-line no-restricted-syntax
get aspectRatio() {
return this.width / this.height;
}
// eslint-disable-next-line no-restricted-syntax
get center() {
return new Vec(this.midX, this.midY);
}
// eslint-disable-next-line no-restricted-syntax
set center(v) {
this.minX = v.x - this.width / 2;
this.minY = v.y - this.height / 2;
}
// eslint-disable-next-line no-restricted-syntax
get corners() {
return [
new Vec(this.minX, this.minY),
new Vec(this.maxX, this.minY),
new Vec(this.maxX, this.maxY),
new Vec(this.minX, this.maxY)
];
}
// eslint-disable-next-line no-restricted-syntax
get cornersAndCenter() {
return [
new Vec(this.minX, this.minY),
new Vec(this.maxX, this.minY),
new Vec(this.maxX, this.maxY),
new Vec(this.minX, this.maxY),
this.center
];
}
// eslint-disable-next-line no-restricted-syntax
get sides() {
const { corners } = this;
return [
[corners[0], corners[1]],
[corners[1], corners[2]],
[corners[2], corners[3]],
[corners[3], corners[0]]
];
}
// eslint-disable-next-line no-restricted-syntax
get size() {
return new Vec(this.w, this.h);
}
toFixed() {
this.x = toPrecision(this.x);
this.y = toPrecision(this.y);
this.w = toPrecision(this.w);
this.h = toPrecision(this.h);
return this;
}
setTo(B) {
this.x = B.x;
this.y = B.y;
this.w = B.w;
this.h = B.h;
return this;
}
set(x = 0, y = 0, w = 0, h = 0) {
this.x = x;
this.y = y;
this.w = w;
this.h = h;
return this;
}
expand(A) {
const minX = Math.min(this.minX, A.minX);
const minY = Math.min(this.minY, A.minY);
const maxX = Math.max(this.maxX, A.maxX);
const maxY = Math.max(this.maxY, A.maxY);
this.x = minX;
this.y = minY;
this.w = maxX - minX;
this.h = maxY - minY;
return this;
}
expandBy(n) {
this.x -= n;
this.y -= n;
this.w += n * 2;
this.h += n * 2;
return this;
}
scale(n) {
this.x /= n;
this.y /= n;
this.w /= n;
this.h /= n;
return this;
}
clone() {
const { x, y, w, h } = this;
return new Box(x, y, w, h);
}
translate(delta) {
this.x += delta.x;
this.y += delta.y;
return this;
}
snapToGrid(size) {
const minX = Math.round(this.minX / size) * size;
const minY = Math.round(this.minY / size) * size;
const maxX = Math.round(this.maxX / size) * size;
const maxY = Math.round(this.maxY / size) * size;
this.minX = minX;
this.minY = minY;
this.width = Math.max(1, maxX - minX);
this.height = Math.max(1, maxY - minY);
}
collides(B) {
return Box.Collides(this, B);
}
contains(B) {
return Box.Contains(this, B);
}
includes(B) {
return Box.Includes(this, B);
}
containsPoint(V, margin = 0) {
return Box.ContainsPoint(this, V, margin);
}
getHandlePoint(handle) {
switch (handle) {
case "top_left":
return new Vec(this.minX, this.minY);
case "top_right":
return new Vec(this.maxX, this.minY);
case "bottom_left":
return new Vec(this.minX, this.maxY);
case "bottom_right":
return new Vec(this.maxX, this.maxY);
case "top":
return new Vec(this.midX, this.minY);
case "right":
return new Vec(this.maxX, this.midY);
case "bottom":
return new Vec(this.midX, this.maxY);
case "left":
return new Vec(this.minX, this.midY);
}
}
toJson() {
return { x: this.minX, y: this.minY, w: this.w, h: this.h };
}
resize(handle, dx, dy) {
const { minX: a0x, minY: a0y, maxX: a1x, maxY: a1y } = this;
let { minX: b0x, minY: b0y, maxX: b1x, maxY: b1y } = this;
switch (handle) {
case "left":
case "top_left":
case "bottom_left": {
b0x += dx;
break;
}
case "right":
case "top_right":
case "bottom_right": {
b1x += dx;
break;
}
}
switch (handle) {
case "top":
case "top_left":
case "top_right": {
b0y += dy;
break;
}
case "bottom":
case "bottom_left":
case "bottom_right": {
b1y += dy;
break;
}
}
const scaleX = (b1x - b0x) / (a1x - a0x);
const scaleY = (b1y - b0y) / (a1y - a0y);
const flipX = scaleX < 0;
const flipY = scaleY < 0;
if (flipX) {
const t = b1x;
b1x = b0x;
b0x = t;
}
if (flipY) {
const t = b1y;
b1y = b0y;
b0y = t;
}
this.minX = b0x;
this.minY = b0y;
this.width = Math.abs(b1x - b0x);
this.height = Math.abs(b1y - b0y);
}
union(box) {
const minX = Math.min(this.minX, box.x);
const minY = Math.min(this.minY, box.y);
const maxX = Math.max(this.maxX, box.w + box.x);
const maxY = Math.max(this.maxY, box.h + box.y);
this.x = minX;
this.y = minY;
this.width = maxX - minX;
this.height = maxY - minY;
return this;
}
static From(box) {
return new Box(box.x, box.y, box.w, box.h);
}
static FromCenter(center, size) {
return new Box(center.x - size.x / 2, center.y - size.y / 2, size.x, size.y);
}
static FromPoints(points) {
if (points.length === 0) return new Box();
let minX = Infinity;
let minY = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
let point;
for (let i = 0, n = points.length; i < n; i++) {
point = points[i];
minX = Math.min(point.x, minX);
minY = Math.min(point.y, minY);
maxX = Math.max(point.x, maxX);
maxY = Math.max(point.y, maxY);
}
return new Box(minX, minY, maxX - minX, maxY - minY);
}
static Expand(A, B) {
const minX = Math.min(B.minX, A.minX);
const minY = Math.min(B.minY, A.minY);
const maxX = Math.max(B.maxX, A.maxX);
const maxY = Math.max(B.maxY, A.maxY);
return new Box(minX, minY, maxX - minX, maxY - minY);
}
static ExpandBy(A, n) {
return new Box(A.minX - n, A.minY - n, A.width + n * 2, A.height + n * 2);
}
static Collides(A, B) {
return !(A.maxX < B.minX || A.minX > B.maxX || A.maxY < B.minY || A.minY > B.maxY);
}
static Contains(A, B) {
return A.minX < B.minX && A.minY < B.minY && A.maxY > B.maxY && A.maxX > B.maxX;
}
static Includes(A, B) {
return Box.Collides(A, B) || Box.Contains(A, B);
}
static ContainsPoint(A, B, margin = 0) {
return !(B.x < A.minX - margin || B.y < A.minY - margin || B.x > A.maxX + margin || B.y > A.maxY + margin);
}
static Common(boxes) {
let minX = Infinity;
let minY = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
for (let i = 0; i < boxes.length; i++) {
const B = boxes[i];
minX = Math.min(minX, B.minX);
minY = Math.min(minY, B.minY);
maxX = Math.max(maxX, B.maxX);
maxY = Math.max(maxY, B.maxY);
}
return new Box(minX, minY, maxX - minX, maxY - minY);
}
static Sides(A, inset = 0) {
const { corners } = A;
return [
[corners[0], corners[1]],
[corners[1], corners[2]],
[corners[2], corners[3]],
[corners[3], corners[0]]
];
}
static Resize(box, handle, dx, dy, isAspectRatioLocked = false) {
const { minX: a0x, minY: a0y, maxX: a1x, maxY: a1y } = box;
let { minX: b0x, minY: b0y, maxX: b1x, maxY: b1y } = box;
switch (handle) {
case "left":
case "top_left":
case "bottom_left": {
b0x += dx;
break;
}
case "right":
case "top_right":
case "bottom_right": {
b1x += dx;
break;
}
}
switch (handle) {
case "top":
case "top_left":
case "top_right": {
b0y += dy;
break;
}
case "bottom":
case "bottom_left":
case "bottom_right": {
b1y += dy;
break;
}
}
const scaleX = (b1x - b0x) / (a1x - a0x);
const scaleY = (b1y - b0y) / (a1y - a0y);
const flipX = scaleX < 0;
const flipY = scaleY < 0;
if (isAspectRatioLocked) {
const aspectRatio = (a1x - a0x) / (a1y - a0y);
const bw = Math.abs(b1x - b0x);
const bh = Math.abs(b1y - b0y);
const tw = bw * (scaleY < 0 ? 1 : -1) * (1 / aspectRatio);
const th = bh * (scaleX < 0 ? 1 : -1) * aspectRatio;
const isTall = aspectRatio < bw / bh;
switch (handle) {
case "top_left": {
if (isTall) b0y = b1y + tw;
else b0x = b1x + th;
break;
}
case "top_right": {
if (isTall) b0y = b1y + tw;
else b1x = b0x - th;
break;
}
case "bottom_right": {
if (isTall) b1y = b0y - tw;
else b1x = b0x - th;
break;
}
case "bottom_left": {
if (isTall) b1y = b0y - tw;
else b0x = b1x + th;
break;
}
case "bottom":
case "top": {
const m = (b0x + b1x) / 2;
const w = bh * aspectRatio;
b0x = m - w / 2;
b1x = m + w / 2;
break;
}
case "left":
case "right": {
const m = (b0y + b1y) / 2;
const h = bw / aspectRatio;
b0y = m - h / 2;
b1y = m + h / 2;
break;
}
}
}
if (flipX) {
const t = b1x;
b1x = b0x;
b0x = t;
}
if (flipY) {
const t = b1y;
b1y = b0y;
b0y = t;
}
const final = new Box(b0x, b0y, Math.abs(b1x - b0x), Math.abs(b1y - b0y));
return {
box: final,
scaleX: +(final.width / box.width * (scaleX > 0 ? 1 : -1)).toFixed(5),
scaleY: +(final.height / box.height * (scaleY > 0 ? 1 : -1)).toFixed(5)
};
}
equals(other) {
return Box.Equals(this, other);
}
static Equals(a, b) {
return b.x === a.x && b.y === a.y && b.w === a.w && b.h === a.h;
}
zeroFix() {
this.w = Math.max(1, this.w);
this.h = Math.max(1, this.h);
return this;
}
static ZeroFix(other) {
return new Box(other.x, other.y, Math.max(1, other.w), Math.max(1, other.h));
}
}
function flipSelectionHandleY(handle) {
switch (handle) {
case "top":
return "bottom";
case "bottom":
return "top";
case "top_left":
return "bottom_left";
case "top_right":
return "bottom_right";
case "bottom_left":
return "top_left";
case "bottom_right":
return "top_right";
default:
return handle;
}
}
function flipSelectionHandleX(handle) {
switch (handle) {
case "left":
return "right";
case "right":
return "left";
case "top_left":
return "top_right";
case "top_right":
return "top_left";
case "bottom_left":
return "bottom_right";
case "bottom_right":
return "bottom_left";
default:
return handle;
}
}
function isSelectionCorner(selection) {
return selection === "top_left" || selection === "top_right" || selection === "bottom_right" || selection === "bottom_left";
}
class Mat {
constructor(a, b, c, d, e, f) {
this.a = a;
this.b = b;
this.c = c;
this.d = d;
this.e = e;
this.f = f;
}
a = 1;
b = 0;
c = 0;
d = 1;
e = 0;
f = 0;
equals(m) {
return this === m || this.a === m.a && this.b === m.b && this.c === m.c && this.d === m.d && this.e === m.e && this.f === m.f;
}
identity() {
this.a = 1;
this.b = 0;
this.c = 0;
this.d = 1;
this.e = 0;
this.f = 0;
return this;
}
multiply(m) {
const m2 = m;
const { a, b, c, d, e, f } = this;
this.a = a * m2.a + c * m2.b;
this.c = a * m2.c + c * m2.d;
this.e = a * m2.e + c * m2.f + e;
this.b = b * m2.a + d * m2.b;
this.d = b * m2.c + d * m2.d;
this.f = b * m2.e + d * m2.f + f;
return this;
}
rotate(r, cx, cy) {
if (r === 0) return this;
if (cx === void 0) return this.multiply(Mat.Rotate(r));
return this.translate(cx, cy).multiply(Mat.Rotate(r)).translate(-cx, -cy);
}
translate(x, y) {
return this.multiply(Mat.Translate(x, y));
}
scale(x, y) {
return this.multiply(Mat.Scale(x, y));
}
invert() {
const { a, b, c, d, e, f } = this;
const denom = a * d - b * c;
this.a = d / denom;
this.b = b / -denom;
this.c = c / -denom;
this.d = a / denom;
this.e = (d * e - c * f) / -denom;
this.f = (b * e - a * f) / denom;
return this;
}
applyToPoint(point) {
return Mat.applyToPoint(this, point);
}
applyToPoints(points) {
return Mat.applyToPoints(this, points);
}
rotation() {
return Mat.Rotation(this);
}
point() {
return Mat.Point(this);
}
decomposed() {
return Mat.Decompose(this);
}
toCssString() {
return Mat.toCssString(this);
}
setTo(model) {
Object.assign(this, model);
return this;
}
decompose() {
return Mat.Decompose(this);
}
clone() {
return new Mat(this.a, this.b, this.c, this.d, this.e, this.f);
}
/* --------------------- Static --------------------- */
static Identity() {
return new Mat(1, 0, 0, 1, 0, 0);
}
static Translate(x, y) {
return new Mat(1, 0, 0, 1, x, y);
}
static Rotate(r, cx, cy) {
if (r === 0) return Mat.Identity();
const cosAngle = Math.cos(r);
const sinAngle = Math.sin(r);
const rotationMatrix = new Mat(cosAngle, sinAngle, -sinAngle, cosAngle, 0, 0);
if (cx === void 0) return rotationMatrix;
return Mat.Compose(Mat.Translate(cx, cy), rotationMatrix, Mat.Translate(-cx, -cy));
}
static Scale(x, y, cx, cy) {
const scaleMatrix = new Mat(x, 0, 0, y, 0, 0);
if (cx === void 0) return scaleMatrix;
return Mat.Compose(Mat.Translate(cx, cy), scaleMatrix, Mat.Translate(-cx, -cy));
}
static Multiply(m1, m2) {
return {
a: m1.a * m2.a + m1.c * m2.b,
c: m1.a * m2.c + m1.c * m2.d,
e: m1.a * m2.e + m1.c * m2.f + m1.e,
b: m1.b * m2.a + m1.d * m2.b,
d: m1.b * m2.c + m1.d * m2.d,
f: m1.b * m2.e + m1.d * m2.f + m1.f
};
}
static Inverse(m) {
const denom = m.a * m.d - m.b * m.c;
return {
a: m.d / denom,
b: m.b / -denom,
c: m.c / -denom,
d: m.a / denom,
e: (m.d * m.e - m.c * m.f) / -denom,
f: (m.b * m.e - m.a * m.f) / denom
};
}
static Absolute(m) {
const denom = m.a * m.d - m.b * m.c;
return {
a: m.d / denom,
b: m.b / -denom,
c: m.c / -denom,
d: m.a / denom,
e: (m.d * m.e - m.c * m.f) / denom,
f: (m.b * m.e - m.a * m.f) / -denom
};
}
static Compose(...matrices) {
const matrix = Mat.Identity();
for (let i = 0, n = matrices.length; i < n; i++) {
matrix.multiply(matrices[i]);
}
return matrix;
}
static Point(m) {
return new Vec(m.e, m.f);
}
static Rotation(m) {
let rotation;
if (m.a !== 0 || m.c !== 0) {
const hypotAc = (m.a * m.a + m.c * m.c) ** 0.5;
rotation = Math.acos(m.a / hypotAc) * (m.c > 0 ? -1 : 1);
} else if (m.b !== 0 || m.d !== 0) {
const hypotBd = (m.b * m.b + m.d * m.d) ** 0.5;
rotation = HALF_PI + Math.acos(m.b / hypotBd) * (m.d > 0 ? -1 : 1);
} else {
rotation = 0;
}
return clampRadians(rotation);
}
static Decompose(m) {
let scaleX, scaleY, rotation;
if (m.a !== 0 || m.c !== 0) {
const hypotAc = (m.a * m.a + m.c * m.c) ** 0.5;
scaleX = hypotAc;
scaleY = (m.a * m.d - m.b * m.c) / hypotAc;
rotation = Math.acos(m.a / hypotAc) * (m.c > 0 ? -1 : 1);
} else if (m.b !== 0 || m.d !== 0) {
const hypotBd = (m.b * m.b + m.d * m.d) ** 0.5;
scaleX = (m.a * m.d - m.b * m.c) / hypotBd;
scaleY = hypotBd;
rotation = HALF_PI + Math.acos(m.b / hypotBd) * (m.d > 0 ? -1 : 1);
} else {
scaleX = 0;
scaleY = 0;
rotation = 0;
}
return {
x: m.e,
y: m.f,
scaleX,
scaleY,
rotation: clampRadians(rotation)
};
}
static Smooth(m, precision = 1e10) {
m.a = Math.round(m.a * precision) / precision;
m.b = Math.round(m.b * precision) / precision;
m.c = Math.round(m.c * precision) / precision;
m.d = Math.round(m.d * precision) / precision;
m.e = Math.round(m.e * precision) / precision;
m.f = Math.round(m.f * precision) / precision;
return m;
}
static toCssString(m) {
return `matrix(${toDomPrecision(m.a)}, ${toDomPrecision(m.b)}, ${toDomPrecision(
m.c
)}, ${toDomPrecision(m.d)}, ${toDomPrecision(m.e)}, ${toDomPrecision(m.f)})`;
}
static applyToPoint(m, point) {
return new Vec(
m.a * point.x + m.c * point.y + m.e,
m.b * point.x + m.d * point.y + m.f,
point.z
);
}
static applyToXY(m, x, y) {
return [m.a * x + m.c * y + m.e, m.b * x + m.d * y + m.f];
}
static applyToPoints(m, points) {
return points.map(
(point) => new Vec(m.a * point.x + m.c * point.y + m.e, m.b * point.x + m.d * point.y + m.f, point.z)
);
}
static applyToBounds(m, box) {
return new Box(m.e + box.minX, m.f + box.minY, box.width, box.height);
}
static From(m) {
return new Mat(m.a, m.b, m.c, m.d, m.e, m.f);
}
static Cast(m) {
return m instanceof Mat ? m : Mat.From(m);
}
}
class Geometry2d {
isFilled = false;
isClosed = true;
isLabel = false;
debugColor;
ignore;
constructor(opts) {
this.isFilled = opts.isFilled;
this.isClosed = opts.isClosed;
this.isLabel = opts.isLabel ?? false;
this.debugColor = opts.debugColor;
this.ignore = opts.ignore;
}
// hitTestPoint(point: Vec, margin = 0, hitInside = false) {
// // We've removed the broad phase here; that should be done outside of the call
// return this.distanceToPoint(point, hitInside) <= margin
// }
hitTestPoint(point, margin = 0, hitInside = false) {
if (this.isClosed && (this.isFilled || hitInside) && pointInPolygon(point, this.vertices)) {
return true;
}
return Vec.Dist2(point, this.nearestPoint(point)) <= margin * margin;
}
distanceToPoint(point, hitInside = false) {
return point.dist(this.nearestPoint(point)) * (this.isClosed && (this.isFilled || hitInside) && pointInPolygon(point, this.vertices) ? -1 : 1);
}
distanceToLineSegment(A, B) {
if (A.equals(B)) return this.distanceToPoint(A);
const { vertices } = this;
let nearest;
let dist = Infinity;
let d, p, q;
for (let i = 0; i < vertices.length; i++) {
p = vertices[i];
q = Vec.NearestPointOnLineSegment(A, B, p, true);
d = Vec.Dist2(p, q);
if (d < dist) {
dist = d;
nearest = q;
}
}
if (!nearest) throw Error("nearest point not found");
return this.isClosed && this.isFilled && pointInPolygon(nearest, this.vertices) ? -dist : dist;
}
hitTestLineSegment(A, B, distance = 0) {
return this.distanceToLineSegment(A, B) <= distance;
}
nearestPointOnLineSegment(A, B) {
const { vertices } = this;
let nearest;
let dist = Infinity;
let d, p, q;
for (let i = 0; i < vertices.length; i++) {
p = vertices[i];
q = Vec.NearestPointOnLineSegment(A, B, p, true);
d = Vec.Dist2(p, q);
if (d < dist) {
dist = d;
nearest = q;
}
}
if (!nearest) throw Error("nearest point not found");
return nearest;
}
isPointInBounds(point, margin = 0) {
const { bounds } = this;
return !(point.x < bounds.minX - margin || point.y < bounds.minY - margin || point.x > bounds.maxX + margin || point.y > bounds.maxY + margin);
}
_vertices;
// eslint-disable-next-line no-restricted-syntax
get vertices() {
if (!this._vertices) {
this._vertices = this.getVertices();
}
return this._vertices;
}
getBounds() {
return Box.FromPoints(this.vertices);
}
_bounds;
// eslint-disable-next-line no-restricted-syntax
get bounds() {
if (!this._bounds) {
this._bounds = this.getBounds();
}
return this._bounds;
}
// eslint-disable-next-line no-restricted-syntax
get center() {
return this.bounds.center;
}
_area;
// eslint-disable-next-line no-restricted-syntax
get area() {
if (!this._area) {
this._area = this.getArea();
}
return this._area;
}
getArea() {
if (!this.isClosed) {
return 0;
}
const { vertices } = this;
let area = 0;
for (let i = 0, n = vertices.length; i < n; i++) {
const curr = vertices[i];
const next = vertices[(i + 1) % n];
area += curr.x * next.y - next.x * curr.y;
}
return area / 2;
}
toSimpleSvgPath() {
let path = "";
const { vertices } = this;
const n = vertices.length;
if (n === 0) return path;
path += `M${vertices[0].x},${vertices[0].y}`;
for (let i = 1; i < n; i++) {
path += `L${vertices[i].x},${vertices[i].y}`;
}
if (this.isClosed) {
path += "Z";
}
return path;
}
_length;
// eslint-disable-next-line no-restricted-syntax
get length() {
if (this._length) return this._length;
this._length = this.getLength();
return this._length;
}
getLength() {
const { vertices } = this;
let n1, p1 = vertices[0], length = 0;
for (let i = 1; i < vertices.length; i++) {
n1 = vertices[i];
length += Vec.Dist2(p1, n1);
p1 = n1;
}
return Math.sqrt(length);
}
}
class Group2d extends Geometry2d {
children = [];
ignoredChildren = [];
constructor(config) {
super({ ...config, isClosed: true, isFilled: false });
for (const child of config.children) {
if (child.ignore) {
this.ignoredChildren.push(child);
} else {
this.children.push(child);
}
}
if (this.children.length === 0) throw Error("Group2d must have at least one child");
}
getVertices() {
return this.children.filter((c) => !c.isLabel).flatMap((c) => c.vertices);
}
nearestPoint(point) {
let dist = Infinity;
let nearest;
const { children } = this;
if (children.length === 0) {
throw Error("no children");
}
let p;
let d;
for (const child of children) {
p = child.nearestPoint(point);
d = Vec.Dist2(p, point);
if (d < dist) {
dist = d;
nearest = p;
}
}
if (!nearest) throw Error("nearest point not found");
return nearest;
}
distanceToPoint(point, hitInside = false) {
return Math.min(...this.children.map((c, i) => c.distanceToPoint(point, hitInside || i > 0)));
}
hitTestPoint(point, margin, hitInside) {
return !!this.children.filter((c) => !c.isLabel).find((c) => c.hitTestPoint(point, margin, hitInside));
}
hitTestLineSegment(A, B, zoom) {
return !!this.children.filter((c) => !c.isLabel).find((c) => c.hitTestLineSegment(A, B, zoom));
}
getArea() {
return this.children[0].area;
}
toSimpleSvgPath() {
let path = "";
for (const child of this.children) {
path += child.toSimpleSvgPath();
}
const corners = Box.FromPoints(this.vertices).corners;
for (let i = 0, n = corners.length; i < n; i++) {
const corner = corners[i];
const prevCorner = corners[(i - 1 + n) % n];
const prevDist = corner.dist(prevCorner);
const nextCorner = corners[(i + 1) % n];
const nextDist = corner.dist(nextCorner);
const A = corner.clone().lrp(prevCorner, 4 / prevDist);
const B = corner;
const C = corner.clone().lrp(nextCorner, 4 / nextDist);
path += `M${A.x},${A.y} L${B.x},${B.y} L${C.x},${C.y} `;
}
return path;
}
getLength() {
return this.children.reduce((a, c) => c.isLabel ? a : a + c.length, 0);
}
getSvgPathData() {
return this.children.map((c, i) => c.isLabel ? "" : c.getSvgPathData(i === 0)).join(" ");
}
}
function useTick$1(isEnabled = true) {
const [_, setTick] = reactExports.useState(0);
const editor = useEditor();
reactExports.useEffect(() => {
if (!isEnabled) return;
const update = () => setTick((tick) => tick + 1);
editor.on("tick", update);
return () => {
editor.off("tick", update);
};
}, [editor, isEnabled]);
}
const GeometryDebuggingView = track(function GeometryDebuggingView2({
showStroke = true,
showVertices = true,
showClosestPointOnOutline = true
}) {
const editor = useEditor();
useTick$1(showClosestPointOnOutline);
const zoomLevel = editor.getZoomLevel();
const renderingShapes = editor.getRenderingShapes();
const {
inputs: { currentPagePoint }
} = editor;
return /* @__PURE__ */ jsxRuntimeExports.jsx(
"svg",
{
style: {
position: "absolute",
pointerEvents: "none",
zIndex: 999999999,
top: 0,
left: 0,
overflow: "visible"
},
children: renderingShapes.map((result) => {
const shape = editor.getShape(result.id);
if (shape.type === "group") return null;
const geometry = editor.getShapeGeometry(shape);
const pageTransform = editor.getShapePageTransform(shape);
const pointInShapeSpace = editor.getPointInShapeSpace(shape, currentPagePoint);
const nearestPointOnShape = geometry.nearestPoint(pointInShapeSpace);
const distanceToPoint = geometry.distanceToPoint(pointInShapeSpace, true);
const dist = Math.abs(distanceToPoint) * zoomLevel;
const hitInside = distanceToPoint < 0;
const { vertices } = geometry;
return /* @__PURE__ */ jsxRuntimeExports.jsxs(
"g",
{
transform: pageTransform.toCssString(),
strokeLinecap: "round",
strokeLinejoin: "round",
children: [
showStroke && /* @__PURE__ */ jsxRuntimeExports.jsx(
"g",
{
stroke: geometry.debugColor ?? "red",
opacity: "1",
strokeWidth: 2 / zoomLevel,
fill: "none",
children: /* @__PURE__ */ jsxRuntimeExports.jsx(GeometryStroke, { geometry })
}
),
showVertices && vertices.map((v, i) => /* @__PURE__ */ jsxRuntimeExports.jsx(
"circle",
{
cx: v.x,
cy: v.y,
r: 2 / zoomLevel,
fill: `hsl(${modulate(i, [0, vertices.length - 1], [120, 200])}, 100%, 50%)`,
stroke: "black",
strokeWidth: 1 / zoomLevel
},
`v${i}`
)),
showClosestPointOnOutline && dist < 150 && /* @__PURE__ */ jsxRuntimeExports.jsx(
"line",
{
x1: nearestPointOnShape.x,
y1: nearestPointOnShape.y,
x2: pointInShapeSpace.x,
y2: pointInShapeSpace.y,
opacity: 1 - dist / 150,
stroke: hitInside ? "goldenrod" : "dodgerblue",
strokeWidth: 2 / zoomLevel
}
)
]
},
result.id + "_outline"
);
})
}
);
});
function GeometryStroke({ geometry }) {
if (geometry instanceof Group2d) {
return /* @__PURE__ */ jsxRuntimeExports.jsx(jsxRuntimeExports.Fragment, { children: [...geometry.children, ...geometry.ignoredChildren].map((child, i) => /* @__PURE__ */ jsxRuntimeExports.jsx(GeometryStroke, { geometry: child }, i)) });
}
return /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: geometry.toSimpleSvgPath() });
}
function uniq(array) {
return _uniq(array);
}
function usePeerIds() {
const editor = useEditor();
const $userIds = useComputed(
"userIds",
() => uniq(editor.getCollaborators().map((p) => p.userId)).sort(),
{ isEqual: (a, b) => a.join(",") === b.join?.(",") },
[editor]
);
return useValue($userIds);
}
function usePresence$1(userId) {
const editor = useEditor();
const latestPresence = useValue(
`latestPresence:${userId}`,
() => {
return editor.getCollaborators().find((c) => c.userId === userId);
},
[editor, userId]
);
return latestPresence ?? null;
}
const LiveCollaborators = track(function Collaborators() {
const peerIds = usePeerIds();
return peerIds.map((id) => /* @__PURE__ */ jsxRuntimeExports.jsx(CollaboratorGuard, { collaboratorId: id }, id));
});
const CollaboratorGuard = track(function CollaboratorGuard2({
collaboratorId
}) {
const editor = useEditor();
const presence = usePresence$1(collaboratorId);
const collaboratorState = useCollaboratorState(editor, presence);
if (!(presence && presence.currentPageId === editor.getCurrentPageId())) {
return null;
}
switch (collaboratorState) {
case "inactive": {
const { followingUserId, highlightedUserIds } = editor.getInstanceState();
if (!(followingUserId === presence.userId || highlightedUserIds.includes(presence.userId))) {
return null;
}
break;
}
case "idle": {
const { highlightedUserIds } = editor.getInstanceState();
if (presence.followingUserId === editor.user.getId() && !(presence.chatMessage || highlightedUserIds.includes(presence.userId))) {
return null;
}
break;
}
}
return /* @__PURE__ */ jsxRuntimeExports.jsx(Collaborator, { latestPresence: presence });
});
const Collaborator = track(function Collaborator2({
latestPresence
}) {
const editor = useEditor();
const {
CollaboratorBrush,
CollaboratorScribble,
CollaboratorCursor,
CollaboratorHint,
CollaboratorShapeIndicator
} = useEditorComponents();
const zoomLevel = editor.getZoomLevel();
const viewportPageBounds = editor.getViewportPageBounds();
const { userId, chatMessage, brush, scribbles, selectedShapeIds, userName, cursor, color } = latestPresence;
const isCursorInViewport = !(cursor.x < viewportPageBounds.minX - 12 / zoomLevel || cursor.y < viewportPageBounds.minY - 16 / zoomLevel || cursor.x > viewportPageBounds.maxX - 12 / zoomLevel || cursor.y > viewportPageBounds.maxY - 16 / zoomLevel);
return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
brush && CollaboratorBrush ? /* @__PURE__ */ jsxRuntimeExports.jsx(
CollaboratorBrush,
{
className: "tl-collaborator__brush",
brush,
color,
opacity: 0.1
},
userId + "_brush"
) : null,
isCursorInViewport && CollaboratorCursor ? /* @__PURE__ */ jsxRuntimeExports.jsx(
CollaboratorCursor,
{
className: "tl-collaborator__cursor",
point: cursor,
color,
zoom: zoomLevel,
name: userName !== "New User" ? userName : null,
chatMessage: chatMessage ?? ""
},
userId + "_cursor"
) : CollaboratorHint ? /* @__PURE__ */ jsxRuntimeExports.jsx(
CollaboratorHint,
{
className: "tl-collaborator__cursor-hint",
point: cursor,
color,
zoom: zoomLevel,
viewport: viewportPageBounds
},
userId + "_cursor_hint"
) : null,
CollaboratorScribble && scribbles.length ? /* @__PURE__ */ jsxRuntimeExports.jsx(jsxRuntimeExports.Fragment, { children: scribbles.map((scribble) => /* @__PURE__ */ jsxRuntimeExports.jsx(
CollaboratorScribble,
{
className: "tl-collaborator__scribble",
scribble,
color,
zoom: zoomLevel,
opacity: scribble.color === "laser" ? 0.5 : 0.1
},
userId + "_scribble_" + scribble.id
)) }) : null,
CollaboratorShapeIndicator && selectedShapeIds.filter((id) => !editor.isShapeHidden(id)).map((shapeId) => /* @__PURE__ */ jsxRuntimeExports.jsx(
CollaboratorShapeIndicator,
{
className: "tl-collaborator__shape-indicator",
shapeId,
color,
opacity: 0.5
},
userId + "_" + shapeId
))
] });
});
function getStateFromElapsedTime(editor, elapsed) {
return elapsed > editor.options.collaboratorInactiveTimeoutMs ? "inactive" : elapsed > editor.options.collaboratorIdleTimeoutMs ? "idle" : "active";
}
function useCollaboratorState(editor, latestPresence) {
const rLastActivityTimestamp = reactExports.useRef(latestPresence?.lastActivityTimestamp ?? -1);
const [state, setState] = reactExports.useState(
() => getStateFromElapsedTime(editor, Date.now() - rLastActivityTimestamp.current)
);
reactExports.useEffect(() => {
const interval = editor.timers.setInterval(() => {
setState(getStateFromElapsedTime(editor, Date.now() - rLastActivityTimestamp.current));
}, editor.options.collaboratorCheckIntervalMs);
return () => clearInterval(interval);
}, [editor]);
if (latestPresence) {
rLastActivityTimestamp.current = latestPresence.lastActivityTimestamp;
}
return state;
}
function MenuClickCapture() {
const editor = useEditor();
const isMenuOpen = useValue("is menu open", () => editor.menus.hasAnyOpenMenus(), [editor]);
const [isPointing, setIsPointing] = reactExports.useState(false);
const showElement = isMenuOpen || isPointing;
const canvasEvents = useCanvasEvents();
const rPointerState = reactExports.useRef({
isDown: false,
isDragging: false,
start: new Vec()
});
const handlePointerDown = reactExports.useCallback(
(e) => {
if (e.button === 0) {
setIsPointing(true);
rPointerState.current = {
isDown: true,
isDragging: false,
start: new Vec(e.clientX, e.clientY)
};
}
editor.menus.clearOpenMenus();
},
[editor]
);
const handlePointerMove = reactExports.useCallback(
(e) => {
if (!rPointerState.current.isDown) return;
if (rPointerState.current.isDragging) {
canvasEvents.onPointerMove?.(e);
return;
}
if (
// We're pointing, but are we dragging?
Vec.Dist2(rPointerState.current.start, new Vec(e.clientX, e.clientY)) > editor.options.dragDistanceSquared
) {
rPointerState.current = {
...rPointerState.current,
isDown: true,
isDragging: true
};
const { x, y } = rPointerState.current.start;
canvasEvents.onPointerDown?.({
...e,
clientX: x,
clientY: y,
button: 0
});
canvasEvents.onPointerMove?.(e);
}
},
[canvasEvents, editor]
);
const handlePointerUp = reactExports.useCallback(
(e) => {
canvasEvents.onPointerUp?.(e);
setIsPointing(false);
rPointerState.current = {
isDown: false,
isDragging: false,
start: new Vec(e.clientX, e.clientY)
};
},
[canvasEvents]
);
return showElement && /* @__PURE__ */ jsxRuntimeExports.jsx(
"div",
{
className: "tlui-menu-click-capture",
"data-testid": "menu-click-capture.content",
...canvasEvents,
onPointerDown: handlePointerDown,
onPointerMove: handlePointerMove,
onPointerUp: handlePointerUp
}
);
}
const Shape = reactExports.memo(function Shape2({
id,
shape,
util,
index,
backgroundIndex,
opacity
}) {
const editor = useEditor();
const { ShapeErrorFallback } = useEditorComponents();
const containerRef = reactExports.useRef(null);
const bgContainerRef = reactExports.useRef(null);
const memoizedStuffRef = reactExports.useRef({
transform: "",
clipPath: "none",
width: 0,
height: 0,
x: 0,
y: 0,
isCulled: false
});
useQuickReactor(
"set shape stuff",
() => {
const shape2 = editor.getShape(id);
if (!shape2) return;
const prev = memoizedStuffRef.current;
const clipPath = editor.getShapeClipPath(id) ?? "none";
if (clipPath !== prev.clipPath) {
setStyleProperty(containerRef.current, "clip-path", clipPath);
setStyleProperty(bgContainerRef.current, "clip-path", clipPath);
prev.clipPath = clipPath;
}
const pageTransform = editor.getShapePageTransform(id);
const transform = Mat.toCssString(pageTransform);
const bounds = editor.getShapeGeometry(shape2).bounds;
if (transform !== prev.transform) {
setStyleProperty(containerRef.current, "transform", transform);
setStyleProperty(bgContainerRef.current, "transform", transform);
prev.transform = transform;
}
const width = Math.max(bounds.width, 1);
const height = Math.max(bounds.height, 1);
if (width !== prev.width || height !== prev.height) {
setStyleProperty(containerRef.current, "width", width + "px");
setStyleProperty(containerRef.current, "height", height + "px");
setStyleProperty(bgContainerRef.current, "width", width + "px");
setStyleProperty(bgContainerRef.current, "height", height + "px");
prev.width = width;
prev.height = height;
}
},
[editor]
);
useQuickReactor(
"set opacity and z-index",
() => {
const container = containerRef.current;
const bgContainer = bgContainerRef.current;
setStyleProperty(container, "opacity", opacity);
setStyleProperty(bgContainer, "opacity", opacity);
setStyleProperty(container, "z-index", index);
setStyleProperty(bgContainer, "z-index", backgroundIndex);
},
[opacity, index, backgroundIndex]
);
useQuickReactor(
"set display",
() => {
const shape2 = editor.getShape(id);
if (!shape2) return;
const culledShapes = editor.getCulledShapes();
const isCulled = culledShapes.has(id);
if (isCulled !== memoizedStuffRef.current.isCulled) {
setStyleProperty(containerRef.current, "display", isCulled ? "none" : "block");
setStyleProperty(bgContainerRef.current, "display", isCulled ? "none" : "block");
memoizedStuffRef.current.isCulled = isCulled;
}
},
[editor]
);
const annotateError = reactExports.useCallback(
(error) => editor.annotateError(error, { origin: "shape", willCrashApp: false }),
[editor]
);
if (!shape) return null;
const isFilledShape = "fill" in shape.props && shape.props.fill !== "none";
return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
util.backgroundComponent && /* @__PURE__ */ jsxRuntimeExports.jsx(
"div",
{
ref: bgContainerRef,
className: "tl-shape tl-shape-background",
"data-shape-type": shape.type,
"data-shape-id": shape.id,
draggable: false,
children: /* @__PURE__ */ jsxRuntimeExports.jsx(OptionalErrorBoundary, { fallback: ShapeErrorFallback, onError: annotateError, children: /* @__PURE__ */ jsxRuntimeExports.jsx(InnerShapeBackground, { shape, util }) })
}
),
/* @__PURE__ */ jsxRuntimeExports.jsx(
"div",
{
ref: containerRef,
className: "tl-shape",
"data-shape-type": shape.type,
"data-shape-is-filled": isFilledShape,
"data-shape-id": shape.id,
draggable: false,
children: /* @__PURE__ */ jsxRuntimeExports.jsx(OptionalErrorBoundary, { fallback: ShapeErrorFallback, onError: annotateError, children: /* @__PURE__ */ jsxRuntimeExports.jsx(InnerShape, { shape, util }) })
}
)
] });
});
const InnerShape = reactExports.memo(
function InnerShape2({ shape, util }) {
return useStateTracking(
"InnerShape:" + shape.type,
() => (
// always fetch the latest shape from the store even if the props/meta have not changed, to avoid
// calling the render method with stale data.
(util.component(util.editor.store.unsafeGetWithoutCapture(shape.id)))
),
[util, shape.id]
);
},
(prev, next) => prev.shape.props === next.shape.props && prev.shape.meta === next.shape.meta && prev.util === next.util
);
const InnerShapeBackground = reactExports.memo(
function InnerShapeBackground2({
shape,
util
}) {
return useStateTracking(
"InnerShape:" + shape.type,
() => (
// always fetch the latest shape from the store even if the props/meta have not changed, to avoid
// calling the render method with stale data.
(util.backgroundComponent?.(util.editor.store.unsafeGetWithoutCapture(shape.id)))
),
[util, shape.id]
);
},
(prev, next) => prev.shape.props === next.shape.props && prev.shape.meta === next.shape.meta && prev.util === next.util
);
function DefaultCanvas({ className }) {
const editor = useEditor();
const { Background, SvgDefs, ShapeIndicators } = useEditorComponents();
const rCanvas = reactExports.useRef(null);
const rHtmlLayer = reactExports.useRef(null);
const rHtmlLayer2 = reactExports.useRef(null);
const container = useContainer();
useScreenBounds(rCanvas);
useDocumentEvents();
useCoarsePointer();
useGestureEvents(rCanvas);
useFixSafariDoubleTapZoomPencilEvents(rCanvas);
const rMemoizedStuff = reactExports.useRef({ lodDisableTextOutline: false, allowTextOutline: true });
useQuickReactor(
"position layers",
function positionLayersWhenCameraMoves() {
const { x, y, z } = editor.getCamera();
if (rMemoizedStuff.current.allowTextOutline && tlenv.isSafari) {
container.style.setProperty("--tl-text-outline", "none");
rMemoizedStuff.current.allowTextOutline = false;
}
if (rMemoizedStuff.current.allowTextOutline && z < editor.options.textShadowLod !== rMemoizedStuff.current.lodDisableTextOutline) {
const lodDisableTextOutline = z < editor.options.textShadowLod;
container.style.setProperty(
"--tl-text-outline",
lodDisableTextOutline ? "none" : `var(--tl-text-outline-reference)`
);
rMemoizedStuff.current.lodDisableTextOutline = lodDisableTextOutline;
}
const offset = z >= 1 ? modulate(z, [1, 8], [0.125, 0.5], true) : modulate(z, [0.1, 1], [-2, 0.125], true);
const transform = `scale(${toDomPrecision(z)}) translate(${toDomPrecision(
x + offset
)}px,${toDomPrecision(y + offset)}px)`;
setStyleProperty(rHtmlLayer.current, "transform", transform);
setStyleProperty(rHtmlLayer2.current, "transform", transform);
},
[editor, container]
);
const events = useCanvasEvents();
const shapeSvgDefs = useValue(
"shapeSvgDefs",
() => {
const shapeSvgDefsByKey = /* @__PURE__ */ new Map();
for (const util of objectMapValues(editor.shapeUtils)) {
if (!util) return;
const defs = util.getCanvasSvgDefs();
for (const { key, component: Component } of defs) {
if (shapeSvgDefsByKey.has(key)) continue;
shapeSvgDefsByKey.set(key, /* @__PURE__ */ jsxRuntimeExports.jsx(Component, {}, key));
}
}
return [...shapeSvgDefsByKey.values()];
},
[editor]
);
const hideShapes = useValue("debug_shapes", () => debugFlags.hideShapes.get(), [debugFlags]);
const debugSvg = useValue("debug_svg", () => debugFlags.debugSvg.get(), [debugFlags]);
const debugGeometry = useValue("debug_geometry", () => debugFlags.debugGeometry.get(), [
debugFlags
]);
const isEditingAnything = useValue(
"isEditingAnything",
() => editor.getEditingShapeId() !== null,
[editor]
);
const isSelectingAnything = useValue(
"isSelectingAnything",
() => !!editor.getSelectedShapeIds().length,
[editor]
);
return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs(
"div",
{
ref: rCanvas,
draggable: false,
"data-iseditinganything": isEditingAnything,
"data-isselectinganything": isSelectingAnything,
className: classNames("tl-canvas", className),
"data-testid": "canvas",
...events,
children: [
/* @__PURE__ */ jsxRuntimeExports.jsx("svg", { className: "tl-svg-context", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("defs", { children: [
shapeSvgDefs,
/* @__PURE__ */ jsxRuntimeExports.jsx(CursorDef, {}),
/* @__PURE__ */ jsxRuntimeExports.jsx(CollaboratorHintDef, {}),
SvgDefs && /* @__PURE__ */ jsxRuntimeExports.jsx(SvgDefs, {})
] }) }),
Background && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "tl-background__wrapper", children: /* @__PURE__ */ jsxRuntimeExports.jsx(Background, {}) }),
/* @__PURE__ */ jsxRuntimeExports.jsx(GridWrapper, {}),
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { ref: rHtmlLayer, className: "tl-html-layer tl-shapes", draggable: false, children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(OnTheCanvasWrapper, {}),
/* @__PURE__ */ jsxRuntimeExports.jsx(SelectionBackgroundWrapper, {}),
hideShapes ? null : debugSvg ? /* @__PURE__ */ jsxRuntimeExports.jsx(ShapesWithSVGs, {}) : /* @__PURE__ */ jsxRuntimeExports.jsx(ShapesToDisplay, {})
] }),
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "tl-overlays", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { ref: rHtmlLayer2, className: "tl-html-layer", children: [
debugGeometry ? /* @__PURE__ */ jsxRuntimeExports.jsx(GeometryDebuggingView, {}) : null,
/* @__PURE__ */ jsxRuntimeExports.jsx(HandlesWrapper, {}),
/* @__PURE__ */ jsxRuntimeExports.jsx(BrushWrapper, {}),
/* @__PURE__ */ jsxRuntimeExports.jsx(ScribbleWrapper, {}),
/* @__PURE__ */ jsxRuntimeExports.jsx(ZoomBrushWrapper, {}),
ShapeIndicators && /* @__PURE__ */ jsxRuntimeExports.jsx(ShapeIndicators, {}),
/* @__PURE__ */ jsxRuntimeExports.jsx(HintedShapeIndicator, {}),
/* @__PURE__ */ jsxRuntimeExports.jsx(SnapIndicatorWrapper, {}),
/* @__PURE__ */ jsxRuntimeExports.jsx(SelectionForegroundWrapper, {}),
/* @__PURE__ */ jsxRuntimeExports.jsx(LiveCollaborators, {})
] }) }),
/* @__PURE__ */ jsxRuntimeExports.jsx(MovingCameraHitTestBlocker, {})
]
}
),
/* @__PURE__ */ jsxRuntimeExports.jsx(MenuClickCapture, {}),
/* @__PURE__ */ jsxRuntimeExports.jsx(InFrontOfTheCanvasWrapper, {})
] });
}
function InFrontOfTheCanvasWrapper() {
const { InFrontOfTheCanvas } = useEditorComponents();
if (!InFrontOfTheCanvas) return null;
return /* @__PURE__ */ jsxRuntimeExports.jsx(InFrontOfTheCanvas, {});
}
function GridWrapper() {
const editor = useEditor();
const gridSize = useValue("gridSize", () => editor.getDocumentSettings().gridSize, [editor]);
const { x, y, z } = useValue("camera", () => editor.getCamera(), [editor]);
const isGridMode = useValue("isGridMode", () => editor.getInstanceState().isGridMode, [editor]);
const { Grid } = useEditorComponents();
if (!(Grid && isGridMode)) return null;
return /* @__PURE__ */ jsxRuntimeExports.jsx(Grid, { x, y, z, size: gridSize });
}
function ScribbleWrapper() {
const editor = useEditor();
const scribbles = useValue("scribbles", () => editor.getInstanceState().scribbles, [editor]);
const zoomLevel = useValue("zoomLevel", () => editor.getZoomLevel(), [editor]);
const { Scribble } = useEditorComponents();
if (!(Scribble && scribbles.length)) return null;
return scribbles.map((scribble) => /* @__PURE__ */ jsxRuntimeExports.jsx(Scribble, { className: "tl-user-scribble", scribble, zoom: zoomLevel }, scribble.id));
}
function BrushWrapper() {
const editor = useEditor();
const brush = useValue("brush", () => editor.getInstanceState().brush, [editor]);
const { Brush } = useEditorComponents();
if (!(Brush && brush)) return null;
return /* @__PURE__ */ jsxRuntimeExports.jsx(Brush, { className: "tl-user-brush", brush });
}
function ZoomBrushWrapper() {
const editor = useEditor();
const zoomBrush = useValue("zoomBrush", () => editor.getInstanceState().zoomBrush, [editor]);
const { ZoomBrush } = useEditorComponents();
if (!(ZoomBrush && zoomBrush)) return null;
return /* @__PURE__ */ jsxRuntimeExports.jsx(ZoomBrush, { className: "tl-user-brush tl-zoom-brush", brush: zoomBrush });
}
function SnapIndicatorWrapper() {
const editor = useEditor();
const lines = useValue("snapLines", () => editor.snaps.getIndicators(), [editor]);
const zoomLevel = useValue("zoomLevel", () => editor.getZoomLevel(), [editor]);
const { SnapIndicator } = useEditorComponents();
if (!(SnapIndicator && lines.length > 0)) return null;
return lines.map((line) => /* @__PURE__ */ jsxRuntimeExports.jsx(SnapIndicator, { className: "tl-user-snapline", line, zoom: zoomLevel }, line.id));
}
function HandlesWrapper() {
const editor = useEditor();
const shapeIdWithHandles = useValue(
"handles shapeIdWithHandles",
() => {
const { isReadonly, isChangingStyle } = editor.getInstanceState();
if (isReadonly || isChangingStyle) return false;
const onlySelectedShape = editor.getOnlySelectedShape();
if (!onlySelectedShape) return false;
const handles = editor.getShapeHandles(onlySelectedShape);
if (!handles) return false;
return onlySelectedShape.id;
},
[editor]
);
if (!shapeIdWithHandles) return null;
return /* @__PURE__ */ jsxRuntimeExports.jsx(HandlesWrapperInner, { shapeId: shapeIdWithHandles });
}
function HandlesWrapperInner({ shapeId }) {
const editor = useEditor();
const { Handles } = useEditorComponents();
const zoomLevel = useValue("zoomLevel", () => editor.getZoomLevel(), [editor]);
const isCoarse = useValue("coarse pointer", () => editor.getInstanceState().isCoarsePointer, [
editor
]);
const transform = useValue("handles transform", () => editor.getShapePageTransform(shapeId), [
editor,
shapeId
]);
const handles = useValue(
"handles",
() => {
const handles2 = editor.getShapeHandles(shapeId);
if (!handles2) return null;
const minDistBetweenVirtualHandlesAndRegularHandles = (isCoarse ? editor.options.coarseHandleRadius : editor.options.handleRadius) / zoomLevel * 2;
return handles2.filter(
(handle) => (
// if the handle isn't a virtual handle, we'll display it
(// but for virtual handles, we'll only display them if they're far enough away from vertex handles
handle.type !== "virtual" || !handles2.some(
(h) => (
// skip the handle we're checking against
(// and check that their distance isn't below the minimum distance
h !== handle && // only check against vertex handles
h.type === "vertex" && Vec.Dist(handle, h) < minDistBetweenVirtualHandlesAndRegularHandles)
)
))
)
).sort((a) => a.type === "vertex" ? 1 : -1);
},
[editor, zoomLevel, isCoarse, shapeId]
);
const isHidden = useValue("isHidden", () => editor.isShapeHidden(shapeId), [editor, shapeId]);
if (!Handles || !handles || !transform || isHidden) {
return null;
}
return /* @__PURE__ */ jsxRuntimeExports.jsx(Handles, { children: /* @__PURE__ */ jsxRuntimeExports.jsx("g", { transform: Mat.toCssString(transform), children: handles.map((handle) => {
return /* @__PURE__ */ jsxRuntimeExports.jsx(
HandleWrapper,
{
shapeId,
handle,
zoom: zoomLevel,
isCoarse
},
handle.id
);
}) }) });
}
function HandleWrapper({
shapeId,
handle,
zoom,
isCoarse
}) {
const events = useHandleEvents(shapeId, handle.id);
const { Handle } = useEditorComponents();
if (!Handle) return null;
return /* @__PURE__ */ jsxRuntimeExports.jsx("g", { "aria-label": "handle", transform: `translate(${handle.x}, ${handle.y})`, ...events, children: /* @__PURE__ */ jsxRuntimeExports.jsx(Handle, { shapeId, handle, zoom, isCoarse }) });
}
function ShapesWithSVGs() {
const editor = useEditor();
const renderingShapes = useValue("rendering shapes", () => editor.getRenderingShapes(), [editor]);
return renderingShapes.map((result) => /* @__PURE__ */ jsxRuntimeExports.jsxs(reactExports.Fragment, { children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(Shape, { ...result }),
/* @__PURE__ */ jsxRuntimeExports.jsx(DebugSvgCopy, { id: result.id, mode: "iframe" })
] }, result.id + "_fragment"));
}
function ReflowIfNeeded() {
const editor = useEditor();
const culledShapesRef = reactExports.useRef(/* @__PURE__ */ new Set());
useQuickReactor(
"reflow for culled shapes",
() => {
const culledShapes = editor.getCulledShapes();
if (culledShapesRef.current.size === culledShapes.size && [...culledShapes].every((id) => culledShapesRef.current.has(id)))
return;
culledShapesRef.current = culledShapes;
const canvas = document.getElementsByClassName("tl-canvas");
if (canvas.length === 0) return;
canvas[0].offsetHeight;
},
[editor]
);
return null;
}
function ShapesToDisplay() {
const editor = useEditor();
const renderingShapes = useValue("rendering shapes", () => editor.getRenderingShapes(), [editor]);
return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
renderingShapes.map((result) => /* @__PURE__ */ jsxRuntimeExports.jsx(Shape, { ...result }, result.id + "_shape")),
tlenv.isSafari && /* @__PURE__ */ jsxRuntimeExports.jsx(ReflowIfNeeded, {})
] });
}
function HintedShapeIndicator() {
const editor = useEditor();
const { ShapeIndicator } = useEditorComponents();
const ids = useValue("hinting shape ids", () => dedupe(editor.getHintingShapeIds()), [editor]);
if (!ids.length) return null;
if (!ShapeIndicator) return null;
return ids.map((id) => /* @__PURE__ */ jsxRuntimeExports.jsx(ShapeIndicator, { className: "tl-user-indicator__hint", shapeId: id }, id + "_hinting"));
}
function CursorDef() {
return /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { id: useSharedSafeId("cursor"), children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs("g", { fill: "rgba(0,0,0,.2)", transform: "translate(-11,-11)", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "m12 24.4219v-16.015l11.591 11.619h-6.781l-.411.124z" }),
/* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "m21.0845 25.0962-3.605 1.535-4.682-11.089 3.686-1.553z" })
] }),
/* @__PURE__ */ jsxRuntimeExports.jsxs("g", { fill: "white", transform: "translate(-12,-12)", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "m12 24.4219v-16.015l11.591 11.619h-6.781l-.411.124z" }),
/* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "m21.0845 25.0962-3.605 1.535-4.682-11.089 3.686-1.553z" })
] }),
/* @__PURE__ */ jsxRuntimeExports.jsxs("g", { fill: "currentColor", transform: "translate(-12,-12)", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "m19.751 24.4155-1.844.774-3.1-7.374 1.841-.775z" }),
/* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "m13 10.814v11.188l2.969-2.866.428-.139h4.768z" })
] })
] });
}
function CollaboratorHintDef() {
const cursorHintId = useSharedSafeId("cursor_hint");
return /* @__PURE__ */ jsxRuntimeExports.jsx("path", { id: cursorHintId, fill: "currentColor", d: "M -2,-5 2,0 -2,5 Z" });
}
function DebugSvgCopy({ id, mode }) {
const editor = useEditor();
const [image, setImage] = reactExports.useState(null);
const isInRoot = useValue(
"is in root",
() => {
const shape = editor.getShape(id);
return shape?.parentId === editor.getCurrentPageId();
},
[editor, id]
);
reactExports.useEffect(() => {
if (!isInRoot) return;
let latest = null;
const unsubscribe = react("shape to svg", async () => {
const renderId = Math.random();
latest = renderId;
const isSingleFrame = editor.isShapeOfType(id, "frame");
const padding = isSingleFrame ? 0 : 10;
let bounds = editor.getShapePageBounds(id);
if (!bounds) return;
bounds = bounds.clone().expandBy(padding);
const result = await editor.getSvgString([id], {
padding,
background: editor.getInstanceState().exportBackground
});
if (latest !== renderId || !result) return;
const svgDataUrl = `data:image/svg+xml;utf8,${encodeURIComponent(result.svg)}`;
setImage({ src: svgDataUrl, bounds });
});
return () => {
latest = null;
unsubscribe();
};
}, [editor, id, isInRoot]);
if (!isInRoot || !image) return null;
if (mode === "iframe") {
return /* @__PURE__ */ jsxRuntimeExports.jsx(
"iframe",
{
src: image.src,
width: image.bounds.width,
height: image.bounds.height,
referrerPolicy: "no-referrer",
style: {
position: "absolute",
top: 0,
left: 0,
border: "none",
transform: `translate(${image.bounds.x}px, ${image.bounds.maxY + 12}px)`,
outline: "1px solid black",
maxWidth: "none"
}
}
);
}
return /* @__PURE__ */ jsxRuntimeExports.jsx(
"img",
{
src: image.src,
width: image.bounds.width,
height: image.bounds.height,
referrerPolicy: "no-referrer",
style: {
position: "absolute",
top: 0,
left: 0,
transform: `translate(${image.bounds.x}px, ${image.bounds.maxY + 12}px)`,
outline: "1px solid black",
maxWidth: "none"
}
}
);
}
function SelectionForegroundWrapper() {
const editor = useEditor();
const selectionRotation = useValue("selection rotation", () => editor.getSelectionRotation(), [
editor
]);
const selectionBounds = useValue(
"selection bounds",
() => editor.getSelectionRotatedPageBounds(),
[editor]
);
const { SelectionForeground } = useEditorComponents();
if (!selectionBounds || !SelectionForeground) return null;
return /* @__PURE__ */ jsxRuntimeExports.jsx(SelectionForeground, { bounds: selectionBounds, rotation: selectionRotation });
}
function SelectionBackgroundWrapper() {
const editor = useEditor();
const selectionRotation = useValue("selection rotation", () => editor.getSelectionRotation(), [
editor
]);
const selectionBounds = useValue(
"selection bounds",
() => editor.getSelectionRotatedPageBounds(),
[editor]
);
const { SelectionBackground } = useEditorComponents();
if (!selectionBounds || !SelectionBackground) return null;
return /* @__PURE__ */ jsxRuntimeExports.jsx(SelectionBackground, { bounds: selectionBounds, rotation: selectionRotation });
}
function OnTheCanvasWrapper() {
const { OnTheCanvas } = useEditorComponents();
if (!OnTheCanvas) return null;
return /* @__PURE__ */ jsxRuntimeExports.jsx(OnTheCanvas, {});
}
function MovingCameraHitTestBlocker() {
const editor = useEditor();
const cameraState = useValue("camera state", () => editor.getCameraState(), [editor]);
return /* @__PURE__ */ jsxRuntimeExports.jsx(
"div",
{
className: classNames("tl-hit-test-blocker", {
"tl-hit-test-blocker__hidden": cameraState === "idle"
})
}
);
}
function DefaultCollaboratorHint({
className,
zoom,
point,
color,
viewport,
opacity = 1
}) {
const rSvg = reactExports.useRef(null);
useTransform(
rSvg,
clamp$2(point.x, viewport.minX + 5 / zoom, viewport.maxX - 5 / zoom),
clamp$2(point.y, viewport.minY + 5 / zoom, viewport.maxY - 5 / zoom),
1 / zoom,
Vec.Angle(viewport.center, point)
);
const cursorHintId = useSharedSafeId("cursor_hint");
return /* @__PURE__ */ jsxRuntimeExports.jsxs("svg", { ref: rSvg, className: classNames("tl-overlays__item", className), children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(
"use",
{
href: `#${cursorHintId}`,
color,
strokeWidth: 3,
stroke: "var(--color-background)"
}
),
/* @__PURE__ */ jsxRuntimeExports.jsx("use", { href: `#${cursorHintId}`, color, opacity })
] });
}
const DefaultCursor = reactExports.memo(function DefaultCursor2({
className,
zoom,
point,
color,
name,
chatMessage
}) {
const rCursor = reactExports.useRef(null);
useTransform(rCursor, point?.x, point?.y, 1 / zoom);
const cursorId = useSharedSafeId("cursor");
if (!point) return null;
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { ref: rCursor, className: classNames("tl-overlays__item", className), children: [
/* @__PURE__ */ jsxRuntimeExports.jsx("svg", { className: "tl-cursor", children: /* @__PURE__ */ jsxRuntimeExports.jsx("use", { href: `#${cursorId}`, color }) }),
chatMessage ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
name && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "tl-nametag-title", style: { color }, children: name }),
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "tl-nametag-chat", style: { backgroundColor: color }, children: chatMessage })
] }) : name && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "tl-nametag", style: { backgroundColor: color }, children: name })
] });
});
function DefaultGrid({ x, y, z, size }) {
const id = useUniqueSafeId("grid");
const editor = useEditor();
const { gridSteps } = editor.options;
return /* @__PURE__ */ jsxRuntimeExports.jsxs("svg", { className: "tl-grid", version: "1.1", xmlns: "http://www.w3.org/2000/svg", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx("defs", { children: gridSteps.map(({ min, mid, step }, i) => {
const s = step * size * z;
const xo = 0.5 + x * z;
const yo = 0.5 + y * z;
const gxo = xo > 0 ? xo % s : s + xo % s;
const gyo = yo > 0 ? yo % s : s + yo % s;
const opacity = z < mid ? modulate(z, [min, mid], [0, 1]) : 1;
return /* @__PURE__ */ jsxRuntimeExports.jsx(
"pattern",
{
id: suffixSafeId(id, `${step}`),
width: s,
height: s,
patternUnits: "userSpaceOnUse",
children: /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { className: "tl-grid-dot", cx: gxo, cy: gyo, r: 1, opacity })
},
i
);
}) }),
gridSteps.map(({ step }, i) => /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { width: "100%", height: "100%", fill: `url(#${id}_${step})` }, i))
] });
}
function DefaultHandle({ handle, isCoarse, className, zoom }) {
const editor = useEditor();
const br = (isCoarse ? editor.options.coarseHandleRadius : editor.options.handleRadius) / zoom;
if (handle.type === "clone") {
const fr2 = 3 / zoom;
const path = `M0,${-fr2} A${fr2},${fr2} 0 0,1 0,${fr2}`;
const index = SIDES.indexOf(handle.id);
return /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { className: classNames(`tl-handle tl-handle__${handle.type}`, className), children: [
/* @__PURE__ */ jsxRuntimeExports.jsx("circle", { className: "tl-handle__bg", r: br }),
/* @__PURE__ */ jsxRuntimeExports.jsx("path", { className: "tl-handle__fg", d: path, transform: `rotate(${ -90 + 90 * index})` })
] });
}
const fr = (handle.type === "create" && isCoarse ? 3 : 4) / Math.max(zoom, 0.25);
return /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { className: classNames(`tl-handle tl-handle__${handle.type}`, className), children: [
/* @__PURE__ */ jsxRuntimeExports.jsx("circle", { className: "tl-handle__bg", r: br }),
/* @__PURE__ */ jsxRuntimeExports.jsx("circle", { className: "tl-handle__fg", r: fr })
] });
}
const DefaultHandles = ({ children }) => {
return /* @__PURE__ */ jsxRuntimeExports.jsx("svg", { className: "tl-user-handles tl-overlays__item", children });
};
const DefaultLoadingScreen = () => {
const { Spinner } = useEditorComponents();
return /* @__PURE__ */ jsxRuntimeExports.jsx(LoadingScreen, { children: Spinner ? /* @__PURE__ */ jsxRuntimeExports.jsx(Spinner, {}) : null });
};
function getSvgPathFromPoints(points, closed = true) {
const len = points.length;
if (len < 2) {
return "";
}
let a = points[0];
let b = points[1];
if (len === 2) {
return `M${precise(a)}L${precise(b)}`;
}
let result = "";
for (let i = 2, max = len - 1; i < max; i++) {
a = points[i];
b = points[i + 1];
result += average(a, b);
}
if (closed) {
return `M${average(points[0], points[1])}Q${precise(points[1])}${average(
points[1],
points[2]
)}T${result}${average(points[len - 1], points[0])}${average(points[0], points[1])}Z`;
} else {
return `M${precise(points[0])}Q${precise(points[1])}${average(points[1], points[2])}${points.length > 3 ? "T" : ""}${result}L${precise(points[len - 1])}`;
}
}
function DefaultScribble({ scribble, zoom, color, opacity, className }) {
if (!scribble.points.length) return null;
return /* @__PURE__ */ jsxRuntimeExports.jsx("svg", { className: className ? classNames("tl-overlays__item", className) : className, children: /* @__PURE__ */ jsxRuntimeExports.jsx(
"path",
{
className: "tl-scribble",
d: getSvgPathFromPoints(scribble.points, false),
stroke: color ?? `var(--color-${scribble.color})`,
fill: "none",
strokeWidth: 8 / zoom,
opacity: opacity ?? scribble.opacity
}
) });
}
function DefaultSelectionBackground({ bounds, rotation }) {
const rDiv = reactExports.useRef(null);
useTransform(rDiv, bounds.x, bounds.y, 1, rotation);
reactExports.useLayoutEffect(() => {
const div = rDiv.current;
if (!div) return;
div.style.width = toDomPrecision(Math.max(1, bounds.width)) + "px";
div.style.height = toDomPrecision(Math.max(1, bounds.height)) + "px";
}, [bounds.width, bounds.height]);
return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { ref: rDiv, className: "tl-selection__bg", draggable: false });
}
function DefaultSelectionForeground({ bounds, rotation }) {
const editor = useEditor();
const rSvg = reactExports.useRef(null);
const onlyShape = useValue("only selected shape", () => editor.getOnlySelectedShape(), [editor]);
const expandOutlineBy = onlyShape ? editor.getShapeUtil(onlyShape).expandSelectionOutlinePx(onlyShape) : 0;
useTransform(rSvg, bounds?.x, bounds?.y, 1, rotation, {
x: -expandOutlineBy,
y: -expandOutlineBy
});
bounds = bounds.clone().expandBy(expandOutlineBy).zeroFix();
return /* @__PURE__ */ jsxRuntimeExports.jsx(
"svg",
{
ref: rSvg,
className: "tl-overlays__item tl-selection__fg",
"data-testid": "selection-foreground",
children: /* @__PURE__ */ jsxRuntimeExports.jsx(
"rect",
{
className: classNames("tl-selection__fg__outline"),
width: toDomPrecision(bounds.width),
height: toDomPrecision(bounds.height)
}
)
}
);
}
const DefaultShapeErrorFallback = () => {
return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "tl-shape-error-boundary" });
};
const EvenInnererIndicator = ({ shape, util }) => {
return useStateTracking(
"Indicator: " + shape.type,
() => (
// always fetch the latest shape from the store even if the props/meta have not changed, to avoid
// calling the render method with stale data.
(util.indicator(util.editor.store.unsafeGetWithoutCapture(shape.id)))
)
);
};
const InnerIndicator = ({ editor, id }) => {
const shape = useValue("shape for indicator", () => editor.store.get(id), [editor, id]);
const { ShapeIndicatorErrorFallback } = useEditorComponents();
if (!shape || shape.isLocked) return null;
return /* @__PURE__ */ jsxRuntimeExports.jsx(
OptionalErrorBoundary,
{
fallback: ShapeIndicatorErrorFallback,
onError: (error) => editor.annotateError(error, { origin: "react.shapeIndicator", willCrashApp: false }),
children: /* @__PURE__ */ jsxRuntimeExports.jsx(EvenInnererIndicator, { shape, util: editor.getShapeUtil(shape) }, shape.id)
}
);
};
const DefaultShapeIndicator = reactExports.memo(function DefaultShapeIndicator2({
shapeId,
className,
color,
hidden,
opacity
}) {
const editor = useEditor();
const rIndicator = reactExports.useRef(null);
useQuickReactor(
"indicator transform",
() => {
const elm = rIndicator.current;
if (!elm) return;
const pageTransform = editor.getShapePageTransform(shapeId);
if (!pageTransform) return;
elm.style.setProperty("transform", pageTransform.toCssString());
},
[editor, shapeId]
);
reactExports.useLayoutEffect(() => {
const elm = rIndicator.current;
if (!elm) return;
elm.style.setProperty("display", hidden ? "none" : "block");
}, [hidden]);
return /* @__PURE__ */ jsxRuntimeExports.jsx("svg", { ref: rIndicator, className: classNames("tl-overlays__item", className), children: /* @__PURE__ */ jsxRuntimeExports.jsx("g", { className: "tl-shape-indicator", stroke: color ?? "var(--color-selected)", opacity, children: /* @__PURE__ */ jsxRuntimeExports.jsx(InnerIndicator, { editor, id: shapeId }) }) });
});
const DefaultShapeIndicatorErrorFallback = () => {
return /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: 4, cy: 4, r: 8, strokeWidth: "1", stroke: "red" });
};
const DefaultShapeIndicators = reactExports.memo(function DefaultShapeIndicators2() {
const editor = useEditor();
const rPreviousSelectedShapeIds = reactExports.useRef(/* @__PURE__ */ new Set());
const idsToDisplay = useValue(
"should display selected ids",
() => {
const prev = rPreviousSelectedShapeIds.current;
const next = /* @__PURE__ */ new Set();
if (
// We only show indicators when in the following states...
editor.isInAny(
"select.idle",
"select.brushing",
"select.scribble_brushing",
"select.editing_shape",
"select.pointing_shape",
"select.pointing_selection",
"select.pointing_handle"
) && // ...but we hide indicators when we've just changed a style (so that the user can see the change)
!editor.getInstanceState().isChangingStyle
) {
const selected = editor.getSelectedShapeIds();
for (const id of selected) {
next.add(id);
}
if (editor.isInAny("select.idle", "select.editing_shape")) {
const instanceState = editor.getInstanceState();
if (instanceState.isHoveringCanvas && !instanceState.isCoarsePointer) {
const hovered = editor.getHoveredShapeId();
if (hovered) next.add(hovered);
}
}
}
if (prev.size !== next.size) {
rPreviousSelectedShapeIds.current = next;
return next;
}
for (const id of next) {
if (!prev.has(id)) {
rPreviousSelectedShapeIds.current = next;
return next;
}
}
return prev;
},
[editor]
);
const renderingShapes = useValue("rendering shapes", () => editor.getRenderingShapes(), [editor]);
const { ShapeIndicator } = useEditorComponents();
if (!ShapeIndicator) return null;
return renderingShapes.map(({ id }) => /* @__PURE__ */ jsxRuntimeExports.jsx(ShapeIndicator, { shapeId: id, hidden: !idsToDisplay.has(id) }, id + "_indicator"));
});
function PointsSnapIndicator({ points, zoom }) {
const l = 2.5 / zoom;
const minX = points.reduce((acc, p) => Math.min(acc, p.x), Infinity);
const maxX = points.reduce((acc, p) => Math.max(acc, p.x), -Infinity);
const minY = points.reduce((acc, p) => Math.min(acc, p.y), Infinity);
const maxY = points.reduce((acc, p) => Math.max(acc, p.y), -Infinity);
const useNWtoSEdireciton = points.some((p) => p.x === minX && p.y === minY);
let firstX, firstY, secondX, secondY;
if (useNWtoSEdireciton) {
firstX = minX;
firstY = minY;
secondX = maxX;
secondY = maxY;
} else {
firstX = minX;
firstY = maxY;
secondX = maxX;
secondY = minY;
}
return /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { className: "tl-snap-indicator", stroke: "lime", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: firstX, y1: firstY, x2: secondX, y2: secondY }),
points.map((p, i) => /* @__PURE__ */ jsxRuntimeExports.jsx("g", { transform: `translate(${p.x},${p.y})`, children: /* @__PURE__ */ jsxRuntimeExports.jsx(
"path",
{
className: "tl-snap-point",
d: `M ${-l},${-l} L ${l},${l} M ${-l},${l} L ${l},${-l}`
}
) }, i))
] });
}
function GapsSnapIndicator({ gaps, direction, zoom }) {
const l = 3.5 / zoom;
let edgeIntersection = [-Infinity, Infinity];
let nextEdgeIntersection = null;
const horizontal = direction === "horizontal";
for (const gap of gaps) {
nextEdgeIntersection = rangeIntersection(
edgeIntersection[0],
edgeIntersection[1],
horizontal ? gap.startEdge[0].y : gap.startEdge[0].x,
horizontal ? gap.startEdge[1].y : gap.startEdge[1].x
);
if (nextEdgeIntersection) {
edgeIntersection = nextEdgeIntersection;
} else {
continue;
}
nextEdgeIntersection = rangeIntersection(
edgeIntersection[0],
edgeIntersection[1],
horizontal ? gap.endEdge[0].y : gap.endEdge[0].x,
horizontal ? gap.endEdge[1].y : gap.endEdge[1].x
);
if (nextEdgeIntersection) {
edgeIntersection = nextEdgeIntersection;
} else {
continue;
}
}
if (edgeIntersection === null) {
return null;
}
const midPoint = (edgeIntersection[0] + edgeIntersection[1]) / 2;
return (
/* @__PURE__ */ jsxRuntimeExports.jsx("g", { className: "tl-snap-indicator", stroke: "cyan", children: gaps.map(({ startEdge, endEdge }, i) => /* @__PURE__ */ jsxRuntimeExports.jsx(reactExports.Fragment, { children: horizontal ? (
// horizontal gap
/* @__PURE__ */ (jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(
"line",
{
x1: startEdge[0].x,
y1: midPoint - 2 * l,
x2: startEdge[1].x,
y2: midPoint + 2 * l
}
),
/* @__PURE__ */ jsxRuntimeExports.jsx(
"line",
{
x1: endEdge[0].x,
y1: midPoint - 2 * l,
x2: endEdge[1].x,
y2: midPoint + 2 * l
}
),
/* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: startEdge[0].x, y1: midPoint, x2: endEdge[0].x, y2: midPoint }),
/* @__PURE__ */ jsxRuntimeExports.jsx(
"line",
{
x1: (startEdge[0].x + endEdge[0].x) / 2,
y1: midPoint - l,
x2: (startEdge[0].x + endEdge[0].x) / 2,
y2: midPoint + l
}
)
] }))
) : (
// vertical gap
/* @__PURE__ */ (jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(
"line",
{
x1: midPoint - 2 * l,
y1: startEdge[0].y,
x2: midPoint + 2 * l,
y2: startEdge[1].y
}
),
/* @__PURE__ */ jsxRuntimeExports.jsx(
"line",
{
x1: midPoint - 2 * l,
y1: endEdge[0].y,
x2: midPoint + 2 * l,
y2: endEdge[1].y
}
),
/* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: midPoint, y1: startEdge[0].y, x2: midPoint, y2: endEdge[0].y }),
/* @__PURE__ */ jsxRuntimeExports.jsx(
"line",
{
x1: midPoint - l,
y1: (startEdge[0].y + endEdge[0].y) / 2,
x2: midPoint + l,
y2: (startEdge[0].y + endEdge[0].y) / 2
}
)
] }))
) }, i)) })
);
}
function DefaultSnapIndicator({ className, line, zoom }) {
return /* @__PURE__ */ jsxRuntimeExports.jsx("svg", { className: classNames("tl-overlays__item", className), children: line.type === "points" ? /* @__PURE__ */ jsxRuntimeExports.jsx(PointsSnapIndicator, { ...line, zoom }) : line.type === "gaps" ? /* @__PURE__ */ jsxRuntimeExports.jsx(GapsSnapIndicator, { ...line, zoom }) : null });
}
function DefaultSpinner() {
return /* @__PURE__ */ jsxRuntimeExports.jsx("svg", { width: 16, height: 16, viewBox: "0 0 16 16", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { strokeWidth: 2, fill: "none", fillRule: "evenodd", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx("circle", { strokeOpacity: 0.25, cx: 8, cy: 8, r: 7, stroke: "currentColor" }),
/* @__PURE__ */ jsxRuntimeExports.jsx("path", { strokeLinecap: "round", d: "M15 8c0-4.5-4.5-7-7-7", stroke: "currentColor", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
"animateTransform",
{
attributeName: "transform",
type: "rotate",
from: "0 8 8",
to: "360 8 8",
dur: "1s",
repeatCount: "indefinite"
}
) })
] }) });
}
const DefaultSvgDefs = () => {
return null;
};
function useIdentity(value, isEqual) {
const ref = reactExports.useRef(value);
if (isEqual(value, ref.current)) {
return ref.current;
}
ref.current = value;
return value;
}
const areNullableArraysShallowEqual = (a, b) => {
a ??= null;
b ??= null;
if (a === b) {
return true;
}
if (!a || !b) {
return false;
}
return areArraysShallowEqual(a, b);
};
function useShallowArrayIdentity(arr) {
return useIdentity(arr, areNullableArraysShallowEqual);
}
const areNullableObjectsShallowEqual = (a, b) => {
a ??= null;
b ??= null;
if (a === b) {
return true;
}
if (!a || !b) {
return false;
}
return areObjectsShallowEqual(a, b);
};
function useShallowObjectIdentity(obj) {
return useIdentity(obj, areNullableObjectsShallowEqual);
}
const EditorComponentsContext = reactExports.createContext(null);
function EditorComponentsProvider({
overrides = {},
children
}) {
const _overrides = useShallowObjectIdentity(overrides);
const value = reactExports.useMemo(
() => ({
Background: DefaultBackground,
SvgDefs: DefaultSvgDefs,
Brush: DefaultBrush,
ZoomBrush: DefaultBrush,
CollaboratorBrush: DefaultBrush,
Cursor: DefaultCursor,
CollaboratorCursor: DefaultCursor,
CollaboratorHint: DefaultCollaboratorHint,
CollaboratorShapeIndicator: DefaultShapeIndicator,
Grid: DefaultGrid,
Scribble: DefaultScribble,
SnapIndicator: DefaultSnapIndicator,
Handles: DefaultHandles,
Handle: DefaultHandle,
CollaboratorScribble: DefaultScribble,
ErrorFallback: DefaultErrorFallback,
ShapeErrorFallback: DefaultShapeErrorFallback,
ShapeIndicatorErrorFallback: DefaultShapeIndicatorErrorFallback,
Spinner: DefaultSpinner,
SelectionBackground: DefaultSelectionBackground,
SelectionForeground: DefaultSelectionForeground,
ShapeIndicators: DefaultShapeIndicators,
ShapeIndicator: DefaultShapeIndicator,
OnTheCanvas: null,
InFrontOfTheCanvas: null,
Canvas: DefaultCanvas,
LoadingScreen: DefaultLoadingScreen,
..._overrides
}),
[_overrides]
);
return /* @__PURE__ */ jsxRuntimeExports.jsx(EditorComponentsContext.Provider, { value, children });
}
function useEditorComponents() {
const components = reactExports.useContext(EditorComponentsContext);
if (!components) {
throw new Error("useEditorComponents must be used inside of ");
}
return components;
}
const runtime = {
openWindow(url, target) {
window.open(url, target, "noopener noreferrer");
},
refreshPage() {
window.location.reload();
},
async hardReset() {
return await window.__tldraw__hardReset?.();
}
};
function hardResetEditor() {
runtime.hardReset();
}
function refreshPage() {
runtime.refreshPage();
}
const BASE_ERROR_URL = "https://github.com/tldraw/tldraw/issues/new";
const DefaultErrorFallback = ({ error, editor }) => {
const containerRef = reactExports.useRef(null);
const [shouldShowError, setShouldShowError] = reactExports.useState(false);
const [didCopy, setDidCopy] = reactExports.useState(false);
const [shouldShowResetConfirmation, setShouldShowResetConfirmation] = reactExports.useState(false);
let Canvas = null;
try {
const components = useEditorComponents();
Canvas = components.Canvas ?? null;
} catch {
}
const errorMessage = error instanceof Error ? error.message : String(error);
const errorStack = error instanceof Error ? error.stack : null;
const isDarkModeFromApp = useValue(
"isDarkMode",
() => {
try {
if (editor) {
return editor.user.getIsDarkMode();
}
} catch {
}
return null;
},
[editor]
);
const [isDarkMode, setIsDarkMode] = reactExports.useState(null);
reactExports.useLayoutEffect(() => {
if (isDarkModeFromApp !== null) {
setIsDarkMode(isDarkModeFromApp);
}
let parent = containerRef.current?.parentElement;
let foundParentThemeClass = false;
while (parent) {
if (parent.classList.contains("tl-theme__dark") || parent.classList.contains("tl-theme__light")) {
foundParentThemeClass = true;
break;
}
parent = parent.parentElement;
}
if (foundParentThemeClass) {
setIsDarkMode(null);
return;
}
if (typeof window !== "undefined" && "matchMedia" in window) {
setIsDarkMode(window.matchMedia("(prefers-color-scheme: dark)").matches);
}
}, [isDarkModeFromApp]);
reactExports.useEffect(() => {
if (didCopy) {
const timeout = editor?.timers.setTimeout(() => {
setDidCopy(false);
}, 2e3);
return () => clearTimeout(timeout);
}
}, [didCopy, editor]);
const copyError = () => {
const textarea = document.createElement("textarea");
textarea.value = errorStack ?? errorMessage;
document.body.appendChild(textarea);
textarea.select();
document.execCommand("copy");
textarea.remove();
setDidCopy(true);
};
const refresh = () => {
refreshPage();
};
const resetLocalState = async () => {
hardResetEditor();
};
const url = new URL(BASE_ERROR_URL);
url.searchParams.set("title", errorMessage);
url.searchParams.set("labels", `bug`);
url.searchParams.set(
"body",
`Hey, I ran into an error while using tldraw:
\`\`\`js
${errorStack ?? errorMessage}
\`\`\`
My browser: ${navigator.userAgent}`
);
return /* @__PURE__ */ jsxRuntimeExports.jsxs(
"div",
{
ref: containerRef,
className: classNames(
"tl-container tl-error-boundary",
// error-boundary is sometimes used outside of the theme
// container, so we need to provide it with a theme for our
// styles to work correctly
isDarkMode === null ? "" : isDarkMode ? "tl-theme__dark" : "tl-theme__light"
),
children: [
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "tl-error-boundary__overlay" }),
editor && // opportunistically attempt to render the canvas to reassure
// the user that their document is still there. there's a good
// chance this won't work (ie the error that we're currently
// notifying the user about originates in the canvas) so it's
// not a big deal if it doesn't work - in that case we just have
// a plain grey background.
/* @__PURE__ */ jsxRuntimeExports.jsx(ErrorBoundary, { onError: noop$2, fallback: () => null, children: /* @__PURE__ */ jsxRuntimeExports.jsx(EditorProvider, { editor, children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "tl-overlay tl-error-boundary__canvas", children: Canvas ? /* @__PURE__ */ jsxRuntimeExports.jsx(Canvas, {}) : null }) }) }),
/* @__PURE__ */ jsxRuntimeExports.jsx(
"div",
{
className: classNames("tl-modal", "tl-error-boundary__content", {
"tl-error-boundary__content__expanded": shouldShowError && !shouldShowResetConfirmation
}),
children: shouldShowResetConfirmation ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
/* @__PURE__ */ jsxRuntimeExports.jsx("h2", { children: "Are you sure?" }),
/* @__PURE__ */ jsxRuntimeExports.jsx("p", { children: "Resetting your data will delete your drawing and cannot be undone." }),
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "tl-error-boundary__content__actions", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => setShouldShowResetConfirmation(false), children: "Cancel" }),
/* @__PURE__ */ jsxRuntimeExports.jsx("button", { className: "tl-error-boundary__reset", onClick: resetLocalState, children: "Reset data" })
] })
] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
/* @__PURE__ */ jsxRuntimeExports.jsx("h2", { children: "Something went wrong" }),
/* @__PURE__ */ jsxRuntimeExports.jsx("p", { children: "Please refresh the page to continue." }),
/* @__PURE__ */ jsxRuntimeExports.jsxs("p", { children: [
"If you keep seeing this screen, you can create a",
" ",
/* @__PURE__ */ jsxRuntimeExports.jsx("a", { href: url.toString(), children: "GitHub issue" }),
" or ask for help on",
" ",
/* @__PURE__ */ jsxRuntimeExports.jsx("a", { href: "https://discord.gg/Cq6cPsTfNy", children: "Discord" }),
". If you are still stuck, you can reset the tldraw data on your machine. This may erase the project you were working on, so try to get help first."
] }),
shouldShowError && /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
"Message:",
/* @__PURE__ */ jsxRuntimeExports.jsx("h4", { children: /* @__PURE__ */ jsxRuntimeExports.jsx("code", { children: errorMessage }) }),
"Stack trace:",
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "tl-error-boundary__content__error", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx("pre", { children: /* @__PURE__ */ jsxRuntimeExports.jsx("code", { children: errorStack ?? errorMessage }) }),
/* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: copyError, children: didCopy ? "Copied!" : "Copy" })
] })
] }),
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "tl-error-boundary__content__actions", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => setShouldShowError(!shouldShowError), children: shouldShowError ? "Hide details" : "Show details" }),
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "tl-error-boundary__content__actions__group", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(
"button",
{
className: "tl-error-boundary__reset",
onClick: () => setShouldShowResetConfirmation(true),
children: "Reset data"
}
),
/* @__PURE__ */ jsxRuntimeExports.jsx("button", { className: "tl-error-boundary__refresh", onClick: refresh, children: "Refresh Page" })
] })
] })
] })
}
)
]
}
);
};
const USER_DATA_KEY = "TLDRAW_USER_DATA_v3";
const userTypeValidator = object({
id: string,
name: string.nullable().optional(),
color: string.nullable().optional(),
// N.B. These are duplicated in TLdrawAppUser.
locale: string.nullable().optional(),
animationSpeed: number.nullable().optional(),
edgeScrollSpeed: number.nullable().optional(),
colorScheme: literalEnum("light", "dark", "system").optional(),
isSnapMode: boolean.nullable().optional(),
isWrapMode: boolean.nullable().optional(),
isDynamicSizeMode: boolean.nullable().optional(),
isPasteAtCursorMode: boolean.nullable().optional()
});
const Versions$1 = {
AddAnimationSpeed: 1,
AddIsSnapMode: 2,
MakeFieldsNullable: 3,
AddEdgeScrollSpeed: 4,
AddExcalidrawSelectMode: 5,
AddDynamicSizeMode: 6,
AllowSystemColorScheme: 7,
AddPasteAtCursor: 8
};
const CURRENT_VERSION = Math.max(...Object.values(Versions$1));
function migrateSnapshot(data) {
if (data.version < Versions$1.AddAnimationSpeed) {
data.user.animationSpeed = 1;
}
if (data.version < Versions$1.AddIsSnapMode) {
data.user.isSnapMode = false;
}
if (data.version < Versions$1.MakeFieldsNullable) ;
if (data.version < Versions$1.AddEdgeScrollSpeed) {
data.user.edgeScrollSpeed = 1;
}
if (data.version < Versions$1.AddExcalidrawSelectMode) {
data.user.isWrapMode = false;
}
if (data.version < Versions$1.AllowSystemColorScheme) {
if (data.user.isDarkMode === true) {
data.user.colorScheme = "dark";
} else if (data.user.isDarkMode === false) {
data.user.colorScheme = "light";
}
delete data.user.isDarkMode;
}
if (data.version < Versions$1.AddDynamicSizeMode) {
data.user.isDynamicSizeMode = false;
}
if (data.version < Versions$1.AddPasteAtCursor) {
data.user.isPasteAtCursorMode = false;
}
data.version = CURRENT_VERSION;
}
const USER_COLORS = [
"#FF802B",
"#EC5E41",
"#F2555A",
"#F04F88",
"#E34BA9",
"#BD54C6",
"#9D5BD2",
"#7B66DC",
"#02B1CC",
"#11B3A3",
"#39B178",
"#55B467"
];
function getRandomColor() {
return USER_COLORS[Math.floor(Math.random() * USER_COLORS.length)];
}
function userPrefersReducedMotion() {
if (typeof window !== "undefined" && "matchMedia" in window) {
return window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches ?? false;
}
return false;
}
const defaultUserPreferences = Object.freeze({
name: "New User",
locale: getDefaultTranslationLocale(),
color: getRandomColor(),
// N.B. These are duplicated in TLdrawAppUser.
edgeScrollSpeed: 1,
animationSpeed: userPrefersReducedMotion() ? 0 : 1,
isSnapMode: false,
isWrapMode: false,
isDynamicSizeMode: false,
isPasteAtCursorMode: false,
colorScheme: "light"
});
function getFreshUserPreferences() {
return {
id: uniqueId(),
color: getRandomColor()
};
}
function migrateUserPreferences(userData) {
if (userData === null || typeof userData !== "object") {
return getFreshUserPreferences();
}
if (!("version" in userData) || !("user" in userData) || typeof userData.version !== "number") {
return getFreshUserPreferences();
}
const snapshot = structuredClone(userData);
migrateSnapshot(snapshot);
try {
return userTypeValidator.validate(snapshot.user);
} catch {
return getFreshUserPreferences();
}
}
function loadUserPreferences() {
const userData = JSON.parse(getFromLocalStorage(USER_DATA_KEY) || "null") ?? null;
return migrateUserPreferences(userData);
}
const globalUserPreferences = atom("globalUserData", null);
function storeUserPreferences() {
setInLocalStorage(
USER_DATA_KEY,
JSON.stringify({
version: CURRENT_VERSION,
user: globalUserPreferences.get()
})
);
}
function setUserPreferences(user) {
userTypeValidator.validate(user);
globalUserPreferences.set(user);
storeUserPreferences();
broadcastUserPreferencesChange();
}
const channel = typeof BroadcastChannel !== "undefined" && true ? new BroadcastChannel("tldraw-user-sync") : null;
channel?.addEventListener("message", (e) => {
const data = e.data;
if (data?.type === broadcastEventKey && data?.origin !== getBroadcastOrigin()) {
globalUserPreferences.set(migrateUserPreferences(data.data));
}
});
let _broadcastOrigin = null;
function getBroadcastOrigin() {
if (_broadcastOrigin === null) {
_broadcastOrigin = uniqueId();
}
return _broadcastOrigin;
}
const broadcastEventKey = "tldraw-user-preferences-change";
function broadcastUserPreferencesChange() {
channel?.postMessage({
type: broadcastEventKey,
origin: getBroadcastOrigin(),
data: {
user: getUserPreferences(),
version: CURRENT_VERSION
}
});
}
function getUserPreferences() {
let prefs = globalUserPreferences.get();
if (!prefs) {
prefs = loadUserPreferences();
setUserPreferences(prefs);
}
return prefs;
}
const defaultLocalStorageUserPrefs = computed(
"defaultLocalStorageUserPrefs",
() => getUserPreferences()
);
function createTLUser(opts = {}) {
return {
userPreferences: opts.userPreferences ?? defaultLocalStorageUserPrefs,
setUserPreferences: opts.setUserPreferences ?? setUserPreferences
};
}
function useTldrawUser(opts) {
const prefs = useShallowObjectIdentity(opts.userPreferences ?? defaultLocalStorageUserPrefs);
const userAtom = useAtom("userAtom", prefs);
reactExports.useEffect(() => {
userAtom.set(prefs);
}, [prefs, userAtom]);
return reactExports.useMemo(
() => createTLUser({
userPreferences: computed("userPreferences", () => {
const userStuff = userAtom.get();
return isSignal(userStuff) ? userStuff.get() : userStuff;
}),
setUserPreferences: opts.setUserPreferences
}),
[userAtom, opts.setUserPreferences]
);
}
var eventemitter3 = {exports: {}};
(function (module) {
var has = Object.prototype.hasOwnProperty
, prefix = '~';
/**
* Constructor to create a storage for our `EE` objects.
* An `Events` instance is a plain object whose properties are event names.
*
* @constructor
* @private
*/
function Events() {}
//
// We try to not inherit from `Object.prototype`. In some engines creating an
// instance in this way is faster than calling `Object.create(null)` directly.
// If `Object.create(null)` is not supported we prefix the event names with a
// character to make sure that the built-in object properties are not
// overridden or used as an attack vector.
//
if (Object.create) {
Events.prototype = Object.create(null);
//
// This hack is needed because the `__proto__` property is still inherited in
// some old browsers like Android 4, iPhone 5.1, Opera 11 and Safari 5.
//
if (!new Events().__proto__) prefix = false;
}
/**
* Representation of a single event listener.
*
* @param {Function} fn The listener function.
* @param {*} context The context to invoke the listener with.
* @param {Boolean} [once=false] Specify if the listener is a one-time listener.
* @constructor
* @private
*/
function EE(fn, context, once) {
this.fn = fn;
this.context = context;
this.once = once || false;
}
/**
* Add a listener for a given event.
*
* @param {EventEmitter} emitter Reference to the `EventEmitter` instance.
* @param {(String|Symbol)} event The event name.
* @param {Function} fn The listener function.
* @param {*} context The context to invoke the listener with.
* @param {Boolean} once Specify if the listener is a one-time listener.
* @returns {EventEmitter}
* @private
*/
function addListener(emitter, event, fn, context, once) {
if (typeof fn !== 'function') {
throw new TypeError('The listener must be a function');
}
var listener = new EE(fn, context || emitter, once)
, evt = prefix ? prefix + event : event;
if (!emitter._events[evt]) emitter._events[evt] = listener, emitter._eventsCount++;
else if (!emitter._events[evt].fn) emitter._events[evt].push(listener);
else emitter._events[evt] = [emitter._events[evt], listener];
return emitter;
}
/**
* Clear event by name.
*
* @param {EventEmitter} emitter Reference to the `EventEmitter` instance.
* @param {(String|Symbol)} evt The Event name.
* @private
*/
function clearEvent(emitter, evt) {
if (--emitter._eventsCount === 0) emitter._events = new Events();
else delete emitter._events[evt];
}
/**
* Minimal `EventEmitter` interface that is molded against the Node.js
* `EventEmitter` interface.
*
* @constructor
* @public
*/
function EventEmitter() {
this._events = new Events();
this._eventsCount = 0;
}
/**
* Return an array listing the events for which the emitter has registered
* listeners.
*
* @returns {Array}
* @public
*/
EventEmitter.prototype.eventNames = function eventNames() {
var names = []
, events
, name;
if (this._eventsCount === 0) return names;
for (name in (events = this._events)) {
if (has.call(events, name)) names.push(prefix ? name.slice(1) : name);
}
if (Object.getOwnPropertySymbols) {
return names.concat(Object.getOwnPropertySymbols(events));
}
return names;
};
/**
* Return the listeners registered for a given event.
*
* @param {(String|Symbol)} event The event name.
* @returns {Array} The registered listeners.
* @public
*/
EventEmitter.prototype.listeners = function listeners(event) {
var evt = prefix ? prefix + event : event
, handlers = this._events[evt];
if (!handlers) return [];
if (handlers.fn) return [handlers.fn];
for (var i = 0, l = handlers.length, ee = new Array(l); i < l; i++) {
ee[i] = handlers[i].fn;
}
return ee;
};
/**
* Return the number of listeners listening to a given event.
*
* @param {(String|Symbol)} event The event name.
* @returns {Number} The number of listeners.
* @public
*/
EventEmitter.prototype.listenerCount = function listenerCount(event) {
var evt = prefix ? prefix + event : event
, listeners = this._events[evt];
if (!listeners) return 0;
if (listeners.fn) return 1;
return listeners.length;
};
/**
* Calls each of the listeners registered for a given event.
*
* @param {(String|Symbol)} event The event name.
* @returns {Boolean} `true` if the event had listeners, else `false`.
* @public
*/
EventEmitter.prototype.emit = function emit(event, a1, a2, a3, a4, a5) {
var evt = prefix ? prefix + event : event;
if (!this._events[evt]) return false;
var listeners = this._events[evt]
, len = arguments.length
, args
, i;
if (listeners.fn) {
if (listeners.once) this.removeListener(event, listeners.fn, undefined, true);
switch (len) {
case 1: return listeners.fn.call(listeners.context), true;
case 2: return listeners.fn.call(listeners.context, a1), true;
case 3: return listeners.fn.call(listeners.context, a1, a2), true;
case 4: return listeners.fn.call(listeners.context, a1, a2, a3), true;
case 5: return listeners.fn.call(listeners.context, a1, a2, a3, a4), true;
case 6: return listeners.fn.call(listeners.context, a1, a2, a3, a4, a5), true;
}
for (i = 1, args = new Array(len -1); i < len; i++) {
args[i - 1] = arguments[i];
}
listeners.fn.apply(listeners.context, args);
} else {
var length = listeners.length
, j;
for (i = 0; i < length; i++) {
if (listeners[i].once) this.removeListener(event, listeners[i].fn, undefined, true);
switch (len) {
case 1: listeners[i].fn.call(listeners[i].context); break;
case 2: listeners[i].fn.call(listeners[i].context, a1); break;
case 3: listeners[i].fn.call(listeners[i].context, a1, a2); break;
case 4: listeners[i].fn.call(listeners[i].context, a1, a2, a3); break;
default:
if (!args) for (j = 1, args = new Array(len -1); j < len; j++) {
args[j - 1] = arguments[j];
}
listeners[i].fn.apply(listeners[i].context, args);
}
}
}
return true;
};
/**
* Add a listener for a given event.
*
* @param {(String|Symbol)} event The event name.
* @param {Function} fn The listener function.
* @param {*} [context=this] The context to invoke the listener with.
* @returns {EventEmitter} `this`.
* @public
*/
EventEmitter.prototype.on = function on(event, fn, context) {
return addListener(this, event, fn, context, false);
};
/**
* Add a one-time listener for a given event.
*
* @param {(String|Symbol)} event The event name.
* @param {Function} fn The listener function.
* @param {*} [context=this] The context to invoke the listener with.
* @returns {EventEmitter} `this`.
* @public
*/
EventEmitter.prototype.once = function once(event, fn, context) {
return addListener(this, event, fn, context, true);
};
/**
* Remove the listeners of a given event.
*
* @param {(String|Symbol)} event The event name.
* @param {Function} fn Only remove the listeners that match this function.
* @param {*} context Only remove the listeners that have this context.
* @param {Boolean} once Only remove one-time listeners.
* @returns {EventEmitter} `this`.
* @public
*/
EventEmitter.prototype.removeListener = function removeListener(event, fn, context, once) {
var evt = prefix ? prefix + event : event;
if (!this._events[evt]) return this;
if (!fn) {
clearEvent(this, evt);
return this;
}
var listeners = this._events[evt];
if (listeners.fn) {
if (
listeners.fn === fn &&
(!once || listeners.once) &&
(!context || listeners.context === context)
) {
clearEvent(this, evt);
}
} else {
for (var i = 0, events = [], length = listeners.length; i < length; i++) {
if (
listeners[i].fn !== fn ||
(once && !listeners[i].once) ||
(context && listeners[i].context !== context)
) {
events.push(listeners[i]);
}
}
//
// Reset the array, or remove it completely if we have no more listeners.
//
if (events.length) this._events[evt] = events.length === 1 ? events[0] : events;
else clearEvent(this, evt);
}
return this;
};
/**
* Remove all listeners, or those of the specified event.
*
* @param {(String|Symbol)} [event] The event name.
* @returns {EventEmitter} `this`.
* @public
*/
EventEmitter.prototype.removeAllListeners = function removeAllListeners(event) {
var evt;
if (event) {
evt = prefix ? prefix + event : event;
if (this._events[evt]) clearEvent(this, evt);
} else {
this._events = new Events();
this._eventsCount = 0;
}
return this;
};
//
// Alias methods names because people roll like that.
//
EventEmitter.prototype.off = EventEmitter.prototype.removeListener;
EventEmitter.prototype.addListener = EventEmitter.prototype.on;
//
// Expose the prefix.
//
EventEmitter.prefixed = prefix;
//
// Allow `EventEmitter` to be imported as module namespace.
//
EventEmitter.EventEmitter = EventEmitter;
//
// Expose the module.
//
{
module.exports = EventEmitter;
}
} (eventemitter3));
var eventemitter3Exports = eventemitter3.exports;
const EventEmitter = /*@__PURE__*/getDefaultExportFromCjs(eventemitter3Exports);
const tabIdKey = "TLDRAW_TAB_ID_v2";
const window$1 = globalThis.window;
function iOS() {
if (!window$1) return false;
return ["iPad Simulator", "iPhone Simulator", "iPod Simulator", "iPad", "iPhone", "iPod"].includes(
// eslint-disable-next-line @typescript-eslint/no-deprecated
window$1.navigator.platform
) || // iPad on iOS 13 detection
tlenv.isDarwin && "ontouchend" in document;
}
const TAB_ID = window$1 ? window$1[tabIdKey] ?? getFromSessionStorage(tabIdKey) ?? `TLDRAW_INSTANCE_STATE_V1_` + uniqueId() : "";
if (window$1) {
window$1[tabIdKey] = TAB_ID;
if (iOS()) {
setInSessionStorage(tabIdKey, TAB_ID);
} else {
deleteFromSessionStorage(tabIdKey);
}
}
window$1?.addEventListener("beforeunload", () => {
setInSessionStorage(tabIdKey, TAB_ID);
});
const Versions = {
Initial: 0
};
const CURRENT_SESSION_STATE_SNAPSHOT_VERSION = Math.max(...Object.values(Versions));
function migrate(snapshot) {
if (snapshot.version < Versions.Initial) ;
snapshot.version = CURRENT_SESSION_STATE_SNAPSHOT_VERSION;
}
const sessionStateSnapshotValidator = object({
version: number,
currentPageId: pageIdValidator.optional(),
isFocusMode: boolean.optional(),
exportBackground: boolean.optional(),
isDebugMode: boolean.optional(),
isToolLocked: boolean.optional(),
isGridMode: boolean.optional(),
pageStates: arrayOf(
object({
pageId: pageIdValidator,
camera: object({
x: number,
y: number,
z: number
}).optional(),
selectedShapeIds: arrayOf(shapeIdValidator).optional(),
focusedGroupId: shapeIdValidator.nullable().optional()
})
).optional()
});
function migrateAndValidateSessionStateSnapshot(state) {
if (!state || typeof state !== "object") {
console.warn("Invalid instance state");
return null;
}
if (!("version" in state) || typeof state.version !== "number") {
console.warn("No version in instance state");
return null;
}
if (state.version !== CURRENT_SESSION_STATE_SNAPSHOT_VERSION) {
state = structuredClone(state);
migrate(state);
}
try {
return sessionStateSnapshotValidator.validate(state);
} catch (e) {
console.warn(e);
return null;
}
}
function createSessionStateSnapshotSignal(store) {
const $allPageIds = store.query.ids("page");
return computed(
"sessionStateSnapshot",
() => {
const instanceState = store.get(TLINSTANCE_ID);
if (!instanceState) return null;
const allPageIds = [...$allPageIds.get()];
return {
version: CURRENT_SESSION_STATE_SNAPSHOT_VERSION,
currentPageId: instanceState.currentPageId,
exportBackground: instanceState.exportBackground,
isFocusMode: instanceState.isFocusMode,
isDebugMode: instanceState.isDebugMode,
isToolLocked: instanceState.isToolLocked,
isGridMode: instanceState.isGridMode,
pageStates: allPageIds.map((id) => {
const ps = store.get(InstancePageStateRecordType.createId(id));
const camera = store.get(CameraRecordType.createId(id));
return {
pageId: id,
camera: {
x: camera?.x ?? 0,
y: camera?.y ?? 0,
z: camera?.z ?? 1
},
selectedShapeIds: ps?.selectedShapeIds ?? [],
focusedGroupId: ps?.focusedGroupId ?? null
};
})
};
},
{ isEqual }
);
}
function loadSessionStateSnapshotIntoStore(store, snapshot, opts) {
const res = migrateAndValidateSessionStateSnapshot(snapshot);
if (!res) return;
const preserved = pluckPreservingValues(store.get(TLINSTANCE_ID));
const primary = opts?.forceOverwrite ? res : preserved;
const secondary = opts?.forceOverwrite ? preserved : res;
const instanceState = store.schema.types.instance.create({
id: TLINSTANCE_ID,
...preserved,
// the integrity checker will ensure that the currentPageId is valid
currentPageId: res.currentPageId,
isDebugMode: primary?.isDebugMode ?? secondary?.isDebugMode,
isFocusMode: primary?.isFocusMode ?? secondary?.isFocusMode,
isToolLocked: primary?.isToolLocked ?? secondary?.isToolLocked,
isGridMode: primary?.isGridMode ?? secondary?.isGridMode,
exportBackground: primary?.exportBackground ?? secondary?.exportBackground
});
store.atomic(() => {
for (const ps of res.pageStates ?? []) {
if (!store.has(ps.pageId)) continue;
const cameraId = CameraRecordType.createId(ps.pageId);
const instancePageState = InstancePageStateRecordType.createId(ps.pageId);
const previousCamera = store.get(cameraId);
const previousInstanceState = store.get(instancePageState);
store.put([
CameraRecordType.create({
id: cameraId,
x: ps.camera?.x ?? previousCamera?.x,
y: ps.camera?.y ?? previousCamera?.y,
z: ps.camera?.z ?? previousCamera?.z
}),
InstancePageStateRecordType.create({
id: instancePageState,
pageId: ps.pageId,
selectedShapeIds: ps.selectedShapeIds ?? previousInstanceState?.selectedShapeIds,
focusedGroupId: ps.focusedGroupId ?? previousInstanceState?.focusedGroupId
})
]);
}
store.put([instanceState]);
store.ensureStoreIsUsable();
});
}
function extractSessionStateFromLegacySnapshot(store) {
const instanceRecords = [];
for (const record of Object.values(store)) {
if (record.typeName?.match(/^(instance.*|pointer|camera)$/)) {
instanceRecords.push(record);
}
}
const oldInstance = instanceRecords.filter(
(r) => r.typeName === "instance" && r.id !== TLINSTANCE_ID
)[0];
if (!oldInstance) return null;
const result = {
version: CURRENT_SESSION_STATE_SNAPSHOT_VERSION,
currentPageId: oldInstance.currentPageId,
exportBackground: !!oldInstance.exportBackground,
isFocusMode: !!oldInstance.isFocusMode,
isDebugMode: !!oldInstance.isDebugMode,
isToolLocked: !!oldInstance.isToolLocked,
isGridMode: false,
pageStates: instanceRecords.filter((r) => r.typeName === "instance_page_state" && r.instanceId === oldInstance.id).map((ps) => {
const camera = store[ps.cameraId] ?? { x: 0, y: 0, z: 1 };
return {
pageId: ps.pageId,
camera: {
x: camera.x,
y: camera.y,
z: camera.z
},
selectedShapeIds: ps.selectedShapeIds,
focusedGroupId: ps.focusedGroupId
};
})
};
try {
sessionStateSnapshotValidator.validate(result);
return result;
} catch {
return null;
}
}
function loadSnapshot(store, _snapshot, opts) {
let snapshot = {};
if ("store" in _snapshot) {
const migrationResult = store.schema.migrateStoreSnapshot(_snapshot);
if (migrationResult.type !== "success") {
throw new Error("Failed to migrate store snapshot: " + migrationResult.reason);
}
snapshot.document = {
schema: store.schema.serialize(),
store: filterEntries(
migrationResult.value,
(_, { typeName }) => store.scopedTypes.document.has(typeName)
)
};
} else {
snapshot = _snapshot;
}
const preservingInstanceState = pluckPreservingValues(store.get(TLINSTANCE_ID));
const preservingSessionState = sessionStateCache.get(store, createSessionStateSnapshotSignal).get();
store.atomic(() => {
if (snapshot.document) {
store.loadStoreSnapshot(snapshot.document);
}
if (preservingInstanceState) {
store.update(TLINSTANCE_ID, (r) => ({ ...r, ...preservingInstanceState }));
}
if (preservingSessionState) {
loadSessionStateSnapshotIntoStore(store, preservingSessionState);
}
if (snapshot.session) {
loadSessionStateSnapshotIntoStore(store, snapshot.session, {
forceOverwrite: opts?.forceOverwriteSessionState
});
}
});
}
const sessionStateCache = new WeakCache();
function getSnapshot(store) {
const sessionState$ = sessionStateCache.get(store, createSessionStateSnapshotSignal);
const session = sessionState$.get();
if (!session) {
throw new Error("Session state is not ready yet");
}
return {
document: store.getStoreSnapshot(),
session
};
}
function checkBindings(customBindings) {
const bindings = [];
const addedCustomBindingTypes = /* @__PURE__ */ new Set();
for (const customBinding of customBindings) {
if (addedCustomBindingTypes.has(customBinding.type)) {
throw new Error(`Binding type "${customBinding.type}" is defined more than once`);
}
bindings.push(customBinding);
addedCustomBindingTypes.add(customBinding.type);
}
return bindings;
}
function SVGContainer({ children, className = "", ...rest }) {
return /* @__PURE__ */ jsxRuntimeExports.jsx("svg", { ...rest, className: classNames("tl-svg-container", className), children });
}
function intersectLineSegmentLineSegment(a1, a2, b1, b2) {
const ABx = a1.x - b1.x;
const ABy = a1.y - b1.y;
const BVx = b2.x - b1.x;
const BVy = b2.y - b1.y;
const AVx = a2.x - a1.x;
const AVy = a2.y - a1.y;
const ua_t = BVx * ABy - BVy * ABx;
const ub_t = AVx * ABy - AVy * ABx;
const u_b = BVy * AVx - BVx * AVy;
if (ua_t === 0 || ub_t === 0) return null;
if (u_b === 0) return null;
if (u_b !== 0) {
const ua = ua_t / u_b;
const ub = ub_t / u_b;
if (0 <= ua && ua <= 1 && 0 <= ub && ub <= 1) {
return Vec.AddXY(a1, ua * AVx, ua * AVy);
}
}
return null;
}
function intersectLineSegmentCircle(a1, a2, c, r) {
const a = (a2.x - a1.x) * (a2.x - a1.x) + (a2.y - a1.y) * (a2.y - a1.y);
const b = 2 * ((a2.x - a1.x) * (a1.x - c.x) + (a2.y - a1.y) * (a1.y - c.y));
const cc = c.x * c.x + c.y * c.y + a1.x * a1.x + a1.y * a1.y - 2 * (c.x * a1.x + c.y * a1.y) - r * r;
const deter = b * b - 4 * a * cc;
if (deter < 0) return null;
if (deter === 0) return null;
const e = Math.sqrt(deter);
const u1 = (-b + e) / (2 * a);
const u2 = (-b - e) / (2 * a);
if ((u1 < 0 || u1 > 1) && (u2 < 0 || u2 > 1)) {
return null;
}
const result = [];
if (0 <= u1 && u1 <= 1) result.push(Vec.Lrp(a1, a2, u1));
if (0 <= u2 && u2 <= 1) result.push(Vec.Lrp(a1, a2, u2));
if (result.length === 0) return null;
return result;
}
function intersectLineSegmentPolyline(a1, a2, points) {
const result = [];
let segmentIntersection;
for (let i = 0, n = points.length - 1; i < n; i++) {
segmentIntersection = intersectLineSegmentLineSegment(a1, a2, points[i], points[i + 1]);
if (segmentIntersection) result.push(segmentIntersection);
}
if (result.length === 0) return null;
return result;
}
function intersectLineSegmentPolygon(a1, a2, points) {
const result = [];
let segmentIntersection;
for (let i = 1, n = points.length; i < n + 1; i++) {
segmentIntersection = intersectLineSegmentLineSegment(
a1,
a2,
points[i - 1],
points[i % points.length]
);
if (segmentIntersection) result.push(segmentIntersection);
}
if (result.length === 0) return null;
return result;
}
function intersectCircleCircle(c1, r1, c2, r2) {
let dx = c2.x - c1.x;
let dy = c2.y - c1.y;
const d = Math.sqrt(dx * dx + dy * dy), x = (d * d - r2 * r2 + r1 * r1) / (2 * d), y = Math.sqrt(r1 * r1 - x * x);
dx /= d;
dy /= d;
return [
new Vec(c1.x + dx * x - dy * y, c1.y + dy * x + dx * y),
new Vec(c1.x + dx * x + dy * y, c1.y + dy * x - dx * y)
];
}
function intersectCirclePolygon(c, r, points) {
const result = [];
let a, b, int;
for (let i = 0, n = points.length; i < n; i++) {
a = points[i];
b = points[(i + 1) % points.length];
int = intersectLineSegmentCircle(a, b, c, r);
if (int) result.push(...int);
}
if (result.length === 0) return null;
return result;
}
function intersectCirclePolyline(c, r, points) {
const result = [];
let a, b, int;
for (let i = 1, n = points.length; i < n; i++) {
a = points[i - 1];
b = points[i];
int = intersectLineSegmentCircle(a, b, c, r);
if (int) result.push(...int);
}
if (result.length === 0) return null;
return result;
}
function ccw(A, B, C) {
return (C.y - A.y) * (B.x - A.x) > (B.y - A.y) * (C.x - A.x);
}
function linesIntersect(A, B, C, D) {
return ccw(A, C, D) !== ccw(B, C, D) && ccw(A, B, C) !== ccw(A, B, D);
}
function intersectPolygonPolygon(polygonA, polygonB) {
const result = /* @__PURE__ */ new Map();
let a, b, c, d;
for (let i = 0, n = polygonA.length; i < n; i++) {
a = polygonA[i];
if (pointInPolygon(a, polygonB)) {
const id = getPointId(a);
if (!result.has(id)) {
result.set(id, a);
}
}
}
for (let i = 0, n = polygonB.length; i < n; i++) {
a = polygonB[i];
if (pointInPolygon(a, polygonA)) {
const id = getPointId(a);
if (!result.has(id)) {
result.set(id, a);
}
}
}
for (let i = 0, n = polygonA.length; i < n; i++) {
a = polygonA[i];
b = polygonA[(i + 1) % polygonA.length];
for (let j = 0, m = polygonB.length; j < m; j++) {
c = polygonB[j];
d = polygonB[(j + 1) % polygonB.length];
const intersection = intersectLineSegmentLineSegment(a, b, c, d);
if (intersection !== null) {
const id = getPointId(intersection);
if (!result.has(id)) {
result.set(id, intersection);
}
}
}
}
if (result.size === 0) return null;
return orderClockwise([...result.values()]);
}
function getPointId(point) {
return `${point.x},${point.y}`;
}
function orderClockwise(points) {
const C = Vec.Average(points);
return points.sort((A, B) => Vec.Angle(C, A) - Vec.Angle(C, B));
}
function polygonsIntersect(a, b) {
let a0, a1, b0, b1;
for (let i = 0, n = a.length; i < n; i++) {
a0 = a[i];
a1 = a[(i + 1) % n];
for (let j = 0, m = b.length; j < m; j++) {
b0 = b[j];
b1 = b[(j + 1) % m];
if (linesIntersect(a0, a1, b0, b1)) return true;
}
}
return false;
}
function polygonIntersectsPolyline(polygon, polyline) {
let a, b, c, d;
for (let i = 0, n = polygon.length; i < n; i++) {
a = polygon[i];
b = polygon[(i + 1) % n];
for (let j = 1, m = polyline.length; j < m; j++) {
c = polyline[j - 1];
d = polyline[j];
if (linesIntersect(a, b, c, d)) return true;
}
}
return false;
}
class Edge2d extends Geometry2d {
start;
end;
d;
u;
ul;
constructor(config) {
super({ ...config, isClosed: false, isFilled: false });
const { start, end } = config;
this.start = start;
this.end = end;
this.d = start.clone().sub(end);
this.u = this.d.clone().uni();
this.ul = this.u.len();
}
getLength() {
return this.d.len();
}
midPoint() {
return this.start.lrp(this.end, 0.5);
}
getVertices() {
return [this.start, this.end];
}
nearestPoint(point) {
const { start, end, d, u, ul: l } = this;
if (d.len() === 0) return start;
if (l === 0) return start;
const k = Vec.Sub(point, start).dpr(u) / l;
const cx = start.x + u.x * k;
if (cx < Math.min(start.x, end.x)) return start.x < end.x ? start : end;
if (cx > Math.max(start.x, end.x)) return start.x > end.x ? start : end;
const cy = start.y + u.y * k;
if (cy < Math.min(start.y, end.y)) return start.y < end.y ? start : end;
if (cy > Math.max(start.y, end.y)) return start.y > end.y ? start : end;
return new Vec(cx, cy);
}
hitTestLineSegment(A, B, distance = 0) {
return linesIntersect(A, B, this.start, this.end) || this.distanceToLineSegment(A, B) <= distance;
}
getSvgPathData(first = true) {
const { start, end } = this;
return `${first ? `M${start.toFixed()}` : ``} L${end.toFixed()}`;
}
}
class Polyline2d extends Geometry2d {
points;
constructor(config) {
super({ isClosed: false, isFilled: false, ...config });
const { points } = config;
this.points = points;
}
_segments;
// eslint-disable-next-line no-restricted-syntax
get segments() {
if (!this._segments) {
this._segments = [];
const { vertices } = this;
for (let i = 0, n = vertices.length - 1; i < n; i++) {
const start = vertices[i];
const end = vertices[i + 1];
this._segments.push(new Edge2d({ start, end }));
}
if (this.isClosed) {
this._segments.push(new Edge2d({ start: vertices[vertices.length - 1], end: vertices[0] }));
}
}
return this._segments;
}
getLength() {
return this.segments.reduce((acc, segment) => acc + segment.length, 0);
}
getVertices() {
return this.points;
}
nearestPoint(A) {
const { segments } = this;
let nearest = this.points[0];
let dist = Infinity;
let p;
let d;
for (let i = 0; i < segments.length; i++) {
p = segments[i].nearestPoint(A);
d = Vec.Dist2(p, A);
if (d < dist) {
nearest = p;
dist = d;
}
}
if (!nearest) throw Error("nearest point not found");
return nearest;
}
hitTestLineSegment(A, B, distance = 0) {
const { segments } = this;
for (let i = 0, n = segments.length; i < n; i++) {
if (segments[i].hitTestLineSegment(A, B, distance)) {
return true;
}
}
return false;
}
getSvgPathData() {
const { vertices } = this;
if (vertices.length < 2) return "";
return vertices.reduce((acc, vertex, i) => {
if (i === 0) return `M ${vertex.x} ${vertex.y}`;
return `${acc} L ${vertex.x} ${vertex.y}`;
}, "");
}
}
class Polygon2d extends Polyline2d {
constructor(config) {
super({ ...config });
this.isClosed = true;
}
}
class Rectangle2d extends Polygon2d {
x;
y;
w;
h;
constructor(config) {
const { x = 0, y = 0, width, height } = config;
super({
...config,
points: [
new Vec(x, y),
new Vec(x + width, y),
new Vec(x + width, y + height),
new Vec(x, y + height)
]
});
this.x = x;
this.y = y;
this.w = width;
this.h = height;
}
getBounds() {
return new Box(this.x, this.y, this.w, this.h);
}
getSvgPathData() {
const { x, y, w, h } = this;
return `M${x},${y} h${w} v${h} h-${w}z`;
}
}
class ShapeUtil {
constructor(editor) {
this.editor = editor;
}
/**
* Props allow you to define the shape's properties in a way that the editor can understand.
* This has two main uses:
*
* 1. Validation. Shapes will be validated using these props to stop bad data from being saved.
* 2. Styles. Each {@link @tldraw/tlschema#StyleProp} in the props can be set on many shapes at
* once, and will be remembered from one shape to the next.
*
* @example
* ```tsx
* import {T, TLBaseShape, TLDefaultColorStyle, DefaultColorStyle, ShapeUtil} from 'tldraw'
*
* type MyShape = TLBaseShape<'mine', {
* color: TLDefaultColorStyle,
* text: string,
* }>
*
* class MyShapeUtil extends ShapeUtil {
* static props = {
* // we use tldraw's built-in color style:
* color: DefaultColorStyle,
* // validate that the text prop is a string:
* text: T.string,
* }
* }
* ```
*/
static props;
/**
* Migrations allow you to make changes to a shape's props over time. Read the
* {@link https://www.tldraw.dev/docs/persistence#Shape-props-migrations | shape prop migrations}
* guide for more information.
*/
static migrations;
/**
* The type of the shape util, which should match the shape's type.
*
* @public
*/
static type;
/**
* Whether the shape can be snapped to by another shape.
*
* @public
*/
canSnap(_shape) {
return true;
}
/**
* Whether the shape can be scrolled while editing.
*
* @public
*/
canScroll(_shape) {
return false;
}
/**
* Whether the shape can be bound to. See {@link TLShapeUtilCanBindOpts} for details.
*
* @public
*/
canBind(_opts) {
return true;
}
/**
* Whether the shape can be double clicked to edit.
*
* @public
*/
canEdit(_shape) {
return false;
}
/**
* Whether the shape can be resized.
*
* @public
*/
canResize(_shape) {
return true;
}
/**
* Whether the shape can be edited in read-only mode.
*
* @public
*/
canEditInReadOnly(_shape) {
return false;
}
/**
* Whether the shape can be cropped.
*
* @public
*/
canCrop(_shape) {
return false;
}
/**
* Whether the shape participates in stacking, aligning, and distributing.
*
* @public
*/
canBeLaidOut(_shape) {
return true;
}
/**
* Does this shape provide a background for its children? If this is true,
* then any children with a `renderBackground` method will have their
* backgrounds rendered _above_ this shape. Otherwise, the children's
* backgrounds will be rendered above either the next ancestor that provides
* a background, or the canvas background.
*
* @internal
*/
providesBackgroundForChildren(_shape) {
return false;
}
/**
* Whether the shape should hide its resize handles when selected.
*
* @public
*/
hideResizeHandles(_shape) {
return false;
}
/**
* Whether the shape should hide its rotation handles when selected.
*
* @public
*/
hideRotateHandle(_shape) {
return false;
}
/**
* Whether the shape should hide its selection bounds background when selected.
*
* @public
*/
hideSelectionBoundsBg(_shape) {
return false;
}
/**
* Whether the shape should hide its selection bounds foreground when selected.
*
* @public
*/
hideSelectionBoundsFg(_shape) {
return false;
}
/**
* Whether the shape's aspect ratio is locked.
*
* @public
*/
isAspectRatioLocked(_shape) {
return false;
}
/**
* Get whether the shape can receive children of a given type.
*
* @param shape - The shape.
* @param type - The shape type.
* @public
*/
canReceiveNewChildrenOfType(_shape, _type) {
return false;
}
/**
* Get whether the shape can receive children of a given type.
*
* @param shape - The shape type.
* @param shapes - The shapes that are being dropped.
* @public
*/
canDropShapes(_shape, _shapes) {
return false;
}
/** @internal */
expandSelectionOutlinePx(shape) {
return 0;
}
/**
* Return elements to be added to the \ section of the canvases SVG context. This can be
* used to define SVG content (e.g. patterns & masks) that can be referred to by ID from svg
* elements returned by `component`.
*
* Each def should have a unique `key`. If multiple defs from different shapes all have the same
* key, only one will be used.
*/
getCanvasSvgDefs() {
return [];
}
/**
* Get the geometry to use when snapping to this this shape in translate/resize operations. See
* {@link BoundsSnapGeometry} for details.
*/
getBoundsSnapGeometry(_shape) {
return {};
}
/**
* Get the geometry to use when snapping handles to this shape. See {@link HandleSnapGeometry}
* for details.
*/
getHandleSnapGeometry(_shape) {
return {};
}
getText(_shape) {
return void 0;
}
}
function getPerfectDashProps(totalLength, strokeWidth, opts = {}) {
const {
closed = false,
snap = 1,
start = "outset",
end = "outset",
lengthRatio = 2,
style = "dashed",
forceSolid = false
} = opts;
let dashLength = 0;
let dashCount = 0;
let ratio = 1;
let gapLength = 0;
let strokeDashoffset = 0;
if (forceSolid) {
return {
strokeDasharray: "none",
strokeDashoffset: "none"
};
}
switch (style) {
case "dashed": {
ratio = 1;
dashLength = Math.min(strokeWidth * lengthRatio, totalLength / 4);
break;
}
case "dotted": {
ratio = 100;
dashLength = strokeWidth / ratio;
break;
}
default: {
return {
strokeDasharray: "none",
strokeDashoffset: "none"
};
}
}
if (!closed) {
if (start === "outset") {
totalLength += dashLength / 2;
strokeDashoffset += dashLength / 2;
} else if (start === "skip") {
totalLength -= dashLength;
strokeDashoffset -= dashLength;
}
if (end === "outset") {
totalLength += dashLength / 2;
} else if (end === "skip") {
totalLength -= dashLength;
}
}
dashCount = Math.floor(totalLength / dashLength / (2 * ratio));
dashCount -= dashCount % snap;
if (dashCount < 3 && style === "dashed") {
if (totalLength / strokeWidth < 4) {
dashLength = totalLength;
dashCount = 1;
gapLength = 0;
} else {
dashLength = totalLength * (1 / 3);
gapLength = totalLength * (1 / 3);
}
} else {
dashLength = totalLength / dashCount / (2 * ratio);
if (closed) {
strokeDashoffset = dashLength / 2;
gapLength = (totalLength - dashCount * dashLength) / dashCount;
} else {
gapLength = (totalLength - dashCount * dashLength) / Math.max(1, dashCount - 1);
}
}
return {
strokeDasharray: [dashLength, gapLength].join(" "),
strokeDashoffset: strokeDashoffset.toString()
};
}
function DashedOutlineBox({ bounds, className }) {
const editor = useEditor();
const zoomLevel = useValue("zoom level", () => editor.getZoomLevel(), [editor]);
return /* @__PURE__ */ jsxRuntimeExports.jsx("g", { className, pointerEvents: "none", strokeLinecap: "round", strokeLinejoin: "round", children: bounds.sides.map((side, i) => {
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
side[0].dist(side[1]),
1 / zoomLevel,
{
style: "dashed",
lengthRatio: 4
}
);
return /* @__PURE__ */ jsxRuntimeExports.jsx(
"line",
{
x1: side[0].x,
y1: side[0].y,
x2: side[1].x,
y2: side[1].y,
strokeDasharray,
strokeDashoffset
},
i
);
}) });
}
class GroupShapeUtil extends ShapeUtil {
static type = "group";
static props = groupShapeProps;
static migrations = groupShapeMigrations;
hideSelectionBoundsFg() {
return true;
}
canBind() {
return false;
}
getDefaultProps() {
return {};
}
getGeometry(shape) {
const children = this.editor.getSortedChildIdsForParent(shape.id);
if (children.length === 0) {
return new Rectangle2d({ width: 1, height: 1, isFilled: false });
}
return new Group2d({
children: children.map((childId) => {
const shape2 = this.editor.getShape(childId);
const geometry = this.editor.getShapeGeometry(childId);
const points = this.editor.getShapeLocalTransform(shape2).applyToPoints(geometry.vertices);
if (geometry.isClosed) {
return new Polygon2d({
points,
isFilled: true
});
}
return new Polyline2d({
points
});
})
});
}
component(shape) {
const isErasing = this.editor.getErasingShapeIds().includes(shape.id);
const { hintingShapeIds } = this.editor.getCurrentPageState();
const isHintingOtherGroup = hintingShapeIds.length > 0 && hintingShapeIds.some(
(id) => id !== shape.id && this.editor.isShapeOfType(this.editor.getShape(id), "group")
);
const isFocused = this.editor.getCurrentPageState().focusedGroupId !== shape.id;
if (!isErasing && // always show the outline while we're erasing the group
// show the outline while the group is focused unless something outside of the group is being hinted
// this happens dropping shapes from a group onto some outside group
(isFocused || isHintingOtherGroup)) {
return null;
}
const bounds = this.editor.getShapeGeometry(shape).bounds;
return /* @__PURE__ */ jsxRuntimeExports.jsx(SVGContainer, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(DashedOutlineBox, { className: "tl-group", bounds }) });
}
indicator(shape) {
const bounds = this.editor.getShapeGeometry(shape).bounds;
return /* @__PURE__ */ jsxRuntimeExports.jsx(DashedOutlineBox, { className: "", bounds });
}
onChildrenChange(group) {
const children = this.editor.getSortedChildIdsForParent(group.id);
if (children.length === 0) {
if (this.editor.getCurrentPageState().focusedGroupId === group.id) {
this.editor.popFocusedGroupId();
}
this.editor.deleteShapes([group.id]);
return;
} else if (children.length === 1) {
if (this.editor.getCurrentPageState().focusedGroupId === group.id) {
this.editor.popFocusedGroupId();
}
this.editor.reparentShapes(children, group.parentId);
this.editor.deleteShapes([group.id]);
return;
}
}
}
const coreShapes = [
// created by grouping interactions, probably the corest core shape that we have
GroupShapeUtil
];
const coreShapeTypes = new Set(coreShapes.map((s) => s.type));
function checkShapesAndAddCore(customShapes) {
const shapes = [...coreShapes];
const addedCustomShapeTypes = /* @__PURE__ */ new Set();
for (const customShape of customShapes) {
if (coreShapeTypes.has(customShape.type)) {
throw new Error(
`Shape type "${customShape.type}" is a core shapes type and cannot be overridden`
);
}
if (addedCustomShapeTypes.has(customShape.type)) {
throw new Error(`Shape type "${customShape.type}" is defined more than once`);
}
shapes.push(customShape);
addedCustomShapeTypes.add(customShape.type);
}
return shapes;
}
function fetchCache(cb, init) {
const cache = /* @__PURE__ */ new Map();
return async function fetchCached(url) {
const existing = cache.get(url);
if (existing) return existing;
const promise = (async () => {
try {
const response = await fetch(url, init);
assert(response.ok);
return await cb(response);
} catch (err) {
console.error(err);
return null;
}
})();
cache.set(url, promise);
return promise;
};
}
const resourceToDataUrl = fetchCache(async (response) => {
return await FileHelpers.blobToDataUrl(await response.blob());
});
const importsRegex = /@import\s+(?:"([^"]+)"|'([^']+)'|url\s*\(\s*(?:"([^"]+)"|'([^']+)'|([^'")]+))\s*\))([^;]+);/gi;
const fontFaceRegex = /@font-face\s*{([^}]+)}/gi;
const urlsRegex = /url\s*\(\s*(?:"([^"]+)"|'([^']+)'|([^'")]+))\s*\)/gi;
const fontFamilyRegex = /(?:^|;)\s*font-family\s*:\s*(?:([^'"][^;\n]+)|"([^"]+)"|'([^']+)')\s*(?:;|$)/gi;
function parseCssImports(css) {
return Array.from(css.matchAll(importsRegex), (m) => ({
url: m[1] || m[2] || m[3] || m[4] || m[5],
extras: m[6]
}));
}
function parseCssFontFaces(css, baseUrl) {
return Array.from(css.matchAll(fontFaceRegex), (m) => {
const fontFace = m[1];
const urls = Array.from(fontFace.matchAll(urlsRegex), (m2) => {
const original = m2[1] || m2[2] || m2[3];
return {
original,
resolved: safeParseUrl(original, baseUrl)?.href ?? null
};
});
const fontFamilies = new Set(
Array.from(fontFace.matchAll(fontFamilyRegex), (m2) => (m2[1] || m2[2] || m2[3]).toLowerCase())
);
return { fontFace, urls, fontFamilies };
});
}
function parseCssFontFamilyValue(value) {
const valueRegex = /\s*(?:([^'"][^;\n\s,]+)|"([^"]+)"|'([^']+)')\s*/gi;
const separatorRegex = /\s*,\s*/gi;
const fontFamilies = /* @__PURE__ */ new Set();
while (true) {
const valueMatch = valueRegex.exec(value);
if (!valueMatch) {
break;
}
const fontFamily = valueMatch[1] || valueMatch[2] || valueMatch[3];
fontFamilies.add(fontFamily.toLowerCase());
separatorRegex.lastIndex = valueRegex.lastIndex;
const separatorMatch = separatorRegex.exec(value);
if (!separatorMatch) {
break;
}
valueRegex.lastIndex = separatorRegex.lastIndex;
}
return fontFamilies;
}
function shouldIncludeCssProperty(property) {
if (property.startsWith("-")) return false;
if (property.startsWith("animation")) return false;
if (property.startsWith("transition")) return false;
if (property === "cursor") return false;
if (property === "pointer-events") return false;
if (property === "user-select") return false;
if (property === "touch-action") return false;
return true;
}
function parseCss(css, baseUrl) {
return {
imports: parseCssImports(css),
fontFaces: parseCssFontFaces(css, baseUrl)
};
}
function parseCssValueUrls(value) {
return Array.from(value.matchAll(urlsRegex), (m) => ({
original: m[0],
url: m[1] || m[2] || m[3]
}));
}
const inheritedProperties = /* @__PURE__ */ new Set([
"border-collapse",
"border-spacing",
"caption-side",
"color",
"cursor",
"direction",
"empty-cells",
"font-family",
"font-size",
"font-style",
"font-variant",
"font-weight",
"font-size-adjust",
"font-stretch",
"font",
"letter-spacing",
"line-height",
"list-style-image",
"list-style-position",
"list-style-type",
"list-style",
"orphans",
"quotes",
"tab-size",
"text-align",
"text-align-last",
"text-decoration-color",
"text-indent",
"text-justify",
"text-shadow",
"text-transform",
"visibility",
"white-space",
"widows",
"word-break",
"word-spacing",
"word-wrap"
]);
function isPropertyInherited(property) {
return inheritedProperties.has(property);
}
var __create$b = Object.create;
var __defProp$b = Object.defineProperty;
var __getOwnPropDesc$b = Object.getOwnPropertyDescriptor;
var __knownSymbol$b = (name, symbol) => (symbol = Symbol[name]) ? symbol : Symbol.for("Symbol." + name);
var __typeError$b = (msg) => {
throw TypeError(msg);
};
var __defNormalProp$b = (obj, key, value) => key in obj ? __defProp$b(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __decoratorStart$b = (base) => [, , , __create$b(null)];
var __decoratorStrings$b = ["class", "method", "getter", "setter", "accessor", "field", "value", "get", "set"];
var __expectFn$b = (fn) => fn !== void 0 && typeof fn !== "function" ? __typeError$b("Function expected") : fn;
var __decoratorContext$b = (kind, name, done, metadata, fns) => ({ kind: __decoratorStrings$b[kind], name, metadata, addInitializer: (fn) => done._ ? __typeError$b("Already initialized") : fns.push(__expectFn$b(fn || null)) });
var __decoratorMetadata$b = (array, target) => __defNormalProp$b(target, __knownSymbol$b("metadata"), array[3]);
var __runInitializers$b = (array, flags, self, value) => {
for (var i = 0, fns = array[flags >> 1], n = fns && fns.length; i < n; i++) fns[i].call(self) ;
return value;
};
var __decorateElement$b = (array, flags, name, decorators, target, extra) => {
var it, done, ctx, access, k = flags & 7, s = false, p = false;
var j = 2 , key = __decoratorStrings$b[k + 5];
var extraInitializers = array[j] || (array[j] = []);
var desc = ((target = target.prototype), __getOwnPropDesc$b(target , name));
for (var i = decorators.length - 1; i >= 0; i--) {
ctx = __decoratorContext$b(k, name, done = {}, array[3], extraInitializers);
{
ctx.static = s, ctx.private = p, access = ctx.access = { has: (x) => name in x };
access.get = (x) => x[name];
}
it = (0, decorators[i])(desc[key] , ctx), done._ = 1;
__expectFn$b(it) && (desc[key] = it );
}
return desc && __defProp$b(target, name, desc), target;
};
var __publicField$b = (obj, key, value) => __defNormalProp$b(obj, typeof key !== "symbol" ? key + "" : key, value);
var _onFontFamilyValue_dec, _init$b;
_onFontFamilyValue_dec = [bind$2];
class FontEmbedder {
constructor() {
__runInitializers$b(_init$b, 5, this);
__publicField$b(this, "fontFacesPromise", null);
__publicField$b(this, "foundFontNames", /* @__PURE__ */ new Set());
__publicField$b(this, "fontFacesToEmbed", /* @__PURE__ */ new Set());
__publicField$b(this, "pendingPromises", []);
}
startFindingCurrentDocumentFontFaces() {
assert(!this.fontFacesPromise, "FontEmbedder already started");
this.fontFacesPromise = getCurrentDocumentFontFaces();
}
onFontFamilyValue(fontFamilyValue) {
assert(this.fontFacesPromise, "FontEmbedder not started");
const fonts = parseCssFontFamilyValue(fontFamilyValue);
for (const font of fonts) {
if (this.foundFontNames.has(font)) return;
this.foundFontNames.add(font);
this.pendingPromises.push(
this.fontFacesPromise.then((fontFaces) => {
const relevantFontFaces = fontFaces.filter((fontFace) => fontFace.fontFamilies.has(font));
for (const fontFace of relevantFontFaces) {
if (this.fontFacesToEmbed.has(fontFace)) continue;
this.fontFacesToEmbed.add(fontFace);
for (const url of fontFace.urls) {
if (!url.resolved || url.embedded) continue;
url.embedded = resourceToDataUrl(url.resolved);
}
}
})
);
}
}
async createCss() {
await Promise.all(this.pendingPromises);
let css = "";
for (const fontFace of this.fontFacesToEmbed) {
let fontFaceString = `@font-face {${fontFace.fontFace}}`;
for (const url of fontFace.urls) {
if (!url.embedded) continue;
const dataUrl = await url.embedded;
if (!dataUrl) continue;
fontFaceString = fontFaceString.replace(url.original, dataUrl);
}
css += fontFaceString;
}
return css;
}
}
_init$b = __decoratorStart$b();
__decorateElement$b(_init$b, 1, "onFontFamilyValue", _onFontFamilyValue_dec, FontEmbedder);
__decoratorMetadata$b(_init$b, FontEmbedder);
async function getCurrentDocumentFontFaces() {
const fontFaces = [];
for (const styleSheet of document.styleSheets) {
let cssRules;
try {
cssRules = styleSheet.cssRules;
} catch {
}
if (cssRules) {
for (const rule of styleSheet.cssRules) {
if (rule instanceof CSSFontFaceRule) {
fontFaces.push(parseCssFontFaces(rule.cssText, styleSheet.href ?? document.baseURI));
} else if (rule instanceof CSSImportRule) {
fontFaces.push(fetchCssFontFaces(rule.href));
}
}
} else if (styleSheet.href) {
fontFaces.push(fetchCssFontFaces(styleSheet.href));
}
}
return compact(await Promise.all(fontFaces)).flat();
}
const fetchCssFontFaces = fetchCache(async (response) => {
const parsed = parseCss(await response.text(), response.url);
const importedFontFaces = await Promise.all(
parsed.imports.map(({ url }) => fetchCssFontFaces(new URL(url, response.url).href))
);
return [...parsed.fontFaces, ...compact(importedFontFaces).flat()];
});
function getRenderedChildNodes(node) {
if (node.shadowRoot) {
return node.shadowRoot.childNodes;
}
if (isShadowSlotElement(node)) {
const assignedNodes = node.assignedNodes();
if (assignedNodes?.length) {
return assignedNodes;
}
}
return node.childNodes;
}
function* getRenderedChildren(node) {
for (const child of getRenderedChildNodes(node)) {
if (isElement(child)) yield child;
}
}
function getWindow(node) {
return node.ownerDocument?.defaultView ?? globalThis;
}
function isElement(node) {
return node instanceof getWindow(node).Element;
}
function isShadowRoot(node) {
return node instanceof getWindow(node).ShadowRoot;
}
function isInShadowRoot(node) {
return "getRootNode" in node && isShadowRoot(node.getRootNode());
}
function isShadowSlotElement(node) {
return isInShadowRoot(node) && node instanceof getWindow(node).HTMLSlotElement;
}
function elementStyle(element) {
return element.style;
}
function getComputedStyle$1(element, pseudoElement) {
return getWindow(element).getComputedStyle(element, pseudoElement);
}
const NO_STYLES = {};
class StyleEmbedder {
constructor(root) {
this.root = root;
}
styles = /* @__PURE__ */ new Map();
fonts = new FontEmbedder();
readRootElementStyles(rootElement) {
this.readElementStyles(rootElement, {
shouldRespectDefaults: false,
shouldSkipInheritedParentStyles: false
});
const children = Array.from(getRenderedChildren(rootElement));
while (children.length) {
const child = children.pop();
children.push(...getRenderedChildren(child));
this.readElementStyles(child, {
shouldRespectDefaults: true,
shouldSkipInheritedParentStyles: true
});
}
}
readElementStyles(element, { shouldRespectDefaults = true, shouldSkipInheritedParentStyles = true }) {
const defaultStyles = shouldRespectDefaults ? getDefaultStylesForTagName(element.tagName.toLowerCase()) : NO_STYLES;
const parentStyles = shouldSkipInheritedParentStyles ? this.styles.get(element.parentElement)?.self ?? NO_STYLES : NO_STYLES;
const info = {
self: styleFromElement(element, { defaultStyles, parentStyles }),
before: styleFromPseudoElement(element, "::before"),
after: styleFromPseudoElement(element, "::after")
};
this.styles.set(element, info);
}
fetchResources() {
const promises = [];
for (const info of this.styles.values()) {
for (const styles of objectMapValues(info)) {
if (!styles) continue;
for (const [property, value] of Object.entries(styles)) {
if (!value) continue;
if (property === "font-family") {
this.fonts.onFontFamilyValue(value);
}
const urlMatches = parseCssValueUrls(value);
if (urlMatches.length === 0) continue;
promises.push(
...urlMatches.map(async ({ url, original }) => {
const dataUrl = (await resourceToDataUrl(url)) ?? "data:";
styles[property] = value.replace(original, `url("${dataUrl}")`);
})
);
}
}
}
return Promise.all(promises);
}
// custom elements are tricky. if we serialize the dom as-is, the custom elements wont have
// their shadow-dom contents serialized. after we've read all the styles, we need to unwrap the
// contents of each custom elements shadow dom directly into the parent element itself.
unwrapCustomElements() {
const visited = /* @__PURE__ */ new Set();
const visit = (element, clonedParent) => {
if (visited.has(element)) return;
visited.add(element);
const shadowRoot = element.shadowRoot;
if (shadowRoot) {
const clonedCustomEl = document.createElement("div");
this.styles.set(clonedCustomEl, this.styles.get(element));
clonedCustomEl.setAttribute("data-tl-custom-element", element.tagName);
(clonedParent ?? element.parentElement).appendChild(clonedCustomEl);
for (const child of shadowRoot.childNodes) {
if (child instanceof Element) {
visit(child, clonedCustomEl);
} else {
clonedCustomEl.appendChild(child.cloneNode(true));
}
}
element.remove();
} else if (clonedParent) {
if (element.tagName.toLowerCase() === "style") {
return;
}
const clonedEl = element.cloneNode(false);
this.styles.set(clonedEl, this.styles.get(element));
clonedParent.appendChild(clonedEl);
for (const child of getRenderedChildNodes(element)) {
if (child instanceof Element) {
visit(child, clonedEl);
} else {
clonedEl.appendChild(child.cloneNode(true));
}
}
}
};
for (const element of this.styles.keys()) {
visit(element, null);
}
}
embedStyles() {
let css = "";
for (const [element, info] of this.styles) {
if (info.after || info.before) {
const className = `pseudo-${uniqueId()}`;
element.classList.add(className);
if (info.before) {
css += `.${className}::before {${formatCss(info.before)}}
`;
}
if (info.after) {
css += `.${className}::after {${formatCss(info.after)}}
`;
}
}
const style = elementStyle(element);
for (const [property, value] of Object.entries(info.self)) {
if (!value) continue;
style.setProperty(property, value);
}
if (style.fontKerning === "auto") {
style.fontKerning = "normal";
}
}
return css;
}
async getFontFaceCss() {
return await this.fonts.createCss();
}
dispose() {
destroyDefaultStyleFrame();
}
}
function styleFromElement(element, { defaultStyles, parentStyles }) {
if (element.computedStyleMap) {
return styleFromComputedStyleMap(element.computedStyleMap(), { defaultStyles, parentStyles });
}
return styleFromComputedStyle(getComputedStyle$1(element), { defaultStyles, parentStyles });
}
function styleFromPseudoElement(element, pseudo) {
const style = getComputedStyle$1(element, pseudo);
const content = style.getPropertyValue("content");
if (content === "" || content === "none") {
return void 0;
}
return styleFromComputedStyle(style, { defaultStyles: NO_STYLES, parentStyles: NO_STYLES });
}
function styleFromComputedStyleMap(style, { defaultStyles, parentStyles }) {
const styles = {};
for (const property of style.keys()) {
if (!shouldIncludeCssProperty(property)) continue;
const value = style.get(property).toString();
if (defaultStyles[property] === value) continue;
if (parentStyles[property] === value && isPropertyInherited(property)) continue;
styles[property] = value;
}
return styles;
}
function styleFromComputedStyle(style, { defaultStyles, parentStyles }) {
const styles = {};
for (const property of style) {
if (!shouldIncludeCssProperty(property)) continue;
const value = style.getPropertyValue(property);
if (defaultStyles[property] === value) continue;
if (parentStyles[property] === value && isPropertyInherited(property)) continue;
styles[property] = value;
}
return styles;
}
function formatCss(style) {
let cssText = "";
for (const [property, value] of Object.entries(style)) {
cssText += `${property}: ${value};`;
}
return cssText;
}
let defaultStyleFrame;
const defaultStylesByTagName = {};
function getDefaultStyleFrame() {
if (!defaultStyleFrame) {
const frame = document.createElement("iframe");
frame.style.display = "none";
document.body.appendChild(frame);
const frameDocument = assertExists(frame.contentDocument, "frame must have a document");
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
const foreignObject = document.createElementNS("http://www.w3.org/2000/svg", "foreignObject");
svg.appendChild(foreignObject);
frameDocument.body.appendChild(svg);
defaultStyleFrame = { iframe: frame, foreignObject, document: frameDocument };
}
return defaultStyleFrame;
}
function destroyDefaultStyleFrame() {
if (defaultStyleFrame) {
document.body.removeChild(defaultStyleFrame.iframe);
defaultStyleFrame = void 0;
}
}
const defaultStyleReadOptions = { defaultStyles: NO_STYLES, parentStyles: NO_STYLES };
function getDefaultStylesForTagName(tagName) {
let existing = defaultStylesByTagName[tagName];
if (!existing) {
const { foreignObject, document: document2 } = getDefaultStyleFrame();
const element = document2.createElement(tagName);
foreignObject.appendChild(element);
existing = element.computedStyleMap ? styleFromComputedStyleMap(element.computedStyleMap(), defaultStyleReadOptions) : styleFromComputedStyle(getComputedStyle$1(element), defaultStyleReadOptions);
foreignObject.removeChild(element);
defaultStylesByTagName[tagName] = existing;
}
return existing;
}
function copyAttrs(source, target) {
const attrs = Array.from(source.attributes);
attrs.forEach((attr) => {
target.setAttribute(attr.name, attr.value);
});
}
function replace(original, replacement) {
original.replaceWith(replacement);
return replacement;
}
async function createImage(dataUrl, cloneAttributesFrom) {
const image = document.createElement("img");
if (cloneAttributesFrom) {
copyAttrs(cloneAttributesFrom, image);
}
image.setAttribute("src", dataUrl ?? "data:");
image.setAttribute("decoding", "sync");
image.setAttribute("loading", "eager");
try {
await image.decode();
} catch {
}
return image;
}
async function getCanvasReplacement(canvas) {
try {
const dataURL = canvas.toDataURL();
return await createImage(dataURL, canvas);
} catch {
return await createImage(null, canvas);
}
}
async function getVideoReplacement(video) {
try {
const dataUrl = await MediaHelpers.getVideoFrameAsDataUrl(video);
return createImage(dataUrl, video);
} catch (err) {
console.error("Could not get video frame", err);
}
if (video.poster) {
const dataUrl = await resourceToDataUrl(video.poster);
return createImage(dataUrl, video);
}
return createImage(null, video);
}
async function embedMedia(node) {
if (node instanceof HTMLCanvasElement) {
return replace(node, await getCanvasReplacement(node));
} else if (node instanceof HTMLVideoElement) {
return replace(node, await getVideoReplacement(node));
} else if (node instanceof HTMLImageElement) {
const src = node.currentSrc || node.src;
const dataUrl = await resourceToDataUrl(src);
node.setAttribute("src", dataUrl ?? "data:");
node.setAttribute("decoding", "sync");
node.setAttribute("loading", "eager");
try {
await node.decode();
} catch {
}
return node;
} else if (node instanceof HTMLInputElement) {
node.setAttribute("value", node.value);
} else if (node instanceof HTMLTextAreaElement) {
node.textContent = node.value;
}
await Promise.all(
Array.from(getRenderedChildren(node), (child) => embedMedia(child))
);
}
function useEvent(handler) {
const handlerRef = reactExports.useRef();
reactExports.useLayoutEffect(() => {
handlerRef.current = handler;
});
reactExports.useDebugValue(handler);
return reactExports.useCallback((...args) => {
const fn = handlerRef.current;
assert(fn, "fn does not exist");
return fn(...args);
}, []);
}
const Context = reactExports.createContext(null);
function SvgExportContextProvider({
context,
editor,
children
}) {
const Provider = editor.options.exportProvider;
return /* @__PURE__ */ jsxRuntimeExports.jsx(EditorProvider, { editor, children: /* @__PURE__ */ jsxRuntimeExports.jsx(ContainerProvider, { container: editor.getContainer(), children: /* @__PURE__ */ jsxRuntimeExports.jsx(Context.Provider, { value: context, children: /* @__PURE__ */ jsxRuntimeExports.jsx(Provider, { children }) }) }) });
}
function useSvgExportContext() {
return reactExports.useContext(Context);
}
function useDelaySvgExport() {
const ctx = reactExports.useContext(Context);
const [promise] = reactExports.useState(promiseWithResolve);
reactExports.useEffect(() => {
ctx?.waitUntil(promise);
return () => {
promise.resolve();
};
}, [promise, ctx]);
return useEvent(() => {
promise.resolve();
});
}
var __create$a = Object.create;
var __defProp$a = Object.defineProperty;
var __getOwnPropDesc$a = Object.getOwnPropertyDescriptor;
var __knownSymbol$a = (name, symbol) => (symbol = Symbol[name]) ? symbol : Symbol.for("Symbol." + name);
var __typeError$a = (msg) => {
throw TypeError(msg);
};
var __defNormalProp$a = (obj, key, value) => key in obj ? __defProp$a(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __decoratorStart$a = (base) => [, , , __create$a(null)];
var __decoratorStrings$a = ["class", "method", "getter", "setter", "accessor", "field", "value", "get", "set"];
var __expectFn$a = (fn) => fn !== void 0 && typeof fn !== "function" ? __typeError$a("Function expected") : fn;
var __decoratorContext$a = (kind, name, done, metadata, fns) => ({ kind: __decoratorStrings$a[kind], name, metadata, addInitializer: (fn) => done._ ? __typeError$a("Already initialized") : fns.push(__expectFn$a(fn || null)) });
var __decoratorMetadata$a = (array, target) => __defNormalProp$a(target, __knownSymbol$a("metadata"), array[3]);
var __runInitializers$a = (array, flags, self, value) => {
for (var i = 0, fns = array[flags >> 1], n = fns && fns.length; i < n; i++) fns[i].call(self) ;
return value;
};
var __decorateElement$a = (array, flags, name, decorators, target, extra) => {
var it, done, ctx, access, k = flags & 7, s = false, p = false;
var j = 2 , key = __decoratorStrings$a[k + 5];
var extraInitializers = array[j] || (array[j] = []);
var desc = ((target = target.prototype), __getOwnPropDesc$a(target , name));
for (var i = decorators.length - 1; i >= 0; i--) {
ctx = __decoratorContext$a(k, name, done = {}, array[3], extraInitializers);
{
ctx.static = s, ctx.private = p, access = ctx.access = { has: (x) => name in x };
access.get = (x) => x[name];
}
it = (0, decorators[i])(desc[key] , ctx), done._ = 1;
__expectFn$a(it) && (desc[key] = it );
}
return desc && __defProp$a(target, name, desc), target;
};
var __publicField$a = (obj, key, value) => __defNormalProp$a(obj, typeof key !== "symbol" ? key + "" : key, value);
var _waitUntil_dec, _init$a;
_waitUntil_dec = [bind$2];
class ExportDelay {
constructor(maxDelayTimeMs) {
this.maxDelayTimeMs = maxDelayTimeMs;
__runInitializers$a(_init$a, 5, this);
__publicField$a(this, "isResolved", false);
__publicField$a(this, "promisesToWaitFor", []);
}
waitUntil(promise) {
if (this.isResolved) {
throw new Error(
"Cannot `waitUntil` - the export has already been resolved. Make sure to call `waitUntil` as soon as possible during an export - ie within the first react effect after rendering."
);
}
this.promisesToWaitFor.push(
promise.catch((err) => console.error("Error whilst waiting for export:", err))
);
}
async resolvePromises() {
let lastLength = null;
while (this.promisesToWaitFor.length !== lastLength) {
lastLength = this.promisesToWaitFor.length;
await Promise.allSettled(this.promisesToWaitFor);
await sleep(0);
}
}
async resolve() {
const timeoutPromise = sleep(this.maxDelayTimeMs).then(() => "timeout");
const resolvePromise = this.resolvePromises().then(() => "resolved");
const result = await Promise.race([timeoutPromise, resolvePromise]);
if (result === "timeout") {
console.warn("[tldraw] Export delay timed out after ${this.maxDelayTimeMs}ms");
}
this.isResolved = true;
}
}
_init$a = __decoratorStart$a();
__decorateElement$a(_init$a, 1, "waitUntil", _waitUntil_dec, ExportDelay);
__decoratorMetadata$a(_init$a, ExportDelay);
function getSvgJsx(editor, ids, opts = {}) {
if (!window.document) throw Error("No document");
const {
scale = 1,
// should we include the background in the export? or is it transparent?
background = false,
padding = editor.options.defaultSvgPadding,
preserveAspectRatio
} = opts;
const isDarkMode = opts.darkMode ?? editor.user.getIsDarkMode();
const shapeIdsToInclude = editor.getShapeAndDescendantIds(ids);
const renderingShapes = editor.getUnorderedRenderingShapes(false).filter(({ id }) => shapeIdsToInclude.has(id));
let bbox = null;
if (opts.bounds) {
bbox = opts.bounds;
} else {
for (const { id } of renderingShapes) {
const maskedPageBounds = editor.getShapeMaskedPageBounds(id);
if (!maskedPageBounds) continue;
if (bbox) {
bbox.union(maskedPageBounds);
} else {
bbox = maskedPageBounds.clone();
}
}
}
if (!bbox) return;
const singleFrameShapeId = ids.length === 1 && editor.isShapeOfType(editor.getShape(ids[0]), "frame") ? ids[0] : null;
if (!singleFrameShapeId) {
bbox.expandBy(padding);
}
const w = bbox.width * scale;
const h = bbox.height * scale;
try {
document.body.focus?.();
} catch {
}
const exportDelay = new ExportDelay(editor.options.maxExportDelayMs);
const initialEffectPromise = promiseWithResolve();
exportDelay.waitUntil(initialEffectPromise);
const svg = /* @__PURE__ */ jsxRuntimeExports.jsx(
SvgExport,
{
editor,
preserveAspectRatio,
scale,
bbox,
background,
singleFrameShapeId,
isDarkMode,
renderingShapes,
onMount: initialEffectPromise.resolve,
waitUntil: exportDelay.waitUntil
}
);
return { jsx: svg, width: w, height: h, exportDelay };
}
function SvgExport({
editor,
preserveAspectRatio,
scale,
bbox,
background,
singleFrameShapeId,
isDarkMode,
renderingShapes,
onMount,
waitUntil
}) {
const masksId = useUniqueSafeId();
const theme = getDefaultColorTheme({ isDarkMode });
const stateAtom = useAtom("export state", { defsById: {}, shapeElements: null });
const { defsById, shapeElements } = useValue(stateAtom);
const addExportDef = useEvent((def) => {
stateAtom.update((state) => {
if (hasOwnProperty$1(state.defsById, def.key)) return state;
const promise = Promise.resolve(def.getElement());
waitUntil(
promise.then((result) => {
stateAtom.update((state2) => ({
...state2,
defsById: { ...state2.defsById, [def.key]: { pending: false, element: result } }
}));
})
);
return {
...state,
defsById: { ...state.defsById, [def.key]: { pending: true, element: promise } }
};
});
});
const exportContext = reactExports.useMemo(
() => ({
isDarkMode,
waitUntil,
addExportDef
}),
[isDarkMode, waitUntil, addExportDef]
);
const didRenderRef = reactExports.useRef(false);
reactExports.useLayoutEffect(() => {
if (didRenderRef.current) {
throw new Error("SvgExport should only render once - do not use with react strict mode");
}
didRenderRef.current = true;
(async () => {
const shapeDefs = {};
const unorderedShapeElementPromises = renderingShapes.map(
async ({ id, opacity, index, backgroundIndex }) => {
if (id === singleFrameShapeId) return [];
const shape = editor.getShape(id);
if (editor.isShapeOfType(shape, "group")) return [];
const elements = [];
const util = editor.getShapeUtil(shape);
if (util.toSvg || util.toBackgroundSvg) {
const [toSvgResult, toBackgroundSvgResult] = await Promise.all([
util.toSvg?.(shape, exportContext),
util.toBackgroundSvg?.(shape, exportContext)
]);
const pageTransform = editor.getShapePageTransform(shape);
let pageTransformString = pageTransform.toCssString();
let scale2 = 1;
if ("scale" in shape.props) {
if (shape.props.scale !== 1) {
scale2 = shape.props.scale;
pageTransformString = `${pageTransformString} scale(${shape.props.scale}, ${shape.props.scale})`;
}
}
const pageMask = editor.getShapeMask(shape.id);
const shapeMask = pageMask ? Mat.From(Mat.Inverse(pageTransform)).applyToPoints(pageMask) : null;
const shapeMaskId = suffixSafeId(masksId, shape.id);
if (shapeMask) {
shapeDefs[shapeMaskId] = {
pending: false,
element: /* @__PURE__ */ jsxRuntimeExports.jsx("clipPath", { id: shapeMaskId, children: /* @__PURE__ */ jsxRuntimeExports.jsx(
"path",
{
d: `M${shapeMask.map(({ x, y }) => `${x / scale2},${y / scale2}`).join("L")}Z`
}
) })
};
}
if (toSvgResult) {
elements.push({
zIndex: index,
element: /* @__PURE__ */ jsxRuntimeExports.jsx(
"g",
{
transform: pageTransformString,
opacity,
clipPath: pageMask ? `url(#${shapeMaskId})` : void 0,
children: toSvgResult
},
`fg_${shape.id}`
)
});
}
if (toBackgroundSvgResult) {
elements.push({
zIndex: backgroundIndex,
element: /* @__PURE__ */ jsxRuntimeExports.jsx(
"g",
{
transform: pageTransformString,
opacity,
clipPath: pageMask ? `url(#${shapeMaskId})` : void 0,
children: toBackgroundSvgResult
},
`bg_${shape.id}`
)
});
}
} else {
elements.push({
zIndex: index,
element: /* @__PURE__ */ jsxRuntimeExports.jsx(
ForeignObjectShape,
{
shape,
util,
component: InnerShape,
className: "tl-shape",
bbox,
opacity
},
`fg_${shape.id}`
)
});
if (util.backgroundComponent) {
elements.push({
zIndex: backgroundIndex,
element: /* @__PURE__ */ jsxRuntimeExports.jsx(
ForeignObjectShape,
{
shape,
util,
component: InnerShapeBackground,
className: "tl-shape tl-shape-background",
bbox,
opacity
},
`bg_${shape.id}`
)
});
}
}
return elements;
}
);
const unorderedShapeElements = (await Promise.all(unorderedShapeElementPromises)).flat();
reactDomExports.flushSync(() => {
stateAtom.update((state) => ({
...state,
shapeElements: unorderedShapeElements.sort((a, b) => a.zIndex - b.zIndex).map(({ element }) => element),
defsById: { ...state.defsById, ...shapeDefs }
}));
});
})();
}, [bbox, editor, exportContext, masksId, renderingShapes, singleFrameShapeId, stateAtom]);
reactExports.useEffect(() => {
if (shapeElements === null) return;
onMount();
}, [onMount, shapeElements]);
return /* @__PURE__ */ jsxRuntimeExports.jsx(SvgExportContextProvider, { editor, context: exportContext, children: /* @__PURE__ */ jsxRuntimeExports.jsxs(
"svg",
{
preserveAspectRatio,
direction: "ltr",
width: bbox.width * scale,
height: bbox.height * scale,
viewBox: `${bbox.minX} ${bbox.minY} ${bbox.width} ${bbox.height}`,
strokeLinecap: "round",
strokeLinejoin: "round",
style: {
backgroundColor: background ? singleFrameShapeId ? theme.solid : theme.background : "transparent"
},
"data-color-mode": isDarkMode ? "dark" : "light",
className: `tl-container tl-theme__force-sRGB ${isDarkMode ? "tl-theme__dark" : "tl-theme__light"}`,
children: [
/* @__PURE__ */ jsxRuntimeExports.jsx("defs", { children: Object.entries(defsById).map(
([key, def]) => def.pending ? null : /* @__PURE__ */ jsxRuntimeExports.jsx(reactExports.Fragment, { children: def.element }, key)
) }),
shapeElements
]
}
) });
}
function ForeignObjectShape({
shape,
util,
className,
component: Component,
bbox,
opacity
}) {
const editor = useEditor();
const transform = Mat.Translate(-bbox.minX, -bbox.minY).multiply(
editor.getShapePageTransform(shape.id)
);
const bounds = editor.getShapeGeometry(shape.id).bounds;
const width = Math.max(bounds.width, 1);
const height = Math.max(bounds.height, 1);
return /* @__PURE__ */ jsxRuntimeExports.jsx(ErrorBoundary, { fallback: () => null, children: /* @__PURE__ */ jsxRuntimeExports.jsx(
"foreignObject",
{
x: bbox.minX,
y: bbox.minY,
width: bbox.w,
height: bbox.h,
className: "tl-shape-foreign-object",
children: /* @__PURE__ */ jsxRuntimeExports.jsx(
"div",
{
className,
"data-shape-type": shape.type,
style: {
clipPath: editor.getShapeClipPath(shape.id),
transform: transform.toCssString(),
width,
height,
opacity
},
children: /* @__PURE__ */ jsxRuntimeExports.jsx(Component, { shape, util })
}
)
}
) });
}
let idCounter$1 = 1;
async function exportToSvg(editor, shapeIds, opts = {}) {
const result = getSvgJsx(editor, shapeIds, opts);
if (!result) return void 0;
const container = editor.getContainer();
const renderTarget = document.createElement("div");
renderTarget.className = "tldraw-svg-export";
renderTarget.inert = true;
renderTarget.tabIndex = -1;
Object.assign(renderTarget.style, {
position: "absolute",
top: "0px",
left: "0px",
width: result.width + "px",
height: result.height + "px",
pointerEvents: "none",
opacity: 0
});
container.appendChild(renderTarget);
const root = createRoot(renderTarget, { identifierPrefix: `export_${idCounter$1++}_` });
try {
await Promise.resolve();
reactDomExports.flushSync(() => {
root.render(result.jsx);
});
await result.exportDelay.resolve();
const svg = renderTarget.firstElementChild;
assert(svg instanceof SVGSVGElement, "Expected an SVG element");
await applyChangesToForeignObjects(svg);
return { svg, width: result.width, height: result.height };
} finally {
setTimeout(() => {
root.unmount();
container.removeChild(renderTarget);
}, 0);
}
}
async function applyChangesToForeignObjects(svg) {
const foreignObjectChildren = [
...svg.querySelectorAll("foreignObject.tl-shape-foreign-object > *")
];
if (!foreignObjectChildren.length) return;
const styleEmbedder = new StyleEmbedder(svg);
try {
styleEmbedder.fonts.startFindingCurrentDocumentFontFaces();
await Promise.all(foreignObjectChildren.map((el) => embedMedia(el)));
for (const el of foreignObjectChildren) {
styleEmbedder.readRootElementStyles(el);
}
await styleEmbedder.fetchResources();
const fontCss = await styleEmbedder.getFontFaceCss();
styleEmbedder.unwrapCustomElements();
const pseudoCss = styleEmbedder.embedStyles();
if (fontCss || pseudoCss) {
const style = document.createElementNS("http://www.w3.org/2000/svg", "style");
style.textContent = `${fontCss}
${pseudoCss}`;
svg.prepend(style);
}
} finally {
styleEmbedder.dispose();
}
}
const tlmenus = {
/**
* A set of strings representing any open menus. When menus are open,
* certain interactions will behave differently; for example, when a
* draw tool is selected and a menu is open, a pointer-down will not
* create a dot (because the user is probably trying to close the menu)
* however a pointer-down event followed by a drag will begin drawing
* a line (because the user is BOTH trying to close the menu AND start
* drawing a line).
*
* @public
*/
menus: atom("open menus", []),
/**
* Get the current open menus.
*
* @param contextId - An optional context to get menus for.
*
* @public
*/
getOpenMenus(contextId) {
if (contextId) return this.menus.get().filter((m) => m.endsWith("-" + contextId));
return this.menus.get();
},
/**
* Add an open menu.
*
* @example
* ```ts
* addOpenMenu('menu-id')
* addOpenMenu('menu-id', myEditorId)
* ```
*
* @param id - The id of the menu to add.
* @param contextId - An optional context to add the menu to.
*
* @public
*/
addOpenMenu(id, contextId = "") {
const idWithContext = contextId ? `${id}-${contextId}` : id;
const menus = new Set(this.menus.get());
if (!menus.has(idWithContext)) {
menus.add(idWithContext);
this.menus.set([...menus]);
}
},
/**
* Delete an open menu.
*
* @example
* ```ts
* deleteOpenMenu('menu-id')
* deleteOpenMenu('menu-id', myEditorId)
* ```
*
* @param id - The id of the menu to delete.
* @param contextId - An optional context to delete the menu from.
*
* @public
*/
deleteOpenMenu(id, contextId = "") {
const idWithContext = contextId ? `${id}-${contextId}` : id;
const menus = new Set(this.menus.get());
if (menus.has(idWithContext)) {
menus.delete(idWithContext);
this.menus.set([...menus]);
}
},
/**
* Clear all open menus.
*
* @example
* ```ts
* clearOpenMenus()
* clearOpenMenus(myEditorId)
* ```
*
* @param contextId - An optional context to clear menus for.
*
* @public
*/
clearOpenMenus(contextId) {
this.menus.set(contextId ? this.menus.get().filter((m) => !m.endsWith("-" + contextId)) : []);
},
_hiddenMenus: [],
/**
* Hide all open menus. Restore them with the `showOpenMenus` method.
*
* @example
* ```ts
* hideOpenMenus()
* hideOpenMenus(myEditorId)
* ```
*
* @param contextId - An optional context to hide menus for.
*
* @public
*/
hideOpenMenus(contextId) {
this._hiddenMenus = [...this.getOpenMenus(contextId)];
if (this._hiddenMenus.length === 0) return;
for (const menu of this._hiddenMenus) {
this.deleteOpenMenu(menu, contextId);
}
},
/**
* Show all hidden menus.
*
* @example
* ```ts
* showOpenMenus()
* showOpenMenus(myEditorId)
* ```
*
* @param contextId - An optional context to show menus for.
*
* @public
*/
showOpenMenus(contextId) {
if (this._hiddenMenus.length === 0) return;
for (const menu of this._hiddenMenus) {
this.addOpenMenu(menu, contextId);
}
this._hiddenMenus = [];
},
/**
* Get whether a menu is open for a given context.
*
* @example
* ```ts
* isMenuOpem(id, myEditorId)
* ```
*
* @param id - The id of the menu to check.
* @param contextId - An optional context to check menus for.
*
* @public
*/
isMenuOpen(id, contextId) {
return this.getOpenMenus(contextId).includes(id);
},
/**
* Get whether any menus are open for a given context.
*
* @example
* ```ts
* hasOpenMenus(myEditorId)
* ```
*
* @param contextId - A context to check menus for.
*
* @public
*/
hasOpenMenus(contextId) {
return this.getOpenMenus(contextId).length > 0;
},
/**
* Get whether any menus are open for any context.
*
* @example
* ```ts
* hasAnyOpenMenus()
* ```
*
* @public
*/
hasAnyOpenMenus() {
return this.getOpenMenus().length > 0;
},
forContext(contextId) {
return {
getOpenMenus: () => this.getOpenMenus(contextId),
addOpenMenu: (id) => this.addOpenMenu(id, contextId),
deleteOpenMenu: (id) => this.deleteOpenMenu(id, contextId),
clearOpenMenus: () => this.clearOpenMenus(contextId),
// Gets whether any menus are open
isMenuOpen: (id) => this.isMenuOpen(id, contextId),
hasOpenMenus: () => this.hasOpenMenus(contextId),
hasAnyOpenMenus: () => this.hasAnyOpenMenus()
};
}
};
const tltime = new Timers();
const defaultTldrawOptions = {
maxShapesPerPage: 4e3,
maxFilesAtOnce: 100,
maxPages: 40,
animationMediumMs: 320,
followChaseViewportSnap: 2,
doubleClickDurationMs: 450,
multiClickDurationMs: 200,
coarseDragDistanceSquared: 36,
// 6 squared
dragDistanceSquared: 16,
// 4 squared
defaultSvgPadding: 32,
cameraSlideFriction: 0.09,
maxPointsPerDrawShape: 500,
gridSteps: [
{ min: -1, mid: 0.15, step: 64 },
{ min: 0.05, mid: 0.375, step: 16 },
{ min: 0.15, mid: 1, step: 4 },
{ min: 0.7, mid: 2.5, step: 1 }
],
collaboratorInactiveTimeoutMs: 6e4,
collaboratorIdleTimeoutMs: 3e3,
collaboratorCheckIntervalMs: 1200,
cameraMovingTimeoutMs: 64,
hitTestMargin: 8,
edgeScrollDelay: 200,
edgeScrollEaseDuration: 200,
edgeScrollSpeed: 25,
edgeScrollDistance: 8,
coarsePointerWidth: 12,
coarseHandleRadius: 20,
handleRadius: 12,
longPressDurationMs: 500,
textShadowLod: 0.35,
adjacentShapeMargin: 10,
flattenImageBoundsExpand: 64,
flattenImageBoundsPadding: 16,
laserDelayMs: 1200,
maxExportDelayMs: 5e3,
temporaryAssetPreviewLifetimeMs: 18e4,
actionShortcutsLocation: "swap",
createTextOnCanvasDoubleClick: true,
exportProvider: reactExports.Fragment
};
function sharedStyleEquals(a, b) {
if (!b) return false;
switch (a.type) {
case "mixed":
return b.type === "mixed";
case "shared":
return b.type === "shared" && a.value === b.value;
default:
throw exhaustiveSwitchError(a);
}
}
class ReadonlySharedStyleMap {
/** @internal */
map;
constructor(entries) {
this.map = new Map(entries);
}
get(prop) {
return this.map.get(prop);
}
getAsKnownValue(prop) {
const value = this.get(prop);
if (!value) return void 0;
if (value.type === "mixed") return void 0;
return value.value;
}
// eslint-disable-next-line no-restricted-syntax
get size() {
return this.map.size;
}
equals(other) {
if (this.size !== other.size) return false;
const checkedKeys = /* @__PURE__ */ new Set();
for (const [styleProp, value] of this) {
if (!sharedStyleEquals(value, other.get(styleProp))) return false;
checkedKeys.add(styleProp);
}
for (const [styleProp, value] of other) {
if (checkedKeys.has(styleProp)) continue;
if (!sharedStyleEquals(value, this.get(styleProp))) return false;
}
return true;
}
keys() {
return this.map.keys();
}
values() {
return this.map.values();
}
entries() {
return this.map.entries();
}
[Symbol.iterator]() {
return this.map[Symbol.iterator]();
}
}
class SharedStyleMap extends ReadonlySharedStyleMap {
set(prop, value) {
this.map.set(prop, value);
}
applyValue(prop, value) {
const existingValue = this.get(prop);
if (!existingValue) {
this.set(prop, { type: "shared", value });
return;
}
switch (existingValue.type) {
case "mixed":
return;
case "shared":
if (existingValue.value !== value) {
this.set(prop, { type: "mixed" });
}
return;
default:
exhaustiveSwitchError(existingValue, "type");
}
}
}
function dataUrlToFile(url, filename, mimeType) {
return fetch(url).then(function(res) {
return res.arrayBuffer();
}).then(function(buf) {
return new File([buf], filename, { type: mimeType });
});
}
const CDN_BASE_URL = "https://cdn.tldraw.com";
function getDefaultCdnBaseUrl() {
return `${CDN_BASE_URL}/${version}`;
}
function createDeepLinkString(deepLink) {
switch (deepLink.type) {
case "shapes": {
const ids = deepLink.shapeIds.map((id) => encodeId(id.slice("shape:".length)));
return `s${ids.join(".")}`;
}
case "page": {
return "p" + encodeId(PageRecordType.parseId(deepLink.pageId));
}
case "viewport": {
const { bounds, pageId } = deepLink;
let res = `v${Math.round(bounds.x)}.${Math.round(bounds.y)}.${Math.round(bounds.w)}.${Math.round(bounds.h)}`;
if (pageId) {
res += "." + encodeId(PageRecordType.parseId(pageId));
}
return res;
}
default:
exhaustiveSwitchError(deepLink);
}
}
function parseDeepLinkString(deepLinkString) {
const type = deepLinkString[0];
switch (type) {
case "s": {
const shapeIds = deepLinkString.slice(1).split(".").filter(Boolean).map((id) => createShapeId(decodeURIComponent(id)));
return { type: "shapes", shapeIds };
}
case "p": {
const pageId = PageRecordType.createId(decodeURIComponent(deepLinkString.slice(1)));
return { type: "page", pageId };
}
case "v": {
const [x, y, w, h, pageId] = deepLinkString.slice(1).split(".");
return {
type: "viewport",
bounds: new Box(Number(x), Number(y), Number(w), Number(h)),
pageId: pageId ? PageRecordType.createId(decodeURIComponent(pageId)) : void 0
};
}
default:
throw Error("Invalid deep link string");
}
}
function encodeId(str) {
return encodeURIComponent(str).replace(/\./g, "%2E");
}
function getIncrementedName(name, others) {
let result = name;
const set = new Set(others);
while (set.has(result)) {
result = /^.*(\d+)$/.exec(result)?.[1] ? result.replace(/(\d+)(?=\D?)$/, (m) => {
return (+m + 1).toString();
}) : `${result} 1`;
}
return result;
}
function getReorderingShapesChanges(editor, operation, ids, opts) {
if (ids.length === 0) return [];
const parents = /* @__PURE__ */ new Map();
for (const shape of compact(ids.map((id) => editor.getShape(id)))) {
const { parentId } = shape;
if (!parents.has(parentId)) {
parents.set(parentId, {
children: compact(
editor.getSortedChildIdsForParent(parentId).map((id) => editor.getShape(id))
),
moving: /* @__PURE__ */ new Set()
});
}
parents.get(parentId).moving.add(shape);
}
const changes = [];
switch (operation) {
case "toBack": {
parents.forEach(({ moving, children }) => reorderToBack(moving, children, changes));
break;
}
case "toFront": {
parents.forEach(({ moving, children }) => reorderToFront(moving, children, changes));
break;
}
case "forward": {
parents.forEach(
({ moving, children }) => reorderForward(editor, moving, children, changes, opts)
);
break;
}
case "backward": {
parents.forEach(
({ moving, children }) => reorderBackward(editor, moving, children, changes, opts)
);
break;
}
}
return changes;
}
function reorderToBack(moving, children, changes) {
const len = children.length;
if (moving.size === len) return;
let below;
let above;
for (let i = 0; i < len; i++) {
const shape = children[i];
if (moving.has(shape)) {
below = shape.index;
moving.delete(shape);
} else {
above = shape.index;
break;
}
}
if (moving.size === 0) {
return;
} else {
const indices = getIndicesBetween(below, above, moving.size);
changes.push(
...Array.from(moving.values()).sort(sortByIndex$1).map((shape, i) => ({ ...shape, index: indices[i] }))
);
}
}
function reorderToFront(moving, children, changes) {
const len = children.length;
if (moving.size === len) return;
let below;
let above;
for (let i = len - 1; i > -1; i--) {
const shape = children[i];
if (moving.has(shape)) {
above = shape.index;
moving.delete(shape);
} else {
below = shape.index;
break;
}
}
if (moving.size === 0) {
return;
} else {
const indices = getIndicesBetween(below, above, moving.size);
changes.push(
...Array.from(moving.values()).sort(sortByIndex$1).map((shape, i) => ({ ...shape, index: indices[i] }))
);
}
}
function getVerticesInPageSpace(editor, shape) {
const geo = editor.getShapeGeometry(shape);
const pageTransform = editor.getShapePageTransform(shape);
if (!geo || !pageTransform) return null;
return pageTransform.applyToPoints(geo.vertices);
}
function getOverlapChecker(editor, moving) {
const movingVertices = Array.from(moving).map((shape) => {
const vertices = getVerticesInPageSpace(editor, shape);
if (!vertices) return null;
return { shape, vertices };
}).filter(Boolean);
const isOverlapping = (child) => {
const vertices = getVerticesInPageSpace(editor, child);
if (!vertices) return false;
return movingVertices.some((other) => {
return polygonsIntersect(other.vertices, vertices);
});
};
return isOverlapping;
}
function reorderForward(editor, moving, children, changes, opts) {
const isOverlapping = getOverlapChecker(editor, moving);
const len = children.length;
if (moving.size === len) return;
let state = { name: "skipping" };
for (let i = 0; i < len; i++) {
const isMoving = moving.has(children[i]);
switch (state.name) {
case "skipping": {
if (!isMoving) continue;
state = { name: "selecting", selectIndex: i };
break;
}
case "selecting": {
if (isMoving) continue;
if (!opts?.considerAllShapes && !isOverlapping(children[i])) continue;
const { selectIndex } = state;
getIndicesBetween(children[i].index, children[i + 1]?.index, i - selectIndex).forEach(
(index, k) => {
const child = children[selectIndex + k];
if (!moving.has(child)) return;
changes.push({ ...child, index });
}
);
state = { name: "skipping" };
break;
}
}
}
}
function reorderBackward(editor, moving, children, changes, opts) {
const isOverlapping = getOverlapChecker(editor, moving);
const len = children.length;
if (moving.size === len) return;
let state = { name: "skipping" };
for (let i = len - 1; i > -1; i--) {
const isMoving = moving.has(children[i]);
switch (state.name) {
case "skipping": {
if (!isMoving) continue;
state = { name: "selecting", selectIndex: i };
break;
}
case "selecting": {
if (isMoving) continue;
if (!opts?.considerAllShapes && !isOverlapping(children[i])) continue;
getIndicesBetween(children[i - 1]?.index, children[i].index, state.selectIndex - i).forEach(
(index, k) => {
const child = children[i + k + 1];
if (!moving.has(child)) return;
changes.push({ ...child, index });
}
);
state = { name: "skipping" };
break;
}
}
}
}
function getRotationSnapshot({
editor,
ids
}) {
const shapes = compact(ids.map((id) => editor.getShape(id)));
const rotation = editor.getShapesSharedRotation(ids);
const rotatedPageBounds = editor.getShapesRotatedPageBounds(ids);
if (!rotatedPageBounds) {
return null;
}
const pageCenter = rotatedPageBounds.center.clone().rotWith(rotatedPageBounds.point, rotation);
return {
pageCenter,
initialCursorAngle: pageCenter.angle(editor.inputs.originPagePoint),
initialShapesRotation: rotation,
shapeSnapshots: shapes.map((shape) => ({
shape,
initialPagePoint: editor.getShapePageTransform(shape.id).point()
}))
};
}
function applyRotationToSnapshotShapes({
delta,
editor,
snapshot,
stage,
centerOverride
}) {
const { pageCenter, shapeSnapshots } = snapshot;
editor.updateShapes(
shapeSnapshots.map(({ shape, initialPagePoint }) => {
const parentTransform = isShapeId(shape.parentId) ? editor.getShapePageTransform(shape.parentId) : Mat.Identity();
const newPagePoint = Vec.RotWith(initialPagePoint, centerOverride ?? pageCenter, delta);
const newLocalPoint = Mat.applyToPoint(
// use the current parent transform in case it has moved/resized since the start
// (e.g. if rotating a shape at the edge of a group)
Mat.Inverse(parentTransform),
newPagePoint
);
const newRotation = canonicalizeRotation(shape.rotation + delta);
return {
id: shape.id,
type: shape.type,
x: newLocalPoint.x,
y: newLocalPoint.y,
rotation: newRotation
};
})
);
const changes = [];
shapeSnapshots.forEach(({ shape }) => {
const current = editor.getShape(shape.id);
if (!current) return;
const util = editor.getShapeUtil(shape);
if (stage === "start" || stage === "one-off") {
const changeStart = util.onRotateStart?.(shape);
if (changeStart) changes.push(changeStart);
}
const changeUpdate = util.onRotate?.(shape, current);
if (changeUpdate) changes.push(changeUpdate);
if (stage === "end" || stage === "one-off") {
const changeEnd = util.onRotateEnd?.(shape, current);
if (changeEnd) changes.push(changeEnd);
}
});
if (changes.length > 0) {
editor.updateShapes(changes);
}
}
const bindingsIndex = (editor) => {
const { store } = editor;
const bindingsHistory = store.query.filterHistory("binding");
const bindingsQuery = store.query.records("binding");
function fromScratch() {
const allBindings = bindingsQuery.get();
const shape2Binding = /* @__PURE__ */ new Map();
for (const binding of allBindings) {
const { fromId, toId } = binding;
const bindingsForFromShape = shape2Binding.get(fromId);
if (!bindingsForFromShape) {
shape2Binding.set(fromId, [binding]);
} else {
bindingsForFromShape.push(binding);
}
const bindingsForToShape = shape2Binding.get(toId);
if (!bindingsForToShape) {
shape2Binding.set(toId, [binding]);
} else {
bindingsForToShape.push(binding);
}
}
return shape2Binding;
}
return computed("arrowBindingsIndex", (_lastValue, lastComputedEpoch) => {
if (isUninitialized(_lastValue)) {
return fromScratch();
}
const lastValue = _lastValue;
const diff = bindingsHistory.getDiffSince(lastComputedEpoch);
if (diff === RESET_VALUE) {
return fromScratch();
}
let nextValue = void 0;
function removingBinding(binding) {
nextValue ??= new Map(lastValue);
const prevFrom = nextValue.get(binding.fromId);
const nextFrom = prevFrom?.filter((b) => b.id !== binding.id);
if (!nextFrom?.length) {
nextValue.delete(binding.fromId);
} else {
nextValue.set(binding.fromId, nextFrom);
}
const prevTo = nextValue.get(binding.toId);
const nextTo = prevTo?.filter((b) => b.id !== binding.id);
if (!nextTo?.length) {
nextValue.delete(binding.toId);
} else {
nextValue.set(binding.toId, nextTo);
}
}
function ensureNewArray(shapeId) {
nextValue ??= new Map(lastValue);
let result = nextValue.get(shapeId);
if (!result) {
result = [];
nextValue.set(shapeId, result);
} else if (result === lastValue.get(shapeId)) {
result = result.slice(0);
nextValue.set(shapeId, result);
}
return result;
}
function addBinding(binding) {
ensureNewArray(binding.fromId).push(binding);
ensureNewArray(binding.toId).push(binding);
}
for (const changes of diff) {
for (const newBinding of objectMapValues(changes.added)) {
addBinding(newBinding);
}
for (const [prev, next] of objectMapValues(changes.updated)) {
removingBinding(prev);
addBinding(next);
}
for (const prev of objectMapValues(changes.removed)) {
removingBinding(prev);
}
}
return nextValue ?? lastValue;
});
};
function isShapeNotVisible(editor, id, viewportPageBounds) {
const maskedPageBounds = editor.getShapeMaskedPageBounds(id);
if (maskedPageBounds === void 0) return true;
return !viewportPageBounds.includes(maskedPageBounds);
}
const notVisibleShapes = (editor) => {
function fromScratch(editor2) {
const shapes = editor2.getCurrentPageShapeIds();
const viewportPageBounds = editor2.getViewportPageBounds();
const notVisibleShapes2 = /* @__PURE__ */ new Set();
shapes.forEach((id) => {
if (isShapeNotVisible(editor2, id, viewportPageBounds)) {
notVisibleShapes2.add(id);
}
});
return notVisibleShapes2;
}
return computed("getCulledShapes", (prevValue) => {
if (isUninitialized(prevValue)) {
return fromScratch(editor);
}
const nextValue = fromScratch(editor);
if (prevValue.size !== nextValue.size) return nextValue;
for (const prev of prevValue) {
if (!nextValue.has(prev)) {
return nextValue;
}
}
return prevValue;
});
};
const parentsToChildren = (store) => {
const shapeIdsQuery = store.query.ids("shape");
const shapeHistory = store.query.filterHistory("shape");
function fromScratch() {
const result = {};
const shapeIds = shapeIdsQuery.get();
const shapes = Array(shapeIds.size);
shapeIds.forEach((id) => shapes.push(store.get(id)));
shapes.sort(sortByIndex$1);
shapes.forEach((shape) => {
if (!result[shape.parentId]) {
result[shape.parentId] = [];
}
result[shape.parentId].push(shape.id);
});
return result;
}
return computed(
"parentsToChildrenWithIndexes",
(lastValue, lastComputedEpoch) => {
if (isUninitialized(lastValue)) {
return fromScratch();
}
const diff = shapeHistory.getDiffSince(lastComputedEpoch);
if (diff === RESET_VALUE) {
return fromScratch();
}
if (diff.length === 0) return lastValue;
let newValue = null;
const ensureNewArray = (parentId) => {
if (!newValue) {
newValue = { ...lastValue };
}
if (!newValue[parentId]) {
newValue[parentId] = [];
} else if (newValue[parentId] === lastValue[parentId]) {
newValue[parentId] = [...newValue[parentId]];
}
};
const toSort = /* @__PURE__ */ new Set();
let changes;
for (let i = 0, n = diff.length; i < n; i++) {
changes = diff[i];
for (const record of Object.values(changes.added)) {
if (!isShape(record)) continue;
ensureNewArray(record.parentId);
newValue[record.parentId].push(record.id);
toSort.add(newValue[record.parentId]);
}
for (const [from, to] of Object.values(changes.updated)) {
if (!isShape(to)) continue;
if (!isShape(from)) continue;
if (from.parentId !== to.parentId) {
ensureNewArray(from.parentId);
ensureNewArray(to.parentId);
newValue[from.parentId].splice(newValue[from.parentId].indexOf(to.id), 1);
newValue[to.parentId].push(to.id);
toSort.add(newValue[to.parentId]);
} else if (from.index !== to.index) {
ensureNewArray(to.parentId);
const idx = newValue[to.parentId].indexOf(to.id);
newValue[to.parentId][idx] = to.id;
toSort.add(newValue[to.parentId]);
}
}
for (const record of Object.values(changes.removed)) {
if (!isShape(record)) continue;
ensureNewArray(record.parentId);
newValue[record.parentId].splice(newValue[record.parentId].indexOf(record.id), 1);
}
}
for (const arr of toSort) {
const shapesInArr = compact(arr.map((id) => store.get(id)));
shapesInArr.sort(sortByIndex$1);
arr.splice(0, arr.length, ...shapesInArr.map((shape) => shape.id));
}
return newValue ?? lastValue;
}
);
};
const isShapeInPage = (store, pageId, shape) => {
while (!isPageId(shape.parentId)) {
const parent = store.get(shape.parentId);
if (!parent) return false;
shape = parent;
}
return shape.parentId === pageId;
};
const deriveShapeIdsInCurrentPage = (store, getCurrentPageId) => {
const shapesIndex = store.query.ids("shape");
let lastPageId = null;
function fromScratch() {
const currentPageId = getCurrentPageId();
lastPageId = currentPageId;
return new Set(
[...shapesIndex.get()].filter((id) => isShapeInPage(store, currentPageId, store.get(id)))
);
}
return computed("_shapeIdsInCurrentPage", (prevValue, lastComputedEpoch) => {
if (isUninitialized(prevValue)) {
return fromScratch();
}
const currentPageId = getCurrentPageId();
if (currentPageId !== lastPageId) {
return fromScratch();
}
const diff = store.history.getDiffSince(lastComputedEpoch);
if (diff === RESET_VALUE) {
return fromScratch();
}
const builder = new IncrementalSetConstructor(
prevValue
);
for (const changes of diff) {
for (const record of Object.values(changes.added)) {
if (isShape(record) && isShapeInPage(store, currentPageId, record)) {
builder.add(record.id);
}
}
for (const [_from, to] of Object.values(changes.updated)) {
if (isShape(to)) {
if (isShapeInPage(store, currentPageId, to)) {
builder.add(to.id);
} else {
builder.remove(to.id);
}
}
}
for (const id of Object.keys(changes.removed)) {
if (isShapeId(id)) {
builder.remove(id);
}
}
}
const result = builder.get();
if (!result) {
return prevValue;
}
return withDiff(result.value, result.diff);
});
};
var __create$9 = Object.create;
var __defProp$9 = Object.defineProperty;
var __getOwnPropDesc$9 = Object.getOwnPropertyDescriptor;
var __knownSymbol$9 = (name, symbol) => (symbol = Symbol[name]) ? symbol : Symbol.for("Symbol." + name);
var __typeError$9 = (msg) => {
throw TypeError(msg);
};
var __defNormalProp$9 = (obj, key, value) => key in obj ? __defProp$9(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __decoratorStart$9 = (base) => [, , , __create$9(null)];
var __decoratorStrings$9 = ["class", "method", "getter", "setter", "accessor", "field", "value", "get", "set"];
var __expectFn$9 = (fn) => fn !== void 0 && typeof fn !== "function" ? __typeError$9("Function expected") : fn;
var __decoratorContext$9 = (kind, name, done, metadata, fns) => ({ kind: __decoratorStrings$9[kind], name, metadata, addInitializer: (fn) => done._ ? __typeError$9("Already initialized") : fns.push(__expectFn$9(fn || null)) });
var __decoratorMetadata$9 = (array, target) => __defNormalProp$9(target, __knownSymbol$9("metadata"), array[3]);
var __runInitializers$9 = (array, flags, self, value) => {
for (var i = 0, fns = array[flags >> 1], n = fns && fns.length; i < n; i++) fns[i].call(self) ;
return value;
};
var __decorateElement$9 = (array, flags, name, decorators, target, extra) => {
var it, done, ctx, access, k = flags & 7, s = false, p = false;
var j = 2 , key = __decoratorStrings$9[k + 5];
var extraInitializers = array[j] || (array[j] = []);
var desc = ((target = target.prototype), __getOwnPropDesc$9(target , name));
for (var i = decorators.length - 1; i >= 0; i--) {
ctx = __decoratorContext$9(k, name, done = {}, array[3], extraInitializers);
{
ctx.static = s, ctx.private = p, access = ctx.access = { has: (x) => name in x };
access.get = (x) => x[name];
}
it = (0, decorators[i])(desc[key] , ctx), done._ = 1;
__expectFn$9(it) && (desc[key] = it );
}
return desc && __defProp$9(target, name, desc), target;
};
var __publicField$9 = (obj, key, value) => __defNormalProp$9(obj, typeof key !== "symbol" ? key + "" : key, value);
var _cancelDoubleClickTimeout_dec, __getClickTimeout_dec, _init$9;
const MAX_CLICK_DISTANCE = 40;
__getClickTimeout_dec = [bind$2], _cancelDoubleClickTimeout_dec = [bind$2];
class ClickManager {
constructor(editor) {
this.editor = editor;
__runInitializers$9(_init$9, 5, this);
__publicField$9(this, "_clickId", "");
__publicField$9(this, "_clickTimeout");
__publicField$9(this, "_clickScreenPoint");
__publicField$9(this, "_previousScreenPoint");
/**
* The current click state.
*
* @internal
*/
__publicField$9(this, "_clickState", "idle");
__publicField$9(this, "lastPointerInfo", {});
}
_getClickTimeout(state, id = uniqueId()) {
this._clickId = id;
clearTimeout(this._clickTimeout);
this._clickTimeout = this.editor.timers.setTimeout(
() => {
if (this._clickState === state && this._clickId === id) {
switch (this._clickState) {
case "pendingTriple": {
this.editor.dispatch({
...this.lastPointerInfo,
type: "click",
name: "double_click",
phase: "settle"
});
break;
}
case "pendingQuadruple": {
this.editor.dispatch({
...this.lastPointerInfo,
type: "click",
name: "triple_click",
phase: "settle"
});
break;
}
case "pendingOverflow": {
this.editor.dispatch({
...this.lastPointerInfo,
type: "click",
name: "quadruple_click",
phase: "settle"
});
break;
}
}
this._clickState = "idle";
}
},
state === "idle" || state === "pendingDouble" ? this.editor.options.doubleClickDurationMs : this.editor.options.multiClickDurationMs
);
}
/**
* The current click state.
*
* @public
*/
// eslint-disable-next-line no-restricted-syntax
get clickState() {
return this._clickState;
}
handlePointerEvent(info) {
switch (info.name) {
case "pointer_down": {
if (!this._clickState) return info;
this._clickScreenPoint = Vec.From(info.point);
if (this._previousScreenPoint && Vec.Dist2(this._previousScreenPoint, this._clickScreenPoint) > MAX_CLICK_DISTANCE ** 2) {
this._clickState = "idle";
}
this._previousScreenPoint = this._clickScreenPoint;
this.lastPointerInfo = info;
switch (this._clickState) {
case "pendingDouble": {
this._clickState = "pendingTriple";
this._clickTimeout = this._getClickTimeout(this._clickState);
return {
...info,
type: "click",
name: "double_click",
phase: "down"
};
}
case "pendingTriple": {
this._clickState = "pendingQuadruple";
this._clickTimeout = this._getClickTimeout(this._clickState);
return {
...info,
type: "click",
name: "triple_click",
phase: "down"
};
}
case "pendingQuadruple": {
this._clickState = "pendingOverflow";
this._clickTimeout = this._getClickTimeout(this._clickState);
return {
...info,
type: "click",
name: "quadruple_click",
phase: "down"
};
}
case "idle": {
this._clickState = "pendingDouble";
break;
}
case "pendingOverflow": {
this._clickState = "overflow";
break;
}
}
this._clickTimeout = this._getClickTimeout(this._clickState);
return info;
}
case "pointer_up": {
if (!this._clickState) return info;
this._clickScreenPoint = Vec.From(info.point);
switch (this._clickState) {
case "pendingTriple": {
return {
...this.lastPointerInfo,
type: "click",
name: "double_click",
phase: "up"
};
}
case "pendingQuadruple": {
return {
...this.lastPointerInfo,
type: "click",
name: "triple_click",
phase: "up"
};
}
case "pendingOverflow": {
return {
...this.lastPointerInfo,
type: "click",
name: "quadruple_click",
phase: "up"
};
}
}
return info;
}
case "pointer_move": {
if (this._clickState !== "idle" && this._clickScreenPoint && Vec.Dist2(this._clickScreenPoint, this.editor.inputs.currentScreenPoint) > (this.editor.getInstanceState().isCoarsePointer ? this.editor.options.coarseDragDistanceSquared : this.editor.options.dragDistanceSquared)) {
this.cancelDoubleClickTimeout();
}
return info;
}
}
return info;
}
cancelDoubleClickTimeout() {
this._clickTimeout = clearTimeout(this._clickTimeout);
this._clickState = "idle";
}
}
_init$9 = __decoratorStart$9();
__decorateElement$9(_init$9, 1, "_getClickTimeout", __getClickTimeout_dec, ClickManager);
__decorateElement$9(_init$9, 1, "cancelDoubleClickTimeout", _cancelDoubleClickTimeout_dec, ClickManager);
__decoratorMetadata$9(_init$9, ClickManager);
class EdgeScrollManager {
constructor(editor) {
this.editor = editor;
}
_isEdgeScrolling = false;
_edgeScrollDuration = -1;
/**
* Update the camera position when the mouse is close to the edge of the screen.
* Run this on every tick when in a state where edge scrolling is enabled.
*
* @public
*/
updateEdgeScrolling(elapsed) {
const { editor } = this;
const edgeScrollProximityFactor = this.getEdgeScroll();
if (edgeScrollProximityFactor.x === 0 && edgeScrollProximityFactor.y === 0) {
if (this._isEdgeScrolling) {
this._isEdgeScrolling = false;
this._edgeScrollDuration = 0;
}
} else {
if (!this._isEdgeScrolling) {
this._isEdgeScrolling = true;
this._edgeScrollDuration = 0;
}
this._edgeScrollDuration += elapsed;
if (this._edgeScrollDuration > editor.options.edgeScrollDelay) {
const eased = editor.options.edgeScrollEaseDuration > 0 ? EASINGS.easeInCubic(
Math.min(
1,
this._edgeScrollDuration / (editor.options.edgeScrollDelay + editor.options.edgeScrollEaseDuration)
)
) : 1;
this.moveCameraWhenCloseToEdge({
x: edgeScrollProximityFactor.x * eased,
y: edgeScrollProximityFactor.y * eased
});
}
}
}
/**
* Helper function to get the scroll proximity factor for a given position.
* @param position - The mouse position on the axis.
* @param dimension - The component dimension on the axis.
* @param isCoarse - Whether the pointer is coarse.
* @param insetStart - Whether the pointer is inset at the start of the axis.
* @param insetEnd - Whether the pointer is inset at the end of the axis.
* @internal
*/
getEdgeProximityFactors(position, dimension, isCoarse, insetStart, insetEnd) {
const { editor } = this;
const dist = editor.options.edgeScrollDistance;
const pw = isCoarse ? editor.options.coarsePointerWidth : 0;
const pMin = position - pw;
const pMax = position + pw;
const min = insetStart ? 0 : dist;
const max = insetEnd ? dimension : dimension - dist;
if (pMin < min) {
return Math.min(1, (min - pMin) / dist);
} else if (pMax > max) {
return -Math.min(1, (pMax - max) / dist);
}
return 0;
}
getEdgeScroll() {
const { editor } = this;
const {
inputs: {
currentScreenPoint: { x, y }
}
} = editor;
const screenBounds = editor.getViewportScreenBounds();
const {
isCoarsePointer,
insets: [t, r, b, l]
} = editor.getInstanceState();
const proximityFactorX = this.getEdgeProximityFactors(x, screenBounds.w, isCoarsePointer, l, r);
const proximityFactorY = this.getEdgeProximityFactors(y, screenBounds.h, isCoarsePointer, t, b);
return {
x: proximityFactorX,
y: proximityFactorY
};
}
/**
* Moves the camera when the mouse is close to the edge of the screen.
* @public
*/
moveCameraWhenCloseToEdge(proximityFactor) {
const { editor } = this;
if (!editor.inputs.isDragging || editor.inputs.isPanning || editor.getCameraOptions().isLocked)
return;
if (proximityFactor.x === 0 && proximityFactor.y === 0) return;
const screenBounds = editor.getViewportScreenBounds();
const screenSizeFactorX = screenBounds.w < 1e3 ? 0.612 : 1;
const screenSizeFactorY = screenBounds.h < 1e3 ? 0.612 : 1;
const zoomLevel = editor.getZoomLevel();
const pxSpeed = editor.user.getEdgeScrollSpeed() * editor.options.edgeScrollSpeed;
const scrollDeltaX = pxSpeed * proximityFactor.x * screenSizeFactorX / zoomLevel;
const scrollDeltaY = pxSpeed * proximityFactor.y * screenSizeFactorY / zoomLevel;
const { x, y, z } = editor.getCamera();
editor.setCamera(new Vec(x + scrollDeltaX, y + scrollDeltaY, z));
}
}
class FocusManager {
constructor(editor, autoFocus) {
this.editor = editor;
this.disposeSideEffectListener = editor.sideEffects.registerAfterChangeHandler(
"instance",
(prev, next) => {
if (prev.isFocused !== next.isFocused) {
this.updateContainerClass();
}
}
);
const currentFocusState = editor.getInstanceState().isFocused;
if (autoFocus !== currentFocusState) {
editor.updateInstanceState({ isFocused: !!autoFocus });
}
this.updateContainerClass();
}
disposeSideEffectListener;
/**
* The editor's focus state and the container's focus state
* are not necessarily always in sync. For that reason we
* can't rely on the css `:focus` or `:focus-within` selectors to style the
* editor when it is in focus.
*
* For that reason we synchronize the editor's focus state with a
* special class on the container: tl-container__focused
*/
updateContainerClass() {
const container = this.editor.getContainer();
const instanceState = this.editor.getInstanceState();
if (instanceState.isFocused) {
container.classList.add("tl-container__focused");
} else {
container.classList.remove("tl-container__focused");
}
}
focus() {
this.editor.getContainer().focus();
}
blur() {
this.editor.complete();
this.editor.getContainer().blur();
}
dispose() {
this.disposeSideEffectListener?.();
}
}
function stack(items) {
return EMPTY_STACK_ITEM;
}
class EmptyStackItem {
length = 0;
head = null;
tail = this;
push(head) {
return new StackItem(head, this);
}
toArray() {
return EMPTY_ARRAY;
}
[Symbol.iterator]() {
return {
next() {
return { value: void 0, done: true };
}
};
}
}
const EMPTY_STACK_ITEM = new EmptyStackItem();
class StackItem {
constructor(head, tail) {
this.head = head;
this.tail = tail;
this.length = tail.length + 1;
}
length;
push(head) {
return new StackItem(head, this);
}
toArray() {
return Array.from(this);
}
[Symbol.iterator]() {
let stack2 = this;
return {
next() {
if (stack2.length) {
const value = stack2.head;
stack2 = stack2.tail;
return { value, done: false };
} else {
return { value: void 0, done: true };
}
}
};
}
}
class HistoryManager {
store;
dispose;
state = "recording" /* Recording */;
pendingDiff = new PendingDiff();
stacks = atom(
"HistoryManager.stacks",
{
undos: stack(),
redos: stack()
},
{
isEqual: (a, b) => a.undos === b.undos && a.redos === b.redos
}
);
annotateError;
constructor(opts) {
this.store = opts.store;
this.annotateError = opts.annotateError ?? noop$2;
this.dispose = this.store.addHistoryInterceptor((entry, source) => {
if (source !== "user") return;
switch (this.state) {
case "recording" /* Recording */:
this.pendingDiff.apply(entry.changes);
this.stacks.update(({ undos }) => ({ undos, redos: stack() }));
break;
case "recordingPreserveRedoStack" /* RecordingPreserveRedoStack */:
this.pendingDiff.apply(entry.changes);
break;
case "paused" /* Paused */:
break;
default:
exhaustiveSwitchError(this.state);
}
});
}
flushPendingDiff() {
if (this.pendingDiff.isEmpty()) return;
const diff = this.pendingDiff.clear();
this.stacks.update(({ undos, redos }) => ({
undos: undos.push({ type: "diff", diff }),
redos
}));
}
getNumUndos() {
return this.stacks.get().undos.length + (this.pendingDiff.isEmpty() ? 0 : 1);
}
getNumRedos() {
return this.stacks.get().redos.length;
}
/** @internal */
_isInBatch = false;
batch(fn, opts) {
const previousState = this.state;
if (previousState !== "paused" /* Paused */ && opts?.history) {
this.state = modeToState[opts.history];
}
try {
if (this._isInBatch) {
transact(fn);
return this;
}
this._isInBatch = true;
try {
transact(fn);
} catch (error) {
this.annotateError(error);
throw error;
} finally {
this._isInBatch = false;
}
return this;
} finally {
this.state = previousState;
}
}
// History
_undo({ pushToRedoStack, toMark = void 0 }) {
const previousState = this.state;
this.state = "paused" /* Paused */;
try {
let { undos, redos } = this.stacks.get();
const pendingDiff = this.pendingDiff.clear();
const isPendingDiffEmpty = isRecordsDiffEmpty(pendingDiff);
const diffToUndo = reverseRecordsDiff(pendingDiff);
if (pushToRedoStack && !isPendingDiffEmpty) {
redos = redos.push({ type: "diff", diff: pendingDiff });
}
let didFindMark = false;
if (isPendingDiffEmpty) {
while (undos.head?.type === "stop") {
const mark = undos.head;
undos = undos.tail;
if (pushToRedoStack) {
redos = redos.push(mark);
}
if (mark.id === toMark) {
didFindMark = true;
break;
}
}
}
if (!didFindMark) {
loop: while (undos.head) {
const undo = undos.head;
undos = undos.tail;
if (pushToRedoStack) {
redos = redos.push(undo);
}
switch (undo.type) {
case "diff":
squashRecordDiffsMutable(diffToUndo, [reverseRecordsDiff(undo.diff)]);
break;
case "stop":
if (!toMark) break loop;
if (undo.id === toMark) {
didFindMark = true;
break loop;
}
break;
default:
exhaustiveSwitchError(undo);
}
}
}
if (!didFindMark && toMark) {
return this;
}
this.store.applyDiff(diffToUndo, { ignoreEphemeralKeys: true });
this.store.ensureStoreIsUsable();
this.stacks.set({ undos, redos });
} finally {
this.state = previousState;
}
return this;
}
undo() {
this._undo({ pushToRedoStack: true });
return this;
}
redo() {
const previousState = this.state;
this.state = "paused" /* Paused */;
try {
this.flushPendingDiff();
let { undos, redos } = this.stacks.get();
if (redos.length === 0) {
return this;
}
while (redos.head?.type === "stop") {
undos = undos.push(redos.head);
redos = redos.tail;
}
const diffToRedo = createEmptyRecordsDiff();
while (redos.head) {
const redo = redos.head;
undos = undos.push(redo);
redos = redos.tail;
if (redo.type === "diff") {
squashRecordDiffsMutable(diffToRedo, [redo.diff]);
} else {
break;
}
}
this.store.applyDiff(diffToRedo, { ignoreEphemeralKeys: true });
this.store.ensureStoreIsUsable();
this.stacks.set({ undos, redos });
} finally {
this.state = previousState;
}
return this;
}
bail() {
this._undo({ pushToRedoStack: false });
return this;
}
bailToMark(id) {
this._undo({ pushToRedoStack: false, toMark: id });
return this;
}
squashToMark(id) {
let top = this.stacks.get().undos;
const popped = [];
while (top.head && !(top.head.type === "stop" && top.head.id === id)) {
if (top.head.type === "diff") {
popped.push(top.head.diff);
}
top = top.tail;
}
if (!top.head || top.head?.id !== id) {
console.error("Could not find mark to squash to: ", id);
return this;
}
if (popped.length === 0) {
return this;
}
const diff = createEmptyRecordsDiff();
squashRecordDiffsMutable(diff, popped.reverse());
this.stacks.update(({ redos }) => ({
undos: top.push({
type: "diff",
diff
}),
redos
}));
return this;
}
/** @internal */
_mark(id) {
transact(() => {
this.flushPendingDiff();
this.stacks.update(({ undos, redos }) => ({ undos: undos.push({ type: "stop", id }), redos }));
});
}
clear() {
this.stacks.set({ undos: stack(), redos: stack() });
this.pendingDiff.clear();
}
/** @internal */
getMarkIdMatching(idSubstring) {
let top = this.stacks.get().undos;
while (top.head) {
if (top.head.type === "stop" && top.head.id.includes(idSubstring)) {
return top.head.id;
}
top = top.tail;
}
return null;
}
/** @internal */
debug() {
const { undos, redos } = this.stacks.get();
return {
undos: undos.toArray(),
redos: redos.toArray(),
pendingDiff: this.pendingDiff.debug(),
state: this.state
};
}
}
const modeToState = {
record: "recording" /* Recording */,
"record-preserveRedoStack": "recordingPreserveRedoStack" /* RecordingPreserveRedoStack */,
ignore: "paused" /* Paused */
};
class PendingDiff {
diff = createEmptyRecordsDiff();
isEmptyAtom = atom("PendingDiff.isEmpty", true);
clear() {
const diff = this.diff;
this.diff = createEmptyRecordsDiff();
this.isEmptyAtom.set(true);
return diff;
}
isEmpty() {
return this.isEmptyAtom.get();
}
apply(diff) {
squashRecordDiffsMutable(this.diff, [diff]);
this.isEmptyAtom.set(isRecordsDiffEmpty(this.diff));
}
debug() {
return { diff: this.diff, isEmpty: this.isEmpty() };
}
}
class ScribbleManager {
constructor(editor) {
this.editor = editor;
}
scribbleItems = /* @__PURE__ */ new Map();
state = "paused";
addScribble(scribble, id = uniqueId()) {
const item = {
id,
scribble: {
id,
size: 20,
color: "accent",
opacity: 0.8,
delay: 0,
points: [],
shrink: 0.1,
taper: true,
...scribble,
state: "starting"
},
timeoutMs: 0,
delayRemaining: scribble.delay ?? 0,
prev: null,
next: null
};
this.scribbleItems.set(id, item);
return item;
}
reset() {
this.editor.updateInstanceState({ scribbles: [] });
this.scribbleItems.clear();
}
/**
* Start stopping the scribble. The scribble won't be removed until its last point is cleared.
*
* @public
*/
stop(id) {
const item = this.scribbleItems.get(id);
if (!item) throw Error(`Scribble with id ${id} not found`);
item.delayRemaining = Math.min(item.delayRemaining, 200);
item.scribble.state = "stopping";
return item;
}
/**
* Set the scribble's next point.
*
* @param id - The id of the scribble to add a point to.
* @param x - The x coordinate of the point.
* @param y - The y coordinate of the point.
* @param z - The z coordinate of the point.
* @public
*/
addPoint(id, x, y, z = 0.5) {
const item = this.scribbleItems.get(id);
if (!item) throw Error(`Scribble with id ${id} not found`);
const { prev } = item;
const point = { x, y, z };
if (!prev || Vec.Dist(prev, point) >= 1) {
item.next = point;
}
return item;
}
/**
* Update on each animation frame.
*
* @param elapsed - The number of milliseconds since the last tick.
* @public
*/
tick(elapsed) {
if (this.scribbleItems.size === 0) return;
this.editor.run(() => {
this.scribbleItems.forEach((item) => {
if (item.scribble.state === "starting") {
const { next: next2, prev: prev2 } = item;
if (next2 && next2 !== prev2) {
item.prev = next2;
item.scribble.points.push(next2);
}
if (item.scribble.points.length > 8) {
item.scribble.state = "active";
}
return;
}
if (item.delayRemaining > 0) {
item.delayRemaining = Math.max(0, item.delayRemaining - elapsed);
}
item.timeoutMs += elapsed;
if (item.timeoutMs >= 16) {
item.timeoutMs = 0;
}
const { delayRemaining, timeoutMs, prev, next, scribble } = item;
switch (scribble.state) {
case "active": {
if (next && next !== prev) {
item.prev = next;
scribble.points.push(next);
if (delayRemaining === 0) {
if (scribble.points.length > 8) {
scribble.points.shift();
}
}
} else {
if (timeoutMs === 0) {
if (scribble.points.length > 1) {
scribble.points.shift();
} else {
item.delayRemaining = scribble.delay;
}
}
}
break;
}
case "stopping": {
if (item.delayRemaining === 0) {
if (timeoutMs === 0) {
if (scribble.points.length === 1) {
this.scribbleItems.delete(item.id);
return;
}
if (scribble.shrink) {
scribble.size = Math.max(1, scribble.size * (1 - scribble.shrink));
}
scribble.points.shift();
}
}
break;
}
}
});
this.editor.updateInstanceState({
scribbles: Array.from(this.scribbleItems.values()).map(({ scribble }) => ({
...scribble,
points: [...scribble.points]
})).slice(-5)
// limit to three as a minor sanity check
});
});
}
}
var __create$8 = Object.create;
var __defProp$8 = Object.defineProperty;
var __getOwnPropDesc$8 = Object.getOwnPropertyDescriptor;
var __knownSymbol$8 = (name, symbol) => (symbol = Symbol[name]) ? symbol : Symbol.for("Symbol." + name);
var __typeError$8 = (msg) => {
throw TypeError(msg);
};
var __defNormalProp$8 = (obj, key, value) => key in obj ? __defProp$8(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __decoratorStart$8 = (base) => [, , , __create$8(null)];
var __decoratorStrings$8 = ["class", "method", "getter", "setter", "accessor", "field", "value", "get", "set"];
var __expectFn$8 = (fn) => fn !== void 0 && typeof fn !== "function" ? __typeError$8("Function expected") : fn;
var __decoratorContext$8 = (kind, name, done, metadata, fns) => ({ kind: __decoratorStrings$8[kind], name, metadata, addInitializer: (fn) => done._ ? __typeError$8("Already initialized") : fns.push(__expectFn$8(fn || null)) });
var __decoratorMetadata$8 = (array, target) => __defNormalProp$8(target, __knownSymbol$8("metadata"), array[3]);
var __runInitializers$8 = (array, flags, self, value) => {
for (var i = 0, fns = array[flags >> 1], n = fns && fns.length; i < n; i++) fns[i].call(self) ;
return value;
};
var __decorateElement$8 = (array, flags, name, decorators, target, extra) => {
var it, done, ctx, access, k = flags & 7, s = false, p = false;
var j = 2 , key = __decoratorStrings$8[k + 5];
var extraInitializers = array[j] || (array[j] = []);
var desc = ((target = target.prototype), __getOwnPropDesc$8(target , name));
for (var i = decorators.length - 1; i >= 0; i--) {
ctx = __decoratorContext$8(k, name, done = {}, array[3], extraInitializers);
{
ctx.static = s, ctx.private = p, access = ctx.access = { has: (x) => name in x };
access.get = (x) => x[name];
}
it = (0, decorators[i])(desc[key] , ctx), done._ = 1;
__expectFn$8(it) && (desc[key] = it );
}
return desc && __defProp$8(target, name, desc), target;
};
var __publicField$8 = (obj, key, value) => __defNormalProp$8(obj, key + "" , value);
var _getVisibleGaps_dec, _getSnappableGapNodes_dec, _getSnappablePoints_dec, _getSnapPointsCache_dec, _init$8;
const round = (x) => {
const decimalPlacesTolerance = 8;
return Math.round(x * 10 ** decimalPlacesTolerance) / 10 ** decimalPlacesTolerance;
};
function findAdjacentGaps(gaps, shapeId, gapLength, direction, intersection) {
const matches = gaps.filter(
(gap) => (direction === "forward" ? gap.startNode.id === shapeId : gap.endNode.id === shapeId) && round(gap.length) === round(gapLength) && rangeIntersection(
gap.breadthIntersection[0],
gap.breadthIntersection[1],
intersection[0],
intersection[1]
)
);
if (matches.length === 0) return [];
const nextNodes = /* @__PURE__ */ new Set();
matches.forEach((match) => {
const node = direction === "forward" ? match.endNode.id : match.startNode.id;
if (!nextNodes.has(node)) {
nextNodes.add(node);
const foundGaps = findAdjacentGaps(
gaps,
node,
gapLength,
direction,
rangeIntersection(
match.breadthIntersection[0],
match.breadthIntersection[1],
intersection[0],
intersection[1]
)
);
matches.push(...foundGaps);
}
});
return matches;
}
function dedupeGapSnaps(snaps) {
snaps.sort((a, b) => b.gaps.length - a.gaps.length);
for (let i = snaps.length - 1; i > 0; i--) {
const snap = snaps[i];
for (let j = i - 1; j >= 0; j--) {
const otherSnap = snaps[j];
if (otherSnap.direction === snap.direction && snap.gaps.every(
(gap) => otherSnap.gaps.some(
(otherGap) => round(gap.startEdge[0].x) === round(otherGap.startEdge[0].x) && round(gap.startEdge[0].y) === round(otherGap.startEdge[0].y) && round(gap.startEdge[1].x) === round(otherGap.startEdge[1].x) && round(gap.startEdge[1].y) === round(otherGap.startEdge[1].y)
) && otherSnap.gaps.some(
(otherGap) => round(gap.endEdge[0].x) === round(otherGap.endEdge[0].x) && round(gap.endEdge[0].y) === round(otherGap.endEdge[0].y) && round(gap.endEdge[1].x) === round(otherGap.endEdge[1].x) && round(gap.endEdge[1].y) === round(otherGap.endEdge[1].y)
)
)) {
snaps.splice(i, 1);
break;
}
}
}
}
_getSnapPointsCache_dec = [computed], _getSnappablePoints_dec = [computed], _getSnappableGapNodes_dec = [computed], _getVisibleGaps_dec = [computed];
class BoundsSnaps {
constructor(manager) {
this.manager = manager;
__runInitializers$8(_init$8, 5, this);
__publicField$8(this, "editor");
this.editor = manager.editor;
}
getSnapPointsCache() {
const { editor } = this;
return editor.store.createComputedCache("snapPoints", (shape) => {
const pageTransform = editor.getShapePageTransform(shape.id);
if (!pageTransform) return void 0;
const boundsSnapGeometry = editor.getShapeUtil(shape).getBoundsSnapGeometry(shape);
const snapPoints = boundsSnapGeometry.points ?? editor.getShapeGeometry(shape).bounds.cornersAndCenter;
if (!pageTransform || !snapPoints) return void 0;
return snapPoints.map((point, i) => {
const { x, y } = Mat.applyToPoint(pageTransform, point);
return { x, y, id: `${shape.id}:${i}` };
});
});
}
getSnapPoints(shapeId) {
return this.getSnapPointsCache().get(shapeId) ?? [];
}
getSnappablePoints() {
const snapPointsCache = this.getSnapPointsCache();
const snappableShapes = this.manager.getSnappableShapes();
const result = [];
for (const shapeId of snappableShapes) {
const snapPoints = snapPointsCache.get(shapeId);
if (snapPoints) {
result.push(...snapPoints);
}
}
return result;
}
getSnappableGapNodes() {
return Array.from(this.manager.getSnappableShapes(), (shapeId) => ({
id: shapeId,
pageBounds: assertExists(this.editor.getShapePageBounds(shapeId))
}));
}
getVisibleGaps() {
const horizontal = [];
const vertical = [];
let startNode, endNode;
const sortedShapesOnCurrentPageHorizontal = this.getSnappableGapNodes().sort((a, b) => {
return a.pageBounds.minX - b.pageBounds.minX;
});
for (let i = 0; i < sortedShapesOnCurrentPageHorizontal.length; i++) {
startNode = sortedShapesOnCurrentPageHorizontal[i];
for (let j = i + 1; j < sortedShapesOnCurrentPageHorizontal.length; j++) {
endNode = sortedShapesOnCurrentPageHorizontal[j];
if (
// is there space between the boxes
startNode.pageBounds.maxX < endNode.pageBounds.minX && // and they overlap in the y axis
rangesOverlap(
startNode.pageBounds.minY,
startNode.pageBounds.maxY,
endNode.pageBounds.minY,
endNode.pageBounds.maxY
)
) {
horizontal.push({
startNode,
endNode,
startEdge: [
new Vec(startNode.pageBounds.maxX, startNode.pageBounds.minY),
new Vec(startNode.pageBounds.maxX, startNode.pageBounds.maxY)
],
endEdge: [
new Vec(endNode.pageBounds.minX, endNode.pageBounds.minY),
new Vec(endNode.pageBounds.minX, endNode.pageBounds.maxY)
],
length: endNode.pageBounds.minX - startNode.pageBounds.maxX,
breadthIntersection: rangeIntersection(
startNode.pageBounds.minY,
startNode.pageBounds.maxY,
endNode.pageBounds.minY,
endNode.pageBounds.maxY
)
});
}
}
}
const sortedShapesOnCurrentPageVertical = sortedShapesOnCurrentPageHorizontal.sort((a, b) => {
return a.pageBounds.minY - b.pageBounds.minY;
});
for (let i = 0; i < sortedShapesOnCurrentPageVertical.length; i++) {
startNode = sortedShapesOnCurrentPageVertical[i];
for (let j = i + 1; j < sortedShapesOnCurrentPageVertical.length; j++) {
endNode = sortedShapesOnCurrentPageVertical[j];
if (
// is there space between the boxes
startNode.pageBounds.maxY < endNode.pageBounds.minY && // do they overlap in the x axis
rangesOverlap(
startNode.pageBounds.minX,
startNode.pageBounds.maxX,
endNode.pageBounds.minX,
endNode.pageBounds.maxX
)
) {
vertical.push({
startNode,
endNode,
startEdge: [
new Vec(startNode.pageBounds.minX, startNode.pageBounds.maxY),
new Vec(startNode.pageBounds.maxX, startNode.pageBounds.maxY)
],
endEdge: [
new Vec(endNode.pageBounds.minX, endNode.pageBounds.minY),
new Vec(endNode.pageBounds.maxX, endNode.pageBounds.minY)
],
length: endNode.pageBounds.minY - startNode.pageBounds.maxY,
breadthIntersection: rangeIntersection(
startNode.pageBounds.minX,
startNode.pageBounds.maxX,
endNode.pageBounds.minX,
endNode.pageBounds.maxX
)
});
}
}
}
return { horizontal, vertical };
}
snapTranslateShapes({
lockedAxis,
initialSelectionPageBounds,
initialSelectionSnapPoints,
dragDelta
}) {
const snapThreshold = this.manager.getSnapThreshold();
const visibleSnapPointsNotInSelection = this.getSnappablePoints();
const selectionPageBounds = initialSelectionPageBounds.clone().translate(dragDelta);
const selectionSnapPoints = initialSelectionSnapPoints.map(
({ x, y }, i) => ({
id: "selection:" + i,
x: x + dragDelta.x,
y: y + dragDelta.y
})
);
const otherNodeSnapPoints = visibleSnapPointsNotInSelection;
const nearestSnapsX = [];
const nearestSnapsY = [];
const minOffset = new Vec(snapThreshold, snapThreshold);
this.collectPointSnaps({
minOffset,
nearestSnapsX,
nearestSnapsY,
otherNodeSnapPoints,
selectionSnapPoints
});
this.collectGapSnaps({
selectionPageBounds,
nearestSnapsX,
nearestSnapsY,
minOffset
});
const nudge = new Vec(
lockedAxis === "x" ? 0 : nearestSnapsX[0]?.nudge ?? 0,
lockedAxis === "y" ? 0 : nearestSnapsY[0]?.nudge ?? 0
);
minOffset.x = 0;
minOffset.y = 0;
nearestSnapsX.length = 0;
nearestSnapsY.length = 0;
selectionSnapPoints.forEach((s) => {
s.x += nudge.x;
s.y += nudge.y;
});
selectionPageBounds.translate(nudge);
this.collectPointSnaps({
minOffset,
nearestSnapsX,
nearestSnapsY,
otherNodeSnapPoints,
selectionSnapPoints
});
this.collectGapSnaps({
selectionPageBounds,
nearestSnapsX,
nearestSnapsY,
minOffset
});
const pointSnapsLines = this.getPointSnapLines({
nearestSnapsX,
nearestSnapsY
});
const gapSnapLines = this.getGapSnapLines({
selectionPageBounds,
nearestSnapsX,
nearestSnapsY
});
this.manager.setIndicators([...gapSnapLines, ...pointSnapsLines]);
return { nudge };
}
snapResizeShapes({
initialSelectionPageBounds,
dragDelta,
handle: originalHandle,
isAspectRatioLocked,
isResizingFromCenter
}) {
const snapThreshold = this.manager.getSnapThreshold();
const {
box: unsnappedResizedPageBounds,
scaleX,
scaleY
} = Box.Resize(
initialSelectionPageBounds,
originalHandle,
isResizingFromCenter ? dragDelta.x * 2 : dragDelta.x,
isResizingFromCenter ? dragDelta.y * 2 : dragDelta.y,
isAspectRatioLocked
);
let handle = originalHandle;
if (scaleX < 0) {
handle = flipSelectionHandleX(handle);
}
if (scaleY < 0) {
handle = flipSelectionHandleY(handle);
}
if (isResizingFromCenter) {
unsnappedResizedPageBounds.center = initialSelectionPageBounds.center;
}
const isXLocked = handle === "top" || handle === "bottom";
const isYLocked = handle === "left" || handle === "right";
const selectionSnapPoints = getResizeSnapPointsForHandle(handle, unsnappedResizedPageBounds);
const otherNodeSnapPoints = this.getSnappablePoints();
const nearestSnapsX = [];
const nearestSnapsY = [];
const minOffset = new Vec(snapThreshold, snapThreshold);
this.collectPointSnaps({
minOffset,
nearestSnapsX,
nearestSnapsY,
otherNodeSnapPoints,
selectionSnapPoints
});
const nudge = new Vec(
isXLocked ? 0 : nearestSnapsX[0]?.nudge ?? 0,
isYLocked ? 0 : nearestSnapsY[0]?.nudge ?? 0
);
if (isAspectRatioLocked && isSelectionCorner(handle) && nudge.len() !== 0) {
const primaryNudgeAxis = nearestSnapsX.length && nearestSnapsY.length ? Math.abs(nudge.x) < Math.abs(nudge.y) ? "x" : "y" : nearestSnapsX.length ? "x" : "y";
const ratio = initialSelectionPageBounds.aspectRatio;
if (primaryNudgeAxis === "x") {
nearestSnapsY.length = 0;
nudge.y = nudge.x / ratio;
if (handle === "bottom_left" || handle === "top_right") {
nudge.y = -nudge.y;
}
} else {
nearestSnapsX.length = 0;
nudge.x = nudge.y * ratio;
if (handle === "bottom_left" || handle === "top_right") {
nudge.x = -nudge.x;
}
}
}
const snappedDelta = Vec.Add(dragDelta, nudge);
const { box: snappedResizedPageBounds } = Box.Resize(
initialSelectionPageBounds,
originalHandle,
isResizingFromCenter ? snappedDelta.x * 2 : snappedDelta.x,
isResizingFromCenter ? snappedDelta.y * 2 : snappedDelta.y,
isAspectRatioLocked
);
if (isResizingFromCenter) {
snappedResizedPageBounds.center = initialSelectionPageBounds.center;
}
const snappedSelectionPoints = getResizeSnapPointsForHandle("any", snappedResizedPageBounds);
nearestSnapsX.length = 0;
nearestSnapsY.length = 0;
minOffset.x = 0;
minOffset.y = 0;
this.collectPointSnaps({
minOffset,
nearestSnapsX,
nearestSnapsY,
otherNodeSnapPoints,
selectionSnapPoints: snappedSelectionPoints
});
const pointSnaps = this.getPointSnapLines({
nearestSnapsX,
nearestSnapsY
});
this.manager.setIndicators([...pointSnaps]);
return { nudge };
}
collectPointSnaps({
selectionSnapPoints,
otherNodeSnapPoints,
minOffset,
nearestSnapsX,
nearestSnapsY
}) {
for (const thisSnapPoint of selectionSnapPoints) {
for (const otherSnapPoint of otherNodeSnapPoints) {
const offset = Vec.Sub(thisSnapPoint, otherSnapPoint);
const offsetX = Math.abs(offset.x);
const offsetY = Math.abs(offset.y);
if (round(offsetX) <= round(minOffset.x)) {
if (round(offsetX) < round(minOffset.x)) {
nearestSnapsX.length = 0;
}
nearestSnapsX.push({
type: "points",
points: { thisPoint: thisSnapPoint, otherPoint: otherSnapPoint },
nudge: otherSnapPoint.x - thisSnapPoint.x
});
minOffset.x = offsetX;
}
if (round(offsetY) <= round(minOffset.y)) {
if (round(offsetY) < round(minOffset.y)) {
nearestSnapsY.length = 0;
}
nearestSnapsY.push({
type: "points",
points: { thisPoint: thisSnapPoint, otherPoint: otherSnapPoint },
nudge: otherSnapPoint.y - thisSnapPoint.y
});
minOffset.y = offsetY;
}
}
}
}
collectGapSnaps({
selectionPageBounds,
minOffset,
nearestSnapsX,
nearestSnapsY
}) {
const { horizontal, vertical } = this.getVisibleGaps();
for (const gap of horizontal) {
if (!rangesOverlap(
gap.breadthIntersection[0],
gap.breadthIntersection[1],
selectionPageBounds.minY,
selectionPageBounds.maxY
)) {
continue;
}
const gapMidX = gap.startEdge[0].x + gap.length / 2;
const centerNudge = gapMidX - selectionPageBounds.center.x;
const gapIsLargerThanSelection = gap.length > selectionPageBounds.width;
if (gapIsLargerThanSelection && round(Math.abs(centerNudge)) <= round(minOffset.x)) {
if (round(Math.abs(centerNudge)) < round(minOffset.x)) {
nearestSnapsX.length = 0;
}
minOffset.x = Math.abs(centerNudge);
const snap = {
type: "gap_center",
gap,
nudge: centerNudge
};
const otherCenterSnap = nearestSnapsX.find(({ type }) => type === "gap_center");
const gapBreadthsOverlap = otherCenterSnap && rangeIntersection(
gap.breadthIntersection[0],
gap.breadthIntersection[1],
otherCenterSnap.gap.breadthIntersection[0],
otherCenterSnap.gap.breadthIntersection[1]
);
if (otherCenterSnap && otherCenterSnap.gap.length > gap.length && gapBreadthsOverlap) {
nearestSnapsX[nearestSnapsX.indexOf(otherCenterSnap)] = snap;
} else if (!otherCenterSnap || !gapBreadthsOverlap) {
nearestSnapsX.push(snap);
}
}
const duplicationLeftX = gap.startNode.pageBounds.minX - gap.length;
const selectionRightX = selectionPageBounds.maxX;
const duplicationLeftNudge = duplicationLeftX - selectionRightX;
if (round(Math.abs(duplicationLeftNudge)) <= round(minOffset.x)) {
if (round(Math.abs(duplicationLeftNudge)) < round(minOffset.x)) {
nearestSnapsX.length = 0;
}
minOffset.x = Math.abs(duplicationLeftNudge);
nearestSnapsX.push({
type: "gap_duplicate",
gap,
protrusionDirection: "left",
nudge: duplicationLeftNudge
});
}
const duplicationRightX = gap.endNode.pageBounds.maxX + gap.length;
const selectionLeftX = selectionPageBounds.minX;
const duplicationRightNudge = duplicationRightX - selectionLeftX;
if (round(Math.abs(duplicationRightNudge)) <= round(minOffset.x)) {
if (round(Math.abs(duplicationRightNudge)) < round(minOffset.x)) {
nearestSnapsX.length = 0;
}
minOffset.x = Math.abs(duplicationRightNudge);
nearestSnapsX.push({
type: "gap_duplicate",
gap,
protrusionDirection: "right",
nudge: duplicationRightNudge
});
}
}
for (const gap of vertical) {
if (!rangesOverlap(
gap.breadthIntersection[0],
gap.breadthIntersection[1],
selectionPageBounds.minX,
selectionPageBounds.maxX
)) {
continue;
}
const gapMidY = gap.startEdge[0].y + gap.length / 2;
const centerNudge = gapMidY - selectionPageBounds.center.y;
const gapIsLargerThanSelection = gap.length > selectionPageBounds.height;
if (gapIsLargerThanSelection && round(Math.abs(centerNudge)) <= round(minOffset.y)) {
if (round(Math.abs(centerNudge)) < round(minOffset.y)) {
nearestSnapsY.length = 0;
}
minOffset.y = Math.abs(centerNudge);
const snap = {
type: "gap_center",
gap,
nudge: centerNudge
};
const otherCenterSnap = nearestSnapsY.find(({ type }) => type === "gap_center");
const gapBreadthsOverlap = otherCenterSnap && rangesOverlap(
otherCenterSnap.gap.breadthIntersection[0],
otherCenterSnap.gap.breadthIntersection[1],
gap.breadthIntersection[0],
gap.breadthIntersection[1]
);
if (otherCenterSnap && otherCenterSnap.gap.length > gap.length && gapBreadthsOverlap) {
nearestSnapsY[nearestSnapsY.indexOf(otherCenterSnap)] = snap;
} else if (!otherCenterSnap || !gapBreadthsOverlap) {
nearestSnapsY.push(snap);
}
continue;
}
const duplicationTopY = gap.startNode.pageBounds.minY - gap.length;
const selectionBottomY = selectionPageBounds.maxY;
const duplicationTopNudge = duplicationTopY - selectionBottomY;
if (round(Math.abs(duplicationTopNudge)) <= round(minOffset.y)) {
if (round(Math.abs(duplicationTopNudge)) < round(minOffset.y)) {
nearestSnapsY.length = 0;
}
minOffset.y = Math.abs(duplicationTopNudge);
nearestSnapsY.push({
type: "gap_duplicate",
gap,
protrusionDirection: "top",
nudge: duplicationTopNudge
});
}
const duplicationBottomY = gap.endNode.pageBounds.maxY + gap.length;
const selectionTopY = selectionPageBounds.minY;
const duplicationBottomNudge = duplicationBottomY - selectionTopY;
if (round(Math.abs(duplicationBottomNudge)) <= round(minOffset.y)) {
if (round(Math.abs(duplicationBottomNudge)) < round(minOffset.y)) {
nearestSnapsY.length = 0;
}
minOffset.y = Math.abs(duplicationBottomNudge);
nearestSnapsY.push({
type: "gap_duplicate",
gap,
protrusionDirection: "bottom",
nudge: duplicationBottomNudge
});
}
}
}
getPointSnapLines({
nearestSnapsX,
nearestSnapsY
}) {
const snapGroupsX = {};
const snapGroupsY = {};
if (nearestSnapsX.length > 0) {
for (const snap of nearestSnapsX) {
if (snap.type === "points") {
const key = round(snap.points.otherPoint.x);
if (!snapGroupsX[key]) {
snapGroupsX[key] = [];
}
snapGroupsX[key].push(snap.points);
}
}
}
if (nearestSnapsY.length > 0) {
for (const snap of nearestSnapsY) {
if (snap.type === "points") {
const key = round(snap.points.otherPoint.y);
if (!snapGroupsY[key]) {
snapGroupsY[key] = [];
}
snapGroupsY[key].push(snap.points);
}
}
}
return Object.values(snapGroupsX).concat(Object.values(snapGroupsY)).map((snapGroup) => ({
id: uniqueId(),
type: "points",
points: dedupe(
snapGroup.map((snap) => Vec.From(snap.otherPoint)).concat(snapGroup.map((snap) => Vec.From(snap.thisPoint))),
(a, b) => a.equals(b)
)
}));
}
getGapSnapLines({
selectionPageBounds,
nearestSnapsX,
nearestSnapsY
}) {
const { vertical, horizontal } = this.getVisibleGaps();
const selectionSides = {
top: selectionPageBounds.sides[0],
right: selectionPageBounds.sides[1],
// need bottom and left to be sorted asc, which .sides is not.
bottom: [selectionPageBounds.corners[3], selectionPageBounds.corners[2]],
left: [selectionPageBounds.corners[0], selectionPageBounds.corners[3]]
};
const result = [];
if (nearestSnapsX.length > 0) {
for (const snap of nearestSnapsX) {
if (snap.type === "points") continue;
const {
gap: { breadthIntersection, startEdge, startNode, endNode, length, endEdge }
} = snap;
switch (snap.type) {
case "gap_center": {
const newGapsLength = (length - selectionPageBounds.width) / 2;
const gapBreadthIntersection = rangeIntersection(
breadthIntersection[0],
breadthIntersection[1],
selectionPageBounds.minY,
selectionPageBounds.maxY
);
result.push({
type: "gaps",
direction: "horizontal",
id: uniqueId(),
gaps: [
...findAdjacentGaps(
horizontal,
startNode.id,
newGapsLength,
"backward",
gapBreadthIntersection
),
{
startEdge,
endEdge: selectionSides.left
},
{
startEdge: selectionSides.right,
endEdge
},
...findAdjacentGaps(
horizontal,
endNode.id,
newGapsLength,
"forward",
gapBreadthIntersection
)
]
});
break;
}
case "gap_duplicate": {
const gapBreadthIntersection = rangeIntersection(
breadthIntersection[0],
breadthIntersection[1],
selectionPageBounds.minY,
selectionPageBounds.maxY
);
result.push({
type: "gaps",
direction: "horizontal",
id: uniqueId(),
gaps: snap.protrusionDirection === "left" ? [
{
startEdge: selectionSides.right,
endEdge: startEdge.map(
(v) => v.clone().addXY(-startNode.pageBounds.width, 0)
)
},
{ startEdge, endEdge },
...findAdjacentGaps(
horizontal,
endNode.id,
length,
"forward",
gapBreadthIntersection
)
] : [
...findAdjacentGaps(
horizontal,
startNode.id,
length,
"backward",
gapBreadthIntersection
),
{ startEdge, endEdge },
{
startEdge: endEdge.map(
(v) => v.clone().addXY(snap.gap.endNode.pageBounds.width, 0)
),
endEdge: selectionSides.left
}
]
});
break;
}
}
}
}
if (nearestSnapsY.length > 0) {
for (const snap of nearestSnapsY) {
if (snap.type === "points") continue;
const {
gap: { breadthIntersection, startEdge, startNode, endNode, length, endEdge }
} = snap;
switch (snap.type) {
case "gap_center": {
const newGapsLength = (length - selectionPageBounds.height) / 2;
const gapBreadthIntersection = rangeIntersection(
breadthIntersection[0],
breadthIntersection[1],
selectionPageBounds.minX,
selectionPageBounds.maxX
);
result.push({
type: "gaps",
direction: "vertical",
id: uniqueId(),
gaps: [
...findAdjacentGaps(
vertical,
startNode.id,
newGapsLength,
"backward",
gapBreadthIntersection
),
{
startEdge,
endEdge: selectionSides.top
},
{
startEdge: selectionSides.bottom,
endEdge
},
...findAdjacentGaps(
vertical,
snap.gap.endNode.id,
newGapsLength,
"forward",
gapBreadthIntersection
)
]
});
break;
}
case "gap_duplicate":
{
const gapBreadthIntersection = rangeIntersection(
breadthIntersection[0],
breadthIntersection[1],
selectionPageBounds.minX,
selectionPageBounds.maxX
);
result.push({
type: "gaps",
direction: "vertical",
id: uniqueId(),
gaps: snap.protrusionDirection === "top" ? [
{
startEdge: selectionSides.bottom,
endEdge: startEdge.map(
(v) => v.clone().addXY(0, -startNode.pageBounds.height)
)
},
{ startEdge, endEdge },
...findAdjacentGaps(
vertical,
endNode.id,
length,
"forward",
gapBreadthIntersection
)
] : [
...findAdjacentGaps(
vertical,
startNode.id,
length,
"backward",
gapBreadthIntersection
),
{ startEdge, endEdge },
{
startEdge: endEdge.map(
(v) => v.clone().addXY(0, endNode.pageBounds.height)
),
endEdge: selectionSides.top
}
]
});
}
break;
}
}
}
dedupeGapSnaps(result);
return result;
}
}
_init$8 = __decoratorStart$8();
__decorateElement$8(_init$8, 1, "getSnapPointsCache", _getSnapPointsCache_dec, BoundsSnaps);
__decorateElement$8(_init$8, 1, "getSnappablePoints", _getSnappablePoints_dec, BoundsSnaps);
__decorateElement$8(_init$8, 1, "getSnappableGapNodes", _getSnappableGapNodes_dec, BoundsSnaps);
__decorateElement$8(_init$8, 1, "getVisibleGaps", _getVisibleGaps_dec, BoundsSnaps);
__decoratorMetadata$8(_init$8, BoundsSnaps);
function getResizeSnapPointsForHandle(handle, selectionPageBounds) {
const { minX, maxX, minY, maxY } = selectionPageBounds;
const result = [];
switch (handle) {
case "top":
case "left":
case "top_left":
case "any":
result.push({
id: "top_left",
handle: "top_left",
x: minX,
y: minY
});
}
switch (handle) {
case "top":
case "right":
case "top_right":
case "any":
result.push({
id: "top_right",
handle: "top_right",
x: maxX,
y: minY
});
}
switch (handle) {
case "bottom":
case "right":
case "bottom_right":
case "any":
result.push({
id: "bottom_right",
handle: "bottom_right",
x: maxX,
y: maxY
});
}
switch (handle) {
case "bottom":
case "left":
case "bottom_left":
case "any":
result.push({
id: "bottom_left",
handle: "bottom_left",
x: minX,
y: maxY
});
}
return result;
}
var __create$7 = Object.create;
var __defProp$7 = Object.defineProperty;
var __getOwnPropDesc$7 = Object.getOwnPropertyDescriptor;
var __knownSymbol$7 = (name, symbol) => (symbol = Symbol[name]) ? symbol : Symbol.for("Symbol." + name);
var __typeError$7 = (msg) => {
throw TypeError(msg);
};
var __defNormalProp$7 = (obj, key, value) => key in obj ? __defProp$7(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __decoratorStart$7 = (base) => [, , , __create$7(null)];
var __decoratorStrings$7 = ["class", "method", "getter", "setter", "accessor", "field", "value", "get", "set"];
var __expectFn$7 = (fn) => fn !== void 0 && typeof fn !== "function" ? __typeError$7("Function expected") : fn;
var __decoratorContext$7 = (kind, name, done, metadata, fns) => ({ kind: __decoratorStrings$7[kind], name, metadata, addInitializer: (fn) => done._ ? __typeError$7("Already initialized") : fns.push(__expectFn$7(fn || null)) });
var __decoratorMetadata$7 = (array, target) => __defNormalProp$7(target, __knownSymbol$7("metadata"), array[3]);
var __runInitializers$7 = (array, flags, self, value) => {
for (var i = 0, fns = array[flags >> 1], n = fns && fns.length; i < n; i++) fns[i].call(self) ;
return value;
};
var __decorateElement$7 = (array, flags, name, decorators, target, extra) => {
var it, done, ctx, access, k = flags & 7, s = false, p = false;
var j = 2 , key = __decoratorStrings$7[k + 5];
var extraInitializers = array[j] || (array[j] = []);
var desc = ((target = target.prototype), __getOwnPropDesc$7(target , name));
for (var i = decorators.length - 1; i >= 0; i--) {
ctx = __decoratorContext$7(k, name, done = {}, array[3], extraInitializers);
{
ctx.static = s, ctx.private = p, access = ctx.access = { has: (x) => name in x };
access.get = (x) => x[name];
}
it = (0, decorators[i])(desc[key] , ctx), done._ = 1;
__expectFn$7(it) && (desc[key] = it );
}
return desc && __defProp$7(target, name, desc), target;
};
var __publicField$7 = (obj, key, value) => __defNormalProp$7(obj, key + "" , value);
var _getSnapGeometryCache_dec, _init$7;
const defaultGetSelfSnapOutline = () => null;
const defaultGetSelfSnapPoints = () => [];
_getSnapGeometryCache_dec = [computed];
class HandleSnaps {
constructor(manager) {
this.manager = manager;
__runInitializers$7(_init$7, 5, this);
__publicField$7(this, "editor");
this.editor = manager.editor;
}
getSnapGeometryCache() {
const { editor } = this;
return editor.store.createComputedCache("handle snap geometry", (shape) => {
const snapGeometry = editor.getShapeUtil(shape).getHandleSnapGeometry(shape);
const getSelfSnapOutline = snapGeometry.getSelfSnapOutline ? snapGeometry.getSelfSnapOutline.bind(snapGeometry) : defaultGetSelfSnapOutline;
const getSelfSnapPoints = snapGeometry.getSelfSnapPoints ? snapGeometry.getSelfSnapPoints.bind(snapGeometry) : defaultGetSelfSnapPoints;
return {
outline: snapGeometry.outline === void 0 ? editor.getShapeGeometry(shape) : snapGeometry.outline,
points: snapGeometry.points ?? [],
getSelfSnapOutline,
getSelfSnapPoints
};
});
}
*iterateSnapPointsInPageSpace(currentShapeId, currentHandle) {
const selfSnapPoints = this.getSnapGeometryCache().get(currentShapeId)?.getSelfSnapPoints(currentHandle);
if (selfSnapPoints && selfSnapPoints.length) {
const shapePageTransform = assertExists(this.editor.getShapePageTransform(currentShapeId));
for (const point of selfSnapPoints) {
yield shapePageTransform.applyToPoint(point);
}
}
for (const shapeId of this.manager.getSnappableShapes()) {
if (shapeId === currentShapeId) continue;
const snapPoints = this.getSnapGeometryCache().get(shapeId)?.points;
if (!snapPoints || !snapPoints.length) continue;
const shapePageTransform = assertExists(this.editor.getShapePageTransform(shapeId));
for (const point of snapPoints) {
yield shapePageTransform.applyToPoint(point);
}
}
}
*iterateSnapOutlines(currentShapeId, currentHandle) {
const selfSnapOutline = this.getSnapGeometryCache().get(currentShapeId)?.getSelfSnapOutline(currentHandle);
if (selfSnapOutline) {
yield { shapeId: currentShapeId, outline: selfSnapOutline };
}
for (const shapeId of this.manager.getSnappableShapes()) {
if (shapeId === currentShapeId) continue;
const snapOutline = this.getSnapGeometryCache().get(shapeId)?.outline;
if (!snapOutline) continue;
yield { shapeId, outline: snapOutline };
}
}
getHandleSnapPosition({
currentShapeId,
handle,
handleInPageSpace
}) {
const snapThreshold = this.manager.getSnapThreshold();
let minDistanceForSnapPoint = snapThreshold;
let nearestSnapPoint = null;
for (const snapPoint of this.iterateSnapPointsInPageSpace(currentShapeId, handle)) {
if (Vec.DistMin(handleInPageSpace, snapPoint, minDistanceForSnapPoint)) {
minDistanceForSnapPoint = Vec.Dist(handleInPageSpace, snapPoint);
nearestSnapPoint = snapPoint;
}
}
if (nearestSnapPoint) return nearestSnapPoint;
let minDistanceForOutline = snapThreshold;
let nearestPointOnOutline = null;
for (const { shapeId, outline } of this.iterateSnapOutlines(currentShapeId, handle)) {
const shapePageTransform = assertExists(this.editor.getShapePageTransform(shapeId));
const pointInShapeSpace = this.editor.getPointInShapeSpace(shapeId, handleInPageSpace);
const nearestShapePointInShapeSpace = outline.nearestPoint(pointInShapeSpace);
const nearestInPageSpace = shapePageTransform.applyToPoint(nearestShapePointInShapeSpace);
if (Vec.DistMin(handleInPageSpace, nearestInPageSpace, minDistanceForOutline)) {
minDistanceForOutline = Vec.Dist(handleInPageSpace, nearestInPageSpace);
nearestPointOnOutline = nearestInPageSpace;
}
}
if (nearestPointOnOutline) return nearestPointOnOutline;
return null;
}
snapHandle({
currentShapeId,
handle
}) {
const currentShapeTransform = assertExists(this.editor.getShapePageTransform(currentShapeId));
const handleInPageSpace = currentShapeTransform.applyToPoint(handle);
const snapPosition = this.getHandleSnapPosition({ currentShapeId, handle, handleInPageSpace });
if (snapPosition) {
this.manager.setIndicators([
{
id: uniqueId(),
type: "points",
points: [snapPosition]
}
]);
return { nudge: Vec.Sub(snapPosition, handleInPageSpace) };
}
return null;
}
}
_init$7 = __decoratorStart$7();
__decorateElement$7(_init$7, 1, "getSnapGeometryCache", _getSnapGeometryCache_dec, HandleSnaps);
__decoratorMetadata$7(_init$7, HandleSnaps);
var __create$6 = Object.create;
var __defProp$6 = Object.defineProperty;
var __getOwnPropDesc$6 = Object.getOwnPropertyDescriptor;
var __knownSymbol$6 = (name, symbol) => (symbol = Symbol[name]) ? symbol : Symbol.for("Symbol." + name);
var __typeError$6 = (msg) => {
throw TypeError(msg);
};
var __defNormalProp$6 = (obj, key, value) => key in obj ? __defProp$6(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __decoratorStart$6 = (base) => [, , , __create$6(null)];
var __decoratorStrings$6 = ["class", "method", "getter", "setter", "accessor", "field", "value", "get", "set"];
var __expectFn$6 = (fn) => fn !== void 0 && typeof fn !== "function" ? __typeError$6("Function expected") : fn;
var __decoratorContext$6 = (kind, name, done, metadata, fns) => ({ kind: __decoratorStrings$6[kind], name, metadata, addInitializer: (fn) => done._ ? __typeError$6("Already initialized") : fns.push(__expectFn$6(fn || null)) });
var __decoratorMetadata$6 = (array, target) => __defNormalProp$6(target, __knownSymbol$6("metadata"), array[3]);
var __runInitializers$6 = (array, flags, self, value) => {
for (var i = 0, fns = array[flags >> 1], n = fns && fns.length; i < n; i++) fns[i].call(self) ;
return value;
};
var __decorateElement$6 = (array, flags, name, decorators, target, extra) => {
var it, done, ctx, access, k = flags & 7, s = false, p = false;
var j = 2 , key = __decoratorStrings$6[k + 5];
var extraInitializers = array[j] || (array[j] = []);
var desc = ((target = target.prototype), __getOwnPropDesc$6(target , name));
for (var i = decorators.length - 1; i >= 0; i--) {
ctx = __decoratorContext$6(k, name, done = {}, array[3], extraInitializers);
{
ctx.static = s, ctx.private = p, access = ctx.access = { has: (x) => name in x };
access.get = (x) => x[name];
}
it = (0, decorators[i])(desc[key] , ctx), done._ = 1;
__expectFn$6(it) && (desc[key] = it );
}
return desc && __defProp$6(target, name, desc), target;
};
var __publicField$6 = (obj, key, value) => __defNormalProp$6(obj, typeof key !== "symbol" ? key + "" : key, value);
var _getCurrentCommonAncestor_dec, _getSnappableShapes_dec, _getSnapThreshold_dec, _init$6;
_getSnapThreshold_dec = [computed], _getSnappableShapes_dec = [computed], _getCurrentCommonAncestor_dec = [computed];
class SnapManager {
constructor(editor) {
this.editor = editor;
__runInitializers$6(_init$6, 5, this);
__publicField$6(this, "shapeBounds");
__publicField$6(this, "handles");
__publicField$6(this, "_snapIndicators", atom("snapLines", void 0));
this.shapeBounds = new BoundsSnaps(this);
this.handles = new HandleSnaps(this);
}
getIndicators() {
return this._snapIndicators.get() ?? EMPTY_ARRAY;
}
clearIndicators() {
if (this.getIndicators().length) {
this._snapIndicators.set(void 0);
}
}
setIndicators(indicators) {
this._snapIndicators.set(indicators);
}
getSnapThreshold() {
return 8 / this.editor.getZoomLevel();
}
getSnappableShapes() {
const { editor } = this;
const renderingBounds = editor.getViewportPageBounds();
const selectedShapeIds = editor.getSelectedShapeIds();
const snappableShapes = /* @__PURE__ */ new Set();
const collectSnappableShapesFromParent = (parentId) => {
if (isShapeId(parentId)) {
const parent = editor.getShape(parentId);
if (parent && editor.isShapeOfType(parent, "frame")) {
snappableShapes.add(parentId);
}
}
const sortedChildIds = editor.getSortedChildIdsForParent(parentId);
for (const childId of sortedChildIds) {
if (selectedShapeIds.includes(childId)) continue;
const childShape = editor.getShape(childId);
if (!childShape) continue;
const util = editor.getShapeUtil(childShape);
if (!util.canSnap(childShape)) continue;
const pageBounds = editor.getShapePageBounds(childId);
if (!(pageBounds && renderingBounds.includes(pageBounds))) continue;
if (editor.isShapeOfType(childShape, "group")) {
collectSnappableShapesFromParent(childId);
continue;
}
snappableShapes.add(childId);
}
};
collectSnappableShapesFromParent(this.getCurrentCommonAncestor() ?? editor.getCurrentPageId());
return snappableShapes;
}
getCurrentCommonAncestor() {
return this.editor.findCommonAncestor(this.editor.getSelectedShapes());
}
}
_init$6 = __decoratorStart$6();
__decorateElement$6(_init$6, 1, "getSnapThreshold", _getSnapThreshold_dec, SnapManager);
__decorateElement$6(_init$6, 1, "getSnappableShapes", _getSnappableShapes_dec, SnapManager);
__decorateElement$6(_init$6, 1, "getCurrentCommonAncestor", _getCurrentCommonAncestor_dec, SnapManager);
__decoratorMetadata$6(_init$6, SnapManager);
const fixNewLines = /\r?\n|\r/g;
function normalizeTextForDom(text) {
return text.replace(fixNewLines, "\n").split("\n").map((x) => x || " ").join("\n");
}
const textAlignmentsForLtr = {
start: "left",
"start-legacy": "left",
middle: "center",
"middle-legacy": "center",
end: "right",
"end-legacy": "right"
};
const spaceCharacterRegex = /\s/;
class TextManager {
constructor(editor) {
this.editor = editor;
this.baseElem = document.createElement("div");
this.baseElem.classList.add("tl-text");
this.baseElem.classList.add("tl-text-measure");
this.baseElem.tabIndex = -1;
}
baseElem;
measureText(textToMeasure, opts) {
const elm = this.baseElem.cloneNode();
this.editor.getContainer().appendChild(elm);
elm.setAttribute("dir", "auto");
elm.style.setProperty("unicode-bidi", "plaintext");
elm.style.setProperty("font-family", opts.fontFamily);
elm.style.setProperty("font-style", opts.fontStyle);
elm.style.setProperty("font-weight", opts.fontWeight);
elm.style.setProperty("font-size", opts.fontSize + "px");
elm.style.setProperty("line-height", opts.lineHeight * opts.fontSize + "px");
elm.style.setProperty("max-width", opts.maxWidth === null ? null : opts.maxWidth + "px");
elm.style.setProperty("min-width", opts.minWidth === null ? null : opts.minWidth + "px");
elm.style.setProperty("padding", opts.padding);
elm.style.setProperty(
"overflow-wrap",
opts.disableOverflowWrapBreaking ? "normal" : "break-word"
);
elm.textContent = normalizeTextForDom(textToMeasure);
const scrollWidth = elm.scrollWidth;
const rect = elm.getBoundingClientRect();
elm.remove();
return {
x: 0,
y: 0,
w: rect.width,
h: rect.height,
scrollWidth
};
}
/**
* Given an html element, measure the position of each span of unbroken
* word/white-space characters within any text nodes it contains.
*/
measureElementTextNodeSpans(element, { shouldTruncateToFirstLine = false } = {}) {
const spans = [];
const elmBounds = element.getBoundingClientRect();
const offsetX = -elmBounds.left;
const offsetY = -elmBounds.top;
const range = new Range();
const textNode = element.childNodes[0];
let idx = 0;
let currentSpan = null;
let prevCharWasSpaceCharacter = null;
let prevCharTop = 0;
let prevCharLeftForRTLTest = 0;
let didTruncate = false;
for (const childNode of element.childNodes) {
if (childNode.nodeType !== Node.TEXT_NODE) continue;
for (const char of childNode.textContent ?? "") {
range.setStart(textNode, idx);
range.setEnd(textNode, idx + char.length);
const rects = range.getClientRects();
const rect = rects[rects.length - 1];
const top = rect.top + offsetY;
const left = rect.left + offsetX;
const right = rect.right + offsetX;
const isRTL = left < prevCharLeftForRTLTest;
const isSpaceCharacter = spaceCharacterRegex.test(char);
if (
// If we're at a word boundary...
isSpaceCharacter !== prevCharWasSpaceCharacter || // ...or we're on a different line...
top !== prevCharTop || // ...or we're at the start of the text and haven't created a span yet...
!currentSpan
) {
if (currentSpan) {
if (shouldTruncateToFirstLine && top !== prevCharTop) {
didTruncate = true;
break;
}
spans.push(currentSpan);
}
currentSpan = {
box: { x: left, y: top, w: rect.width, h: rect.height },
text: char
};
prevCharLeftForRTLTest = left;
} else {
if (isRTL) {
currentSpan.box.x = left;
}
currentSpan.box.w = isRTL ? currentSpan.box.w + rect.width : right - currentSpan.box.x;
currentSpan.text += char;
}
if (char === "\n") {
prevCharLeftForRTLTest = 0;
}
prevCharWasSpaceCharacter = isSpaceCharacter;
prevCharTop = top;
idx += char.length;
}
}
if (currentSpan) {
spans.push(currentSpan);
}
return { spans, didTruncate };
}
/**
* Measure text into individual spans. Spans are created by rendering the
* text, then dividing it up according to line breaks and word boundaries.
*
* It works by having the browser render the text, then measuring the
* position of each character. You can use this to replicate the text-layout
* algorithm of the current browser in e.g. an SVG export.
*/
measureTextSpans(textToMeasure, opts) {
if (textToMeasure === "") return [];
const elm = this.baseElem.cloneNode();
this.editor.getContainer().appendChild(elm);
const elementWidth = Math.ceil(opts.width - opts.padding * 2);
elm.setAttribute("dir", "auto");
elm.style.setProperty("unicode-bidi", "plaintext");
elm.style.setProperty("width", `${elementWidth}px`);
elm.style.setProperty("height", "min-content");
elm.style.setProperty("font-size", `${opts.fontSize}px`);
elm.style.setProperty("font-family", opts.fontFamily);
elm.style.setProperty("font-weight", opts.fontWeight);
elm.style.setProperty("line-height", `${opts.lineHeight * opts.fontSize}px`);
elm.style.setProperty("text-align", textAlignmentsForLtr[opts.textAlign]);
const shouldTruncateToFirstLine = opts.overflow === "truncate-ellipsis" || opts.overflow === "truncate-clip";
if (shouldTruncateToFirstLine) {
elm.style.setProperty("overflow-wrap", "anywhere");
elm.style.setProperty("word-break", "break-all");
}
const normalizedText = normalizeTextForDom(textToMeasure);
elm.textContent = normalizedText;
const { spans, didTruncate } = this.measureElementTextNodeSpans(elm, {
shouldTruncateToFirstLine
});
if (opts.overflow === "truncate-ellipsis" && didTruncate) {
elm.textContent = "\u2026";
const ellipsisWidth = Math.ceil(this.measureElementTextNodeSpans(elm).spans[0].box.w);
elm.style.setProperty("width", `${elementWidth - ellipsisWidth}px`);
elm.textContent = normalizedText;
const truncatedSpans = this.measureElementTextNodeSpans(elm, {
shouldTruncateToFirstLine: true
}).spans;
const lastSpan = truncatedSpans[truncatedSpans.length - 1];
truncatedSpans.push({
text: "\u2026",
box: {
x: Math.min(lastSpan.box.x + lastSpan.box.w, opts.width - opts.padding - ellipsisWidth),
y: lastSpan.box.y,
w: ellipsisWidth,
h: lastSpan.box.h
}
});
return truncatedSpans;
}
elm.remove();
return spans;
}
}
var __create$5 = Object.create;
var __defProp$5 = Object.defineProperty;
var __getOwnPropDesc$5 = Object.getOwnPropertyDescriptor;
var __knownSymbol$5 = (name, symbol) => (symbol = Symbol[name]) ? symbol : Symbol.for("Symbol." + name);
var __typeError$5 = (msg) => {
throw TypeError(msg);
};
var __defNormalProp$5 = (obj, key, value) => key in obj ? __defProp$5(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __decoratorStart$5 = (base) => [, , , __create$5(null)];
var __decoratorStrings$5 = ["class", "method", "getter", "setter", "accessor", "field", "value", "get", "set"];
var __expectFn$5 = (fn) => fn !== void 0 && typeof fn !== "function" ? __typeError$5("Function expected") : fn;
var __decoratorContext$5 = (kind, name, done, metadata, fns) => ({ kind: __decoratorStrings$5[kind], name, metadata, addInitializer: (fn) => done._ ? __typeError$5("Already initialized") : fns.push(__expectFn$5(fn || null)) });
var __decoratorMetadata$5 = (array, target) => __defNormalProp$5(target, __knownSymbol$5("metadata"), array[3]);
var __runInitializers$5 = (array, flags, self, value) => {
for (var i = 0, fns = array[flags >> 1], n = fns && fns.length; i < n; i++) fns[i].call(self) ;
return value;
};
var __decorateElement$5 = (array, flags, name, decorators, target, extra) => {
var it, done, ctx, access, k = flags & 7, s = false, p = false;
var j = 2 , key = __decoratorStrings$5[k + 5];
var extraInitializers = array[j] || (array[j] = []);
var desc = ((target = target.prototype), __getOwnPropDesc$5(target , name));
for (var i = decorators.length - 1; i >= 0; i--) {
ctx = __decoratorContext$5(k, name, done = {}, array[3], extraInitializers);
{
ctx.static = s, ctx.private = p, access = ctx.access = { has: (x) => name in x };
access.get = (x) => x[name];
}
it = (0, decorators[i])(desc[key] , ctx), done._ = 1;
__expectFn$5(it) && (desc[key] = it );
}
return desc && __defProp$5(target, name, desc), target;
};
var __publicField$5 = (obj, key, value) => __defNormalProp$5(obj, typeof key !== "symbol" ? key + "" : key, value);
var _dispose_dec$1, _tick_dec, _init$5;
const throttleToNextFrame = throttleToNextFrame$1;
_tick_dec = [bind$2], _dispose_dec$1 = [bind$2];
class TickManager {
constructor(editor) {
this.editor = editor;
__runInitializers$5(_init$5, 5, this);
__publicField$5(this, "cancelRaf");
__publicField$5(this, "isPaused", true);
__publicField$5(this, "now", 0);
__publicField$5(this, "prevPoint", new Vec());
this.editor.disposables.add(this.dispose);
this.start();
}
start() {
this.isPaused = false;
this.cancelRaf?.();
this.cancelRaf = throttleToNextFrame(this.tick);
this.now = Date.now();
}
tick() {
if (this.isPaused) {
return;
}
const now = Date.now();
const elapsed = now - this.now;
this.now = now;
this.updatePointerVelocity(elapsed);
this.editor.emit("frame", elapsed);
this.editor.emit("tick", elapsed);
this.cancelRaf = throttleToNextFrame(this.tick);
}
dispose() {
this.isPaused = true;
this.cancelRaf?.();
}
updatePointerVelocity(elapsed) {
const {
prevPoint,
editor: {
inputs: { currentScreenPoint, pointerVelocity }
}
} = this;
if (elapsed === 0) return;
const delta = Vec.Sub(currentScreenPoint, prevPoint);
this.prevPoint = currentScreenPoint.clone();
const length = delta.len();
const direction = length ? delta.div(length) : new Vec(0, 0);
const next = pointerVelocity.clone().lrp(direction.mul(length / elapsed), 0.5);
if (Math.abs(next.x) < 0.01) next.x = 0;
if (Math.abs(next.y) < 0.01) next.y = 0;
if (!pointerVelocity.equals(next)) {
this.editor.inputs.pointerVelocity = next;
}
}
}
_init$5 = __decoratorStart$5();
__decorateElement$5(_init$5, 1, "tick", _tick_dec, TickManager);
__decorateElement$5(_init$5, 1, "dispose", _dispose_dec$1, TickManager);
__decoratorMetadata$5(_init$5, TickManager);
var __create$4 = Object.create;
var __defProp$4 = Object.defineProperty;
var __getOwnPropDesc$4 = Object.getOwnPropertyDescriptor;
var __knownSymbol$4 = (name, symbol) => (symbol = Symbol[name]) ? symbol : Symbol.for("Symbol." + name);
var __typeError$4 = (msg) => {
throw TypeError(msg);
};
var __defNormalProp$4 = (obj, key, value) => key in obj ? __defProp$4(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __decoratorStart$4 = (base) => [, , , __create$4(null)];
var __decoratorStrings$4 = ["class", "method", "getter", "setter", "accessor", "field", "value", "get", "set"];
var __expectFn$4 = (fn) => fn !== void 0 && typeof fn !== "function" ? __typeError$4("Function expected") : fn;
var __decoratorContext$4 = (kind, name, done, metadata, fns) => ({ kind: __decoratorStrings$4[kind], name, metadata, addInitializer: (fn) => done._ ? __typeError$4("Already initialized") : fns.push(__expectFn$4(fn || null)) });
var __decoratorMetadata$4 = (array, target) => __defNormalProp$4(target, __knownSymbol$4("metadata"), array[3]);
var __runInitializers$4 = (array, flags, self, value) => {
for (var i = 0, fns = array[flags >> 1], n = fns && fns.length; i < n; i++) fns[i].call(self) ;
return value;
};
var __decorateElement$4 = (array, flags, name, decorators, target, extra) => {
var it, done, ctx, access, k = flags & 7, s = false, p = false;
var j = 2 , key = __decoratorStrings$4[k + 5];
var extraInitializers = array[j] || (array[j] = []);
var desc = ((target = target.prototype), __getOwnPropDesc$4(target , name));
for (var i = decorators.length - 1; i >= 0; i--) {
ctx = __decoratorContext$4(k, name, done = {}, array[3], extraInitializers);
{
ctx.static = s, ctx.private = p, access = ctx.access = { has: (x) => name in x };
access.get = (x) => x[name];
}
it = (0, decorators[i])(desc[key] , ctx), done._ = 1;
__expectFn$4(it) && (desc[key] = it );
}
return desc && __defProp$4(target, name, desc), target;
};
var __publicField$4 = (obj, key, value) => __defNormalProp$4(obj, typeof key !== "symbol" ? key + "" : key, value);
var _getIsPasteAtCursorMode_dec, _getIsDynamicResizeMode_dec, _getIsWrapMode_dec, _getIsSnapMode_dec, _getColor_dec, _getLocale_dec, _getName_dec, _getId_dec, _getAnimationSpeed_dec, _getEdgeScrollSpeed_dec, _getIsDarkMode_dec, _getUserPreferences_dec, _init$4;
_getUserPreferences_dec = [computed], _getIsDarkMode_dec = [computed], _getEdgeScrollSpeed_dec = [computed], _getAnimationSpeed_dec = [computed], _getId_dec = [computed], _getName_dec = [computed], _getLocale_dec = [computed], _getColor_dec = [computed], _getIsSnapMode_dec = [computed], _getIsWrapMode_dec = [computed], _getIsDynamicResizeMode_dec = [computed], _getIsPasteAtCursorMode_dec = [computed];
class UserPreferencesManager {
constructor(user, inferDarkMode) {
this.user = user;
this.inferDarkMode = inferDarkMode;
__runInitializers$4(_init$4, 5, this);
__publicField$4(this, "systemColorScheme", atom("systemColorScheme", "light"));
__publicField$4(this, "disposables", /* @__PURE__ */ new Set());
if (typeof window === "undefined" || !("matchMedia" in window)) return;
const darkModeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
if (darkModeMediaQuery?.matches) {
this.systemColorScheme.set("dark");
}
const handleChange = (e) => {
if (e.matches) {
this.systemColorScheme.set("dark");
} else {
this.systemColorScheme.set("light");
}
};
darkModeMediaQuery?.addEventListener("change", handleChange);
this.disposables.add(() => darkModeMediaQuery?.removeEventListener("change", handleChange));
}
dispose() {
this.disposables.forEach((d) => d());
}
updateUserPreferences(userPreferences) {
this.user.setUserPreferences({
...this.user.userPreferences.get(),
...userPreferences
});
}
getUserPreferences() {
return {
id: this.getId(),
name: this.getName(),
locale: this.getLocale(),
color: this.getColor(),
animationSpeed: this.getAnimationSpeed(),
isSnapMode: this.getIsSnapMode(),
colorScheme: this.user.userPreferences.get().colorScheme,
isDarkMode: this.getIsDarkMode(),
isWrapMode: this.getIsWrapMode(),
isDynamicResizeMode: this.getIsDynamicResizeMode()
};
}
getIsDarkMode() {
switch (this.user.userPreferences.get().colorScheme) {
case "dark":
return true;
case "light":
return false;
case "system":
return this.systemColorScheme.get() === "dark";
default:
return this.inferDarkMode ? this.systemColorScheme.get() === "dark" : false;
}
}
getEdgeScrollSpeed() {
return this.user.userPreferences.get().edgeScrollSpeed ?? defaultUserPreferences.edgeScrollSpeed;
}
getAnimationSpeed() {
return this.user.userPreferences.get().animationSpeed ?? defaultUserPreferences.animationSpeed;
}
getId() {
return this.user.userPreferences.get().id;
}
getName() {
return this.user.userPreferences.get().name ?? defaultUserPreferences.name;
}
getLocale() {
return this.user.userPreferences.get().locale ?? defaultUserPreferences.locale;
}
getColor() {
return this.user.userPreferences.get().color ?? defaultUserPreferences.color;
}
getIsSnapMode() {
return this.user.userPreferences.get().isSnapMode ?? defaultUserPreferences.isSnapMode;
}
getIsWrapMode() {
return this.user.userPreferences.get().isWrapMode ?? defaultUserPreferences.isWrapMode;
}
getIsDynamicResizeMode() {
return this.user.userPreferences.get().isDynamicSizeMode ?? defaultUserPreferences.isDynamicSizeMode;
}
getIsPasteAtCursorMode() {
return this.user.userPreferences.get().isPasteAtCursorMode ?? defaultUserPreferences.isPasteAtCursorMode;
}
}
_init$4 = __decoratorStart$4();
__decorateElement$4(_init$4, 1, "getUserPreferences", _getUserPreferences_dec, UserPreferencesManager);
__decorateElement$4(_init$4, 1, "getIsDarkMode", _getIsDarkMode_dec, UserPreferencesManager);
__decorateElement$4(_init$4, 1, "getEdgeScrollSpeed", _getEdgeScrollSpeed_dec, UserPreferencesManager);
__decorateElement$4(_init$4, 1, "getAnimationSpeed", _getAnimationSpeed_dec, UserPreferencesManager);
__decorateElement$4(_init$4, 1, "getId", _getId_dec, UserPreferencesManager);
__decorateElement$4(_init$4, 1, "getName", _getName_dec, UserPreferencesManager);
__decorateElement$4(_init$4, 1, "getLocale", _getLocale_dec, UserPreferencesManager);
__decorateElement$4(_init$4, 1, "getColor", _getColor_dec, UserPreferencesManager);
__decorateElement$4(_init$4, 1, "getIsSnapMode", _getIsSnapMode_dec, UserPreferencesManager);
__decorateElement$4(_init$4, 1, "getIsWrapMode", _getIsWrapMode_dec, UserPreferencesManager);
__decorateElement$4(_init$4, 1, "getIsDynamicResizeMode", _getIsDynamicResizeMode_dec, UserPreferencesManager);
__decorateElement$4(_init$4, 1, "getIsPasteAtCursorMode", _getIsPasteAtCursorMode_dec, UserPreferencesManager);
__decoratorMetadata$4(_init$4, UserPreferencesManager);
const EVENT_NAME_MAP = {
wheel: "onWheel",
pointer_down: "onPointerDown",
pointer_move: "onPointerMove",
long_press: "onLongPress",
pointer_up: "onPointerUp",
right_click: "onRightClick",
middle_click: "onMiddleClick",
key_down: "onKeyDown",
key_up: "onKeyUp",
key_repeat: "onKeyRepeat",
cancel: "onCancel",
complete: "onComplete",
interrupt: "onInterrupt",
double_click: "onDoubleClick",
triple_click: "onTripleClick",
quadruple_click: "onQuadrupleClick",
tick: "onTick"
};
const STATE_NODES_TO_MEASURE = [
"brushing",
"cropping",
"dragging",
"dragging_handle",
"drawing",
"erasing",
"lasering",
"resizing",
"rotating",
"scribble_brushing",
"translating"
];
class StateNode {
constructor(editor, parent) {
this.editor = editor;
const { id, children, initial, isLockable } = this.constructor;
this.id = id;
this._isActive = atom("toolIsActive" + this.id, false);
this._current = atom("toolState" + this.id, void 0);
this._path = computed("toolPath" + this.id, () => {
const current = this.getCurrent();
return this.id + (current ? `.${current.getPath()}` : "");
});
this.parent = parent ?? {};
if (this.parent) {
if (children && initial) {
this.type = "branch";
this.initial = initial;
this.children = Object.fromEntries(
children().map((Ctor) => [Ctor.id, new Ctor(this.editor, this)])
);
this._current.set(this.children[this.initial]);
} else {
this.type = "leaf";
}
} else {
this.type = "root";
if (children && initial) {
this.initial = initial;
this.children = Object.fromEntries(
children().map((Ctor) => [Ctor.id, new Ctor(this.editor, this)])
);
this._current.set(this.children[this.initial]);
}
}
this.isLockable = isLockable;
this.performanceTracker = new PerformanceTracker();
}
performanceTracker;
static id;
static initial;
static children;
static isLockable = true;
id;
type;
shapeType;
initial;
children;
isLockable;
parent;
/**
* This node's path of active state nodes
*
* @public
*/
getPath() {
return this._path.get();
}
_path;
/**
* This node's current active child node, if any.
*
* @public
*/
getCurrent() {
return this._current.get();
}
_current;
/**
* Whether this node is active.
*
* @public
*/
getIsActive() {
return this._isActive.get();
}
_isActive;
/**
* Transition to a new active child state node.
*
* @example
* ```ts
* parentState.transition('childStateA')
* parentState.transition('childStateB', { myData: 4 })
*```
*
* @param id - The id of the child state node to transition to.
* @param info - Any data to pass to the `onEnter` and `onExit` handlers.
*
* @public
*/
transition(id, info = {}) {
const path = id.split(".");
let currState = this;
for (let i = 0; i < path.length; i++) {
const id2 = path[i];
const prevChildState = currState.getCurrent();
const nextChildState = currState.children?.[id2];
if (!nextChildState) {
throw Error(`${currState.id} - no child state exists with the id ${id2}.`);
}
if (prevChildState?.id !== nextChildState.id) {
prevChildState?.exit(info, id2);
currState._current.set(nextChildState);
nextChildState.enter(info, prevChildState?.id || "initial");
if (!nextChildState.getIsActive()) break;
}
currState = nextChildState;
}
return this;
}
handleEvent(info) {
const cbName = EVENT_NAME_MAP[info.name];
const currentActiveChild = this._current.__unsafe__getWithoutCapture();
this[cbName]?.(info);
if (this._isActive.__unsafe__getWithoutCapture() && currentActiveChild && currentActiveChild === this._current.__unsafe__getWithoutCapture()) {
currentActiveChild.handleEvent(info);
}
}
// todo: move this logic into transition
enter(info, from) {
if (debugFlags.measurePerformance.get() && STATE_NODES_TO_MEASURE.includes(this.id)) {
this.performanceTracker.start(this.id);
}
this._isActive.set(true);
this.onEnter?.(info, from);
if (this.children && this.initial && this.getIsActive()) {
const initial = this.children[this.initial];
this._current.set(initial);
initial.enter(info, from);
}
}
// todo: move this logic into transition
exit(info, from) {
if (debugFlags.measurePerformance.get() && this.performanceTracker.isStarted()) {
this.performanceTracker.stop();
}
this._isActive.set(false);
this.onExit?.(info, from);
if (!this.getIsActive()) {
this.getCurrent()?.exit(info, from);
}
}
/**
* This is a hack / escape hatch that will tell the editor to
* report a different state as active (in `getCurrentToolId()`) when
* this state is active. This is usually used when a tool transitions
* to a child of a different state for a certain interaction and then
* returns to the original tool when that interaction completes; and
* where we would want to show the original tool as active in the UI.
*
* @public
*/
_currentToolIdMask = atom("curent tool id mask", void 0);
getCurrentToolIdMask() {
return this._currentToolIdMask.get();
}
setCurrentToolIdMask(id) {
this._currentToolIdMask.set(id);
}
}
class RootState extends StateNode {
static id = "root";
static initial = "";
static children() {
return [];
}
onKeyDown(info) {
switch (info.code) {
case "KeyZ": {
if (!(info.shiftKey || info.ctrlKey)) {
const currentTool = this.getCurrent();
if (currentTool && currentTool.getCurrent()?.id === "idle" && this.children["zoom"]) {
this.editor.setCurrentTool("zoom", { ...info, onInteractionEnd: currentTool.id });
}
}
break;
}
}
}
}
var __create$3 = Object.create;
var __defProp$3 = Object.defineProperty;
var __getOwnPropDesc$3 = Object.getOwnPropertyDescriptor;
var __knownSymbol$3 = (name, symbol) => (symbol = Symbol[name]) ? symbol : Symbol.for("Symbol." + name);
var __typeError$3 = (msg) => {
throw TypeError(msg);
};
var __defNormalProp$3 = (obj, key, value) => key in obj ? __defProp$3(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __decoratorStart$3 = (base) => [, , , __create$3(base?.[__knownSymbol$3("metadata")] ?? null)];
var __decoratorStrings$3 = ["class", "method", "getter", "setter", "accessor", "field", "value", "get", "set"];
var __expectFn$3 = (fn) => fn !== void 0 && typeof fn !== "function" ? __typeError$3("Function expected") : fn;
var __decoratorContext$3 = (kind, name, done, metadata, fns) => ({ kind: __decoratorStrings$3[kind], name, metadata, addInitializer: (fn) => done._ ? __typeError$3("Already initialized") : fns.push(__expectFn$3(fn || null)) });
var __decoratorMetadata$3 = (array, target) => __defNormalProp$3(target, __knownSymbol$3("metadata"), array[3]);
var __runInitializers$3 = (array, flags, self, value) => {
for (var i = 0, fns = array[flags >> 1], n = fns && fns.length; i < n; i++) fns[i].call(self) ;
return value;
};
var __decorateElement$3 = (array, flags, name, decorators, target, extra) => {
var it, done, ctx, access, k = flags & 7, s = false, p = false;
var j = 2 , key = __decoratorStrings$3[k + 5];
var extraInitializers = array[j] || (array[j] = []);
var desc = ((target = target.prototype), __getOwnPropDesc$3(target , name));
for (var i = decorators.length - 1; i >= 0; i--) {
ctx = __decoratorContext$3(k, name, done = {}, array[3], extraInitializers);
{
ctx.static = s, ctx.private = p, access = ctx.access = { has: (x) => name in x };
access.get = (x) => x[name];
}
it = (0, decorators[i])(desc[key] , ctx), done._ = 1;
__expectFn$3(it) && (desc[key] = it );
}
return desc && __defProp$3(target, name, desc), target;
};
var __publicField$3 = (obj, key, value) => __defNormalProp$3(obj, typeof key !== "symbol" ? key + "" : key, value);
var __setMetaKeyTimeout_dec, __setCtrlKeyTimeout_dec, __setAltKeyTimeout_dec, __setShiftKeyTimeout_dec, _getIsReadonly_dec, _getIsFocused_dec, _getSharedOpacity_dec, _getSharedStyles_dec, __getSelectionSharedStyles_dec, __getBindingsIndexCache_dec, _getCurrentPageRenderingShapesSorted_dec, _getCurrentPageShapesSorted_dec, _getCurrentPageShapes_dec, _getCurrentPageBounds_dec, _getCulledShapes_dec, __notVisibleShapes_dec, __getShapeMaskedPageBoundsCache_dec, __getShapeMaskCache_dec, __getShapeClipPathCache_dec, __getShapePageBoundsCache_dec, __getShapePageTransformCache_dec, __getShapeHandlesCache_dec, __getShapeGeometryCache_dec, __getAllAssetsQuery_dec, _getCurrentPageShapeIdsSorted_dec, _getCurrentPageId_dec, _getPages_dec, __getAllPagesQuery_dec, _getRenderingShapes_dec, _getCollaboratorsOnCurrentPage_dec, _getCollaborators_dec, __getCollaboratorsQuery_dec, _getViewportPageBounds_dec, _getViewportScreenCenter_dec, _getViewportScreenBounds_dec, _getZoomLevel_dec, _getCameraForFollowing_dec, _getViewportPageBoundsForFollowing_dec, _getCamera_dec, __unsafe_getCameraId_dec, _getErasingShapes_dec, _getErasingShapeIds_dec, _getHintingShape_dec, _getHintingShapeIds_dec, _getHoveredShape_dec, _getHoveredShapeId_dec, _getEditingShape_dec, _getEditingShapeId_dec, _getFocusedGroup_dec, _getFocusedGroupId_dec, _getSelectionRotatedScreenBounds_dec, _getSelectionRotatedPageBounds_dec, _getSelectionRotation_dec, _getSelectionPageBounds_dec, _getOnlySelectedShape_dec, _getOnlySelectedShapeId_dec, _getSelectedShapes_dec, _getSelectedShapeIds_dec, __getCurrentPageStateId_dec, _getCurrentPageState_dec, __getPageStatesQuery_dec, _getPageStates_dec, _getIsMenuOpen_dec, _getOpenMenus_dec, _getInstanceState_dec, _getDocumentSettings_dec, _getCurrentToolId_dec, _getCurrentTool_dec, _getPath_dec, _getCanRedo_dec, _getCanUndo_dec, _getIsShapeHiddenCache_dec, _a$1, _init$3;
class Editor extends (_a$1 = EventEmitter, _getIsShapeHiddenCache_dec = [computed], _getCanUndo_dec = [computed], _getCanRedo_dec = [computed], _getPath_dec = [computed], _getCurrentTool_dec = [computed], _getCurrentToolId_dec = [computed], _getDocumentSettings_dec = [computed], _getInstanceState_dec = [computed], _getOpenMenus_dec = [computed], _getIsMenuOpen_dec = [computed], _getPageStates_dec = [computed], __getPageStatesQuery_dec = [computed], _getCurrentPageState_dec = [computed], __getCurrentPageStateId_dec = [computed], _getSelectedShapeIds_dec = [computed], _getSelectedShapes_dec = [computed], _getOnlySelectedShapeId_dec = [computed], _getOnlySelectedShape_dec = [computed], _getSelectionPageBounds_dec = [computed], _getSelectionRotation_dec = [computed], _getSelectionRotatedPageBounds_dec = [computed], _getSelectionRotatedScreenBounds_dec = [computed], _getFocusedGroupId_dec = [computed], _getFocusedGroup_dec = [computed], _getEditingShapeId_dec = [computed], _getEditingShape_dec = [computed], _getHoveredShapeId_dec = [computed], _getHoveredShape_dec = [computed], _getHintingShapeIds_dec = [computed], _getHintingShape_dec = [computed], _getErasingShapeIds_dec = [computed], _getErasingShapes_dec = [computed], __unsafe_getCameraId_dec = [computed], _getCamera_dec = [computed], _getViewportPageBoundsForFollowing_dec = [computed], _getCameraForFollowing_dec = [computed], _getZoomLevel_dec = [computed], _getViewportScreenBounds_dec = [computed], _getViewportScreenCenter_dec = [computed], _getViewportPageBounds_dec = [computed], __getCollaboratorsQuery_dec = [computed], _getCollaborators_dec = [computed], _getCollaboratorsOnCurrentPage_dec = [computed], _getRenderingShapes_dec = [computed], __getAllPagesQuery_dec = [computed], _getPages_dec = [computed], _getCurrentPageId_dec = [computed], _getCurrentPageShapeIdsSorted_dec = [computed], __getAllAssetsQuery_dec = [computed], __getShapeGeometryCache_dec = [computed], __getShapeHandlesCache_dec = [computed], __getShapePageTransformCache_dec = [computed], __getShapePageBoundsCache_dec = [computed], __getShapeClipPathCache_dec = [computed], __getShapeMaskCache_dec = [computed], __getShapeMaskedPageBoundsCache_dec = [computed], __notVisibleShapes_dec = [computed], _getCulledShapes_dec = [computed], _getCurrentPageBounds_dec = [computed], _getCurrentPageShapes_dec = [computed], _getCurrentPageShapesSorted_dec = [computed], _getCurrentPageRenderingShapesSorted_dec = [computed], __getBindingsIndexCache_dec = [computed], __getSelectionSharedStyles_dec = [computed], _getSharedStyles_dec = [computed({ isEqual: (a, b) => a.equals(b) })], _getSharedOpacity_dec = [computed], _getIsFocused_dec = [computed], _getIsReadonly_dec = [computed], __setShiftKeyTimeout_dec = [bind$2], __setAltKeyTimeout_dec = [bind$2], __setCtrlKeyTimeout_dec = [bind$2], __setMetaKeyTimeout_dec = [bind$2], _a$1) {
constructor({
store,
user,
shapeUtils,
bindingUtils,
tools,
getContainer,
cameraOptions,
initialState,
autoFocus,
inferDarkMode,
options,
isShapeHidden
}) {
super();
__runInitializers$3(_init$3, 5, this);
__publicField$3(this, "_isShapeHiddenPredicate");
__publicField$3(this, "options");
__publicField$3(this, "contextId", uniqueId());
/**
* The editor's store
*
* @public
*/
__publicField$3(this, "store");
/**
* The root state of the statechart.
*
* @public
*/
__publicField$3(this, "root");
/**
* A set of functions to call when the app is disposed.
*
* @public
*/
__publicField$3(this, "disposables", /* @__PURE__ */ new Set());
/**
* Whether the editor is disposed.
*
* @public
*/
__publicField$3(this, "isDisposed", false);
/** @internal */
__publicField$3(this, "_tickManager");
/**
* A manager for the app's snapping feature.
*
* @public
*/
__publicField$3(this, "snaps");
/**
* A manager for the any asynchronous events and making sure they're
* cleaned up upon disposal.
*
* @public
*/
__publicField$3(this, "timers", tltime.forContext(this.contextId));
/**
* A manager for the user and their preferences.
*
* @public
*/
__publicField$3(this, "user");
/**
* A helper for measuring text.
*
* @public
*/
__publicField$3(this, "textMeasure");
/**
* A manager for the editor's environment.
*
* @deprecated This is deprecated and will be removed in a future version. Use the `tlenv` global export instead.
* @public
*/
__publicField$3(this, "environment", tlenv);
/**
* A manager for the editor's scribbles.
*
* @public
*/
__publicField$3(this, "scribbles");
/**
* A manager for side effects and correct state enforcement. See {@link @tldraw/store#StoreSideEffects} for details.
*
* @public
*/
__publicField$3(this, "sideEffects");
/**
* A manager for moving the camera when the mouse is at the edge of the screen.
*
* @public
*/
__publicField$3(this, "edgeScrollManager");
/**
* A manager for ensuring correct focus. See FocusManager for details.
*
* @internal
*/
__publicField$3(this, "focusManager");
/**
* The current HTML element containing the editor.
*
* @example
* ```ts
* const container = editor.getContainer()
* ```
*
* @public
*/
__publicField$3(this, "getContainer");
/* ------------------- Shape Utils ------------------ */
/**
* A map of shape utility classes (TLShapeUtils) by shape type.
*
* @public
*/
__publicField$3(this, "shapeUtils");
__publicField$3(this, "styleProps");
/* ------------------- Binding Utils ------------------ */
/**
* A map of shape utility classes (TLShapeUtils) by shape type.
*
* @public
*/
__publicField$3(this, "bindingUtils");
/* --------------------- History -------------------- */
/**
* A manager for the app's history.
*
* @readonly
*/
__publicField$3(this, "history");
__publicField$3(this, "_shouldIgnoreShapeLock", false);
/** @internal */
__publicField$3(this, "_crashingError", null);
/** @internal */
__publicField$3(this, "_isChangingStyleTimeout", -1);
// Menus
__publicField$3(this, "menus", tlmenus.forContext(this.contextId));
__publicField$3(this, "_cameraOptions", atom("camera options", DEFAULT_CAMERA_OPTIONS));
/** @internal */
__publicField$3(this, "_viewportAnimation", null);
// Viewport
/** @internal */
__publicField$3(this, "_willSetInitialBounds", true);
// Following
// When we are 'locked on' to a user, our camera is derived from their camera.
__publicField$3(this, "_isLockedOnFollowingUser", atom("isLockedOnFollowingUser", false));
// Camera state
// Camera state does two things: first, it allows us to subscribe to whether
// the camera is moving or not; and second, it allows us to update the rendering
// shapes on the canvas. Changing the rendering shapes may cause shapes to
// unmount / remount in the DOM, which is expensive; and computing visibility is
// also expensive in large projects. For this reason, we use a second bounding
// box just for rendering, and we only update after the camera stops moving.
__publicField$3(this, "_cameraState", atom("camera state", "idle"));
__publicField$3(this, "_cameraStateTimeoutRemaining", 0);
/* @internal */
__publicField$3(this, "_currentPageShapeIds");
// Parents and children
/**
* A cache of parents to children.
*
* @internal
*/
__publicField$3(this, "_parentIdsToChildIds");
__publicField$3(this, "animatingShapes", /* @__PURE__ */ new Map());
/* --------------------- Content -------------------- */
/** @internal */
__publicField$3(this, "externalAssetContentHandlers", {
file: null,
url: null
});
/** @internal */
__publicField$3(this, "temporaryAssetPreview", /* @__PURE__ */ new Map());
/** @internal */
__publicField$3(this, "externalContentHandlers", {
text: null,
files: null,
embed: null,
"svg-text": null,
url: null
});
/* --------------------- Events --------------------- */
/**
* The app's current input state.
*
* @public
*/
__publicField$3(this, "inputs", {
/** The most recent pointer down's position in the current page space. */
originPagePoint: new Vec(),
/** The most recent pointer down's position in screen space. */
originScreenPoint: new Vec(),
/** The previous pointer position in the current page space. */
previousPagePoint: new Vec(),
/** The previous pointer position in screen space. */
previousScreenPoint: new Vec(),
/** The most recent pointer position in the current page space. */
currentPagePoint: new Vec(),
/** The most recent pointer position in screen space. */
currentScreenPoint: new Vec(),
/** A set containing the currently pressed keys. */
keys: /* @__PURE__ */ new Set(),
/** A set containing the currently pressed buttons. */
buttons: /* @__PURE__ */ new Set(),
/** Whether the input is from a pe. */
isPen: false,
/** Whether the shift key is currently pressed. */
shiftKey: false,
/** Whether the meta key is currently pressed. */
metaKey: false,
/** Whether the control or command key is currently pressed. */
ctrlKey: false,
/** Whether the alt or option key is currently pressed. */
altKey: false,
/** Whether the user is dragging. */
isDragging: false,
/** Whether the user is pointing. */
isPointing: false,
/** Whether the user is pinching. */
isPinching: false,
/** Whether the user is editing. */
isEditing: false,
/** Whether the user is panning. */
isPanning: false,
/** Whether the user is spacebar panning. */
isSpacebarPanning: false,
/** Velocity of mouse pointer, in pixels per millisecond */
pointerVelocity: new Vec()
});
/**
* A manager for recording multiple click events.
*
* @internal
*/
__publicField$3(this, "_clickManager", new ClickManager(this));
/**
* The previous cursor. Used for restoring the cursor after pan events.
*
* @internal
*/
__publicField$3(this, "_prevCursor", "default");
/** @internal */
__publicField$3(this, "_shiftKeyTimeout", -1);
/** @internal */
__publicField$3(this, "_altKeyTimeout", -1);
/** @internal */
__publicField$3(this, "_ctrlKeyTimeout", -1);
/** @internal */
__publicField$3(this, "_metaKeyTimeout", -1);
/** @internal */
__publicField$3(this, "_restoreToolId", "select");
/** @internal */
__publicField$3(this, "_pinchStart", 1);
/** @internal */
__publicField$3(this, "_didPinch", false);
/** @internal */
__publicField$3(this, "_selectedShapeIdsAtPointerDown", []);
/** @internal */
__publicField$3(this, "_longPressTimeout", -1);
/** @internal */
__publicField$3(this, "capturedPointerId", null);
/** @internal */
__publicField$3(this, "performanceTracker");
/** @internal */
__publicField$3(this, "performanceTrackerTimeout", -1);
__publicField$3(this, "_pendingEventsForNextTick", []);
this._isShapeHiddenPredicate = isShapeHidden;
this.options = { ...defaultTldrawOptions, ...options };
this.store = store;
this.disposables.add(this.store.dispose.bind(this.store));
this.history = new HistoryManager({
store,
annotateError: (error) => {
this.annotateError(error, { origin: "history.batch", willCrashApp: true });
this.crash(error);
}
});
this.snaps = new SnapManager(this);
this.disposables.add(this.timers.dispose);
this._cameraOptions.set({ ...DEFAULT_CAMERA_OPTIONS, ...cameraOptions });
this.user = new UserPreferencesManager(user ?? createTLUser(), inferDarkMode ?? false);
this.disposables.add(() => this.user.dispose());
this.getContainer = getContainer;
this.textMeasure = new TextManager(this);
this._tickManager = new TickManager(this);
class NewRoot extends RootState {
static initial = initialState ?? "";
}
this.root = new NewRoot(this);
this.root.children = {};
const allShapeUtils = checkShapesAndAddCore(shapeUtils);
const _shapeUtils = {};
const _styleProps = {};
const allStylesById = /* @__PURE__ */ new Map();
for (const Util of allShapeUtils) {
const util = new Util(this);
_shapeUtils[Util.type] = util;
const propKeysByStyle = getShapePropKeysByStyle(Util.props ?? {});
_styleProps[Util.type] = propKeysByStyle;
for (const style of propKeysByStyle.keys()) {
if (!allStylesById.has(style.id)) {
allStylesById.set(style.id, style);
} else if (allStylesById.get(style.id) !== style) {
throw Error(
`Multiple style props with id "${style.id}" in use. Style prop IDs must be unique.`
);
}
}
}
this.shapeUtils = _shapeUtils;
this.styleProps = _styleProps;
const allBindingUtils = checkBindings(bindingUtils);
const _bindingUtils = {};
for (const Util of allBindingUtils) {
const util = new Util(this);
_bindingUtils[Util.type] = util;
}
this.bindingUtils = _bindingUtils;
for (const Tool of [...tools]) {
if (hasOwnProperty$1(this.root.children, Tool.id)) {
throw Error(`Can't override tool with id "${Tool.id}"`);
}
this.root.children[Tool.id] = new Tool(this, this.root);
}
this.scribbles = new ScribbleManager(this);
const cleanupInstancePageState = (prevPageState, shapesNoLongerInPage) => {
let nextPageState = null;
const selectedShapeIds = prevPageState.selectedShapeIds.filter(
(id) => !shapesNoLongerInPage.has(id)
);
if (selectedShapeIds.length !== prevPageState.selectedShapeIds.length) {
if (!nextPageState) nextPageState = { ...prevPageState };
nextPageState.selectedShapeIds = selectedShapeIds;
}
const erasingShapeIds = prevPageState.erasingShapeIds.filter(
(id) => !shapesNoLongerInPage.has(id)
);
if (erasingShapeIds.length !== prevPageState.erasingShapeIds.length) {
if (!nextPageState) nextPageState = { ...prevPageState };
nextPageState.erasingShapeIds = erasingShapeIds;
}
if (prevPageState.hoveredShapeId && shapesNoLongerInPage.has(prevPageState.hoveredShapeId)) {
if (!nextPageState) nextPageState = { ...prevPageState };
nextPageState.hoveredShapeId = null;
}
if (prevPageState.editingShapeId && shapesNoLongerInPage.has(prevPageState.editingShapeId)) {
if (!nextPageState) nextPageState = { ...prevPageState };
nextPageState.editingShapeId = null;
}
const hintingShapeIds = prevPageState.hintingShapeIds.filter(
(id) => !shapesNoLongerInPage.has(id)
);
if (hintingShapeIds.length !== prevPageState.hintingShapeIds.length) {
if (!nextPageState) nextPageState = { ...prevPageState };
nextPageState.hintingShapeIds = hintingShapeIds;
}
if (prevPageState.focusedGroupId && shapesNoLongerInPage.has(prevPageState.focusedGroupId)) {
if (!nextPageState) nextPageState = { ...prevPageState };
nextPageState.focusedGroupId = null;
}
return nextPageState;
};
this.sideEffects = this.store.sideEffects;
let deletedBindings = /* @__PURE__ */ new Map();
const deletedShapeIds = /* @__PURE__ */ new Set();
const invalidParents = /* @__PURE__ */ new Set();
let invalidBindingTypes = /* @__PURE__ */ new Set();
this.disposables.add(
this.sideEffects.registerOperationCompleteHandler(() => {
deletedShapeIds.clear();
for (const parentId of invalidParents) {
invalidParents.delete(parentId);
const parent = this.getShape(parentId);
if (!parent) continue;
const util = this.getShapeUtil(parent);
const changes = util.onChildrenChange?.(parent);
if (changes?.length) {
this.updateShapes(changes);
}
}
if (invalidBindingTypes.size) {
const t = invalidBindingTypes;
invalidBindingTypes = /* @__PURE__ */ new Set();
for (const type of t) {
const util = this.getBindingUtil(type);
util.onOperationComplete?.();
}
}
if (deletedBindings.size) {
const t = deletedBindings;
deletedBindings = /* @__PURE__ */ new Map();
for (const opts of t.values()) {
this.getBindingUtil(opts.binding).onAfterDelete?.(opts);
}
}
this.emit("update");
})
);
this.disposables.add(
this.sideEffects.register({
shape: {
afterChange: (shapeBefore, shapeAfter) => {
for (const binding of this.getBindingsInvolvingShape(shapeAfter)) {
invalidBindingTypes.add(binding.type);
if (binding.fromId === shapeAfter.id) {
this.getBindingUtil(binding).onAfterChangeFromShape?.({
binding,
shapeBefore,
shapeAfter
});
}
if (binding.toId === shapeAfter.id) {
this.getBindingUtil(binding).onAfterChangeToShape?.({
binding,
shapeBefore,
shapeAfter
});
}
}
if (shapeBefore.parentId !== shapeAfter.parentId) {
const notifyBindingAncestryChange = (id) => {
const descendantShape = this.getShape(id);
if (!descendantShape) return;
for (const binding of this.getBindingsInvolvingShape(descendantShape)) {
invalidBindingTypes.add(binding.type);
if (binding.fromId === descendantShape.id) {
this.getBindingUtil(binding).onAfterChangeFromShape?.({
binding,
shapeBefore: descendantShape,
shapeAfter: descendantShape
});
}
if (binding.toId === descendantShape.id) {
this.getBindingUtil(binding).onAfterChangeToShape?.({
binding,
shapeBefore: descendantShape,
shapeAfter: descendantShape
});
}
}
};
notifyBindingAncestryChange(shapeAfter.id);
this.visitDescendants(shapeAfter.id, notifyBindingAncestryChange);
}
if (shapeBefore.parentId !== shapeAfter.parentId && isPageId(shapeAfter.parentId)) {
const allMovingIds = /* @__PURE__ */ new Set([shapeBefore.id]);
this.visitDescendants(shapeBefore.id, (id) => {
allMovingIds.add(id);
});
for (const instancePageState of this.getPageStates()) {
if (instancePageState.pageId === shapeAfter.parentId) continue;
const nextPageState = cleanupInstancePageState(instancePageState, allMovingIds);
if (nextPageState) {
this.store.put([nextPageState]);
}
}
}
if (shapeBefore.parentId && isShapeId(shapeBefore.parentId)) {
invalidParents.add(shapeBefore.parentId);
}
if (shapeAfter.parentId !== shapeBefore.parentId && isShapeId(shapeAfter.parentId)) {
invalidParents.add(shapeAfter.parentId);
}
},
beforeDelete: (shape) => {
if (deletedShapeIds.has(shape.id)) return;
if (shape.parentId && isShapeId(shape.parentId)) {
invalidParents.add(shape.parentId);
}
deletedShapeIds.add(shape.id);
const deleteBindingIds = [];
for (const binding of this.getBindingsInvolvingShape(shape)) {
invalidBindingTypes.add(binding.type);
deleteBindingIds.push(binding.id);
const util = this.getBindingUtil(binding);
if (binding.fromId === shape.id) {
util.onBeforeIsolateToShape?.({ binding, removedShape: shape });
util.onBeforeDeleteFromShape?.({ binding, shape });
} else {
util.onBeforeIsolateFromShape?.({ binding, removedShape: shape });
util.onBeforeDeleteToShape?.({ binding, shape });
}
}
if (deleteBindingIds.length) {
this.deleteBindings(deleteBindingIds);
}
const deletedIds = /* @__PURE__ */ new Set([shape.id]);
const updates = compact(
this.getPageStates().map((pageState) => {
return cleanupInstancePageState(pageState, deletedIds);
})
);
if (updates.length) {
this.store.put(updates);
}
}
},
binding: {
beforeCreate: (binding) => {
const next = this.getBindingUtil(binding).onBeforeCreate?.({ binding });
if (next) return next;
return binding;
},
afterCreate: (binding) => {
invalidBindingTypes.add(binding.type);
this.getBindingUtil(binding).onAfterCreate?.({ binding });
},
beforeChange: (bindingBefore, bindingAfter) => {
const updated = this.getBindingUtil(bindingAfter).onBeforeChange?.({
bindingBefore,
bindingAfter
});
if (updated) return updated;
return bindingAfter;
},
afterChange: (bindingBefore, bindingAfter) => {
invalidBindingTypes.add(bindingAfter.type);
this.getBindingUtil(bindingAfter).onAfterChange?.({ bindingBefore, bindingAfter });
},
beforeDelete: (binding) => {
this.getBindingUtil(binding).onBeforeDelete?.({ binding });
},
afterDelete: (binding) => {
this.getBindingUtil(binding).onAfterDelete?.({ binding });
invalidBindingTypes.add(binding.type);
}
},
page: {
afterCreate: (record) => {
const cameraId = CameraRecordType.createId(record.id);
const _pageStateId = InstancePageStateRecordType.createId(record.id);
if (!this.store.has(cameraId)) {
this.store.put([CameraRecordType.create({ id: cameraId })]);
}
if (!this.store.has(_pageStateId)) {
this.store.put([
InstancePageStateRecordType.create({ id: _pageStateId, pageId: record.id })
]);
}
},
afterDelete: (record, source) => {
if (this.getInstanceState()?.currentPageId === record.id) {
const backupPageId = this.getPages().find((p) => p.id !== record.id)?.id;
if (backupPageId) {
this.store.put([{ ...this.getInstanceState(), currentPageId: backupPageId }]);
} else if (source === "user") {
this.store.ensureStoreIsUsable();
}
}
const cameraId = CameraRecordType.createId(record.id);
const instance_PageStateId = InstancePageStateRecordType.createId(record.id);
this.store.remove([cameraId, instance_PageStateId]);
}
},
instance: {
afterChange: (prev, next, source) => {
if (!this.store.has(next.currentPageId)) {
const backupPageId = this.store.has(prev.currentPageId) ? prev.currentPageId : this.getPages()[0]?.id;
if (backupPageId) {
this.store.update(next.id, (instance) => ({
...instance,
currentPageId: backupPageId
}));
} else if (source === "user") {
this.store.ensureStoreIsUsable();
}
}
}
},
instance_page_state: {
afterChange: (prev, next) => {
if (prev?.selectedShapeIds !== next?.selectedShapeIds) {
const filtered = next.selectedShapeIds.filter((id) => {
let parentId = this.getShape(id)?.parentId;
while (isShapeId(parentId)) {
if (next.selectedShapeIds.includes(parentId)) {
return false;
}
parentId = this.getShape(parentId)?.parentId;
}
return true;
});
let nextFocusedGroupId = null;
if (filtered.length > 0) {
const commonGroupAncestor = this.findCommonAncestor(
compact(filtered.map((id) => this.getShape(id))),
(shape) => this.isShapeOfType(shape, "group")
);
if (commonGroupAncestor) {
nextFocusedGroupId = commonGroupAncestor;
}
} else {
if (next?.focusedGroupId) {
nextFocusedGroupId = next.focusedGroupId;
}
}
if (filtered.length !== next.selectedShapeIds.length || nextFocusedGroupId !== next.focusedGroupId) {
this.store.put([
{
...next,
selectedShapeIds: filtered,
focusedGroupId: nextFocusedGroupId ?? null
}
]);
}
}
}
}
})
);
this._currentPageShapeIds = deriveShapeIdsInCurrentPage(
this.store,
() => this.getCurrentPageId()
);
this._parentIdsToChildIds = parentsToChildren(this.store);
this.disposables.add(
this.store.listen((changes) => {
this.emit("change", changes);
})
);
this.disposables.add(this.history.dispose);
this.run(
() => {
this.store.ensureStoreIsUsable();
this._updateCurrentPageState({
editingShapeId: null,
hoveredShapeId: null,
erasingShapeIds: []
});
},
{ history: "ignore" }
);
if (initialState && this.root.children[initialState] === void 0) {
throw Error(`No state found for initialState "${initialState}".`);
}
this.root.enter(void 0, "initial");
this.edgeScrollManager = new EdgeScrollManager(this);
this.focusManager = new FocusManager(this, autoFocus);
this.disposables.add(this.focusManager.dispose.bind(this.focusManager));
if (this.getInstanceState().followingUserId) {
this.stopFollowingUser();
}
this.on("tick", this._flushEventsForTick);
this.timers.requestAnimationFrame(() => {
this._tickManager.start();
});
this.performanceTracker = new PerformanceTracker();
if (this.store.props.collaboration?.mode) {
const mode = this.store.props.collaboration.mode;
this.disposables.add(
react("update collaboration mode", () => {
this.store.put([{ ...this.getInstanceState(), isReadonly: mode.get() === "readonly" }]);
})
);
}
}
getIsShapeHiddenCache() {
if (!this._isShapeHiddenPredicate) return null;
return this.store.createComputedCache("isShapeHidden", (shape) => {
const hiddenParent = this.findShapeAncestor(shape, (p) => this.isShapeHidden(p));
if (hiddenParent) return true;
return this._isShapeHiddenPredicate(shape, this) ?? false;
});
}
isShapeHidden(shapeOrId) {
if (!this._isShapeHiddenPredicate) return false;
return !!this.getIsShapeHiddenCache().get(
typeof shapeOrId === "string" ? shapeOrId : shapeOrId.id
);
}
/**
* Dispose the editor.
*
* @public
*/
dispose() {
this.disposables.forEach((dispose) => dispose());
this.disposables.clear();
this.isDisposed = true;
}
getShapeUtil(arg) {
const type = typeof arg === "string" ? arg : arg.type;
const shapeUtil = getOwnProperty(this.shapeUtils, type);
assert(shapeUtil, `No shape util found for type "${type}"`);
return shapeUtil;
}
getBindingUtil(arg) {
const type = typeof arg === "string" ? arg : arg.type;
const bindingUtil = getOwnProperty(this.bindingUtils, type);
assert(bindingUtil, `No binding util found for type "${type}"`);
return bindingUtil;
}
/**
* Undo to the last mark.
*
* @example
* ```ts
* editor.undo()
* ```
*
* @public
*/
undo() {
this._flushEventsForTick(0);
this.complete();
this.history.undo();
return this;
}
getCanUndo() {
return this.history.getNumUndos() > 0;
}
/**
* Redo to the next mark.
*
* @example
* ```ts
* editor.redo()
* ```
*
* @public
*/
redo() {
this._flushEventsForTick(0);
this.complete();
this.history.redo();
return this;
}
clearHistory() {
this.history.clear();
return this;
}
getCanRedo() {
return this.history.getNumRedos() > 0;
}
/**
* Create a new "mark", or stopping point, in the undo redo history. Creating a mark will clear
* any redos.
*
* @example
* ```ts
* editor.mark()
* editor.mark('flip shapes')
* ```
*
* @param markId - The mark's id, usually the reason for adding the mark.
*
* @public
* @deprecated use {@link Editor.markHistoryStoppingPoint} instead
*/
mark(markId) {
if (typeof markId === "string") {
console.warn(
`[tldraw] \`editor.history.mark("${markId}")\` is deprecated. Please use \`const myMarkId = editor.markHistoryStoppingPoint()\` instead.`
);
} else {
console.warn(
"[tldraw] `editor.mark()` is deprecated. Use `editor.markHistoryStoppingPoint()` instead."
);
}
this.history._mark(markId ?? uniqueId());
return this;
}
/**
* Create a new "mark", or stopping point, in the undo redo history. Creating a mark will clear
* any redos. You typically want to do this just before a user interaction begins or is handled.
*
* @example
* ```ts
* editor.markHistoryStoppingPoint()
* editor.flipShapes(editor.getSelectedShapes())
* ```
* @example
* ```ts
* const beginRotateMark = editor.markHistoryStoppingPoint()
* // if the use cancels the rotation, you can bail back to this mark
* editor.bailToMark(beginRotateMark)
* ```
*
* @public
* @param name - The name of the mark, useful for debugging the undo/redo stacks
* @returns a unique id for the mark that can be used with `squashToMark` or `bailToMark`.
*/
markHistoryStoppingPoint(name) {
const id = `[${name ?? "stop"}]_${uniqueId()}`;
this.history._mark(id);
return id;
}
/**
* @internal this is only used to implement some backwards-compatibility logic. Should be fine to delete after 6 months or whatever.
*/
getMarkIdMatching(idSubstring) {
return this.history.getMarkIdMatching(idSubstring);
}
/**
* Coalesces all changes since the given mark into a single change, removing any intermediate marks.
*
* This is useful if you need to 'compress' the recent history to simplify the undo/redo experience of a complex interaction.
*
* @example
* ```ts
* const bumpShapesMark = editor.markHistoryStoppingPoint()
* // ... some changes
* editor.squashToMark(bumpShapesMark)
* ```
*
* @param markId - The mark id to squash to.
*/
squashToMark(markId) {
this.history.squashToMark(markId);
return this;
}
/**
* Undo to the closest mark, discarding the changes so they cannot be redone.
*
* @example
* ```ts
* editor.bail()
* ```
*
* @public
*/
bail() {
this.history.bail();
return this;
}
/**
* Undo to the given mark, discarding the changes so they cannot be redone.
*
* @example
* ```ts
* const beginDrag = editor.markHistoryStoppingPoint()
* // ... some changes
* editor.bailToMark(beginDrag)
* ```
*
* @public
*/
bailToMark(id) {
this.history.bailToMark(id);
return this;
}
/**
* Run a function in a transaction with optional options for context.
* You can use the options to change the way that history is treated
* or allow changes to locked shapes.
*
* @example
* ```ts
* // updating with
* editor.run(() => {
* editor.updateShape({ ...myShape, x: 100 })
* }, { history: "ignore" })
*
* // forcing changes / deletions for locked shapes
* editor.toggleLock([myShape])
* editor.run(() => {
* editor.updateShape({ ...myShape, x: 100 })
* editor.deleteShape(myShape)
* }, { ignoreShapeLock: true }, )
* ```
*
* @param fn - The callback function to run.
* @param opts - The options for the batch.
*
*
* @public
*/
run(fn, opts) {
const previousIgnoreShapeLock = this._shouldIgnoreShapeLock;
this._shouldIgnoreShapeLock = opts?.ignoreShapeLock ?? previousIgnoreShapeLock;
try {
this.history.batch(fn, opts);
} finally {
this._shouldIgnoreShapeLock = previousIgnoreShapeLock;
}
return this;
}
/**
* @deprecated Use `Editor.run` instead.
*/
batch(fn, opts) {
return this.run(fn, opts);
}
/* --------------------- Errors --------------------- */
/** @internal */
annotateError(error, {
origin,
willCrashApp,
tags,
extras
}) {
const defaultAnnotations = this.createErrorAnnotations(origin, willCrashApp);
annotateError(error, {
tags: { ...defaultAnnotations.tags, ...tags },
extras: { ...defaultAnnotations.extras, ...extras }
});
if (willCrashApp) {
this.store.markAsPossiblyCorrupted();
}
return this;
}
/** @internal */
createErrorAnnotations(origin, willCrashApp) {
try {
const editingShapeId = this.getEditingShapeId();
return {
tags: {
origin,
willCrashApp
},
extras: {
activeStateNode: this.root.getPath(),
selectedShapes: this.getSelectedShapes(),
editingShape: editingShapeId ? this.getShape(editingShapeId) : void 0,
inputs: this.inputs
}
};
} catch {
return {
tags: {
origin,
willCrashApp
},
extras: {}
};
}
}
/**
* We can't use an `atom` here because there's a chance that when `crashAndReportError` is called,
* we're in a transaction that's about to be rolled back due to the same error we're currently
* reporting.
*
* Instead, to listen to changes to this value, you need to listen to app's `crash` event.
*
* @internal
*/
getCrashingError() {
return this._crashingError;
}
/** @internal */
crash(error) {
this._crashingError = error;
this.store.markAsPossiblyCorrupted();
this.emit("crash", { error });
return this;
}
getPath() {
return this.root.getPath().split("root.")[1];
}
/**
* Get whether a certain tool (or other state node) is currently active.
*
* @example
* ```ts
* editor.isIn('select')
* editor.isIn('select.brushing')
* ```
*
* @param path - The path of active states, separated by periods.
*
* @public
*/
isIn(path) {
const ids = path.split(".").reverse();
let state = this.root;
while (ids.length > 0) {
const id = ids.pop();
if (!id) return true;
const current = state.getCurrent();
if (current?.id === id) {
if (ids.length === 0) return true;
state = current;
continue;
} else return false;
}
return false;
}
/**
* Get whether the state node is in any of the given active paths.
*
* @example
* ```ts
* state.isInAny('select', 'erase')
* state.isInAny('select.brushing', 'erase.idle')
* ```
*
* @public
*/
isInAny(...paths) {
return paths.some((path) => this.isIn(path));
}
/**
* Set the selected tool.
*
* @example
* ```ts
* editor.setCurrentTool('hand')
* editor.setCurrentTool('hand', { date: Date.now() })
* ```
*
* @param id - The id of the tool to select.
* @param info - Arbitrary data to pass along into the transition.
*
* @public
*/
setCurrentTool(id, info = {}) {
this.root.transition(id, info);
return this;
}
getCurrentTool() {
return this.root.getCurrent();
}
getCurrentToolId() {
const currentTool = this.getCurrentTool();
if (!currentTool) return "";
return currentTool.getCurrentToolIdMask() ?? currentTool.id;
}
/**
* Get a descendant by its path.
*
* @example
* ```ts
* state.getStateDescendant('select')
* state.getStateDescendant('select.brushing')
* ```
*
* @param path - The descendant's path of state ids, separated by periods.
*
* @public
*/
getStateDescendant(path) {
const ids = path.split(".").reverse();
let state = this.root;
while (ids.length > 0) {
const id = ids.pop();
if (!id) return state;
const childState = state.children?.[id];
if (!childState) return void 0;
state = childState;
}
return state;
}
getDocumentSettings() {
return this.store.get(TLDOCUMENT_ID);
}
/**
* Update the global document settings that apply to all users.
*
* @public
**/
updateDocumentSettings(settings) {
this.run(
() => {
this.store.put([{ ...this.getDocumentSettings(), ...settings }]);
},
{ history: "ignore" }
);
return this;
}
getInstanceState() {
return this.store.get(TLINSTANCE_ID);
}
/**
* Update the instance's state.
*
* @param partial - A partial object to update the instance state with.
* @param historyOptions - History batch options.
*
* @public
*/
updateInstanceState(partial, historyOptions) {
this._updateInstanceState(partial, { history: "ignore", ...historyOptions });
if (partial.isChangingStyle !== void 0) {
clearTimeout(this._isChangingStyleTimeout);
if (partial.isChangingStyle === true) {
this._isChangingStyleTimeout = this.timers.setTimeout(() => {
this._updateInstanceState({ isChangingStyle: false }, { history: "ignore" });
}, 2e3);
}
}
return this;
}
/** @internal */
_updateInstanceState(partial, opts) {
this.run(() => {
this.store.put([
{
...this.getInstanceState(),
...partial
}
]);
}, opts);
}
getOpenMenus() {
return this.menus.getOpenMenus();
}
/**
* @deprecated Use `editor.menus.addOpenMenu` instead.
*
* @public
*/
addOpenMenu(id) {
this.menus.addOpenMenu(id);
return this;
}
/**
* @deprecated Use `editor.menus.deleteOpenMenu` instead.
*
* @public
*/
deleteOpenMenu(id) {
this.menus.deleteOpenMenu(id);
return this;
}
/**
* @deprecated Use `editor.menus.clearOpenMenus` instead.
*
* @public
*/
clearOpenMenus() {
this.menus.clearOpenMenus();
return this;
}
getIsMenuOpen() {
return this.menus.hasAnyOpenMenus();
}
/* --------------------- Cursor --------------------- */
/**
* Set the cursor.
*
* @param cursor - The cursor to set.
* @public
*/
setCursor(cursor) {
this.updateInstanceState({ cursor: { ...this.getInstanceState().cursor, ...cursor } });
return this;
}
getPageStates() {
return this._getPageStatesQuery().get();
}
_getPageStatesQuery() {
return this.store.query.records("instance_page_state");
}
getCurrentPageState() {
return this.store.get(this._getCurrentPageStateId());
}
_getCurrentPageStateId() {
return InstancePageStateRecordType.createId(this.getCurrentPageId());
}
/**
* Update this instance's page state.
*
* @example
* ```ts
* editor.updateCurrentPageState({ id: 'page1', editingShapeId: 'shape:123' })
* ```
*
* @param partial - The partial of the page state object containing the changes.
*
* @public
*/
updateCurrentPageState(partial) {
this._updateCurrentPageState(partial);
return this;
}
_updateCurrentPageState(partial) {
this.store.update(partial.id ?? this.getCurrentPageState().id, (state) => ({
...state,
...partial
}));
}
getSelectedShapeIds() {
return this.getCurrentPageState().selectedShapeIds;
}
getSelectedShapes() {
const { selectedShapeIds } = this.getCurrentPageState();
return compact(selectedShapeIds.map((id) => this.store.get(id)));
}
/**
* Select one or more shapes.
*
* @example
* ```ts
* editor.setSelectedShapes(['id1'])
* editor.setSelectedShapes(['id1', 'id2'])
* ```
*
* @param shapes - The shape (or shape ids) to select.
*
* @public
*/
setSelectedShapes(shapes) {
return this.run(
() => {
const ids = shapes.map((shape) => typeof shape === "string" ? shape : shape.id);
const { selectedShapeIds: prevSelectedShapeIds } = this.getCurrentPageState();
const prevSet = new Set(prevSelectedShapeIds);
if (ids.length === prevSet.size && ids.every((id) => prevSet.has(id))) return null;
this.store.put([{ ...this.getCurrentPageState(), selectedShapeIds: ids }]);
},
{ history: "record-preserveRedoStack" }
);
}
/**
* Determine whether or not any of a shape's ancestors are selected.
*
* @param shape - The shape (or shape id) of the shape to check.
*
* @public
*/
isAncestorSelected(shape) {
const id = typeof shape === "string" ? shape : shape?.id ?? null;
const _shape = this.getShape(id);
if (!_shape) return false;
const selectedShapeIds = this.getSelectedShapeIds();
return !!this.findShapeAncestor(_shape, (parent) => selectedShapeIds.includes(parent.id));
}
/**
* Select one or more shapes.
*
* @example
* ```ts
* editor.select('id1')
* editor.select('id1', 'id2')
* ```
*
* @param shapes - The shape (or the shape ids) to select.
*
* @public
*/
select(...shapes) {
const ids = typeof shapes[0] === "string" ? shapes : shapes.map((shape) => shape.id);
this.setSelectedShapes(ids);
return this;
}
/**
* Remove a shape from the existing set of selected shapes.
*
* @example
* ```ts
* editor.deselect(shape.id)
* ```
*
* @public
*/
deselect(...shapes) {
const ids = typeof shapes[0] === "string" ? shapes : shapes.map((shape) => shape.id);
const selectedShapeIds = this.getSelectedShapeIds();
if (selectedShapeIds.length > 0 && ids.length > 0) {
this.setSelectedShapes(selectedShapeIds.filter((id) => !ids.includes(id)));
}
return this;
}
/**
* Select all direct children of the current page.
*
* @example
* ```ts
* editor.selectAll()
* ```
*
* @public
*/
selectAll() {
const ids = this.getSortedChildIdsForParent(this.getCurrentPageId());
if (ids.length <= 0) return this;
this.setSelectedShapes(this._getUnlockedShapeIds(ids));
return this;
}
/**
* Clear the selection.
*
* @example
* ```ts
* editor.selectNone()
* ```
*
* @public
*/
selectNone() {
if (this.getSelectedShapeIds().length > 0) {
this.setSelectedShapes([]);
}
return this;
}
getOnlySelectedShapeId() {
return this.getOnlySelectedShape()?.id ?? null;
}
getOnlySelectedShape() {
const selectedShapes = this.getSelectedShapes();
return selectedShapes.length === 1 ? selectedShapes[0] : null;
}
/**
* @internal
*/
getShapesPageBounds(shapeIds) {
const bounds = compact(shapeIds.map((id) => this.getShapePageBounds(id)));
if (bounds.length === 0) return null;
return Box.Common(bounds);
}
getSelectionPageBounds() {
return this.getShapesPageBounds(this.getSelectedShapeIds());
}
/**
* @internal
*/
getShapesSharedRotation(shapeIds) {
let foundFirst = false;
let rotation = 0;
for (let i = 0, n = shapeIds.length; i < n; i++) {
const pageTransform = this.getShapePageTransform(shapeIds[i]);
if (!pageTransform) continue;
if (foundFirst) {
if (pageTransform.rotation() !== rotation) {
return 0;
}
} else {
foundFirst = true;
rotation = pageTransform.rotation();
}
}
return rotation;
}
getSelectionRotation() {
return this.getShapesSharedRotation(this.getSelectedShapeIds());
}
/**
* @internal
*/
getShapesRotatedPageBounds(shapeIds) {
if (shapeIds.length === 0) {
return void 0;
}
const selectionRotation = this.getShapesSharedRotation(shapeIds);
if (selectionRotation === 0) {
return this.getShapesPageBounds(shapeIds) ?? void 0;
}
if (shapeIds.length === 1) {
const bounds = this.getShapeGeometry(shapeIds[0]).bounds.clone();
const pageTransform = this.getShapePageTransform(shapeIds[0]);
bounds.point = pageTransform.applyToPoint(bounds.point);
return bounds;
}
const boxFromRotatedVertices = Box.FromPoints(
shapeIds.flatMap((id) => {
const pageTransform = this.getShapePageTransform(id);
if (!pageTransform) return [];
return pageTransform.applyToPoints(this.getShapeGeometry(id).bounds.corners);
}).map((p) => p.rot(-selectionRotation))
);
boxFromRotatedVertices.point = boxFromRotatedVertices.point.rot(selectionRotation);
return boxFromRotatedVertices;
}
getSelectionRotatedPageBounds() {
return this.getShapesRotatedPageBounds(this.getSelectedShapeIds());
}
getSelectionRotatedScreenBounds() {
const bounds = this.getSelectionRotatedPageBounds();
if (!bounds) return void 0;
const { x, y } = this.pageToScreen(bounds.point);
const zoom = this.getZoomLevel();
return new Box(x, y, bounds.width * zoom, bounds.height * zoom);
}
getFocusedGroupId() {
return this.getCurrentPageState().focusedGroupId ?? this.getCurrentPageId();
}
getFocusedGroup() {
const focusedGroupId = this.getFocusedGroupId();
return focusedGroupId ? this.getShape(focusedGroupId) : void 0;
}
/**
* Set the current focused group shape.
*
* @param shape - The group shape id (or group shape's id) to set as the focused group shape.
*
* @public
*/
setFocusedGroup(shape) {
const id = typeof shape === "string" ? shape : shape?.id ?? null;
if (id !== null) {
const shape2 = this.getShape(id);
if (!shape2) {
throw Error(`Editor.setFocusedGroup: Shape with id ${id} does not exist`);
}
if (!this.isShapeOfType(shape2, "group")) {
throw Error(
`Editor.setFocusedGroup: Cannot set focused group to shape of type ${shape2.type}`
);
}
}
if (id === this.getFocusedGroupId()) return this;
return this.run(
() => {
this.store.update(this.getCurrentPageState().id, (s) => ({ ...s, focusedGroupId: id }));
},
{ history: "record-preserveRedoStack" }
);
}
/**
* Exit the current focused group, moving up to the next parent group if there is one.
*
* @public
*/
popFocusedGroupId() {
const focusedGroup = this.getFocusedGroup();
if (focusedGroup) {
const match = this.findShapeAncestor(
focusedGroup,
(shape) => this.isShapeOfType(shape, "group")
);
this.setFocusedGroup(match?.id ?? null);
this.select(focusedGroup.id);
} else {
this.setFocusedGroup(null);
this.selectNone();
}
return this;
}
getEditingShapeId() {
return this.getCurrentPageState().editingShapeId;
}
getEditingShape() {
const editingShapeId = this.getEditingShapeId();
return editingShapeId ? this.getShape(editingShapeId) : void 0;
}
/**
* Set the current editing shape.
*
* @example
* ```ts
* editor.setEditingShape(myShape)
* editor.setEditingShape(myShape.id)
* ```
*
* @param shape - The shape (or shape id) to set as editing.
*
* @public
*/
setEditingShape(shape) {
const id = typeof shape === "string" ? shape : shape?.id ?? null;
if (id !== this.getEditingShapeId()) {
if (id) {
const shape2 = this.getShape(id);
if (shape2 && this.getShapeUtil(shape2).canEdit(shape2)) {
this.run(
() => {
this._updateCurrentPageState({ editingShapeId: id });
},
{ history: "ignore" }
);
return this;
}
}
this.run(
() => {
this._updateCurrentPageState({ editingShapeId: null });
},
{ history: "ignore" }
);
}
return this;
}
getHoveredShapeId() {
return this.getCurrentPageState().hoveredShapeId;
}
getHoveredShape() {
const hoveredShapeId = this.getHoveredShapeId();
return hoveredShapeId ? this.getShape(hoveredShapeId) : void 0;
}
/**
* Set the editor's current hovered shape.
*
* @example
* ```ts
* editor.setHoveredShape(myShape)
* editor.setHoveredShape(myShape.id)
* ```
*
* @param shape - The shape (or shape id) to set as hovered.
*
* @public
*/
setHoveredShape(shape) {
const id = typeof shape === "string" ? shape : shape?.id ?? null;
if (id === this.getHoveredShapeId()) return this;
this.run(
() => {
this.updateCurrentPageState({ hoveredShapeId: id });
},
{ history: "ignore" }
);
return this;
}
getHintingShapeIds() {
return this.getCurrentPageState().hintingShapeIds;
}
getHintingShape() {
const hintingShapeIds = this.getHintingShapeIds();
return compact(hintingShapeIds.map((id) => this.getShape(id)));
}
/**
* Set the editor's current hinting shapes.
*
* @example
* ```ts
* editor.setHintingShapes([myShape])
* editor.setHintingShapes([myShape.id])
* ```
*
* @param shapes - The shapes (or shape ids) to set as hinting.
*
* @public
*/
setHintingShapes(shapes) {
const ids = typeof shapes[0] === "string" ? shapes : shapes.map((shape) => shape.id);
this.run(
() => {
this._updateCurrentPageState({ hintingShapeIds: dedupe(ids) });
},
{ history: "ignore" }
);
return this;
}
getErasingShapeIds() {
return this.getCurrentPageState().erasingShapeIds;
}
getErasingShapes() {
const erasingShapeIds = this.getErasingShapeIds();
return compact(erasingShapeIds.map((id) => this.getShape(id)));
}
/**
* Set the editor's current erasing shapes.
*
* @example
* ```ts
* editor.setErasingShapes([myShape])
* editor.setErasingShapes([myShape.id])
* ```
*
* @param shapes - The shapes (or shape ids) to set as hinting.
*
* @public
*/
setErasingShapes(shapes) {
const ids = typeof shapes[0] === "string" ? shapes : shapes.map((shape) => shape.id);
ids.sort();
const erasingShapeIds = this.getErasingShapeIds();
this.run(
() => {
if (ids.length === erasingShapeIds.length) {
for (let i = 0; i < ids.length; i++) {
if (ids[i] !== erasingShapeIds[i]) {
this._updateCurrentPageState({ erasingShapeIds: ids });
break;
}
}
} else {
this._updateCurrentPageState({ erasingShapeIds: ids });
}
},
{ history: "ignore" }
);
return this;
}
// Cropping
/**
* The current cropping shape's id.
*
* @public
*/
getCroppingShapeId() {
return this.getCurrentPageState().croppingShapeId;
}
/**
* Set the current cropping shape.
*
* @example
* ```ts
* editor.setCroppingShape(myShape)
* editor.setCroppingShape(myShape.id)
* ```
*
*
* @param shape - The shape (or shape id) to set as cropping.
*
* @public
*/
setCroppingShape(shape) {
const id = typeof shape === "string" ? shape : shape?.id ?? null;
if (id !== this.getCroppingShapeId()) {
this.run(
() => {
if (!id) {
this.updateCurrentPageState({ croppingShapeId: null });
} else {
const shape2 = this.getShape(id);
const util = this.getShapeUtil(shape2);
if (shape2 && util.canCrop(shape2)) {
this.updateCurrentPageState({ croppingShapeId: id });
}
}
},
{ history: "ignore" }
);
}
return this;
}
_unsafe_getCameraId() {
return CameraRecordType.createId(this.getCurrentPageId());
}
getCamera() {
const baseCamera = this.store.get(this._unsafe_getCameraId());
if (this._isLockedOnFollowingUser.get()) {
const followingCamera = this.getCameraForFollowing();
if (followingCamera) {
return { ...baseCamera, ...followingCamera };
}
}
return baseCamera;
}
getViewportPageBoundsForFollowing() {
const followingUserId = this.getInstanceState().followingUserId;
if (!followingUserId) return null;
const leaderPresence = this.getCollaborators().find((c) => c.userId === followingUserId);
if (!leaderPresence) return null;
const { w: lw, h: lh } = leaderPresence.screenBounds;
const { x: lx, y: ly, z: lz } = leaderPresence.camera;
const theirViewport = new Box(-lx, -ly, lw / lz, lh / lz);
const ourViewport = this.getViewportScreenBounds().clone();
const ourAspectRatio = ourViewport.width / ourViewport.height;
ourViewport.width = theirViewport.width;
ourViewport.height = ourViewport.width / ourAspectRatio;
if (ourViewport.height < theirViewport.height) {
ourViewport.height = theirViewport.height;
ourViewport.width = ourViewport.height * ourAspectRatio;
}
ourViewport.center = theirViewport.center;
return ourViewport;
}
getCameraForFollowing() {
const viewport = this.getViewportPageBoundsForFollowing();
if (!viewport) return null;
return {
x: -viewport.x,
y: -viewport.y,
z: this.getViewportScreenBounds().w / viewport.width
};
}
getZoomLevel() {
return this.getCamera().z;
}
/**
* Get the camera's initial or reset zoom level.
*
* @example
* ```ts
* editor.getInitialZoom()
* ```
*
* @public */
getInitialZoom() {
const cameraOptions = this.getCameraOptions();
if (!cameraOptions.constraints) return 1;
if (cameraOptions.constraints.initialZoom === "default") return 1;
const { zx, zy } = getCameraFitXFitY(this, cameraOptions);
switch (cameraOptions.constraints.initialZoom) {
case "fit-min": {
return Math.max(zx, zy);
}
case "fit-max": {
return Math.min(zx, zy);
}
case "fit-x": {
return zx;
}
case "fit-y": {
return zy;
}
case "fit-min-100": {
return Math.min(1, Math.max(zx, zy));
}
case "fit-max-100": {
return Math.min(1, Math.min(zx, zy));
}
case "fit-x-100": {
return Math.min(1, zx);
}
case "fit-y-100": {
return Math.min(1, zy);
}
default: {
throw exhaustiveSwitchError(cameraOptions.constraints.initialZoom);
}
}
}
/**
* Get the camera's base level for calculating actual zoom levels based on the zoom steps.
*
* @example
* ```ts
* editor.getBaseZoom()
* ```
*
* @public */
getBaseZoom() {
const cameraOptions = this.getCameraOptions();
if (!cameraOptions.constraints) return 1;
if (cameraOptions.constraints.baseZoom === "default") return 1;
const { zx, zy } = getCameraFitXFitY(this, cameraOptions);
switch (cameraOptions.constraints.baseZoom) {
case "fit-min": {
return Math.max(zx, zy);
}
case "fit-max": {
return Math.min(zx, zy);
}
case "fit-x": {
return zx;
}
case "fit-y": {
return zy;
}
case "fit-min-100": {
return Math.min(1, Math.max(zx, zy));
}
case "fit-max-100": {
return Math.min(1, Math.min(zx, zy));
}
case "fit-x-100": {
return Math.min(1, zx);
}
case "fit-y-100": {
return Math.min(1, zy);
}
default: {
throw exhaustiveSwitchError(cameraOptions.constraints.baseZoom);
}
}
}
/**
* Get the current camera options.
*
* @example
* ```ts
* editor.getCameraOptions()
* ```
*
* @public */
getCameraOptions() {
return this._cameraOptions.get();
}
/**
* Set the camera options. Changing the options won't immediately change the camera itself, so you may want to call `setCamera` after changing the options.
*
* @example
* ```ts
* editor.setCameraOptions(myCameraOptions)
* editor.setCamera(editor.getCamera())
* ```
*
* @param opts - The camera options to set.
*
* @public */
setCameraOptions(opts) {
const next = structuredClone({
...this._cameraOptions.__unsafe__getWithoutCapture(),
...opts
});
if (next.zoomSteps?.length < 1) next.zoomSteps = [1];
this._cameraOptions.set(next);
this.setCamera(this.getCamera());
return this;
}
/** @internal */
getConstrainedCamera(point, opts) {
const currentCamera = this.getCamera();
let { x, y, z = currentCamera.z } = point;
if (!opts?.force) {
const cameraOptions = this.getCameraOptions();
const zoomMin = cameraOptions.zoomSteps[0];
const zoomMax = last$1(cameraOptions.zoomSteps);
const vsb = this.getViewportScreenBounds();
if (cameraOptions.constraints) {
const { constraints } = cameraOptions;
const py = Math.min(constraints.padding.y, vsb.w / 2);
const px = Math.min(constraints.padding.x, vsb.h / 2);
const bounds = Box.From(cameraOptions.constraints.bounds);
const zx = (vsb.w - px * 2) / bounds.w;
const zy = (vsb.h - py * 2) / bounds.h;
const baseZoom = this.getBaseZoom();
const maxZ = zoomMax * baseZoom;
const minZ = zoomMin * baseZoom;
if (opts?.reset) {
z = this.getInitialZoom();
}
if (z < minZ || z > maxZ) {
const { x: cx, y: cy, z: cz } = currentCamera;
const cxA = -cx + vsb.w / cz / 2;
const cyA = -cy + vsb.h / cz / 2;
z = clamp$2(z, minZ, maxZ);
const cxB = -cx + vsb.w / z / 2;
const cyB = -cy + vsb.h / z / 2;
x = cx + cxB - cxA;
y = cy + cyB - cyA;
}
const minX = px / z - bounds.x;
const minY = py / z - bounds.y;
const freeW = (vsb.w - px * 2) / z - bounds.w;
const freeH = (vsb.h - py * 2) / z - bounds.h;
const originX = minX + freeW * constraints.origin.x;
const originY = minY + freeH * constraints.origin.y;
const behaviorX = typeof constraints.behavior === "string" ? constraints.behavior : constraints.behavior.x;
const behaviorY = typeof constraints.behavior === "string" ? constraints.behavior : constraints.behavior.y;
if (opts?.reset) {
x = originX;
y = originY;
} else {
switch (behaviorX) {
case "fixed": {
x = originX;
break;
}
case "contain": {
if (z < zx) x = originX;
else x = clamp$2(x, minX + freeW, minX);
break;
}
case "inside": {
if (z < zx) x = clamp$2(x, minX, (vsb.w - px) / z - bounds.w);
else x = clamp$2(x, minX + freeW, minX);
break;
}
case "outside": {
x = clamp$2(x, px / z - bounds.w, (vsb.w - px) / z);
break;
}
case "free": {
break;
}
default: {
throw exhaustiveSwitchError(behaviorX);
}
}
switch (behaviorY) {
case "fixed": {
y = originY;
break;
}
case "contain": {
if (z < zy) y = originY;
else y = clamp$2(y, minY + freeH, minY);
break;
}
case "inside": {
if (z < zy) y = clamp$2(y, minY, (vsb.h - py) / z - bounds.h);
else y = clamp$2(y, minY + freeH, minY);
break;
}
case "outside": {
y = clamp$2(y, py / z - bounds.h, (vsb.h - py) / z);
break;
}
case "free": {
break;
}
default: {
throw exhaustiveSwitchError(behaviorY);
}
}
}
} else {
if (z > zoomMax || z < zoomMin) {
const { x: cx, y: cy, z: cz } = currentCamera;
z = clamp$2(z, zoomMin, zoomMax);
x = cx + (-cx + vsb.w / z / 2) - (-cx + vsb.w / cz / 2);
y = cy + (-cy + vsb.h / z / 2) - (-cy + vsb.h / cz / 2);
}
}
}
return { x, y, z };
}
/** @internal */
_setCamera(point, opts) {
const currentCamera = this.getCamera();
const { x, y, z } = this.getConstrainedCamera(point, opts);
if (currentCamera.x === x && currentCamera.y === y && currentCamera.z === z) {
return this;
}
transact(() => {
const camera = { ...currentCamera, x, y, z };
this.run(
() => {
this.store.put([camera]);
},
{ history: "ignore" }
);
const { currentScreenPoint, currentPagePoint } = this.inputs;
const { screenBounds } = this.store.unsafeGetWithoutCapture(TLINSTANCE_ID);
if (currentScreenPoint.x / z - x !== currentPagePoint.x || currentScreenPoint.y / z - y !== currentPagePoint.y) {
const event = {
type: "pointer",
target: "canvas",
name: "pointer_move",
// weird but true: we need to put the screen point back into client space
point: Vec.AddXY(currentScreenPoint, screenBounds.x, screenBounds.y),
pointerId: INTERNAL_POINTER_IDS.CAMERA_MOVE,
ctrlKey: this.inputs.ctrlKey,
altKey: this.inputs.altKey,
shiftKey: this.inputs.shiftKey,
metaKey: this.inputs.metaKey,
accelKey: isAccelKey(this.inputs),
button: 0,
isPen: this.getInstanceState().isPenMode ?? false
};
if (opts?.immediate) {
this._flushEventForTick(event);
} else {
this.dispatch(event);
}
}
this._tickCameraState();
});
return this;
}
/**
* Set the current camera.
*
* @example
* ```ts
* editor.setCamera({ x: 0, y: 0})
* editor.setCamera({ x: 0, y: 0, z: 1.5})
* editor.setCamera({ x: 0, y: 0, z: 1.5}, { animation: { duration: 1000, easing: (t) => t * t } })
* ```
*
* @param point - The new camera position.
* @param opts - The camera move options.
*
* @public
*/
setCamera(point, opts) {
const { isLocked } = this._cameraOptions.__unsafe__getWithoutCapture();
if (isLocked && !opts?.force) return this;
this.stopCameraAnimation();
if (this.getInstanceState().followingUserId) {
this.stopFollowingUser();
}
const _point = Vec.Cast(point);
if (!Number.isFinite(_point.x)) _point.x = 0;
if (!Number.isFinite(_point.y)) _point.y = 0;
if (_point.z === void 0 || !Number.isFinite(_point.z)) point.z = this.getZoomLevel();
const camera = this.getConstrainedCamera(_point, opts);
if (opts?.animation) {
const { width, height } = this.getViewportScreenBounds();
this._animateToViewport(
new Box(-camera.x, -camera.y, width / camera.z, height / camera.z),
opts
);
} else {
this._setCamera(camera, {
...opts,
// we already did the constraining, so we don't need to do it again
force: true
});
}
return this;
}
/**
* Center the camera on a point (in the current page space).
*
* @example
* ```ts
* editor.centerOnPoint({ x: 100, y: 100 })
* editor.centerOnPoint({ x: 100, y: 100 }, { animation: { duration: 200 } })
* ```
*
* @param point - The point in the current page space to center on.
* @param opts - The camera move options.
*
* @public
*/
centerOnPoint(point, opts) {
const { isLocked } = this.getCameraOptions();
if (isLocked && !opts?.force) return this;
const { width: pw, height: ph } = this.getViewportPageBounds();
this.setCamera(new Vec(-(point.x - pw / 2), -(point.y - ph / 2), this.getCamera().z), opts);
return this;
}
/**
* Zoom the camera to fit the current page's content in the viewport.
*
* @example
* ```ts
* editor.zoomToFit()
* editor.zoomToFit({ animation: { duration: 200 } })
* ```
*
* @param opts - The camera move options.
*
* @public
*/
zoomToFit(opts) {
const ids = [...this.getCurrentPageShapeIds()];
if (ids.length <= 0) return this;
const pageBounds = Box.Common(compact(ids.map((id) => this.getShapePageBounds(id))));
this.zoomToBounds(pageBounds, opts);
return this;
}
/**
* Set the zoom back to 100%.
*
* @example
* ```ts
* editor.resetZoom()
* editor.resetZoom(editor.getViewportScreenCenter(), { animation: { duration: 200 } })
* editor.resetZoom(editor.getViewportScreenCenter(), { animation: { duration: 200 } })
* ```
*
* @param point - The screen point to zoom out on. Defaults to the viewport screen center.
* @param opts - The camera move options.
*
* @public
*/
resetZoom(point = this.getViewportScreenCenter(), opts) {
const { isLocked, constraints } = this.getCameraOptions();
if (isLocked && !opts?.force) return this;
const currentCamera = this.getCamera();
const { x: cx, y: cy, z: cz } = currentCamera;
const { x, y } = point;
let z = 1;
if (constraints) {
const initialZoom = this.getInitialZoom();
if (cz !== initialZoom) {
z = initialZoom;
}
}
this.setCamera(
new Vec(cx + (x / z - x) - (x / cz - x), cy + (y / z - y) - (y / cz - y), z),
opts
);
return this;
}
/**
* Zoom the camera in.
*
* @example
* ```ts
* editor.zoomIn()
* editor.zoomIn(editor.getViewportScreenCenter(), { animation: { duration: 200 } })
* editor.zoomIn(editor.inputs.currentScreenPoint, { animation: { duration: 200 } })
* ```
*
* @param point - The screen point to zoom in on. Defaults to the screen center
* @param opts - The camera move options.
*
* @public
*/
zoomIn(point = this.getViewportScreenCenter(), opts) {
const { isLocked } = this.getCameraOptions();
if (isLocked && !opts?.force) return this;
const { x: cx, y: cy, z: cz } = this.getCamera();
const { zoomSteps } = this.getCameraOptions();
if (zoomSteps !== null && zoomSteps.length > 1) {
const baseZoom = this.getBaseZoom();
let zoom = last$1(zoomSteps) * baseZoom;
for (let i = 1; i < zoomSteps.length; i++) {
const z1 = zoomSteps[i - 1] * baseZoom;
const z2 = zoomSteps[i] * baseZoom;
if (z2 - cz <= (z2 - z1) / 2) continue;
zoom = z2;
break;
}
this.setCamera(
new Vec(
cx + (point.x / zoom - point.x) - (point.x / cz - point.x),
cy + (point.y / zoom - point.y) - (point.y / cz - point.y),
zoom
),
opts
);
}
return this;
}
/**
* Zoom the camera out.
*
* @example
* ```ts
* editor.zoomOut()
* editor.zoomOut(editor.getViewportScreenCenter(), { animation: { duration: 120 } })
* editor.zoomOut(editor.inputs.currentScreenPoint, { animation: { duration: 120 } })
* ```
*
* @param point - The point to zoom out on. Defaults to the viewport screen center.
* @param opts - The camera move options.
*
* @public
*/
zoomOut(point = this.getViewportScreenCenter(), opts) {
const { isLocked } = this.getCameraOptions();
if (isLocked && !opts?.force) return this;
const { zoomSteps } = this.getCameraOptions();
if (zoomSteps !== null && zoomSteps.length > 1) {
const baseZoom = this.getBaseZoom();
const { x: cx, y: cy, z: cz } = this.getCamera();
let zoom = zoomSteps[0] * baseZoom;
for (let i = zoomSteps.length - 1; i > 0; i--) {
const z1 = zoomSteps[i - 1] * baseZoom;
const z2 = zoomSteps[i] * baseZoom;
if (z2 - cz >= (z2 - z1) / 2) continue;
zoom = z1;
break;
}
this.setCamera(
new Vec(
cx + (point.x / zoom - point.x) - (point.x / cz - point.x),
cy + (point.y / zoom - point.y) - (point.y / cz - point.y),
zoom
),
opts
);
}
return this;
}
/**
* Zoom the camera to fit the current selection in the viewport.
*
* @example
* ```ts
* editor.zoomToSelection()
* editor.zoomToSelection({ animation: { duration: 200 } })
* ```
*
* @param opts - The camera move options.
*
* @public
*/
zoomToSelection(opts) {
const { isLocked } = this.getCameraOptions();
if (isLocked && !opts?.force) return this;
const selectionPageBounds = this.getSelectionPageBounds();
if (selectionPageBounds) {
this.zoomToBounds(selectionPageBounds, {
targetZoom: Math.max(1, this.getZoomLevel()),
...opts
});
}
return this;
}
/**
* Zoom the camera to fit a bounding box (in the current page space).
*
* @example
* ```ts
* editor.zoomToBounds(myBounds)
* editor.zoomToBounds(myBounds, { animation: { duration: 200 } })
* editor.zoomToBounds(myBounds, { animation: { duration: 200 }, inset: 0, targetZoom: 1 })
* ```
*
* @param bounds - The bounding box.
* @param opts - The camera move options, target zoom, or custom inset amount.
*
* @public
*/
zoomToBounds(bounds, opts) {
const cameraOptions = this._cameraOptions.__unsafe__getWithoutCapture();
if (cameraOptions.isLocked && !opts?.force) return this;
const viewportScreenBounds = this.getViewportScreenBounds();
const inset = opts?.inset ?? Math.min(ZOOM_TO_FIT_PADDING, viewportScreenBounds.width * 0.28);
const baseZoom = this.getBaseZoom();
const zoomMin = cameraOptions.zoomSteps[0];
const zoomMax = last$1(cameraOptions.zoomSteps);
let zoom = clamp$2(
Math.min(
(viewportScreenBounds.width - inset) / bounds.w,
(viewportScreenBounds.height - inset) / bounds.h
),
zoomMin * baseZoom,
zoomMax * baseZoom
);
if (opts?.targetZoom !== void 0) {
zoom = Math.min(opts.targetZoom, zoom);
}
this.setCamera(
new Vec(
-bounds.x + (viewportScreenBounds.width - bounds.w * zoom) / 2 / zoom,
-bounds.y + (viewportScreenBounds.height - bounds.h * zoom) / 2 / zoom,
zoom
),
opts
);
return this;
}
/**
* Stop the current camera animation, if any.
*
* @example
* ```ts
* editor.stopCameraAnimation()
* ```
*
* @public
*/
stopCameraAnimation() {
this.emit("stop-camera-animation");
return this;
}
/** @internal */
_animateViewport(ms) {
if (!this._viewportAnimation) return;
this._viewportAnimation.elapsed += ms;
const { elapsed, easing, duration, start, end } = this._viewportAnimation;
if (elapsed > duration) {
this.off("tick", this._animateViewport);
this._viewportAnimation = null;
this._setCamera(new Vec(-end.x, -end.y, this.getViewportScreenBounds().width / end.width));
return;
}
const remaining = duration - elapsed;
const t = easing(1 - remaining / duration);
const left = start.minX + (end.minX - start.minX) * t;
const top = start.minY + (end.minY - start.minY) * t;
const right = start.maxX + (end.maxX - start.maxX) * t;
this._setCamera(new Vec(-left, -top, this.getViewportScreenBounds().width / (right - left)), {
force: true
});
}
/** @internal */
_animateToViewport(targetViewportPage, opts = { animation: DEFAULT_ANIMATION_OPTIONS }) {
const { animation, ...rest } = opts;
if (!animation) return;
const { duration = 0, easing = EASINGS.easeInOutCubic } = animation;
const animationSpeed = this.user.getAnimationSpeed();
const viewportPageBounds = this.getViewportPageBounds();
this.stopCameraAnimation();
if (this.getInstanceState().followingUserId) {
this.stopFollowingUser();
}
if (duration === 0 || animationSpeed === 0) {
return this._setCamera(
new Vec(
-targetViewportPage.x,
-targetViewportPage.y,
this.getViewportScreenBounds().width / targetViewportPage.width
),
{ ...rest }
);
}
this._viewportAnimation = {
elapsed: 0,
duration: duration / animationSpeed,
easing,
start: viewportPageBounds.clone(),
end: targetViewportPage.clone()
};
this.once("stop-camera-animation", () => {
this.off("tick", this._animateViewport);
this._viewportAnimation = null;
});
this.on("tick", this._animateViewport);
return this;
}
/**
* Slide the camera in a certain direction.
*
* @example
* ```ts
* editor.slideCamera({ speed: 1, direction: { x: 1, y: 0 }, friction: 0.1 })
* ```
*
* @param opts - Options for the slide
* @public
*/
slideCamera(opts = {}) {
const { isLocked } = this.getCameraOptions();
if (isLocked && !opts?.force) return this;
const animationSpeed = this.user.getAnimationSpeed();
if (animationSpeed === 0) return this;
this.stopCameraAnimation();
const {
speed,
friction = this.options.cameraSlideFriction,
direction,
speedThreshold = 0.01
} = opts;
let currentSpeed = Math.min(speed, 1);
const cancel = () => {
this.off("tick", moveCamera);
this.off("stop-camera-animation", cancel);
};
this.once("stop-camera-animation", cancel);
const moveCamera = (elapsed) => {
const { x: cx, y: cy, z: cz } = this.getCamera();
const movementVec = Vec.Mul(direction, currentSpeed * elapsed / cz);
currentSpeed *= 1 - friction;
if (currentSpeed < speedThreshold) {
cancel();
} else {
this._setCamera(new Vec(cx + movementVec.x, cy + movementVec.y, cz));
}
};
this.on("tick", moveCamera);
return this;
}
/**
* Animate the camera to a user's cursor position. This also briefly show the user's cursor if it's not currently visible.
*
* @example
* ```ts
* editor.zoomToUser(myUserId)
* editor.zoomToUser(myUserId, { animation: { duration: 200 } })
* ```
*
* @param userId - The id of the user to animate to.
* @param opts - The camera move options.
* @public
*/
zoomToUser(userId, opts = { animation: { duration: 500 } }) {
const presence = this.getCollaborators().find((c) => c.userId === userId);
if (!presence) return this;
this.run(() => {
if (this.getInstanceState().followingUserId !== null) {
this.stopFollowingUser();
}
const isOnSamePage = presence.currentPageId === this.getCurrentPageId();
if (!isOnSamePage) {
this.setCurrentPage(presence.currentPageId);
}
if (opts && opts.animation && !isOnSamePage) {
opts.animation = void 0;
}
this.centerOnPoint(presence.cursor, opts);
const { highlightedUserIds } = this.getInstanceState();
this.updateInstanceState({ highlightedUserIds: [...highlightedUserIds, userId] });
this.timers.setTimeout(() => {
const highlightedUserIds2 = [...this.getInstanceState().highlightedUserIds];
const index = highlightedUserIds2.indexOf(userId);
if (index < 0) return;
highlightedUserIds2.splice(index, 1);
this.updateInstanceState({ highlightedUserIds: highlightedUserIds2 });
}, this.options.collaboratorIdleTimeoutMs);
});
return this;
}
/**
* Update the viewport. The viewport will measure the size and screen position of its container
* element. This should be done whenever the container's position on the screen changes.
*
* @example
* ```ts
* editor.updateViewportScreenBounds(new Box(0, 0, 1280, 1024))
* editor.updateViewportScreenBounds(new Box(0, 0, 1280, 1024), true)
* ```
*
* @param screenBounds - The new screen bounds of the viewport.
* @param center - Whether to preserve the viewport page center as the viewport changes.
*
* @public
*/
updateViewportScreenBounds(screenBounds, center = false) {
if (screenBounds instanceof HTMLElement) {
const rect = screenBounds.getBoundingClientRect();
screenBounds = new Box(
rect.left || rect.x,
rect.top || rect.y,
Math.max(rect.width, 1),
Math.max(rect.height, 1)
);
} else {
screenBounds.width = Math.max(screenBounds.width, 1);
screenBounds.height = Math.max(screenBounds.height, 1);
}
const insets = [
// top
screenBounds.minY !== 0,
// right
!approximately(document.body.scrollWidth, screenBounds.maxX, 1),
// bottom
!approximately(document.body.scrollHeight, screenBounds.maxY, 1),
// left
screenBounds.minX !== 0
];
const { _willSetInitialBounds } = this;
this._willSetInitialBounds = false;
const { screenBounds: prevScreenBounds, insets: prevInsets } = this.getInstanceState();
if (screenBounds.equals(prevScreenBounds) && insets.every((v, i) => v === prevInsets[i])) {
return this;
}
if (_willSetInitialBounds) {
this.updateInstanceState({ screenBounds: screenBounds.toJson(), insets });
this.setCamera(this.getCamera());
} else {
if (center && !this.getInstanceState().followingUserId) {
const before = this.getViewportPageBounds().center;
this.updateInstanceState({ screenBounds: screenBounds.toJson(), insets });
this.centerOnPoint(before);
} else {
this.updateInstanceState({ screenBounds: screenBounds.toJson(), insets });
this._setCamera(Vec.From({ ...this.getCamera() }));
}
}
this._tickCameraState();
return this;
}
getViewportScreenBounds() {
const { x, y, w, h } = this.getInstanceState().screenBounds;
return new Box(x, y, w, h);
}
getViewportScreenCenter() {
const viewportScreenBounds = this.getViewportScreenBounds();
return new Vec(
viewportScreenBounds.midX - viewportScreenBounds.minX,
viewportScreenBounds.midY - viewportScreenBounds.minY
);
}
getViewportPageBounds() {
const { w, h } = this.getViewportScreenBounds();
const { x: cx, y: cy, z: cz } = this.getCamera();
return new Box(-cx, -cy, w / cz, h / cz);
}
/**
* Convert a point in screen space to a point in the current page space.
*
* @example
* ```ts
* editor.screenToPage({ x: 100, y: 100 })
* ```
*
* @param point - The point in screen space.
*
* @public
*/
screenToPage(point) {
const { screenBounds } = this.store.unsafeGetWithoutCapture(TLINSTANCE_ID);
const { x: cx, y: cy, z: cz = 1 } = this.getCamera();
return new Vec(
(point.x - screenBounds.x) / cz - cx,
(point.y - screenBounds.y) / cz - cy,
point.z ?? 0.5
);
}
/**
* Convert a point in the current page space to a point in current screen space.
*
* @example
* ```ts
* editor.pageToScreen({ x: 100, y: 100 })
* ```
*
* @param point - The point in page space.
*
* @public
*/
pageToScreen(point) {
const { screenBounds } = this.store.unsafeGetWithoutCapture(TLINSTANCE_ID);
const { x: cx, y: cy, z: cz = 1 } = this.getCamera();
return new Vec(
(point.x + cx) * cz + screenBounds.x,
(point.y + cy) * cz + screenBounds.y,
point.z ?? 0.5
);
}
/**
* Convert a point in the current page space to a point in current viewport space.
*
* @example
* ```ts
* editor.pageToViewport({ x: 100, y: 100 })
* ```
*
* @param point - The point in page space.
*
* @public
*/
pageToViewport(point) {
const { x: cx, y: cy, z: cz = 1 } = this.getCamera();
return new Vec((point.x + cx) * cz, (point.y + cy) * cz, point.z ?? 0.5);
}
_getCollaboratorsQuery() {
return this.store.query.records("instance_presence", () => ({
userId: { neq: this.user.getId() }
}));
}
getCollaborators() {
const allPresenceRecords = this._getCollaboratorsQuery().get();
if (!allPresenceRecords.length) return EMPTY_ARRAY;
const userIds = [...new Set(allPresenceRecords.map((c) => c.userId))].sort();
return userIds.map((id) => {
const latestPresence = allPresenceRecords.filter((c) => c.userId === id).sort((a, b) => b.lastActivityTimestamp - a.lastActivityTimestamp)[0];
return latestPresence;
});
}
getCollaboratorsOnCurrentPage() {
const currentPageId = this.getCurrentPageId();
return this.getCollaborators().filter((c) => c.currentPageId === currentPageId);
}
/**
* Start viewport-following a user.
*
* @example
* ```ts
* editor.startFollowingUser(myUserId)
* ```
*
* @param userId - The id of the user to follow.
*
* @public
*/
startFollowingUser(userId) {
this.stopFollowingUser();
const leaderPresences = this._getCollaboratorsQuery().get().filter((p) => p.userId === userId);
if (!leaderPresences.length) {
console.warn("User not found");
return this;
}
const thisUserId = this.user.getId();
if (!thisUserId) {
console.warn("You should set the userId for the current instance before following a user");
}
if (leaderPresences.some((p) => p.followingUserId === thisUserId)) {
return this;
}
const latestLeaderPresence = computed("latestLeaderPresence", () => {
return this.getCollaborators().find((p) => p.userId === userId);
});
transact(() => {
this.updateInstanceState({ followingUserId: userId }, { history: "ignore" });
const dispose = react("update current page", () => {
const leaderPresence = latestLeaderPresence.get();
if (!leaderPresence) {
this.stopFollowingUser();
return;
}
if (leaderPresence.currentPageId !== this.getCurrentPageId() && this.getPage(leaderPresence.currentPageId)) {
this.run(
() => {
this.store.put([
{ ...this.getInstanceState(), currentPageId: leaderPresence.currentPageId }
]);
this._isLockedOnFollowingUser.set(true);
},
{ history: "ignore" }
);
}
});
const cancel = () => {
dispose();
this._isLockedOnFollowingUser.set(false);
this.off("frame", moveTowardsUser);
this.off("stop-following", cancel);
};
const moveTowardsUser = () => {
const leaderPresence = latestLeaderPresence.get();
if (!leaderPresence) {
this.stopFollowingUser();
return;
}
if (this._isLockedOnFollowingUser.get()) return;
const animationSpeed = this.user.getAnimationSpeed();
if (animationSpeed === 0) {
this._isLockedOnFollowingUser.set(true);
return;
}
const targetViewport = this.getViewportPageBoundsForFollowing();
if (!targetViewport) {
this.stopFollowingUser();
return;
}
const currentViewport = this.getViewportPageBounds();
const diffX = Math.abs(targetViewport.minX - currentViewport.minX) + Math.abs(targetViewport.maxX - currentViewport.maxX);
const diffY = Math.abs(targetViewport.minY - currentViewport.minY) + Math.abs(targetViewport.maxY - currentViewport.maxY);
if (diffX < this.options.followChaseViewportSnap && diffY < this.options.followChaseViewportSnap) {
this._isLockedOnFollowingUser.set(true);
return;
}
const t = clamp$2(animationSpeed * 0.5, 0.1, 0.8);
const nextViewport = new Box(
lerp(currentViewport.minX, targetViewport.minX, t),
lerp(currentViewport.minY, targetViewport.minY, t),
lerp(currentViewport.width, targetViewport.width, t),
lerp(currentViewport.height, targetViewport.height, t)
);
const nextCamera = new Vec(
-nextViewport.x,
-nextViewport.y,
this.getViewportScreenBounds().width / nextViewport.width
);
this.stopCameraAnimation();
this._setCamera(nextCamera);
};
this.once("stop-following", cancel);
this.addListener("frame", moveTowardsUser);
moveTowardsUser();
});
return this;
}
/**
* Stop viewport-following a user.
*
* @example
* ```ts
* editor.stopFollowingUser()
* ```
* @public
*/
stopFollowingUser() {
this.run(
() => {
this.store.put([this.getCamera()]);
this._isLockedOnFollowingUser.set(false);
this.updateInstanceState({ followingUserId: null });
this.emit("stop-following");
},
{ history: "ignore" }
);
return this;
}
/** @internal */
getUnorderedRenderingShapes(useEditorState) {
const renderingShapes = [];
let nextIndex = this.options.maxShapesPerPage * 2;
let nextBackgroundIndex = this.options.maxShapesPerPage;
const erasingShapeIds = this.getErasingShapeIds();
const addShapeById = (id, opacity, isAncestorErasing) => {
const shape = this.getShape(id);
if (!shape) return;
if (this.isShapeHidden(shape)) return;
opacity *= shape.opacity;
let isShapeErasing = false;
const util = this.getShapeUtil(shape);
if (useEditorState) {
isShapeErasing = !isAncestorErasing && erasingShapeIds.includes(id);
if (isShapeErasing) {
opacity *= 0.32;
}
}
renderingShapes.push({
id,
shape,
util,
index: nextIndex,
backgroundIndex: nextBackgroundIndex,
opacity
});
nextIndex += 1;
nextBackgroundIndex += 1;
const childIds = this.getSortedChildIdsForParent(id);
if (!childIds.length) return;
let backgroundIndexToRestore = null;
if (util.providesBackgroundForChildren(shape)) {
backgroundIndexToRestore = nextBackgroundIndex;
nextBackgroundIndex = nextIndex;
nextIndex += this.options.maxShapesPerPage;
}
for (const childId of childIds) {
addShapeById(childId, opacity, isAncestorErasing || isShapeErasing);
}
if (backgroundIndexToRestore !== null) {
nextBackgroundIndex = backgroundIndexToRestore;
}
};
const pages = useEditorState ? [this.getCurrentPage()] : this.getPages();
for (const page of pages) {
for (const childId of this.getSortedChildIdsForParent(page.id)) {
addShapeById(childId, 1, false);
}
}
return renderingShapes;
}
_decayCameraStateTimeout(elapsed) {
this._cameraStateTimeoutRemaining -= elapsed;
if (this._cameraStateTimeoutRemaining > 0) return;
this.off("tick", this._decayCameraStateTimeout);
this._cameraState.set("idle");
}
_tickCameraState() {
this._cameraStateTimeoutRemaining = this.options.cameraMovingTimeoutMs;
if (this._cameraState.__unsafe__getWithoutCapture() !== "idle") return;
this._cameraState.set("moving");
this.on("tick", this._decayCameraStateTimeout);
}
/**
* Whether the camera is moving or idle.
*
* @example
* ```ts
* editor.getCameraState()
* ```
*
* @public
*/
getCameraState() {
return this._cameraState.get();
}
getRenderingShapes() {
const renderingShapes = this.getUnorderedRenderingShapes(true);
return renderingShapes.sort(sortById);
}
_getAllPagesQuery() {
return this.store.query.records("page");
}
getPages() {
return this._getAllPagesQuery().get().sort(sortByIndex$1);
}
/**
* The current page.
*
* @example
* ```ts
* editor.getCurrentPage()
* ```
*
* @public
*/
getCurrentPage() {
return this.getPage(this.getCurrentPageId());
}
getCurrentPageId() {
return this.getInstanceState().currentPageId;
}
/**
* Get a page.
*
* @example
* ```ts
* editor.getPage(myPage.id)
* editor.getPage(myPage)
* ```
*
* @param page - The page (or the page id) to get.
*
* @public
*/
getPage(page) {
return this.store.get(typeof page === "string" ? page : page.id);
}
/**
* An array of all of the shapes on the current page.
*
* @example
* ```ts
* editor.getCurrentPageIds()
* ```
*
* @public
*/
getCurrentPageShapeIds() {
return this._currentPageShapeIds.get();
}
getCurrentPageShapeIdsSorted() {
return Array.from(this.getCurrentPageShapeIds()).sort();
}
/**
* Get the ids of shapes on a page.
*
* @example
* ```ts
* const idsOnPage1 = editor.getPageShapeIds('page1')
* const idsOnPage2 = editor.getPageShapeIds(myPage2)
* ```
*
* @param page - The page (or the page id) to get the shape ids for.
*
* @public
**/
getPageShapeIds(page) {
const pageId = typeof page === "string" ? page : page.id;
const result = this.store.query.exec("shape", { parentId: { eq: pageId } });
return this.getShapeAndDescendantIds(result.map((s) => s.id));
}
/**
* Set the current page.
*
* @example
* ```ts
* editor.setCurrentPage('page1')
* editor.setCurrentPage(myPage1)
* ```
*
* @param page - The page (or the page id) to set as the current page.
*
* @public
*/
setCurrentPage(page) {
const pageId = typeof page === "string" ? page : page.id;
if (!this.store.has(pageId)) {
console.error("Tried to set the current page id to a page that doesn't exist.");
return this;
}
this.stopFollowingUser();
this.complete();
return this.run(
() => {
this.store.put([{ ...this.getInstanceState(), currentPageId: pageId }]);
this.setCamera(this.getCamera());
},
{ history: "record-preserveRedoStack" }
);
}
/**
* Update a page.
*
* @example
* ```ts
* editor.updatePage({ id: 'page2', name: 'Page 2' })
* ```
*
* @param partial - The partial of the shape to update.
*
* @public
*/
updatePage(partial) {
if (this.getIsReadonly()) return this;
const prev = this.getPage(partial.id);
if (!prev) return this;
return this.run(() => this.store.update(partial.id, (page) => ({ ...page, ...partial })));
}
/**
* Create a page.
*
* @example
* ```ts
* editor.createPage(myPage)
* editor.createPage({ name: 'Page 2' })
* ```
*
* @param page - The page (or page partial) to create.
*
* @public
*/
createPage(page) {
this.run(() => {
if (this.getIsReadonly()) return;
if (this.getPages().length >= this.options.maxPages) return;
const pages = this.getPages();
const name = getIncrementedName(
page.name ?? "Page 1",
pages.map((p) => p.name)
);
let index = page.index;
if (!index || pages.some((p) => p.index === index)) {
index = getIndexAbove(pages[pages.length - 1].index);
}
const newPage = PageRecordType.create({
meta: {},
...page,
name,
index
});
this.store.put([newPage]);
});
return this;
}
/**
* Delete a page.
*
* @example
* ```ts
* editor.deletePage('page1')
* ```
*
* @param page - The page (or the page id) to delete.
*
* @public
*/
deletePage(page) {
const id = typeof page === "string" ? page : page.id;
this.run(() => {
if (this.getIsReadonly()) return;
const pages = this.getPages();
if (pages.length === 1) return;
const deletedPage = this.getPage(id);
if (!deletedPage) return;
if (id === this.getCurrentPageId()) {
const index = pages.findIndex((page2) => page2.id === id);
const next = pages[index - 1] ?? pages[index + 1];
this.setCurrentPage(next.id);
}
this.store.remove([deletedPage.id]);
});
return this;
}
/**
* Duplicate a page.
*
* @param page - The page (or the page id) to duplicate. Defaults to the current page.
* @param createId - The id of the new page. Defaults to a new id.
*
* @public
*/
duplicatePage(page, createId = PageRecordType.createId()) {
if (this.getPages().length >= this.options.maxPages) return this;
const id = typeof page === "string" ? page : page.id;
const freshPage = this.getPage(id);
if (!freshPage) return this;
const prevCamera = { ...this.getCamera() };
const content = this.getContentFromCurrentPage(this.getSortedChildIdsForParent(freshPage.id));
this.run(() => {
const pages = this.getPages();
const index = getIndexBetween(freshPage.index, pages[pages.indexOf(freshPage) + 1]?.index);
this.createPage({ name: freshPage.name + " Copy", id: createId, index });
this.setCurrentPage(createId);
this.setCamera(prevCamera);
if (content) {
return this.putContentOntoCurrentPage(content);
}
});
return this;
}
/**
* Rename a page.
*
* @example
* ```ts
* editor.renamePage('page1', 'My Page')
* ```
*
* @param page - The page (or the page id) to rename.
* @param name - The new name.
*
* @public
*/
renamePage(page, name) {
const id = typeof page === "string" ? page : page.id;
if (this.getIsReadonly()) return this;
this.updatePage({ id, name });
return this;
}
_getAllAssetsQuery() {
return this.store.query.records("asset");
}
/**
* Get all assets in the editor.
*
* @public
*/
getAssets() {
return this._getAllAssetsQuery().get();
}
/**
* Create one or more assets.
*
* @example
* ```ts
* editor.createAssets([...myAssets])
* ```
*
* @param assets - The assets to create.
*
* @public
*/
createAssets(assets) {
if (this.getIsReadonly()) return this;
if (assets.length <= 0) return this;
this.run(() => this.store.put(assets), { history: "ignore" });
return this;
}
/**
* Update one or more assets.
*
* @example
* ```ts
* editor.updateAssets([{ id: 'asset1', name: 'New name' }])
* ```
*
* @param assets - The assets to update.
*
* @public
*/
updateAssets(assets) {
if (this.getIsReadonly()) return this;
if (assets.length <= 0) return this;
this.run(
() => {
this.store.put(
assets.map((partial) => ({
...this.store.get(partial.id),
...partial
}))
);
},
{ history: "ignore" }
);
return this;
}
/**
* Delete one or more assets.
*
* @example
* ```ts
* editor.deleteAssets(['asset1', 'asset2'])
* ```
*
* @param assets - The assets (or asset ids) to delete.
*
* @public
*/
deleteAssets(assets) {
if (this.getIsReadonly()) return this;
const ids = typeof assets[0] === "string" ? assets : assets.map((a) => a.id);
if (ids.length <= 0) return this;
this.run(() => this.store.remove(ids), { history: "ignore" });
return this;
}
/**
* Get an asset by its id.
*
* @example
* ```ts
* editor.getAsset('asset1')
* ```
*
* @param asset - The asset (or asset id) to get.
*
* @public
*/
getAsset(asset) {
return this.store.get(typeof asset === "string" ? asset : asset.id);
}
async resolveAssetUrl(assetId, context) {
if (!assetId) return null;
const asset = this.getAsset(assetId);
if (!asset) return null;
const { screenScale = 1, shouldResolveToOriginal = false } = context;
const zoomStepFunction = (zoom) => Math.pow(2, Math.ceil(Math.log2(zoom)));
const steppedScreenScale = Math.max(0.125, zoomStepFunction(screenScale));
const networkEffectiveType = "connection" in navigator ? navigator.connection.effectiveType : null;
const dpr = this.getInstanceState().devicePixelRatio;
return await this.store.props.assets.resolve(asset, {
screenScale: screenScale || 1,
steppedScreenScale,
dpr,
networkEffectiveType,
shouldResolveToOriginal
});
}
/**
* Upload an asset to the store's asset service, returning a URL that can be used to resolve the
* asset.
*/
async uploadAsset(asset, file) {
return await this.store.props.assets.upload(asset, file);
}
_getShapeGeometryCache() {
return this.store.createComputedCache(
"bounds",
(shape) => this.getShapeUtil(shape).getGeometry(shape),
(a, b) => a.props === b.props
);
}
/**
* Get the geometry of a shape.
*
* @example
* ```ts
* editor.getShapeGeometry(myShape)
* editor.getShapeGeometry(myShapeId)
* ```
*
* @param shape - The shape (or shape id) to get the geometry for.
*
* @public
*/
getShapeGeometry(shape) {
return this._getShapeGeometryCache().get(typeof shape === "string" ? shape : shape.id);
}
_getShapeHandlesCache() {
return this.store.createComputedCache("handles", (shape) => {
return this.getShapeUtil(shape).getHandles?.(shape);
});
}
/**
* Get the handles (if any) for a shape.
*
* @example
* ```ts
* editor.getShapeHandles(myShape)
* editor.getShapeHandles(myShapeId)
* ```
*
* @param shape - The shape (or shape id) to get the handles for.
* @public
*/
getShapeHandles(shape) {
return this._getShapeHandlesCache().get(typeof shape === "string" ? shape : shape.id);
}
/**
* Get the local transform for a shape as a matrix model. This transform reflects both its
* translation (x, y) from from either its parent's top left corner, if the shape's parent is
* another shape, or else from the 0,0 of the page, if the shape's parent is the page; and the
* shape's rotation.
*
* @example
* ```ts
* editor.getShapeLocalTransform(myShape)
* ```
*
* @param shape - The shape to get the local transform for.
*
* @public
*/
getShapeLocalTransform(shape) {
const id = typeof shape === "string" ? shape : shape.id;
const freshShape = this.getShape(id);
if (!freshShape) throw Error("Editor.getTransform: shape not found");
return Mat.Identity().translate(freshShape.x, freshShape.y).rotate(freshShape.rotation);
}
_getShapePageTransformCache() {
return this.store.createComputedCache("pageTransformCache", (shape) => {
if (isPageId(shape.parentId)) {
return this.getShapeLocalTransform(shape);
}
const parentTransform = this._getShapePageTransformCache().get(shape.parentId) ?? Mat.Identity();
return Mat.Compose(parentTransform, this.getShapeLocalTransform(shape));
});
}
/**
* Get the local transform of a shape's parent as a matrix model.
*
* @example
* ```ts
* editor.getShapeParentTransform(myShape)
* ```
*
* @param shape - The shape (or shape id) to get the parent transform for.
*
* @public
*/
getShapeParentTransform(shape) {
const id = typeof shape === "string" ? shape : shape.id;
const freshShape = this.getShape(id);
if (!freshShape || isPageId(freshShape.parentId)) return Mat.Identity();
return this._getShapePageTransformCache().get(freshShape.parentId) ?? Mat.Identity();
}
/**
* Get the transform of a shape in the current page space.
*
* @example
* ```ts
* editor.getShapePageTransform(myShape)
* editor.getShapePageTransform(myShapeId)
* ```
*
* @param shape - The shape (or shape id) to get the page transform for.
*
* @public
*/
getShapePageTransform(shape) {
const id = typeof shape === "string" ? shape : shape.id;
return this._getShapePageTransformCache().get(id) ?? Mat.Identity();
}
_getShapePageBoundsCache() {
return this.store.createComputedCache("pageBoundsCache", (shape) => {
const pageTransform = this._getShapePageTransformCache().get(shape.id);
if (!pageTransform) return new Box();
const result = Box.FromPoints(
Mat.applyToPoints(pageTransform, this.getShapeGeometry(shape).vertices)
);
return result;
});
}
/**
* Get the bounds of a shape in the current page space.
*
* @example
* ```ts
* editor.getShapePageBounds(myShape)
* editor.getShapePageBounds(myShapeId)
* ```
*
* @param shape - The shape (or shape id) to get the bounds for.
*
* @public
*/
getShapePageBounds(shape) {
return this._getShapePageBoundsCache().get(typeof shape === "string" ? shape : shape.id);
}
_getShapeClipPathCache() {
return this.store.createComputedCache("clipPathCache", (shape) => {
const pageMask = this._getShapeMaskCache().get(shape.id);
if (!pageMask) return void 0;
if (pageMask.length === 0) {
return `polygon(0px 0px, 0px 0px, 0px 0px)`;
}
const pageTransform = this._getShapePageTransformCache().get(shape.id);
if (!pageTransform) return void 0;
const localMask = Mat.applyToPoints(Mat.Inverse(pageTransform), pageMask);
return `polygon(${localMask.map((p) => `${p.x}px ${p.y}px`).join(",")})`;
});
}
/**
* Get the clip path for a shape.
*
* @example
* ```ts
* const clipPath = editor.getShapeClipPath(shape)
* const clipPath = editor.getShapeClipPath(shape.id)
* ```
*
* @param shape - The shape (or shape id) to get the clip path for.
*
* @returns The clip path or undefined.
*
* @public
*/
getShapeClipPath(shape) {
return this._getShapeClipPathCache().get(typeof shape === "string" ? shape : shape.id);
}
_getShapeMaskCache() {
return this.store.createComputedCache("pageMaskCache", (shape) => {
if (isPageId(shape.parentId)) return void 0;
const frameAncestors = this.getShapeAncestors(shape.id).filter(
(shape2) => this.isShapeOfType(shape2, "frame")
);
if (frameAncestors.length === 0) return void 0;
const pageMask = frameAncestors.map(
(s) => (
// Apply the frame transform to the frame outline to get the frame outline in the current page space
(this._getShapePageTransformCache().get(s.id).applyToPoints(this.getShapeGeometry(s).vertices))
)
).reduce((acc, b) => {
if (!(b && acc)) return void 0;
const intersection = intersectPolygonPolygon(acc, b);
if (intersection) {
return intersection.map(Vec.Cast);
}
return [];
});
return pageMask;
});
}
/**
* Get the mask (in the current page space) for a shape.
*
* @example
* ```ts
* const pageMask = editor.getShapeMask(shape.id)
* ```
*
* @param shape - The shape (or the shape id) of the shape to get the mask for.
*
* @returns The mask for the shape.
*
* @public
*/
getShapeMask(shape) {
return this._getShapeMaskCache().get(typeof shape === "string" ? shape : shape.id);
}
/**
* Get the bounds of a shape in the current page space, incorporating any masks. For example, if the
* shape were the child of a frame and was half way out of the frame, the bounds would be the half
* of the shape that was in the frame.
*
* @example
* ```ts
* editor.getShapeMaskedPageBounds(myShape)
* editor.getShapeMaskedPageBounds(myShapeId)
* ```
*
* @param shape - The shape to get the masked bounds for.
*
* @public
*/
getShapeMaskedPageBounds(shape) {
if (typeof shape !== "string") shape = shape.id;
return this._getShapeMaskedPageBoundsCache().get(shape);
}
_getShapeMaskedPageBoundsCache() {
return this.store.createComputedCache("shapeMaskedPageBoundsCache", (shape) => {
const pageBounds = this._getShapePageBoundsCache().get(shape.id);
if (!pageBounds) return;
const pageMask = this._getShapeMaskCache().get(shape.id);
if (pageMask) {
if (pageMask.length === 0) return void 0;
const { corners } = pageBounds;
if (corners.every((p, i) => p && Vec.Equals(p, pageMask[i]))) return pageBounds.clone();
const intersection = intersectPolygonPolygon(pageMask, corners);
if (!intersection) return;
return Box.FromPoints(intersection);
}
return pageBounds;
});
}
/**
* Get the ancestors of a shape.
*
* @example
* ```ts
* const ancestors = editor.getShapeAncestors(myShape)
* const ancestors = editor.getShapeAncestors(myShapeId)
* ```
*
* @param shape - The shape (or shape id) to get the ancestors for.
* @param acc - The accumulator.
*
* @public
*/
getShapeAncestors(shape, acc = []) {
const id = typeof shape === "string" ? shape : shape.id;
const freshShape = this.getShape(id);
if (!freshShape) return acc;
const parentId = freshShape.parentId;
if (isPageId(parentId)) {
acc.reverse();
return acc;
}
const parent = this.store.get(parentId);
if (!parent) return acc;
acc.push(parent);
return this.getShapeAncestors(parent, acc);
}
/**
* Find the first ancestor matching the given predicate
*
* @example
* ```ts
* const ancestor = editor.findShapeAncestor(myShape)
* const ancestor = editor.findShapeAncestor(myShape.id)
* const ancestor = editor.findShapeAncestor(myShape.id, (shape) => shape.type === 'frame')
* ```
*
* @param shape - The shape to check the ancestors for.
* @param predicate - The predicate to match.
*
* @public
*/
findShapeAncestor(shape, predicate) {
const id = typeof shape === "string" ? shape : shape.id;
const freshShape = this.getShape(id);
if (!freshShape) return;
const parentId = freshShape.parentId;
if (isPageId(parentId)) return;
const parent = this.getShape(parentId);
if (!parent) return;
return predicate(parent) ? parent : this.findShapeAncestor(parent, predicate);
}
/**
* Returns true if the the given shape has the given ancestor.
*
* @param shape - The shape.
* @param ancestorId - The id of the ancestor.
*
* @public
*/
hasAncestor(shape, ancestorId) {
const id = typeof shape === "string" ? shape : shape?.id;
const freshShape = id && this.getShape(id);
if (!freshShape) return false;
if (freshShape.parentId === ancestorId) return true;
return this.hasAncestor(this.getShapeParent(freshShape), ancestorId);
}
/**
* Get the common ancestor of two or more shapes that matches a predicate.
*
* @param shapes - The shapes (or shape ids) to check.
* @param predicate - The predicate to match.
*/
findCommonAncestor(shapes, predicate) {
if (shapes.length === 0) {
return;
}
const ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
const freshShapes = compact(ids.map((id) => this.getShape(id)));
if (freshShapes.length === 1) {
const parentId = freshShapes[0].parentId;
if (isPageId(parentId)) {
return;
}
return predicate ? this.findShapeAncestor(freshShapes[0], predicate)?.id : parentId;
}
const [nodeA, ...others] = freshShapes;
let ancestor = this.getShapeParent(nodeA);
while (ancestor) {
if (predicate && !predicate(ancestor)) {
ancestor = this.getShapeParent(ancestor);
continue;
}
if (others.every((shape) => this.hasAncestor(shape, ancestor.id))) {
return ancestor.id;
}
ancestor = this.getShapeParent(ancestor);
}
return void 0;
}
isShapeOrAncestorLocked(arg) {
const shape = typeof arg === "string" ? this.getShape(arg) : arg;
if (shape === void 0) return false;
if (shape.isLocked) return true;
return this.isShapeOrAncestorLocked(this.getShapeParent(shape));
}
_notVisibleShapes() {
return notVisibleShapes(this);
}
getCulledShapes() {
const notVisibleShapes2 = this._notVisibleShapes().get();
const selectedShapeIds = this.getSelectedShapeIds();
const editingId = this.getEditingShapeId();
const culledShapes = new Set(notVisibleShapes2);
if (editingId) {
culledShapes.delete(editingId);
}
selectedShapeIds.forEach((id) => {
culledShapes.delete(id);
});
return culledShapes;
}
getCurrentPageBounds() {
let commonBounds;
this.getCurrentPageShapeIdsSorted().forEach((shapeId) => {
const bounds = this.getShapeMaskedPageBounds(shapeId);
if (!bounds) return;
if (!commonBounds) {
commonBounds = bounds.clone();
} else {
commonBounds = commonBounds.expand(bounds);
}
});
return commonBounds;
}
/**
* Get the top-most selected shape at the given point, ignoring groups.
*
* @param point - The point to check.
*
* @returns The top-most selected shape at the given point, or undefined if there is no shape at the point.
*/
getSelectedShapeAtPoint(point) {
const selectedShapeIds = this.getSelectedShapeIds();
return this.getCurrentPageShapesSorted().filter((shape) => shape.type !== "group" && selectedShapeIds.includes(shape.id)).reverse().find((shape) => this.isPointInShape(shape, point, { hitInside: true, margin: 0 }));
}
/**
* Get the shape at the current point.
*
* @param point - The point to check.
* @param opts - Options for the check: `hitInside` to check if the point is inside the shape, `margin` to check if the point is within a margin of the shape, `hitFrameInside` to check if the point is inside the frame, and `filter` to filter the shapes to check.
*
* @returns The shape at the given point, or undefined if there is no shape at the point.
*/
getShapeAtPoint(point, opts = {}) {
const zoomLevel = this.getZoomLevel();
const viewportPageBounds = this.getViewportPageBounds();
const {
filter,
margin = 0,
hitLocked = false,
hitLabels = false,
hitInside = false,
hitFrameInside = false
} = opts;
let inHollowSmallestArea = Infinity;
let inHollowSmallestAreaHit = null;
let inMarginClosestToEdgeDistance = Infinity;
let inMarginClosestToEdgeHit = null;
const shapesToCheck = (opts.renderingOnly ? this.getCurrentPageRenderingShapesSorted() : this.getCurrentPageShapesSorted()).filter((shape) => {
if (shape.isLocked && !hitLocked || this.isShapeHidden(shape) || this.isShapeOfType(shape, "group"))
return false;
const pageMask = this.getShapeMask(shape);
if (pageMask && !pointInPolygon(point, pageMask)) return false;
if (filter) return filter(shape);
return true;
});
for (let i = shapesToCheck.length - 1; i >= 0; i--) {
const shape = shapesToCheck[i];
const geometry = this.getShapeGeometry(shape);
const isGroup = geometry instanceof Group2d;
const pointInShapeSpace = this.getPointInShapeSpace(shape, point);
if (this.isShapeOfType(shape, "frame") || (this.isShapeOfType(shape, "arrow") || this.isShapeOfType(shape, "geo") && shape.props.fill === "none") && shape.props.text.trim()) {
for (const childGeometry of geometry.children) {
if (childGeometry.isLabel && childGeometry.isPointInBounds(pointInShapeSpace)) {
return shape;
}
}
}
if (this.isShapeOfType(shape, "frame")) {
const distance2 = geometry.distanceToPoint(pointInShapeSpace, hitInside);
if (Math.abs(distance2) <= margin) {
return inMarginClosestToEdgeHit || shape;
}
if (geometry.hitTestPoint(pointInShapeSpace, 0, true)) {
return inMarginClosestToEdgeHit || inHollowSmallestAreaHit || (hitFrameInside ? shape : void 0);
}
continue;
}
let distance;
if (isGroup) {
let minDistance = Infinity;
for (const childGeometry of geometry.children) {
if (childGeometry.isLabel && !hitLabels) continue;
const tDistance = childGeometry.distanceToPoint(pointInShapeSpace, hitInside);
if (tDistance < minDistance) {
minDistance = tDistance;
}
}
distance = minDistance;
} else {
if (margin === 0 && (geometry.bounds.w < 1 || geometry.bounds.h < 1)) {
distance = geometry.distanceToPoint(pointInShapeSpace, hitInside);
} else {
if (geometry.bounds.containsPoint(pointInShapeSpace, margin)) {
distance = geometry.distanceToPoint(pointInShapeSpace, hitInside);
} else {
distance = Infinity;
}
}
}
if (geometry.isClosed) {
if (distance <= margin) {
if (geometry.isFilled || isGroup && geometry.children[0].isFilled) {
return inMarginClosestToEdgeHit || shape;
} else {
if (this.getShapePageBounds(shape).contains(viewportPageBounds)) continue;
if (Math.abs(distance) < margin) {
if (Math.abs(distance) < inMarginClosestToEdgeDistance) {
inMarginClosestToEdgeDistance = Math.abs(distance);
inMarginClosestToEdgeHit = shape;
}
} else if (!inMarginClosestToEdgeHit) {
const { area } = geometry;
if (area < inHollowSmallestArea) {
inHollowSmallestArea = area;
inHollowSmallestAreaHit = shape;
}
}
}
}
} else {
if (distance < this.options.hitTestMargin / zoomLevel) {
return shape;
}
}
}
return inMarginClosestToEdgeHit || inHollowSmallestAreaHit || void 0;
}
/**
* Get the shapes, if any, at a given page point.
*
* @example
* ```ts
* editor.getShapesAtPoint({ x: 100, y: 100 })
* editor.getShapesAtPoint({ x: 100, y: 100 }, { hitInside: true, exact: true })
* ```
*
* @param point - The page point to test.
* @param opts - The options for the hit point testing.
*
* @public
*/
getShapesAtPoint(point, opts = {}) {
return this.getCurrentPageShapes().filter(
(shape) => !this.isShapeHidden(shape) && this.isPointInShape(shape, point, opts)
);
}
/**
* Test whether a point (in the current page space) will will a shape. This method takes into account masks,
* such as when a shape is the child of a frame and is partially clipped by the frame.
*
* @example
* ```ts
* editor.isPointInShape({ x: 100, y: 100 }, myShape)
* ```
*
* @param shape - The shape to test against.
* @param point - The page point to test (in the current page space).
* @param opts - The options for the hit point testing.
*
* @public
*/
isPointInShape(shape, point, opts = {}) {
const { hitInside = false, margin = 0 } = opts;
const id = typeof shape === "string" ? shape : shape.id;
const pageMask = this.getShapeMask(id);
if (pageMask && !pointInPolygon(point, pageMask)) return false;
return this.getShapeGeometry(id).hitTestPoint(
this.getPointInShapeSpace(shape, point),
margin,
hitInside
);
}
/**
* Convert a point in the current page space to a point in the local space of a shape. For example, if a
* shape's page point were `{ x: 100, y: 100 }`, a page point at `{ x: 110, y: 110 }` would be at
* `{ x: 10, y: 10 }` in the shape's local space.
*
* @example
* ```ts
* editor.getPointInShapeSpace(myShape, { x: 100, y: 100 })
* ```
*
* @param shape - The shape to get the point in the local space of.
* @param point - The page point to get in the local space of the shape.
*
* @public
*/
getPointInShapeSpace(shape, point) {
const id = typeof shape === "string" ? shape : shape.id;
return this._getShapePageTransformCache().get(id).clone().invert().applyToPoint(point);
}
/**
* Convert a delta in the current page space to a point in the local space of a shape's parent.
*
* @example
* ```ts
* editor.getPointInParentSpace(myShape.id, { x: 100, y: 100 })
* ```
*
* @param shape - The shape to get the point in the local space of.
* @param point - The page point to get in the local space of the shape.
*
* @public
*/
getPointInParentSpace(shape, point) {
const id = typeof shape === "string" ? shape : shape.id;
const freshShape = this.getShape(id);
if (!freshShape) return new Vec(0, 0);
if (isPageId(freshShape.parentId)) return Vec.From(point);
const parentTransform = this.getShapePageTransform(freshShape.parentId);
if (!parentTransform) return Vec.From(point);
return parentTransform.clone().invert().applyToPoint(point);
}
getCurrentPageShapes() {
return Array.from(this.getCurrentPageShapeIds(), (id) => this.store.get(id));
}
getCurrentPageShapesSorted() {
const result = [];
const topLevelShapes = this.getSortedChildIdsForParent(this.getCurrentPageId());
for (let i = 0, n = topLevelShapes.length; i < n; i++) {
pushShapeWithDescendants(this, topLevelShapes[i], result);
}
return result;
}
getCurrentPageRenderingShapesSorted() {
const culledShapes = this.getCulledShapes();
return this.getCurrentPageShapesSorted().filter(
({ id }) => !culledShapes.has(id) && !this.isShapeHidden(id)
);
}
isShapeOfType(arg, type) {
const shape = typeof arg === "string" ? this.getShape(arg) : arg;
if (!shape) return false;
return shape.type === type;
}
/**
* Get a shape by its id.
*
* @example
* ```ts
* editor.getShape('box1')
* ```
*
* @param shape - The shape (or the id of the shape) to get.
*
* @public
*/
getShape(shape) {
const id = typeof shape === "string" ? shape : shape.id;
if (!isShapeId(id)) return void 0;
return this.store.get(id);
}
/**
* Get the parent shape for a given shape. Returns undefined if the shape is the direct child of
* the page.
*
* @example
* ```ts
* editor.getShapeParent(myShape)
* ```
*
* @public
*/
getShapeParent(shape) {
const id = typeof shape === "string" ? shape : shape?.id;
if (!id) return void 0;
const freshShape = this.getShape(id);
if (freshShape === void 0 || !isShapeId(freshShape.parentId)) return void 0;
return this.store.get(freshShape.parentId);
}
/**
* If siblingShape and targetShape are siblings, this returns targetShape. If targetShape has an
* ancestor who is a sibling of siblingShape, this returns that ancestor. Otherwise, this returns
* undefined.
*
* @internal
*/
getShapeNearestSibling(siblingShape, targetShape) {
if (!targetShape) {
return void 0;
}
if (targetShape.parentId === siblingShape.parentId) {
return targetShape;
}
const ancestor = this.findShapeAncestor(
targetShape,
(ancestor2) => ancestor2.parentId === siblingShape.parentId
);
return ancestor;
}
/**
* Get whether the given shape is the descendant of the given page.
*
* @example
* ```ts
* editor.isShapeInPage(myShape)
* editor.isShapeInPage(myShape, 'page1')
* ```
*
* @param shape - The shape to check.
* @param pageId - The id of the page to check against. Defaults to the current page.
*
* @public
*/
isShapeInPage(shape, pageId = this.getCurrentPageId()) {
const id = typeof shape === "string" ? shape : shape.id;
const shapeToCheck = this.getShape(id);
if (!shapeToCheck) return false;
let shapeIsInPage = false;
if (shapeToCheck.parentId === pageId) {
shapeIsInPage = true;
} else {
let parent = this.getShape(shapeToCheck.parentId);
isInPageSearch: while (parent) {
if (parent.parentId === pageId) {
shapeIsInPage = true;
break isInPageSearch;
}
parent = this.getShape(parent.parentId);
}
}
return shapeIsInPage;
}
/**
* Get the id of the containing page for a given shape.
*
* @param shape - The shape to get the page id for.
*
* @returns The id of the page that contains the shape, or undefined if the shape is undefined.
*
* @public
*/
getAncestorPageId(shape) {
const id = typeof shape === "string" ? shape : shape?.id;
const _shape = id && this.getShape(id);
if (!_shape) return void 0;
if (isPageId(_shape.parentId)) {
return _shape.parentId;
} else {
return this.getAncestorPageId(this.getShape(_shape.parentId));
}
}
/**
* Reparent shapes to a new parent. This operation preserves the shape's current page positions /
* rotations.
*
* @example
* ```ts
* editor.reparentShapes([box1, box2], 'frame1')
* editor.reparentShapes([box1.id, box2.id], 'frame1')
* editor.reparentShapes([box1.id, box2.id], 'frame1', 4)
* ```
*
* @param shapes - The shapes (or shape ids) of the shapes to reparent.
* @param parentId - The id of the new parent shape.
* @param insertIndex - The index to insert the children.
*
* @public
*/
reparentShapes(shapes, parentId, insertIndex) {
const ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
if (ids.length === 0) return this;
const changes = [];
const parentTransform = isPageId(parentId) ? Mat.Identity() : this.getShapePageTransform(parentId);
const parentPageRotation = parentTransform.rotation();
let indices = [];
const sibs = compact(this.getSortedChildIdsForParent(parentId).map((id) => this.getShape(id)));
if (insertIndex) {
const sibWithInsertIndex = sibs.find((s) => s.index === insertIndex);
if (sibWithInsertIndex) {
const sibAbove = sibs[sibs.indexOf(sibWithInsertIndex) + 1];
if (sibAbove) {
indices = getIndicesBetween(insertIndex, sibAbove.index, ids.length);
} else {
indices = getIndicesAbove(insertIndex, ids.length);
}
} else {
const sibAbove = sibs.sort(sortByIndex$1).find((s) => s.index > insertIndex);
if (sibAbove) {
indices = getIndicesBetween(insertIndex, sibAbove.index, ids.length);
} else {
indices = getIndicesAbove(insertIndex, ids.length);
}
}
} else {
const sib = sibs.length && sibs[sibs.length - 1];
indices = sib ? getIndicesAbove(sib.index, ids.length) : getIndices(ids.length);
}
const invertedParentTransform = parentTransform.clone().invert();
const shapesToReparent = compact(ids.map((id) => this.getShape(id)));
this.run(
() => {
for (let i = 0; i < shapesToReparent.length; i++) {
const shape = shapesToReparent[i];
const pageTransform = this.getShapePageTransform(shape);
if (!pageTransform) continue;
const pagePoint = pageTransform.point();
if (!pagePoint) continue;
const newPoint = invertedParentTransform.applyToPoint(pagePoint);
const newRotation = pageTransform.rotation() - parentPageRotation;
changes.push({
id: shape.id,
type: shape.type,
parentId,
x: newPoint.x,
y: newPoint.y,
rotation: newRotation,
index: indices[i]
});
}
this.updateShapes(changes);
},
{ ignoreShapeLock: true }
);
return this;
}
/**
* Get the index above the highest child of a given parent.
*
* @param parent - The parent (or the id) of the parent.
*
* @returns The index.
*
* @public
*/
getHighestIndexForParent(parent) {
const parentId = typeof parent === "string" ? parent : parent.id;
const children = this._parentIdsToChildIds.get()[parentId];
if (!children || children.length === 0) {
return "a1";
}
const shape = this.getShape(children[children.length - 1]);
return getIndexAbove(shape.index);
}
/**
* Get an array of all the children of a shape.
*
* @example
* ```ts
* editor.getSortedChildIdsForParent('frame1')
* ```
*
* @param parent - The parent (or the id) of the parent shape.
*
* @public
*/
getSortedChildIdsForParent(parent) {
const parentId = typeof parent === "string" ? parent : parent.id;
const ids = this._parentIdsToChildIds.get()[parentId];
if (!ids) return EMPTY_ARRAY;
return ids;
}
/**
* Run a visitor function for all descendants of a shape.
*
* @example
* ```ts
* editor.visitDescendants('frame1', myCallback)
* ```
*
* @param parent - The parent (or the id) of the parent shape.
* @param visitor - The visitor function.
*
* @public
*/
visitDescendants(parent, visitor) {
const parentId = typeof parent === "string" ? parent : parent.id;
const children = this.getSortedChildIdsForParent(parentId);
for (const id of children) {
if (visitor(id) === false) continue;
this.visitDescendants(id, visitor);
}
return this;
}
/**
* Get the shape ids of all descendants of the given shapes (including the shapes themselves). IDs are returned in z-index order.
*
* @param ids - The ids of the shapes to get descendants of.
*
* @returns The descendant ids.
*
* @public
*/
getShapeAndDescendantIds(ids) {
const shapeIds = /* @__PURE__ */ new Set();
for (const shape of ids.map((id) => this.getShape(id)).sort(sortByIndex$1)) {
shapeIds.add(shape.id);
this.visitDescendants(shape, (descendantId) => {
shapeIds.add(descendantId);
});
}
return shapeIds;
}
/**
* Get the shape that some shapes should be dropped on at a given point.
*
* @param point - The point to find the parent for.
* @param droppingShapes - The shapes that are being dropped.
*
* @returns The shape to drop on.
*
* @public
*/
getDroppingOverShape(point, droppingShapes = []) {
const currentPageShapesSorted = this.getCurrentPageShapesSorted();
for (let i = currentPageShapesSorted.length - 1; i >= 0; i--) {
const shape = currentPageShapesSorted[i];
if (
// ignore hidden shapes
this.isShapeHidden(shape) || // don't allow dropping on selected shapes
this.getSelectedShapeIds().includes(shape.id) || // only allow shapes that can receive children
!this.getShapeUtil(shape).canDropShapes(shape, droppingShapes) || // don't allow dropping a shape on itself or one of it's children
droppingShapes.find((s) => s.id === shape.id || this.hasAncestor(shape, s.id))
) {
continue;
}
const maskedPageBounds = this.getShapeMaskedPageBounds(shape.id);
if (maskedPageBounds && maskedPageBounds.containsPoint(point) && this.getShapeGeometry(shape).hitTestPoint(this.getPointInShapeSpace(shape, point), 0, true)) {
return shape;
}
}
}
/**
* Get the shape that should be selected when you click on a given shape, assuming there is
* nothing already selected. It will not return anything higher than or including the current
* focus layer.
*
* @param shape - The shape to get the outermost selectable shape for.
* @param filter - A function to filter the selectable shapes.
*
* @returns The outermost selectable shape.
*
* @public
*/
getOutermostSelectableShape(shape, filter) {
const id = typeof shape === "string" ? shape : shape.id;
const freshShape = this.getShape(id);
let match = freshShape;
let node = freshShape;
const focusedGroup = this.getFocusedGroup();
while (node) {
if (this.isShapeOfType(node, "group") && focusedGroup?.id !== node.id && !this.hasAncestor(focusedGroup, node.id) && (filter?.(node) ?? true)) {
match = node;
} else if (focusedGroup?.id === node.id) {
break;
}
node = this.getShapeParent(node);
}
return match;
}
_getBindingsIndexCache() {
const index = bindingsIndex(this);
return this.store.createComputedCache("bindingsIndex", (shape) => {
return index.get().get(shape.id);
});
}
/**
* Get a binding from the store by its ID if it exists.
*/
getBinding(id) {
return this.store.get(id);
}
/**
* Get all bindings of a certain type _from_ a particular shape. These are the bindings whose
* `fromId` matched the shape's ID.
*/
getBindingsFromShape(shape, type) {
const id = typeof shape === "string" ? shape : shape.id;
return this.getBindingsInvolvingShape(id).filter(
(b) => b.fromId === id && b.type === type
);
}
/**
* Get all bindings of a certain type _to_ a particular shape. These are the bindings whose
* `toId` matches the shape's ID.
*/
getBindingsToShape(shape, type) {
const id = typeof shape === "string" ? shape : shape.id;
return this.getBindingsInvolvingShape(id).filter(
(b) => b.toId === id && b.type === type
);
}
/**
* Get all bindings involving a particular shape. This includes bindings where the shape is the
* `fromId` or `toId`. If a type is provided, only bindings of that type are returned.
*/
getBindingsInvolvingShape(shape, type) {
const id = typeof shape === "string" ? shape : shape.id;
const result = this._getBindingsIndexCache().get(id) ?? EMPTY_ARRAY;
if (!type) return result;
return result.filter((b) => b.type === type);
}
/**
* Create bindings from a list of partial bindings. You can omit the ID and most props of a
* binding, but the `type`, `toId`, and `fromId` must all be provided.
*/
createBindings(partials) {
const bindings = [];
for (const partial of partials) {
const fromShape = this.getShape(partial.fromId);
const toShape = this.getShape(partial.toId);
if (!fromShape || !toShape) continue;
if (!this.canBindShapes({ fromShape, toShape, binding: partial })) continue;
const util = this.getBindingUtil(partial.type);
const defaultProps = util.getDefaultProps();
const binding = this.store.schema.types.binding.create({
...partial,
id: partial.id ?? createBindingId(),
props: {
...defaultProps,
...partial.props
}
});
bindings.push(binding);
}
this.store.put(bindings);
return this;
}
/**
* Create a single binding from a partial. You can omit the ID and most props of a binding, but
* the `type`, `toId`, and `fromId` must all be provided.
*/
createBinding(partial) {
return this.createBindings([partial]);
}
/**
* Update bindings from a list of partial bindings. Each partial must include an ID, which will
* be used to match the binding to it's existing record. If there is no existing record, that
* binding is skipped. The changes from the partial are merged into the existing record.
*/
updateBindings(partials) {
const updated = [];
for (const partial of partials) {
if (!partial) continue;
const current = this.getBinding(partial.id);
if (!current) continue;
const updatedBinding = applyPartialToRecordWithProps(current, partial);
if (updatedBinding === current) continue;
const fromShape = this.getShape(updatedBinding.fromId);
const toShape = this.getShape(updatedBinding.toId);
if (!fromShape || !toShape) continue;
if (!this.canBindShapes({ fromShape, toShape, binding: updatedBinding })) continue;
updated.push(updatedBinding);
}
this.store.put(updated);
return this;
}
/**
* Update a binding from a partial binding. Each partial must include an ID, which will be used
* to match the binding to it's existing record. If there is no existing record, that binding is
* skipped. The changes from the partial are merged into the existing record.
*/
updateBinding(partial) {
return this.updateBindings([partial]);
}
/**
* Delete several bindings by their IDs. If a binding ID doesn't exist, it's ignored.
*/
deleteBindings(bindings, { isolateShapes = false } = {}) {
const ids = bindings.map((binding) => typeof binding === "string" ? binding : binding.id);
if (isolateShapes) {
this.store.atomic(() => {
for (const id of ids) {
const binding = this.getBinding(id);
if (!binding) continue;
const util = this.getBindingUtil(binding);
util.onBeforeIsolateFromShape?.({ binding, removedShape: this.getShape(binding.toId) });
util.onBeforeIsolateToShape?.({ binding, removedShape: this.getShape(binding.fromId) });
this.store.remove([id]);
}
});
} else {
this.store.remove(ids);
}
return this;
}
/**
* Delete a binding by its ID. If the binding doesn't exist, it's ignored.
*/
deleteBinding(binding, opts) {
return this.deleteBindings([binding], opts);
}
canBindShapes({
fromShape,
toShape,
binding
}) {
const fromShapeType = typeof fromShape === "string" ? fromShape : fromShape.type;
const toShapeType = typeof toShape === "string" ? toShape : toShape.type;
const bindingType = typeof binding === "string" ? binding : binding.type;
const canBindOpts = { fromShapeType, toShapeType, bindingType };
if (fromShapeType === toShapeType) {
return this.getShapeUtil(fromShapeType).canBind(canBindOpts);
}
return this.getShapeUtil(fromShapeType).canBind(canBindOpts) && this.getShapeUtil(toShapeType).canBind(canBindOpts);
}
/* -------------------- Commands -------------------- */
/**
* Rotate shapes by a delta in radians.
*
* @example
* ```ts
* editor.rotateShapesBy(editor.getSelectedShapeIds(), Math.PI)
* editor.rotateShapesBy(editor.getSelectedShapeIds(), Math.PI / 2)
* ```
*
* @param shapes - The shapes (or shape ids) of the shapes to move.
* @param delta - The delta in radians to apply to the selection rotation.
* @param opts - The options for the rotation.
*/
rotateShapesBy(shapes, delta, opts) {
const ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
if (ids.length <= 0) return this;
const snapshot = getRotationSnapshot({ editor: this, ids });
if (!snapshot) return this;
applyRotationToSnapshotShapes({
delta,
snapshot,
editor: this,
stage: "one-off",
centerOverride: opts?.center
});
return this;
}
getChangesToTranslateShape(initialShape, newShapeCoords) {
let workingShape = initialShape;
const util = this.getShapeUtil(initialShape);
workingShape = applyPartialToRecordWithProps(
workingShape,
util.onTranslateStart?.(workingShape) ?? void 0
);
workingShape = applyPartialToRecordWithProps(workingShape, {
id: initialShape.id,
type: initialShape.type,
x: newShapeCoords.x,
y: newShapeCoords.y
});
workingShape = applyPartialToRecordWithProps(
workingShape,
util.onTranslate?.(initialShape, workingShape) ?? void 0
);
workingShape = applyPartialToRecordWithProps(
workingShape,
util.onTranslateEnd?.(initialShape, workingShape) ?? void 0
);
return workingShape;
}
/**
* Move shapes by a delta.
*
* @example
* ```ts
* editor.nudgeShapes(['box1', 'box2'], { x: 8, y: 8 })
* ```
*
* @param shapes - The shapes (or shape ids) to move.
* @param offset - The offset to apply to the shapes.
*/
nudgeShapes(shapes, offset) {
const ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
if (ids.length <= 0) return this;
const changes = [];
for (const id of ids) {
const shape = this.getShape(id);
const localDelta = Vec.From(offset);
const parentTransform = this.getShapeParentTransform(shape);
if (parentTransform) localDelta.rot(-parentTransform.rotation());
changes.push(this.getChangesToTranslateShape(shape, localDelta.add(shape)));
}
this.updateShapes(changes);
return this;
}
/**
* Duplicate shapes.
*
* @example
* ```ts
* editor.duplicateShapes(['box1', 'box2'], { x: 8, y: 8 })
* editor.duplicateShapes(editor.getSelectedShapes(), { x: 8, y: 8 })
* ```
*
* @param shapes - The shapes (or shape ids) to duplicate.
* @param offset - The offset (in pixels) to apply to the duplicated shapes.
*
* @public
*/
duplicateShapes(shapes, offset) {
this.run(() => {
const ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
if (ids.length <= 0) return this;
const initialIds = new Set(ids);
const shapeIdSet = this.getShapeAndDescendantIds(ids);
const orderedShapeIds = [...shapeIdSet].reverse();
const shapeIds = /* @__PURE__ */ new Map();
for (const shapeId of shapeIdSet) {
shapeIds.set(shapeId, createShapeId());
}
const { shapesToCreateWithOriginals, bindingsToCreate } = withIsolatedShapes(
this,
shapeIdSet,
(bindingIdsToMaintain) => {
const bindingsToCreate2 = [];
for (const originalId of bindingIdsToMaintain) {
const originalBinding = this.getBinding(originalId);
if (!originalBinding) continue;
const duplicatedId = createBindingId();
bindingsToCreate2.push({
...originalBinding,
id: duplicatedId,
fromId: assertExists(shapeIds.get(originalBinding.fromId)),
toId: assertExists(shapeIds.get(originalBinding.toId))
});
}
const shapesToCreateWithOriginals2 = [];
for (const originalId of orderedShapeIds) {
const duplicatedId = assertExists(shapeIds.get(originalId));
const originalShape = this.getShape(originalId);
if (!originalShape) continue;
let ox = 0;
let oy = 0;
if (offset && initialIds.has(originalId)) {
const parentTransform = this.getShapeParentTransform(originalShape);
const vec = new Vec(offset.x, offset.y).rot(-parentTransform.rotation());
ox = vec.x;
oy = vec.y;
}
shapesToCreateWithOriginals2.push({
shape: {
...originalShape,
id: duplicatedId,
x: originalShape.x + ox,
y: originalShape.y + oy,
// Use a dummy index for now, it will get updated outside of the `withIsolatedShapes`
index: "a1",
parentId: shapeIds.get(originalShape.parentId) ?? originalShape.parentId
},
originalShape
});
}
return { shapesToCreateWithOriginals: shapesToCreateWithOriginals2, bindingsToCreate: bindingsToCreate2 };
}
);
shapesToCreateWithOriginals.forEach(({ shape, originalShape }) => {
const parentId = originalShape.parentId;
const siblings = this.getSortedChildIdsForParent(parentId);
const currentIndex = siblings.indexOf(originalShape.id);
const siblingAboveId = siblings[currentIndex + 1];
const siblingAbove = siblingAboveId ? this.getShape(siblingAboveId) : void 0;
const index = getIndexBetween(originalShape.index, siblingAbove?.index);
shape.index = index;
});
const shapesToCreate = shapesToCreateWithOriginals.map(({ shape }) => shape);
const maxShapesReached = shapesToCreate.length + this.getCurrentPageShapeIds().size > this.options.maxShapesPerPage;
if (maxShapesReached) {
alertMaxShapes(this);
return;
}
this.createShapes(shapesToCreate);
this.createBindings(bindingsToCreate);
this.setSelectedShapes(compact(ids.map((id) => shapeIds.get(id))));
if (offset !== void 0) {
const selectionPageBounds = this.getSelectionPageBounds();
const viewportPageBounds = this.getViewportPageBounds();
if (selectionPageBounds && !viewportPageBounds.contains(selectionPageBounds)) {
this.centerOnPoint(selectionPageBounds.center, {
animation: { duration: this.options.animationMediumMs }
});
}
}
});
return this;
}
/**
* Move shapes to page.
*
* @example
* ```ts
* editor.moveShapesToPage(['box1', 'box2'], 'page1')
* ```
*
* @param shapes - The shapes (or shape ids) of the shapes to move.
* @param pageId - The id of the page where the shapes will be moved.
*
* @public
*/
moveShapesToPage(shapes, pageId) {
const ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
if (ids.length === 0) return this;
if (this.getIsReadonly()) return this;
const currentPageId = this.getCurrentPageId();
if (pageId === currentPageId) return this;
if (!this.store.has(pageId)) return this;
const content = this.getContentFromCurrentPage(ids);
if (!content) return this;
if (this.getPageShapeIds(pageId).size + content.shapes.length > this.options.maxShapesPerPage) {
alertMaxShapes(this, pageId);
return this;
}
const fromPageZ = this.getCamera().z;
this.run(() => {
this.deleteShapes(ids);
this.setCurrentPage(pageId);
this.setFocusedGroup(null);
this.selectNone();
this.putContentOntoCurrentPage(content, {
select: true,
preserveIds: true,
preservePosition: true
});
this.setCamera({ ...this.getCamera(), z: fromPageZ });
this.centerOnPoint(this.getSelectionRotatedPageBounds().center);
});
return this;
}
/**
* Toggle the lock state of one or more shapes. If there is a mix of locked and unlocked shapes, all shapes will be locked.
*
* @param shapes - The shapes (or shape ids) to toggle.
*
* @public
*/
toggleLock(shapes) {
const ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
if (this.getIsReadonly() || ids.length === 0) return this;
let allLocked = true, allUnlocked = true;
const shapesToToggle = [];
for (const id of ids) {
const shape = this.getShape(id);
if (shape) {
shapesToToggle.push(shape);
if (shape.isLocked) {
allUnlocked = false;
} else {
allLocked = false;
}
}
}
this.run(() => {
if (allUnlocked) {
this.updateShapes(
shapesToToggle.map((shape) => ({ id: shape.id, type: shape.type, isLocked: true }))
);
this.setSelectedShapes([]);
} else if (allLocked) {
this.updateShapes(
shapesToToggle.map((shape) => ({ id: shape.id, type: shape.type, isLocked: false }))
);
} else {
this.updateShapes(
shapesToToggle.map((shape) => ({ id: shape.id, type: shape.type, isLocked: true }))
);
}
});
return this;
}
/**
* Send shapes to the back of the page's object list.
*
* @example
* ```ts
* editor.sendToBack(['id1', 'id2'])
* editor.sendToBack(box1, box2)
* ```
*
* @param shapes - The shapes (or shape ids) to move.
*
* @public
*/
sendToBack(shapes) {
const ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
const changes = getReorderingShapesChanges(this, "toBack", ids, {
considerAllShapes: true
});
if (changes) this.updateShapes(changes);
return this;
}
/**
* Send shapes backward in the page's object list.
*
* @example
* ```ts
* editor.sendBackward(['id1', 'id2'])
* editor.sendBackward([box1, box2])
* ```
*
* By default, the operation will only consider overlapping shapes.
* To consider all shapes, pass `{ considerAllShapes: true }` in the options.
*
* @example
* ```ts
* editor.sendBackward(['id1', 'id2'], { considerAllShapes: true })
* ```
*
* @param shapes - The shapes (or shape ids) to move.
* @param opts - The options for the backward operation.
*
* @public
*/
sendBackward(shapes, opts = {}) {
const ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
const changes = getReorderingShapesChanges(this, "backward", ids, opts);
if (changes) this.updateShapes(changes);
return this;
}
/**
* Bring shapes forward in the page's object list.
*
* @example
* ```ts
* editor.bringForward(['id1', 'id2'])
* editor.bringForward(box1, box2)
* ```
*
* By default, the operation will only consider overlapping shapes.
* To consider all shapes, pass `{ considerAllShapes: true }` in the options.
*
* @example
* ```ts
* editor.bringForward(['id1', 'id2'], { considerAllShapes: true })
* ```
*
* @param shapes - The shapes (or shape ids) to move.
* @param opts - The options for the forward operation.
*
* @public
*/
bringForward(shapes, opts = {}) {
const ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
const changes = getReorderingShapesChanges(this, "forward", ids, opts);
if (changes) this.updateShapes(changes);
return this;
}
/**
* Bring shapes to the front of the page's object list.
*
* @example
* ```ts
* editor.bringToFront(['id1', 'id2'])
* editor.bringToFront([box1, box2])
* ```
*
* @param shapes - The shapes (or shape ids) to move.
*
* @public
*/
bringToFront(shapes) {
const ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
const changes = getReorderingShapesChanges(this, "toFront", ids);
if (changes) this.updateShapes(changes);
return this;
}
/**
* Flip shape positions.
*
* @example
* ```ts
* editor.flipShapes([box1, box2], 'horizontal', 32)
* editor.flipShapes(editor.getSelectedShapeIds(), 'horizontal', 32)
* ```
*
* @param shapes - The ids of the shapes to flip.
* @param operation - Whether to flip horizontally or vertically.
*
* @public
*/
flipShapes(shapes, operation) {
const ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
if (this.getIsReadonly()) return this;
let shapesToFlip = compact(ids.map((id) => this.getShape(id)));
if (!shapesToFlip.length) return this;
shapesToFlip = compact(
shapesToFlip.map((shape) => {
if (this.isShapeOfType(shape, "group")) {
return this.getSortedChildIdsForParent(shape.id).map((id) => this.getShape(id));
}
return shape;
}).flat()
);
const scaleOriginPage = Box.Common(
compact(shapesToFlip.map((id) => this.getShapePageBounds(id)))
).center;
this.run(() => {
for (const shape of shapesToFlip) {
const bounds = this.getShapeGeometry(shape).bounds;
const initialPageTransform = this.getShapePageTransform(shape.id);
if (!initialPageTransform) continue;
this.resizeShape(
shape.id,
{ x: operation === "horizontal" ? -1 : 1, y: operation === "vertical" ? -1 : 1 },
{
initialBounds: bounds,
initialPageTransform,
initialShape: shape,
mode: "scale_shape",
isAspectRatioLocked: this.getShapeUtil(shape).isAspectRatioLocked(shape),
scaleOrigin: scaleOriginPage,
scaleAxisRotation: 0
}
);
}
});
return this;
}
/**
* Stack shape.
*
* @example
* ```ts
* editor.stackShapes([box1, box2], 'horizontal', 32)
* editor.stackShapes(editor.getSelectedShapeIds(), 'horizontal', 32)
* ```
*
* @param shapes - The shapes (or shape ids) to stack.
* @param operation - Whether to stack horizontally or vertically.
* @param gap - The gap to leave between shapes.
*
* @public
*/
stackShapes(shapes, operation, gap) {
const ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
if (this.getIsReadonly()) return this;
const shapesToStack = ids.map((id) => this.getShape(id)).filter((shape) => {
if (!shape) return false;
return this.getShapeUtil(shape).canBeLaidOut(shape);
});
const len = shapesToStack.length;
if (gap === 0 && len < 3 || len < 2) return this;
const pageBounds = Object.fromEntries(
shapesToStack.map((shape) => [shape.id, this.getShapePageBounds(shape)])
);
let val;
let min;
let max;
let dim;
if (operation === "horizontal") {
val = "x";
min = "minX";
max = "maxX";
dim = "width";
} else {
val = "y";
min = "minY";
max = "maxY";
dim = "height";
}
let shapeGap;
if (gap === 0) {
const gaps = [];
shapesToStack.sort((a, b) => pageBounds[a.id][min] - pageBounds[b.id][min]);
for (let i = 0; i < len - 1; i++) {
const shape = shapesToStack[i];
const nextShape = shapesToStack[i + 1];
const bounds = pageBounds[shape.id];
const nextBounds = pageBounds[nextShape.id];
const gap2 = nextBounds[min] - bounds[max];
const current = gaps.find((g) => g.gap === gap2);
if (current) {
current.count++;
} else {
gaps.push({ gap: gap2, count: 1 });
}
}
let maxCount = 0;
gaps.forEach((g) => {
if (g.count > maxCount) {
maxCount = g.count;
shapeGap = g.gap;
}
});
if (maxCount === 1) {
shapeGap = Math.max(0, gaps.reduce((a, c) => a + c.gap * c.count, 0) / (len - 1));
}
} else {
shapeGap = gap;
}
const changes = [];
let v = pageBounds[shapesToStack[0].id][max];
shapesToStack.forEach((shape, i) => {
if (i === 0) return;
const delta = { x: 0, y: 0 };
delta[val] = v + shapeGap - pageBounds[shape.id][val];
const parent = this.getShapeParent(shape);
const localDelta = parent ? Vec.Rot(delta, -this.getShapePageTransform(parent).decompose().rotation) : delta;
const translateStartChanges = this.getShapeUtil(shape).onTranslateStart?.(shape);
changes.push(
translateStartChanges ? {
...translateStartChanges,
[val]: shape[val] + localDelta[val]
} : {
id: shape.id,
type: shape.type,
[val]: shape[val] + localDelta[val]
}
);
v += pageBounds[shape.id][dim] + shapeGap;
});
this.updateShapes(changes);
return this;
}
/**
* Pack shapes into a grid centered on their current position. Based on potpack (https://github.com/mapbox/potpack).
*
* @example
* ```ts
* editor.packShapes([box1, box2], 32)
* editor.packShapes(editor.getSelectedShapeIds(), 32)
* ```
*
*
* @param shapes - The shapes (or shape ids) to pack.
* @param gap - The padding to apply to the packed shapes. Defaults to 16.
*/
packShapes(shapes, gap) {
const ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
if (this.getIsReadonly()) return this;
if (ids.length < 2) return this;
const shapesToPack = ids.map((id) => this.getShape(id)).filter((shape2) => {
if (!shape2) return false;
return this.getShapeUtil(shape2).canBeLaidOut(shape2);
});
const shapePageBounds = {};
const nextShapePageBounds = {};
let shape, bounds, area = 0;
for (let i = 0; i < shapesToPack.length; i++) {
shape = shapesToPack[i];
bounds = this.getShapePageBounds(shape);
shapePageBounds[shape.id] = bounds;
nextShapePageBounds[shape.id] = bounds.clone();
area += bounds.width * bounds.height;
}
const commonBounds = Box.Common(compact(Object.values(shapePageBounds)));
const maxWidth = commonBounds.width;
shapesToPack.sort((a, b) => shapePageBounds[b.id].height - shapePageBounds[a.id].height);
const startWidth = Math.max(Math.ceil(Math.sqrt(area / 0.95)), maxWidth);
const spaces = [new Box(commonBounds.x, commonBounds.y, startWidth, Infinity)];
let width = 0;
let height = 0;
let space;
let last2;
for (let i = 0; i < shapesToPack.length; i++) {
shape = shapesToPack[i];
bounds = nextShapePageBounds[shape.id];
for (let i2 = spaces.length - 1; i2 >= 0; i2--) {
space = spaces[i2];
if (bounds.width > space.width || bounds.height > space.height) continue;
bounds.x = space.x;
bounds.y = space.y;
height = Math.max(height, bounds.maxY);
width = Math.max(width, bounds.maxX);
if (bounds.width === space.width && bounds.height === space.height) {
last2 = spaces.pop();
if (i2 < spaces.length) spaces[i2] = last2;
} else if (bounds.height === space.height) {
space.x += bounds.width + gap;
space.width -= bounds.width + gap;
} else if (bounds.width === space.width) {
space.y += bounds.height + gap;
space.height -= bounds.height + gap;
} else {
spaces.push(
new Box(
space.x + (bounds.width + gap),
space.y,
space.width - (bounds.width + gap),
bounds.height
)
);
space.y += bounds.height + gap;
space.height -= bounds.height + gap;
}
break;
}
}
const commonAfter = Box.Common(Object.values(nextShapePageBounds));
const centerDelta = Vec.Sub(commonBounds.center, commonAfter.center);
let nextBounds;
const changes = [];
for (let i = 0; i < shapesToPack.length; i++) {
shape = shapesToPack[i];
bounds = shapePageBounds[shape.id];
nextBounds = nextShapePageBounds[shape.id];
const delta = Vec.Sub(nextBounds.point, bounds.point).add(centerDelta);
const parentTransform = this.getShapeParentTransform(shape);
if (parentTransform) delta.rot(-parentTransform.rotation());
const change = {
id: shape.id,
type: shape.type,
x: shape.x + delta.x,
y: shape.y + delta.y
};
const translateStartChange = this.getShapeUtil(shape).onTranslateStart?.({
...shape,
...change
});
if (translateStartChange) {
changes.push({ ...change, ...translateStartChange });
} else {
changes.push(change);
}
}
if (changes.length) {
this.updateShapes(changes);
}
return this;
}
/**
* Align shape positions.
*
* @example
* ```ts
* editor.alignShapes([box1, box2], 'left')
* editor.alignShapes(editor.getSelectedShapeIds(), 'left')
* ```
*
* @param shapes - The shapes (or shape ids) to align.
* @param operation - The align operation to apply.
*
* @public
*/
alignShapes(shapes, operation) {
const ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
if (this.getIsReadonly()) return this;
if (ids.length < 2) return this;
const shapesToAlign = compact(ids.map((id) => this.getShape(id)));
const shapePageBounds = Object.fromEntries(
shapesToAlign.map((shape) => [shape.id, this.getShapePageBounds(shape)])
);
const commonBounds = Box.Common(compact(Object.values(shapePageBounds)));
const changes = [];
shapesToAlign.forEach((shape) => {
const pageBounds = shapePageBounds[shape.id];
if (!pageBounds) return;
const delta = { x: 0, y: 0 };
switch (operation) {
case "top": {
delta.y = commonBounds.minY - pageBounds.minY;
break;
}
case "center-vertical": {
delta.y = commonBounds.midY - pageBounds.minY - pageBounds.height / 2;
break;
}
case "bottom": {
delta.y = commonBounds.maxY - pageBounds.minY - pageBounds.height;
break;
}
case "left": {
delta.x = commonBounds.minX - pageBounds.minX;
break;
}
case "center-horizontal": {
delta.x = commonBounds.midX - pageBounds.minX - pageBounds.width / 2;
break;
}
case "right": {
delta.x = commonBounds.maxX - pageBounds.minX - pageBounds.width;
break;
}
}
const parent = this.getShapeParent(shape);
const localDelta = parent ? Vec.Rot(delta, -this.getShapePageTransform(parent).decompose().rotation) : delta;
changes.push(this.getChangesToTranslateShape(shape, Vec.Add(shape, localDelta)));
});
this.updateShapes(changes);
return this;
}
/**
* Distribute shape positions.
*
* @example
* ```ts
* editor.distributeShapes([box1, box2], 'horizontal')
* editor.distributeShapes(editor.getSelectedShapeIds(), 'horizontal')
* ```
*
* @param shapes - The shapes (or shape ids) to distribute.
* @param operation - Whether to distribute shapes horizontally or vertically.
*
* @public
*/
distributeShapes(shapes, operation) {
const ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
if (this.getIsReadonly()) return this;
if (ids.length < 3) return this;
const len = ids.length;
const shapesToDistribute = compact(ids.map((id) => this.getShape(id)));
const pageBounds = Object.fromEntries(
shapesToDistribute.map((shape) => [shape.id, this.getShapePageBounds(shape)])
);
let val;
let min;
let max;
let mid;
let dim;
if (operation === "horizontal") {
val = "x";
min = "minX";
max = "maxX";
mid = "midX";
dim = "width";
} else {
val = "y";
min = "minY";
max = "maxY";
mid = "midY";
dim = "height";
}
const changes = [];
const first = shapesToDistribute.sort(
(a, b) => pageBounds[a.id][min] - pageBounds[b.id][min]
)[0];
const last2 = shapesToDistribute.sort((a, b) => pageBounds[b.id][max] - pageBounds[a.id][max])[0];
const midFirst = pageBounds[first.id][mid];
const step = (pageBounds[last2.id][mid] - midFirst) / (len - 1);
const v = midFirst + step;
shapesToDistribute.filter((shape) => shape !== first && shape !== last2).sort((a, b) => pageBounds[a.id][mid] - pageBounds[b.id][mid]).forEach((shape, i) => {
const delta = { x: 0, y: 0 };
delta[val] = v + step * i - pageBounds[shape.id][dim] / 2 - pageBounds[shape.id][val];
const parent = this.getShapeParent(shape);
const localDelta = parent ? Vec.Rot(delta, -this.getShapePageTransform(parent).rotation()) : delta;
changes.push(this.getChangesToTranslateShape(shape, Vec.Add(shape, localDelta)));
});
this.updateShapes(changes);
return this;
}
/**
* Stretch shape sizes and positions to fill their common bounding box.
*
* @example
* ```ts
* editor.stretchShapes([box1, box2], 'horizontal')
* editor.stretchShapes(editor.getSelectedShapeIds(), 'horizontal')
* ```
*
* @param shapes - The shapes (or shape ids) to stretch.
* @param operation - Whether to stretch shapes horizontally or vertically.
*
* @public
*/
stretchShapes(shapes, operation) {
const ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
if (this.getIsReadonly()) return this;
if (ids.length < 2) return this;
const shapesToStretch = compact(ids.map((id) => this.getShape(id)));
const shapeBounds = Object.fromEntries(ids.map((id) => [id, this.getShapeGeometry(id).bounds]));
const shapePageBounds = Object.fromEntries(ids.map((id) => [id, this.getShapePageBounds(id)]));
const commonBounds = Box.Common(compact(Object.values(shapePageBounds)));
switch (operation) {
case "vertical": {
this.run(() => {
for (const shape of shapesToStretch) {
const pageRotation = this.getShapePageTransform(shape).rotation();
if (pageRotation % PI2) continue;
const bounds = shapeBounds[shape.id];
const pageBounds = shapePageBounds[shape.id];
const localOffset = new Vec(0, commonBounds.minY - pageBounds.minY);
const parentTransform = this.getShapeParentTransform(shape);
if (parentTransform) localOffset.rot(-parentTransform.rotation());
const { x, y } = Vec.Add(localOffset, shape);
this.updateShapes([{ id: shape.id, type: shape.type, x, y }]);
const scale = new Vec(1, commonBounds.height / pageBounds.height);
this.resizeShape(shape.id, scale, {
initialBounds: bounds,
scaleOrigin: new Vec(pageBounds.center.x, commonBounds.minY),
isAspectRatioLocked: this.getShapeUtil(shape).isAspectRatioLocked(shape),
scaleAxisRotation: 0
});
}
});
break;
}
case "horizontal": {
this.run(() => {
for (const shape of shapesToStretch) {
const bounds = shapeBounds[shape.id];
const pageBounds = shapePageBounds[shape.id];
const pageRotation = this.getShapePageTransform(shape).rotation();
if (pageRotation % PI2) continue;
const localOffset = new Vec(commonBounds.minX - pageBounds.minX, 0);
const parentTransform = this.getShapeParentTransform(shape);
if (parentTransform) localOffset.rot(-parentTransform.rotation());
const { x, y } = Vec.Add(localOffset, shape);
this.updateShapes([{ id: shape.id, type: shape.type, x, y }]);
const scale = new Vec(commonBounds.width / pageBounds.width, 1);
this.resizeShape(shape.id, scale, {
initialBounds: bounds,
scaleOrigin: new Vec(commonBounds.minX, pageBounds.center.y),
isAspectRatioLocked: this.getShapeUtil(shape).isAspectRatioLocked(shape),
scaleAxisRotation: 0
});
}
});
break;
}
}
return this;
}
/**
* Resize a shape.
*
* @param shape - The shape (or the shape id of the shape) to resize.
* @param scale - The scale factor to apply to the shape.
* @param opts - Additional options.
*
* @public
*/
resizeShape(shape, scale, opts = {}) {
const id = typeof shape === "string" ? shape : shape.id;
if (this.getIsReadonly()) return this;
if (!Number.isFinite(scale.x)) scale = new Vec(1, scale.y);
if (!Number.isFinite(scale.y)) scale = new Vec(scale.x, 1);
const initialShape = opts.initialShape ?? this.getShape(id);
if (!initialShape) return this;
const scaleOrigin = opts.scaleOrigin ?? this.getShapePageBounds(id)?.center;
if (!scaleOrigin) return this;
const pageTransform = opts.initialPageTransform ? Mat.Cast(opts.initialPageTransform) : this.getShapePageTransform(id);
if (!pageTransform) return this;
const pageRotation = pageTransform.rotation();
if (pageRotation == null) return this;
const scaleAxisRotation = opts.scaleAxisRotation ?? pageRotation;
const initialBounds = opts.initialBounds ?? this.getShapeGeometry(id).bounds;
if (!initialBounds) return this;
const isAspectRatioLocked = opts.isAspectRatioLocked ?? this.getShapeUtil(initialShape).isAspectRatioLocked(initialShape);
if (!areAnglesCompatible(pageRotation, scaleAxisRotation)) {
return this._resizeUnalignedShape(id, scale, {
...opts,
initialBounds,
scaleOrigin,
scaleAxisRotation,
initialPageTransform: pageTransform,
isAspectRatioLocked,
initialShape
});
}
const util = this.getShapeUtil(initialShape);
if (isAspectRatioLocked) {
if (Math.abs(scale.x) > Math.abs(scale.y)) {
scale = new Vec(scale.x, Math.sign(scale.y) * Math.abs(scale.x));
} else {
scale = new Vec(Math.sign(scale.x) * Math.abs(scale.y), scale.y);
}
}
if (util.onResize && util.canResize(initialShape)) {
const newPagePoint = this._scalePagePoint(
Mat.applyToPoint(pageTransform, new Vec(0, 0)),
scaleOrigin,
scale,
scaleAxisRotation
);
const newLocalPoint = this.getPointInParentSpace(initialShape.id, newPagePoint);
const myScale = new Vec(scale.x, scale.y);
const areWidthAndHeightAlignedWithCorrectAxis = approximately(
(pageRotation - scaleAxisRotation) % Math.PI,
0
);
myScale.x = areWidthAndHeightAlignedWithCorrectAxis ? scale.x : scale.y;
myScale.y = areWidthAndHeightAlignedWithCorrectAxis ? scale.y : scale.x;
const initialPagePoint = Mat.applyToPoint(pageTransform, new Vec());
const { x, y } = this.getPointInParentSpace(initialShape.id, initialPagePoint);
let workingShape = initialShape;
if (!opts.skipStartAndEndCallbacks) {
workingShape = applyPartialToRecordWithProps(
initialShape,
util.onResizeStart?.(initialShape) ?? void 0
);
}
workingShape = applyPartialToRecordWithProps(workingShape, {
id,
type: initialShape.type,
x: newLocalPoint.x,
y: newLocalPoint.y,
...util.onResize(
{ ...initialShape, x, y },
{
newPoint: newLocalPoint,
handle: opts.dragHandle ?? "bottom_right",
// don't set isSingle to true for children
mode: opts.mode ?? "scale_shape",
scaleX: myScale.x,
scaleY: myScale.y,
initialBounds,
initialShape
}
)
});
if (!opts.skipStartAndEndCallbacks) {
workingShape = applyPartialToRecordWithProps(
workingShape,
util.onResizeEnd?.(initialShape, workingShape) ?? void 0
);
}
this.updateShapes([workingShape]);
} else {
const initialPageCenter = Mat.applyToPoint(pageTransform, initialBounds.center);
const newPageCenter = this._scalePagePoint(
initialPageCenter,
scaleOrigin,
scale,
scaleAxisRotation
);
const initialPageCenterInParentSpace = this.getPointInParentSpace(
initialShape.id,
initialPageCenter
);
const newPageCenterInParentSpace = this.getPointInParentSpace(initialShape.id, newPageCenter);
const delta = Vec.Sub(newPageCenterInParentSpace, initialPageCenterInParentSpace);
this.updateShapes([
{
id,
type: initialShape.type,
x: initialShape.x + delta.x,
y: initialShape.y + delta.y
}
]);
}
return this;
}
/** @internal */
_scalePagePoint(point, scaleOrigin, scale, scaleAxisRotation) {
const relativePoint = Vec.RotWith(point, scaleOrigin, -scaleAxisRotation).sub(scaleOrigin);
const newRelativePagePoint = Vec.MulV(relativePoint, scale);
const destination = Vec.Add(newRelativePagePoint, scaleOrigin).rotWith(
scaleOrigin,
scaleAxisRotation
);
return destination;
}
/** @internal */
_resizeUnalignedShape(id, scale, options) {
const { type } = options.initialShape;
const shapeScale = new Vec(scale.x, scale.y);
if (Math.abs(scale.x) > Math.abs(scale.y)) {
shapeScale.x = Math.sign(scale.x) * Math.abs(scale.y);
} else {
shapeScale.y = Math.sign(scale.y) * Math.abs(scale.x);
}
this.resizeShape(id, shapeScale, {
initialShape: options.initialShape,
initialBounds: options.initialBounds,
isAspectRatioLocked: options.isAspectRatioLocked
});
if (Math.sign(scale.x) * Math.sign(scale.y) < 0) {
let { rotation } = Mat.Decompose(options.initialPageTransform);
rotation -= 2 * rotation;
this.updateShapes([{ id, type, rotation }]);
}
const preScaleShapePageCenter = Mat.applyToPoint(
options.initialPageTransform,
options.initialBounds.center
);
const postScaleShapePageCenter = this._scalePagePoint(
preScaleShapePageCenter,
options.scaleOrigin,
scale,
options.scaleAxisRotation
);
const pageBounds = this.getShapePageBounds(id);
const pageTransform = this.getShapePageTransform(id);
const currentPageCenter = pageBounds.center;
const shapePageTransformOrigin = pageTransform.point();
if (!currentPageCenter || !shapePageTransformOrigin) return this;
const pageDelta = Vec.Sub(postScaleShapePageCenter, currentPageCenter);
const postScaleShapePagePoint = Vec.Add(shapePageTransformOrigin, pageDelta);
const { x, y } = this.getPointInParentSpace(id, postScaleShapePagePoint);
this.updateShapes([{ id, type, x, y }]);
return this;
}
/**
* Get the initial meta value for a shape.
*
* @example
* ```ts
* editor.getInitialMetaForShape = (shape) => {
* if (shape.type === 'note') {
* return { createdBy: myCurrentUser.id }
* }
* }
* ```
*
* @param shape - The shape to get the initial meta for.
*
* @public
*/
getInitialMetaForShape(_shape) {
return {};
}
/**
* Create a single shape.
*
* @example
* ```ts
* editor.createShape(myShape)
* editor.createShape({ id: 'box1', type: 'text', props: { text: "ok" } })
* ```
*
* @param shape - The shape (or shape partial) to create.
*
* @public
*/
createShape(shape) {
this.createShapes([shape]);
return this;
}
/**
* Create shapes.
*
* @example
* ```ts
* editor.createShapes([myShape])
* editor.createShapes([{ id: 'box1', type: 'text', props: { text: "ok" } }])
* ```
*
* @param shapes - The shapes (or shape partials) to create.
*
* @public
*/
createShapes(shapes) {
if (!Array.isArray(shapes)) {
throw Error("Editor.createShapes: must provide an array of shapes or shape partials");
}
if (this.getIsReadonly()) return this;
if (shapes.length <= 0) return this;
const currentPageShapeIds = this.getCurrentPageShapeIds();
const maxShapesReached = shapes.length + currentPageShapeIds.size > this.options.maxShapesPerPage;
if (maxShapesReached) {
alertMaxShapes(this);
return this;
}
const focusedGroupId = this.getFocusedGroupId();
this.run(() => {
const currentPageShapesSorted = this.getCurrentPageShapesSorted();
const partials = shapes.map((partial) => {
if (!partial.id) {
partial = { id: createShapeId(), ...partial };
}
if (!partial.parentId || !(this.store.has(partial.parentId) || shapes.some((p) => p.id === partial.parentId))) {
let parentId = this.getFocusedGroupId();
for (let i = currentPageShapesSorted.length - 1; i >= 0; i--) {
const parent = currentPageShapesSorted[i];
if (!this.isShapeHidden(parent) && this.getShapeUtil(parent).canReceiveNewChildrenOfType(parent, partial.type) && this.isPointInShape(
parent,
// If no parent is provided, then we can treat the
// shape's provided x/y as being in the page's space.
{ x: partial.x ?? 0, y: partial.y ?? 0 },
{
margin: 0,
hitInside: true
}
)) {
parentId = parent.id;
break;
}
}
const prevParentId = partial.parentId;
if (parentId === partial.id) {
parentId = focusedGroupId;
}
if (parentId !== prevParentId) {
partial = { ...partial };
partial.parentId = parentId;
if (isShapeId(parentId)) {
const point = this.getPointInShapeSpace(this.getShape(parentId), {
x: partial.x ?? 0,
y: partial.y ?? 0
});
partial.x = point.x;
partial.y = point.y;
partial.rotation = -this.getShapePageTransform(parentId).rotation() + (partial.rotation ?? 0);
}
}
}
return partial;
});
const parentIndices = /* @__PURE__ */ new Map();
const shapeRecordsToCreate = [];
const { opacityForNextShape } = this.getInstanceState();
for (const partial of partials) {
const util = this.getShapeUtil(partial);
let index = partial.index;
if (!index) {
const parentId = partial.parentId ?? focusedGroupId;
if (!parentIndices.has(parentId)) {
parentIndices.set(parentId, this.getHighestIndexForParent(parentId));
}
index = parentIndices.get(parentId);
parentIndices.set(parentId, getIndexAbove(index));
}
const initialProps = util.getDefaultProps();
for (const [style, propKey] of this.styleProps[partial.type]) {
initialProps[propKey] = this.getStyleForNextShape(style);
}
let shapeRecordToCreate = this.store.schema.types.shape.create({
...partial,
index,
opacity: partial.opacity ?? opacityForNextShape,
parentId: partial.parentId ?? focusedGroupId,
props: "props" in partial ? { ...initialProps, ...partial.props } : initialProps
});
if (shapeRecordToCreate.index === void 0) {
throw Error("no index!");
}
const next = this.getShapeUtil(shapeRecordToCreate).onBeforeCreate?.(shapeRecordToCreate);
if (next) {
shapeRecordToCreate = next;
}
shapeRecordsToCreate.push(shapeRecordToCreate);
}
shapeRecordsToCreate.forEach((shape) => {
shape.meta = {
...this.getInitialMetaForShape(shape),
...shape.meta
};
});
this.store.put(shapeRecordsToCreate);
});
return this;
}
/**
* Animate a shape.
*
* @example
* ```ts
* editor.animateShape({ id: 'box1', type: 'box', x: 100, y: 100 })
* editor.animateShape({ id: 'box1', type: 'box', x: 100, y: 100 }, { animation: { duration: 100, ease: t => t*t } })
* ```
*
* @param partial - The shape partial to update.
* @param opts - The animation's options.
*
* @public
*/
animateShape(partial, opts = { animation: DEFAULT_ANIMATION_OPTIONS }) {
return this.animateShapes([partial], opts);
}
/**
* Animate shapes.
*
* @example
* ```ts
* editor.animateShapes([{ id: 'box1', type: 'box', x: 100, y: 100 }])
* editor.animateShapes([{ id: 'box1', type: 'box', x: 100, y: 100 }], { animation: { duration: 100, ease: t => t*t } })
* ```
*
* @param partials - The shape partials to update.
* @param opts - The animation's options.
*
* @public
*/
animateShapes(partials, opts = { animation: DEFAULT_ANIMATION_OPTIONS }) {
if (!opts.animation) return this;
const { duration = 500, easing = EASINGS.linear } = opts.animation;
const animationId = uniqueId();
let remaining = duration;
let t;
const animations = [];
let partial, result;
for (let i = 0, n = partials.length; i < n; i++) {
partial = partials[i];
if (!partial) continue;
const shape = this.getShape(partial.id);
if (!shape) continue;
result = {
start: structuredClone(shape),
end: applyPartialToRecordWithProps(structuredClone(shape), partial)
};
animations.push(result);
this.animatingShapes.set(shape.id, animationId);
}
const handleTick = (elapsed) => {
remaining -= elapsed;
if (remaining < 0) {
const { animatingShapes: animatingShapes2 } = this;
const partialsToUpdate = partials.filter(
(p) => p && animatingShapes2.get(p.id) === animationId
);
if (partialsToUpdate.length) {
this.updateShapes(partialsToUpdate);
}
this.off("tick", handleTick);
return;
}
t = easing(1 - remaining / duration);
const { animatingShapes } = this;
const updates = [];
let animationIdForShape;
for (let i = 0, n = animations.length; i < n; i++) {
const { start, end } = animations[i];
animationIdForShape = animatingShapes.get(start.id);
if (animationIdForShape !== animationId) continue;
updates.push({
...end,
x: start.x + (end.x - start.x) * t,
y: start.y + (end.y - start.y) * t,
opacity: start.opacity + (end.opacity - start.opacity) * t,
rotation: start.rotation + (end.rotation - start.rotation) * t,
props: this.getShapeUtil(end).getInterpolatedProps?.(start, end, t) ?? end.props
});
}
this._updateShapes(updates);
};
this.on("tick", handleTick);
return this;
}
groupShapes(shapes, opts = {}) {
const { groupId = createShapeId(), select = true } = opts;
if (!Array.isArray(shapes)) {
throw Error("Editor.groupShapes: must provide an array of shapes or shape ids");
}
if (this.getIsReadonly()) return this;
const ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
if (ids.length <= 1) return this;
const shapesToGroup = compact(
(this._shouldIgnoreShapeLock ? ids : this._getUnlockedShapeIds(ids)).map(
(id) => this.getShape(id)
)
);
const sortedShapeIds = shapesToGroup.sort(sortByIndex$1).map((s) => s.id);
const pageBounds = Box.Common(compact(shapesToGroup.map((id) => this.getShapePageBounds(id))));
const { x, y } = pageBounds.point;
const parentId = this.findCommonAncestor(shapesToGroup) ?? this.getCurrentPageId();
if (this.getCurrentToolId() !== "select") return this;
if (!this.isIn("select.idle")) {
this.cancel();
}
const shapesWithRootParent = shapesToGroup.filter((shape) => shape.parentId === parentId).sort(sortByIndex$1);
const highestIndex = shapesWithRootParent[shapesWithRootParent.length - 1]?.index;
this.run(() => {
this.createShapes([
{
id: groupId,
type: "group",
parentId,
index: highestIndex,
x,
y,
opacity: 1,
props: {}
}
]);
this.reparentShapes(sortedShapeIds, groupId);
if (select) {
this.select(groupId);
}
});
return this;
}
ungroupShapes(shapes, opts = {}) {
if (this.getIsReadonly()) return this;
const { select = true } = opts;
const ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
const shapesToUngroup = compact(
(this._shouldIgnoreShapeLock ? ids : this._getUnlockedShapeIds(ids)).map(
(id) => this.getShape(id)
)
);
if (shapesToUngroup.length === 0) return this;
if (this.getCurrentToolId() !== "select") return this;
if (!this.isIn("select.idle")) {
this.cancel();
}
const idsToSelect = /* @__PURE__ */ new Set();
const groups = [];
shapesToUngroup.forEach((shape) => {
if (this.isShapeOfType(shape, "group")) {
groups.push(shape);
} else {
idsToSelect.add(shape.id);
}
});
if (groups.length === 0) return this;
this.run(() => {
let group;
for (let i = 0, n = groups.length; i < n; i++) {
group = groups[i];
const childIds = this.getSortedChildIdsForParent(group.id);
for (let j = 0, n2 = childIds.length; j < n2; j++) {
idsToSelect.add(childIds[j]);
}
this.reparentShapes(childIds, group.parentId, group.index);
}
this.deleteShapes(groups.map((group2) => group2.id));
if (select) {
this.select(...idsToSelect);
}
});
return this;
}
/**
* Update a shape using a partial of the shape.
*
* @example
* ```ts
* editor.updateShape({ id: 'box1', type: 'geo', props: { w: 100, h: 100 } })
* ```
*
* @param partial - The shape partial to update.
*
* @public
*/
updateShape(partial) {
this.updateShapes([partial]);
return this;
}
/**
* Update shapes using partials of each shape.
*
* @example
* ```ts
* editor.updateShapes([{ id: 'box1', type: 'geo', props: { w: 100, h: 100 } }])
* ```
*
* @param partials - The shape partials to update.
*
* @public
*/
updateShapes(partials) {
const compactedPartials = Array(partials.length);
for (let i = 0, n = partials.length; i < n; i++) {
const partial = partials[i];
if (!partial) continue;
const shape = this.getShape(partial.id);
if (!shape) continue;
if (!this._shouldIgnoreShapeLock) {
if (shape.isLocked) {
if (!(Object.hasOwn(partial, "isLocked") && !partial.isLocked)) {
continue;
}
} else if (this.isShapeOrAncestorLocked(shape)) {
continue;
}
}
this.animatingShapes.delete(partial.id);
compactedPartials.push(partial);
}
this._updateShapes(compactedPartials);
return this;
}
/** @internal */
_updateShapes(_partials) {
if (this.getIsReadonly()) return;
this.run(() => {
const updates = [];
let shape;
let updated;
for (let i = 0, n = _partials.length; i < n; i++) {
const partial = _partials[i];
if (!partial) continue;
shape = this.getShape(partial.id);
if (!shape) continue;
updated = applyPartialToRecordWithProps(shape, partial);
if (updated === shape) continue;
updated = this.getShapeUtil(shape).onBeforeUpdate?.(shape, updated) ?? updated;
updates.push(updated);
}
this.store.put(updates);
});
}
/** @internal */
_getUnlockedShapeIds(ids) {
return ids.filter((id) => !this.getShape(id)?.isLocked);
}
deleteShapes(_ids) {
if (this.getIsReadonly()) return this;
if (!Array.isArray(_ids)) {
throw Error("Editor.deleteShapes: must provide an array of shapes or shapeIds");
}
const shapeIds = typeof _ids[0] === "string" ? _ids : _ids.map((s) => s.id);
const shapeIdsToDelete = this._shouldIgnoreShapeLock ? shapeIds : this._getUnlockedShapeIds(shapeIds);
if (shapeIdsToDelete.length === 0) return this;
const allShapeIdsToDelete = new Set(shapeIdsToDelete);
for (const id of shapeIdsToDelete) {
this.visitDescendants(id, (childId) => {
allShapeIdsToDelete.add(childId);
});
}
return this.run(() => this.store.remove([...allShapeIdsToDelete]));
}
deleteShape(_id) {
this.deleteShapes([typeof _id === "string" ? _id : _id.id]);
return this;
}
/* --------------------- Styles --------------------- */
/**
* Get all the current styles among the users selected shapes
*
* @internal
*/
_extractSharedStyles(shape, sharedStyleMap) {
if (this.isShapeOfType(shape, "group")) {
const childIds = this._parentIdsToChildIds.get()[shape.id];
if (!childIds) return;
for (let i = 0, n = childIds.length; i < n; i++) {
this._extractSharedStyles(this.getShape(childIds[i]), sharedStyleMap);
}
} else {
for (const [style, propKey] of this.styleProps[shape.type]) {
sharedStyleMap.applyValue(style, getOwnProperty(shape.props, propKey));
}
}
}
_getSelectionSharedStyles() {
const selectedShapes = this.getSelectedShapes();
const sharedStyles = new SharedStyleMap();
for (const selectedShape of selectedShapes) {
this._extractSharedStyles(selectedShape, sharedStyles);
}
return sharedStyles;
}
/**
* Get the style for the next shape.
*
* @example
* ```ts
* const color = editor.getStyleForNextShape(DefaultColorStyle)
* ```
*
* @param style - The style to get.
*
* @public */
getStyleForNextShape(style) {
const value = this.getInstanceState().stylesForNextShape[style.id];
return value === void 0 ? style.defaultValue : value;
}
getShapeStyleIfExists(shape, style) {
const styleKey = this.styleProps[shape.type].get(style);
if (styleKey === void 0) return void 0;
return getOwnProperty(shape.props, styleKey);
}
getSharedStyles() {
if (this.isIn("select") && this.getSelectedShapeIds().length > 0) {
return this._getSelectionSharedStyles();
}
const currentTool = this.root.getCurrent();
const styles = new SharedStyleMap();
if (!currentTool) return styles;
if (currentTool.shapeType) {
for (const style of this.styleProps[currentTool.shapeType].keys()) {
styles.applyValue(style, this.getStyleForNextShape(style));
}
}
return styles;
}
getSharedOpacity() {
if (this.isIn("select") && this.getSelectedShapeIds().length > 0) {
const shapesToCheck = [];
const addShape = (shapeId) => {
const shape = this.getShape(shapeId);
if (!shape) return;
if (this.isShapeOfType(shape, "group")) {
for (const childId of this.getSortedChildIdsForParent(shape.id)) {
addShape(childId);
}
} else {
shapesToCheck.push(shape);
}
};
for (const shapeId of this.getSelectedShapeIds()) {
addShape(shapeId);
}
let opacity = null;
for (const shape of shapesToCheck) {
if (opacity === null) {
opacity = shape.opacity;
} else if (opacity !== shape.opacity) {
return { type: "mixed" };
}
}
if (opacity !== null) return { type: "shared", value: opacity };
}
return { type: "shared", value: this.getInstanceState().opacityForNextShape };
}
/**
* Set the opacity for the next shapes. This will effect subsequently created shapes.
*
* @example
* ```ts
* editor.setOpacityForNextShapes(0.5)
* ```
*
* @param opacity - The opacity to set. Must be a number between 0 and 1 inclusive.
* @param historyOptions - The history options for the change.
*/
setOpacityForNextShapes(opacity, historyOptions) {
this.updateInstanceState({ opacityForNextShape: opacity }, historyOptions);
return this;
}
/**
* Set the current opacity. This will effect any selected shapes.
*
* @example
* ```ts
* editor.setOpacityForSelectedShapes(0.5)
* ```
*
* @param opacity - The opacity to set. Must be a number between 0 and 1 inclusive.
*/
setOpacityForSelectedShapes(opacity) {
const selectedShapes = this.getSelectedShapes();
if (selectedShapes.length > 0) {
const shapesToUpdate = [];
const addShapeById = (shape) => {
if (this.isShapeOfType(shape, "group")) {
const childIds = this.getSortedChildIdsForParent(shape);
for (const childId of childIds) {
addShapeById(this.getShape(childId));
}
} else {
shapesToUpdate.push(shape);
}
};
for (const id of selectedShapes) {
addShapeById(id);
}
this.updateShapes(
shapesToUpdate.map((shape) => {
return {
id: shape.id,
type: shape.type,
opacity
};
})
);
}
return this;
}
/**
* Set the value of a {@link @tldraw/tlschema#StyleProp} for the next shapes. This change will be applied to subsequently created shapes.
*
* @example
* ```ts
* editor.setStyleForNextShapes(DefaultColorStyle, 'red')
* editor.setStyleForNextShapes(DefaultColorStyle, 'red', { ephemeral: true })
* ```
*
* @param style - The style to set.
* @param value - The value to set.
* @param historyOptions - The history options for the change.
*
* @public
*/
setStyleForNextShapes(style, value, historyOptions) {
const stylesForNextShape = this.getInstanceState().stylesForNextShape;
this.updateInstanceState(
{ stylesForNextShape: { ...stylesForNextShape, [style.id]: value } },
historyOptions
);
return this;
}
/**
* Set the value of a {@link @tldraw/tlschema#StyleProp}. This change will be applied to the currently selected shapes.
*
* @example
* ```ts
* editor.setStyleForSelectedShapes(DefaultColorStyle, 'red')
* ```
*
* @param style - The style to set.
* @param value - The value to set.
*
* @public
*/
setStyleForSelectedShapes(style, value) {
const selectedShapes = this.getSelectedShapes();
if (selectedShapes.length > 0) {
const updates = [];
const addShapeById = (shape) => {
if (this.isShapeOfType(shape, "group")) {
const childIds = this.getSortedChildIdsForParent(shape.id);
for (const childId of childIds) {
addShapeById(this.getShape(childId));
}
} else {
const util = this.getShapeUtil(shape);
const stylePropKey = this.styleProps[shape.type].get(style);
if (stylePropKey) {
const shapePartial = {
id: shape.id,
type: shape.type,
props: { [stylePropKey]: value }
};
updates.push({
util,
originalShape: shape,
updatePartial: shapePartial
});
}
}
};
for (const shape of selectedShapes) {
addShapeById(shape);
}
this.updateShapes(updates.map(({ updatePartial }) => updatePartial));
}
return this;
}
/**
* Register an external asset handler. This handler will be called when the editor needs to
* create an asset for some external content, like an image/video file or a bookmark URL. For
* example, the 'file' type handler will be called when a user drops an image onto the canvas.
*
* The handler should extract any relevant metadata for the asset, upload it to blob storage
* using {@link Editor.uploadAsset} if needed, and return the asset with the metadata & uploaded
* URL.
*
* @example
* ```ts
* editor.registerExternalAssetHandler('file', myHandler)
* ```
*
* @param type - The type of external content.
* @param handler - The handler to use for this content type.
*
* @public
*/
registerExternalAssetHandler(type, handler) {
this.externalAssetContentHandlers[type] = handler;
return this;
}
/**
* Register a temporary preview of an asset. This is useful for showing a ghost image of
* something that is being uploaded. Retrieve the placeholder with
* {@link Editor.getTemporaryAssetPreview}. Placeholders last for 3 minutes by default, but this
* can be configured using
*
* @example
* ```ts
* editor.createTemporaryAssetPreview(assetId, file)
* ```
*
* @param assetId - The asset's id.
* @param file - The raw file.
*
* @public
*/
createTemporaryAssetPreview(assetId, file) {
if (this.temporaryAssetPreview.has(assetId)) {
return this.temporaryAssetPreview.get(assetId);
}
const objectUrl = URL.createObjectURL(file);
this.temporaryAssetPreview.set(assetId, objectUrl);
setTimeout(() => {
this.temporaryAssetPreview.delete(assetId);
URL.revokeObjectURL(objectUrl);
}, this.options.temporaryAssetPreviewLifetimeMs);
return objectUrl;
}
/**
* Get temporary preview of an asset. This is useful for showing a ghost
* image of something that is being uploaded.
*
* @example
* ```ts
* editor.getTemporaryAssetPreview('someId')
* ```
*
* @param assetId - The asset's id.
*
* @public
*/
getTemporaryAssetPreview(assetId) {
return this.temporaryAssetPreview.get(assetId);
}
/**
* Get an asset for an external asset content type.
*
* @example
* ```ts
* const asset = await editor.getAssetForExternalContent({ type: 'file', file: myFile })
* const asset = await editor.getAssetForExternalContent({ type: 'url', url: myUrl })
* ```
*
* @param info - Info about the external content.
* @returns The asset.
*/
async getAssetForExternalContent(info) {
return await this.externalAssetContentHandlers[info.type]?.(info);
}
hasExternalAssetHandler(type) {
return !!this.externalAssetContentHandlers[type];
}
/**
* Register an external content handler. This handler will be called when the editor receives
* external content of the provided type. For example, the 'image' type handler will be called
* when a user drops an image onto the canvas.
*
* @example
* ```ts
* editor.registerExternalContentHandler('text', myHandler)
* ```
* @example
* ```ts
* editor.registerExternalContentHandler<'embed', MyEmbedType>('embed', myHandler)
* ```
*
* @param type - The type of external content.
* @param handler - The handler to use for this content type.
*
* @public
*/
registerExternalContentHandler(type, handler) {
this.externalContentHandlers[type] = handler;
return this;
}
/**
* Handle external content, such as files, urls, embeds, or plain text which has been put into the app, for example by pasting external text or dropping external images onto canvas.
*
* @param info - Info about the external content.
*/
async putExternalContent(info) {
return this.externalContentHandlers[info.type]?.(info);
}
/**
* Get content that can be exported for the given shape ids.
*
* @param shapes - The shapes (or shape ids) to get content for.
*
* @returns The exported content.
*
* @public
*/
getContentFromCurrentPage(shapes) {
const ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
if (!ids) return;
if (ids.length === 0) return;
const shapeIds = this.getShapeAndDescendantIds(ids);
return withIsolatedShapes(this, shapeIds, (bindingIdsToKeep) => {
const bindings = [];
for (const id of bindingIdsToKeep) {
const binding = this.getBinding(id);
if (!binding) continue;
bindings.push(binding);
}
const rootShapeIds = [];
const shapes2 = [];
for (const shapeId of shapeIds) {
const shape = this.getShape(shapeId);
if (!shape) continue;
const isRootShape = !shapeIds.has(shape.parentId);
if (isRootShape) {
const pageTransform = this.getShapePageTransform(shape.id);
const pagePoint = pageTransform.point();
shapes2.push({
...shape,
x: pagePoint.x,
y: pagePoint.y,
rotation: pageTransform.rotation(),
parentId: this.getCurrentPageId()
});
rootShapeIds.push(shape.id);
} else {
shapes2.push(shape);
}
}
const assets = [];
const seenAssetIds = /* @__PURE__ */ new Set();
for (const shape of shapes2) {
if (!("assetId" in shape.props)) continue;
const assetId = shape.props.assetId;
if (!assetId || seenAssetIds.has(assetId)) continue;
seenAssetIds.add(assetId);
const asset = this.getAsset(assetId);
if (!asset) continue;
assets.push(asset);
}
return {
schema: this.store.schema.serialize(),
shapes: shapes2,
rootShapeIds,
bindings,
assets
};
});
}
async resolveAssetsInContent(content) {
if (!content) return void 0;
const assets = [];
await Promise.allSettled(
content.assets.map(async (asset) => {
if ((asset.type === "image" || asset.type === "video") && !asset.props.src?.startsWith("data:image") && !asset.props.src?.startsWith("data:video") && !asset.props.src?.startsWith("http")) {
const assetWithDataUrl = structuredClone(asset);
const objectUrl = await this.store.props.assets.resolve(asset, {
screenScale: 1,
steppedScreenScale: 1,
dpr: 1,
networkEffectiveType: null,
shouldResolveToOriginal: true
});
assetWithDataUrl.props.src = await FileHelpers.blobToDataUrl(
await fetch(objectUrl).then((r) => r.blob())
);
assets.push(assetWithDataUrl);
} else {
assets.push(asset);
}
})
);
content.assets = assets;
return content;
}
/**
* Place content into the editor.
*
* @param content - The content.
* @param opts - Options for placing the content.
*
* @public
*/
putContentOntoCurrentPage(content, opts = {}) {
if (this.getIsReadonly()) return this;
if (!content.schema) {
throw Error("Could not put content:\ncontent is missing a schema.");
}
const { select = false, preserveIds = false, preservePosition = false } = opts;
let { point = void 0 } = opts;
const currentPageId = this.getCurrentPageId();
const { rootShapeIds } = content;
const assets = [];
const shapes = [];
const bindings = [];
const store = {
store: {
...Object.fromEntries(content.assets.map((asset) => [asset.id, asset])),
...Object.fromEntries(content.shapes.map((shape) => [shape.id, shape])),
...Object.fromEntries(
content.bindings?.map((bindings2) => [bindings2.id, bindings2]) ?? []
)
},
schema: content.schema
};
const result = this.store.schema.migrateStoreSnapshot(store);
if (result.type === "error") {
throw Error("Could not put content: could not migrate content");
}
for (const record of Object.values(result.value)) {
switch (record.typeName) {
case "asset": {
assets.push(record);
break;
}
case "shape": {
shapes.push(record);
break;
}
case "binding": {
bindings.push(record);
break;
}
}
}
const shapeIdMap = new Map(
preserveIds ? shapes.map((shape) => [shape.id, shape.id]) : shapes.map((shape) => [shape.id, createShapeId()])
);
const bindingIdMap = new Map(
preserveIds ? bindings.map((binding) => [binding.id, binding.id]) : bindings.map((binding) => [binding.id, createBindingId()])
);
let pasteParentId = this.getCurrentPageId();
let lowestDepth = Infinity;
let lowestAncestors = [];
for (const shape of this.getSelectedShapes()) {
if (lowestDepth === 0) break;
const isFrame = this.isShapeOfType(shape, "frame");
const ancestors = this.getShapeAncestors(shape);
if (isFrame) ancestors.push(shape);
const depth = isFrame ? ancestors.length + 1 : ancestors.length;
if (depth < lowestDepth) {
lowestDepth = depth;
lowestAncestors = ancestors;
pasteParentId = isFrame ? shape.id : shape.parentId;
} else if (depth === lowestDepth) {
if (lowestAncestors.length !== ancestors.length) {
throw Error(`Ancestors: ${lowestAncestors.length} !== ${ancestors.length}`);
}
if (lowestAncestors.length === 0) {
pasteParentId = currentPageId;
break;
} else {
pasteParentId = currentPageId;
for (let i = 0; i < lowestAncestors.length; i++) {
if (ancestors[i] !== lowestAncestors[i]) break;
pasteParentId = ancestors[i].id;
}
}
}
}
let isDuplicating = false;
if (!isPageId(pasteParentId)) {
const parent = this.getShape(pasteParentId);
if (parent) {
if (!this.getViewportPageBounds().includes(this.getShapePageBounds(parent))) {
pasteParentId = currentPageId;
} else {
if (rootShapeIds.length === 1) {
const rootShape = shapes.find((s) => s.id === rootShapeIds[0]);
if (this.isShapeOfType(parent, "frame") && this.isShapeOfType(rootShape, "frame") && rootShape.props.w === parent?.props.w && rootShape.props.h === parent?.props.h) {
isDuplicating = true;
}
}
}
} else {
pasteParentId = currentPageId;
}
}
if (!isDuplicating) {
isDuplicating = shapeIdMap.has(pasteParentId);
}
if (isDuplicating) {
pasteParentId = this.getShape(pasteParentId).parentId;
}
let index = this.getHighestIndexForParent(pasteParentId);
const rootShapes = [];
const newShapes = shapes.map((oldShape) => {
const newId = shapeIdMap.get(oldShape.id);
const newShape = { ...oldShape, id: newId };
if (rootShapeIds.includes(oldShape.id)) {
newShape.parentId = currentPageId;
rootShapes.push(newShape);
}
if (shapeIdMap.has(newShape.parentId)) {
newShape.parentId = shapeIdMap.get(oldShape.parentId);
} else {
rootShapeIds.push(newShape.id);
newShape.index = index;
index = getIndexAbove(index);
}
return newShape;
});
if (newShapes.length + this.getCurrentPageShapeIds().size > this.options.maxShapesPerPage) {
alertMaxShapes(this);
return this;
}
const newBindings = bindings.map(
(oldBinding) => ({
...oldBinding,
id: assertExists(bindingIdMap.get(oldBinding.id)),
fromId: assertExists(shapeIdMap.get(oldBinding.fromId)),
toId: assertExists(shapeIdMap.get(oldBinding.toId))
})
);
const assetsToCreate = [];
const assetsToUpdate = [];
for (const asset of assets) {
if (this.store.has(asset.id)) {
continue;
}
if (asset.type === "image" && asset.props.src?.startsWith("data:image") || asset.type === "video" && asset.props.src?.startsWith("data:video")) {
assetsToUpdate.push(structuredClone(asset));
asset.props.src = null;
}
assetsToCreate.push(asset);
}
Promise.allSettled(
assetsToUpdate.map(async (asset) => {
const file = await dataUrlToFile(
asset.props.src,
asset.props.name,
asset.props.mimeType ?? "image/png"
);
const newAsset = await this.getAssetForExternalContent({
type: "file",
file,
assetId: asset.id
});
if (!newAsset) {
this.deleteAssets([asset.id]);
return;
}
this.updateAssets([{ ...newAsset, id: asset.id }]);
})
);
this.run(() => {
if (assetsToCreate.length > 0) {
this.createAssets(assetsToCreate);
}
this.createShapes(newShapes);
this.createBindings(newBindings);
if (select) {
this.select(...rootShapes.map((s) => s.id));
}
if (pasteParentId !== currentPageId) {
this.reparentShapes(
rootShapes.map((s) => s.id),
pasteParentId
);
}
const newCreatedShapes = newShapes.map((s) => this.getShape(s.id));
const bounds = Box.Common(newCreatedShapes.map((s) => this.getShapePageBounds(s)));
if (point === void 0) {
if (!isPageId(pasteParentId)) {
const shape = this.getShape(pasteParentId);
point = Mat.applyToPoint(
this.getShapePageTransform(shape),
this.getShapeGeometry(shape).bounds.center
);
} else {
const viewportPageBounds = this.getViewportPageBounds();
if (preservePosition || viewportPageBounds.includes(Box.From(bounds))) {
point = bounds.center;
} else {
point = viewportPageBounds.center;
}
}
}
if (rootShapes.length === 1) {
const onlyRoot = rootShapes[0];
if (this.isShapeOfType(onlyRoot, "frame")) {
while (this.getShapesAtPoint(point).some(
(shape) => this.isShapeOfType(shape, "frame") && shape.props.w === onlyRoot.props.w && shape.props.h === onlyRoot.props.h
)) {
point.x += bounds.w + 16;
}
}
}
const pageCenter = Box.Common(
compact(rootShapes.map(({ id }) => this.getShapePageBounds(id)))
).center;
const offset = Vec.Sub(point, pageCenter);
this.updateShapes(
rootShapes.map(({ id }) => {
const s = this.getShape(id);
const localRotation = this.getShapeParentTransform(id).decompose().rotation;
const localDelta = Vec.Rot(offset, -localRotation);
return { id: s.id, type: s.type, x: s.x + localDelta.x, y: s.y + localDelta.y };
})
);
});
return this;
}
/**
* Get an exported SVG element of the given shapes.
*
* @param shapes - The shapes (or shape ids) to export.
* @param opts - Options for the export.
*
* @returns The SVG element.
*
* @public
*/
async getSvgElement(shapes, opts = {}) {
const ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
if (ids.length === 0) return void 0;
return exportToSvg(this, ids, opts);
}
/**
* Get an exported SVG string of the given shapes.
*
* @param shapes - The shapes (or shape ids) to export.
* @param opts - Options for the export.
*
* @returns The SVG element.
*
* @public
*/
async getSvgString(shapes, opts = {}) {
const result = await this.getSvgElement(shapes, opts);
if (!result) return void 0;
const serializer = new XMLSerializer();
return {
svg: serializer.serializeToString(result.svg),
width: result.width,
height: result.height
};
}
/** @deprecated Use {@link Editor.getSvgString} or {@link Editor.getSvgElement} instead. */
async getSvg(shapes, opts = {}) {
const result = await this.getSvgElement(shapes, opts);
if (!result) return void 0;
return result.svg;
}
/**
* Update the input points from a pointer, pinch, or wheel event.
*
* @param info - The event info.
*/
_updateInputsFromEvent(info) {
const {
pointerVelocity,
previousScreenPoint,
previousPagePoint,
currentScreenPoint,
currentPagePoint
} = this.inputs;
const { screenBounds } = this.store.unsafeGetWithoutCapture(TLINSTANCE_ID);
const { x: cx, y: cy, z: cz } = unsafe__withoutCapture(() => this.getCamera());
const sx = info.point.x - screenBounds.x;
const sy = info.point.y - screenBounds.y;
const sz = info.point.z ?? 0.5;
previousScreenPoint.setTo(currentScreenPoint);
previousPagePoint.setTo(currentPagePoint);
currentScreenPoint.set(sx, sy);
const nx = sx / cz - cx;
const ny = sy / cz - cy;
if (isFinite(nx) && isFinite(ny)) {
currentPagePoint.set(nx, ny, sz);
}
this.inputs.isPen = info.type === "pointer" && info.isPen;
if (info.name === "pointer_down" || this.inputs.isPinching) {
pointerVelocity.set(0, 0);
this.inputs.originScreenPoint.setTo(currentScreenPoint);
this.inputs.originPagePoint.setTo(currentPagePoint);
}
this.run(
() => {
this.store.put([
{
id: TLPOINTER_ID,
typeName: "pointer",
x: currentPagePoint.x,
y: currentPagePoint.y,
lastActivityTimestamp: (
// If our pointer moved only because we're following some other user, then don't
// update our last activity timestamp; otherwise, update it to the current timestamp.
(info.type === "pointer" && info.pointerId === INTERNAL_POINTER_IDS.CAMERA_MOVE ? this.store.unsafeGetWithoutCapture(TLPOINTER_ID)?.lastActivityTimestamp ?? this._tickManager.now : this._tickManager.now)
),
meta: {}
}
]);
},
{ history: "ignore" }
);
}
/**
* Dispatch a cancel event.
*
* @example
* ```ts
* editor.cancel()
* ```
*
* @public
*/
cancel() {
this.dispatch({ type: "misc", name: "cancel" });
return this;
}
/**
* Dispatch an interrupt event.
*
* @example
* ```ts
* editor.interrupt()
* ```
*
* @public
*/
interrupt() {
this.dispatch({ type: "misc", name: "interrupt" });
return this;
}
/**
* Dispatch a complete event.
*
* @example
* ```ts
* editor.complete()
* ```
*
* @public
*/
complete() {
this.dispatch({ type: "misc", name: "complete" });
return this;
}
/**
* Puts the editor into focused mode.
*
* This makes the editor eligible to receive keyboard events and some pointer events (move, wheel).
*
* @example
* ```ts
* editor.focus()
* ```
*
* By default this also dispatches a 'focus' event to the container element. To prevent this, pass `focusContainer: false`.
*
* @example
* ```ts
* editor.focus({ focusContainer: false })
* ```
*
* @public
*/
focus({ focusContainer = true } = {}) {
if (this.getIsFocused()) return this;
if (focusContainer) this.focusManager.focus();
this.updateInstanceState({ isFocused: true });
return this;
}
/**
* Switches off the editor's focused mode.
*
* This makes the editor ignore keyboard events and some pointer events (move, wheel).
*
* @example
* ```ts
* editor.blur()
* ```
* By default this also dispatches a 'blur' event to the container element. To prevent this, pass `blurContainer: false`.
*
* @example
* ```ts
* editor.blur({ blurContainer: false })
* ```
*
* @public
*/
blur({ blurContainer = true } = {}) {
if (!this.getIsFocused()) return this;
if (blurContainer) {
this.focusManager.blur();
} else {
this.complete();
}
this.updateInstanceState({ isFocused: false });
return this;
}
getIsFocused() {
return this.getInstanceState().isFocused;
}
getIsReadonly() {
return this.getInstanceState().isReadonly;
}
/**
* @public
* @returns a snapshot of the store's UI and document state
*/
getSnapshot() {
return getSnapshot(this.store);
}
/**
* Loads a snapshot into the editor.
* @param snapshot - The snapshot to load.
* @param opts - The options for loading the snapshot.
* @returns
*/
loadSnapshot(snapshot, opts) {
loadSnapshot(this.store, snapshot, opts);
return this;
}
_zoomToFitPageContentAt100Percent() {
const bounds = this.getCurrentPageBounds();
if (bounds) {
this.zoomToBounds(bounds, { immediate: true, targetZoom: this.getBaseZoom() });
}
}
_navigateToDeepLink(deepLink) {
this.run(() => {
switch (deepLink.type) {
case "page": {
const page = this.getPage(deepLink.pageId);
if (page) {
this.setCurrentPage(page);
}
this._zoomToFitPageContentAt100Percent();
return;
}
case "shapes": {
const allShapes = compact(deepLink.shapeIds.map((id) => this.getShape(id)));
const byPage = {};
for (const shape of allShapes) {
const pageId2 = this.getAncestorPageId(shape);
if (!pageId2) continue;
byPage[pageId2] ??= [];
byPage[pageId2].push(shape);
}
const [pageId, shapes] = Object.entries(byPage).sort(
([_, a], [__, b]) => b.length - a.length
)[0] ?? ["", []];
if (!pageId || !shapes.length) {
this._zoomToFitPageContentAt100Percent();
} else {
this.setCurrentPage(pageId);
const bounds = Box.Common(shapes.map((s) => this.getShapePageBounds(s)));
this.zoomToBounds(bounds, { immediate: true, targetZoom: this.getBaseZoom() });
}
return;
}
case "viewport": {
if (deepLink.pageId) {
if (!this.getPage(deepLink.pageId)) {
this._zoomToFitPageContentAt100Percent();
return;
}
this.setCurrentPage(deepLink.pageId);
}
this.zoomToBounds(deepLink.bounds, { immediate: true, inset: 0 });
return;
}
default:
exhaustiveSwitchError(deepLink);
}
});
}
/**
* Handles navigating to the content specified by the query param in the given URL.
*
* Use {@link Editor#createDeepLink} to create a URL with a deep link query param.
*
* If no URL is provided, it will look for the param in the current `window.location.href`.
*
* @example
* ```ts
* editor.navigateToDeepLink()
* ```
*
* The default parameter name is 'd'. You can override this by providing the `param` option.
*
* @example
* ```ts
* // disable page parameter and change viewport parameter to 'c'
* editor.navigateToDeepLink({
* param: 'x',
* url: 'https://my-app.com/my-document?x=200.12.454.23.xyz123',
* })
* ```
*
* @param opts - Options for loading the state from the URL.
*/
navigateToDeepLink(opts) {
if (opts && "type" in opts) {
this._navigateToDeepLink(opts);
return this;
}
const url = new URL(opts?.url ?? window.location.href);
const deepLinkString = url.searchParams.get(opts?.param ?? "d");
if (!deepLinkString) {
this._zoomToFitPageContentAt100Percent();
return this;
}
try {
this._navigateToDeepLink(parseDeepLinkString(deepLinkString));
} catch (e) {
console.warn(e);
this._zoomToFitPageContentAt100Percent();
}
return this;
}
/**
* Turns the given URL into a deep link by adding a query parameter.
*
* e.g. `https://my-app.com/my-document?d=100.100.200.200.xyz123`
*
* If no URL is provided, it will use the current `window.location.href`.
*
* @example
* ```ts
* // create a deep link to the current page + viewport
* navigator.clipboard.writeText(editor.createDeepLink())
* ```
*
* You can link to a particular set of shapes by providing a `to` parameter.
*
* @example
* ```ts
* // create a deep link to the set of currently selected shapes
* navigator.clipboard.writeText(editor.createDeepLink({
* to: { type: 'selection', shapeIds: editor.getSelectedShapeIds() }
* }))
* ```
*
* The default query param is 'd'. You can override this by providing a `param` parameter.
*
* @example
* ```ts
* // Use `x` as the param name instead
* editor.createDeepLink({ param: 'x' })
* ```
*
* @param opts - Options for adding the state to the URL.
* @returns the updated URL
*/
createDeepLink(opts) {
const url = new URL(opts?.url ?? window.location.href);
url.searchParams.set(
opts?.param ?? "d",
createDeepLinkString(
opts?.to ?? {
type: "viewport",
pageId: this.options.maxPages === 1 ? void 0 : this.getCurrentPageId(),
bounds: this.getViewportPageBounds()
}
)
);
return url;
}
/**
* Register a listener for changes to a deep link for the current document.
*
* You'll typically want to use this indirectly via the {@link TldrawEditorBaseProps.deepLinks} prop on the `` component.
*
* By default this will update `window.location` in place, but you can provide a custom callback
* to handle state changes on your own.
*
* @example
* ```ts
* editor.registerDeepLinkListener({
* onChange(url) {
* window.history.replaceState({}, document.title, url.toString())
* }
* })
* ```
*
* You can also provide a custom URL to update, in which case you must also provide `onChange`.
*
* @example
* ```ts
* editor.registerDeepLinkListener({
* getUrl: () => `https://my-app.com/my-document`,
* onChange(url) {
* setShareUrl(url.toString())
* }
* })
* ```
*
* By default this will update with a debounce interval of 500ms, but you can provide a custom interval.
*
* @example
* ```ts
* editor.registerDeepLinkListener({ debounceMs: 1000 })
* ```
* The default parameter name is `d`. You can override this by providing a `param` option.
*
* @example
* ```ts
* editor.registerDeepLinkListener({ param: 'x' })
* ```
* @param opts - Options for setting up the listener.
* @returns a function that will stop the listener.
*/
registerDeepLinkListener(opts) {
if (opts?.getUrl && !opts?.onChange) {
throw Error(
"[tldraw:urlStateSync] If you specify getUrl, you must also specify the onChange callback."
);
}
const url$ = computed("url with state", () => {
const url = opts?.getUrl?.(this) ?? window.location.href;
const urlWithState = this.createDeepLink({
param: opts?.param,
url,
to: opts?.getTarget?.(this)
});
return urlWithState.toString();
});
const announceChange = opts?.onChange ?? (() => {
const url = this.createDeepLink({
param: opts?.param,
to: opts?.getTarget?.(this)
});
window.history.replaceState({}, document.title, url.toString());
});
const scheduleEffect = debounce((execute) => execute(), opts?.debounceMs ?? 500);
const unlisten = react(
"update url on state change",
() => announceChange(new URL(url$.get()), this),
{ scheduleEffect }
);
return () => {
unlisten();
scheduleEffect.cancel();
};
}
/**
* Prevent a double click event from firing the next time the user clicks
*
* @public
*/
cancelDoubleClick() {
this._clickManager.cancelDoubleClickTimeout();
}
_setShiftKeyTimeout() {
this.inputs.shiftKey = false;
this.dispatch({
type: "keyboard",
name: "key_up",
key: "Shift",
shiftKey: this.inputs.shiftKey,
ctrlKey: this.inputs.ctrlKey,
altKey: this.inputs.altKey,
metaKey: this.inputs.metaKey,
accelKey: isAccelKey(this.inputs),
code: "ShiftLeft"
});
}
_setAltKeyTimeout() {
this.inputs.altKey = false;
this.dispatch({
type: "keyboard",
name: "key_up",
key: "Alt",
shiftKey: this.inputs.shiftKey,
ctrlKey: this.inputs.ctrlKey,
altKey: this.inputs.altKey,
metaKey: this.inputs.metaKey,
accelKey: isAccelKey(this.inputs),
code: "AltLeft"
});
}
_setCtrlKeyTimeout() {
this.inputs.ctrlKey = false;
this.dispatch({
type: "keyboard",
name: "key_up",
key: "Ctrl",
shiftKey: this.inputs.shiftKey,
ctrlKey: this.inputs.ctrlKey,
altKey: this.inputs.altKey,
metaKey: this.inputs.metaKey,
accelKey: isAccelKey(this.inputs),
code: "ControlLeft"
});
}
_setMetaKeyTimeout() {
this.inputs.metaKey = false;
this.dispatch({
type: "keyboard",
name: "key_up",
key: "Meta",
shiftKey: this.inputs.shiftKey,
ctrlKey: this.inputs.ctrlKey,
altKey: this.inputs.altKey,
metaKey: this.inputs.metaKey,
accelKey: isAccelKey(this.inputs),
code: "MetaLeft"
});
}
/**
* Dispatch an event to the editor.
*
* @example
* ```ts
* editor.dispatch(myPointerEvent)
* ```
*
* @param info - The event info.
*
* @public
*/
dispatch(info) {
this._pendingEventsForNextTick.push(info);
if (!(info.type === "pointer" && info.name === "pointer_move" || info.type === "wheel" || info.type === "pinch")) {
this._flushEventsForTick(0);
}
return this;
}
_flushEventsForTick(elapsed) {
this.run(() => {
if (this._pendingEventsForNextTick.length > 0) {
const events = [...this._pendingEventsForNextTick];
this._pendingEventsForNextTick.length = 0;
for (const info of events) {
this._flushEventForTick(info);
}
}
if (elapsed > 0) {
this.root.handleEvent({ type: "misc", name: "tick", elapsed });
}
this.scribbles.tick(elapsed);
});
}
_flushEventForTick(info) {
if (this.getCrashingError()) return this;
const { inputs } = this;
const { type } = info;
if (info.type === "misc") {
if (info.name === "cancel" || info.name === "complete") {
this.inputs.isDragging = false;
if (this.inputs.isPanning) {
this.inputs.isPanning = false;
this.inputs.isSpacebarPanning = false;
this.setCursor({ type: this._prevCursor, rotation: 0 });
}
}
this.root.handleEvent(info);
return;
}
if (info.shiftKey) {
clearTimeout(this._shiftKeyTimeout);
this._shiftKeyTimeout = -1;
inputs.shiftKey = true;
} else if (!info.shiftKey && inputs.shiftKey && this._shiftKeyTimeout === -1) {
this._shiftKeyTimeout = this.timers.setTimeout(this._setShiftKeyTimeout, 150);
}
if (info.altKey) {
clearTimeout(this._altKeyTimeout);
this._altKeyTimeout = -1;
inputs.altKey = true;
} else if (!info.altKey && inputs.altKey && this._altKeyTimeout === -1) {
this._altKeyTimeout = this.timers.setTimeout(this._setAltKeyTimeout, 150);
}
if (info.ctrlKey) {
clearTimeout(this._ctrlKeyTimeout);
this._ctrlKeyTimeout = -1;
inputs.ctrlKey = true;
} else if (!info.ctrlKey && inputs.ctrlKey && this._ctrlKeyTimeout === -1) {
this._ctrlKeyTimeout = this.timers.setTimeout(this._setCtrlKeyTimeout, 150);
}
if (info.metaKey) {
clearTimeout(this._metaKeyTimeout);
this._metaKeyTimeout = -1;
inputs.metaKey = true;
} else if (!info.metaKey && inputs.metaKey && this._metaKeyTimeout === -1) {
this._metaKeyTimeout = this.timers.setTimeout(this._setMetaKeyTimeout, 150);
}
const { originPagePoint, currentPagePoint } = inputs;
if (!inputs.isPointing) {
inputs.isDragging = false;
}
const instanceState = this.store.unsafeGetWithoutCapture(TLINSTANCE_ID);
const pageState = this.store.get(this._getCurrentPageStateId());
const cameraOptions = this._cameraOptions.__unsafe__getWithoutCapture();
switch (type) {
case "pinch": {
if (cameraOptions.isLocked) return;
clearTimeout(this._longPressTimeout);
this._updateInputsFromEvent(info);
switch (info.name) {
case "pinch_start": {
if (inputs.isPinching) return;
if (!inputs.isEditing) {
this._pinchStart = this.getCamera().z;
if (!this._selectedShapeIdsAtPointerDown.length) {
this._selectedShapeIdsAtPointerDown = [...pageState.selectedShapeIds];
}
this._didPinch = true;
inputs.isPinching = true;
this.interrupt();
}
return;
}
case "pinch": {
if (!inputs.isPinching) return;
const {
point: { z = 1 },
delta: { x: dx, y: dy }
} = info;
const { x, y } = Vec.SubXY(
info.point,
instanceState.screenBounds.x,
instanceState.screenBounds.y
);
this.stopCameraAnimation();
if (instanceState.followingUserId) {
this.stopFollowingUser();
}
const { x: cx, y: cy, z: cz } = unsafe__withoutCapture(() => this.getCamera());
const { panSpeed, zoomSpeed } = cameraOptions;
this._setCamera(
new Vec(
cx + dx * panSpeed / cz - x / cz + x / (z * zoomSpeed),
cy + dy * panSpeed / cz - y / cz + y / (z * zoomSpeed),
z * zoomSpeed
),
{ immediate: true }
);
return;
}
case "pinch_end": {
if (!inputs.isPinching) return this;
inputs.isPinching = false;
const { _selectedShapeIdsAtPointerDown: shapesToReselect } = this;
this.setSelectedShapes(this._selectedShapeIdsAtPointerDown);
this._selectedShapeIdsAtPointerDown = [];
if (this._didPinch) {
this._didPinch = false;
if (shapesToReselect.length > 0) {
this.once("tick", () => {
if (!this._didPinch) {
this.setSelectedShapes(shapesToReselect);
}
});
}
}
return;
}
}
}
case "wheel": {
if (cameraOptions.isLocked) return;
this._updateInputsFromEvent(info);
const { panSpeed, zoomSpeed, wheelBehavior } = cameraOptions;
if (wheelBehavior !== "none") {
this.stopCameraAnimation();
if (instanceState.followingUserId) {
this.stopFollowingUser();
}
const { x: cx, y: cy, z: cz } = unsafe__withoutCapture(() => this.getCamera());
const { x: dx, y: dy, z: dz = 0 } = info.delta;
let behavior = wheelBehavior;
if (inputs.ctrlKey) behavior = wheelBehavior === "pan" ? "zoom" : "pan";
switch (behavior) {
case "zoom": {
const { x, y } = this.inputs.currentScreenPoint;
let delta = dz;
if (wheelBehavior === "zoom") {
if (Math.abs(dy) > 10) {
delta = 10 * Math.sign(dy) / 100;
} else {
delta = dy / 100;
}
}
const zoom = cz + (delta ?? 0) * zoomSpeed * cz;
this._setCamera(
new Vec(
cx + (x / zoom - x) - (x / cz - x),
cy + (y / zoom - y) - (y / cz - y),
zoom
),
{ immediate: true }
);
this.maybeTrackPerformance("Zooming");
return;
}
case "pan": {
this._setCamera(new Vec(cx + dx * panSpeed / cz, cy + dy * panSpeed / cz, cz), {
immediate: true
});
this.maybeTrackPerformance("Panning");
return;
}
}
}
break;
}
case "pointer": {
if (inputs.isPinching) return;
this._updateInputsFromEvent(info);
const { isPen } = info;
const { isPenMode } = instanceState;
switch (info.name) {
case "pointer_down": {
if (isPenMode && !isPen) return;
if (!this.inputs.isPanning) {
this._longPressTimeout = this.timers.setTimeout(() => {
const vsb = this.getViewportScreenBounds();
this.dispatch({
...info,
// important! non-obvious!! the screenpoint was adjusted using the
// viewport bounds, and will be again when this event is handled...
// so we need to counter-adjust from the stored value so that the
// new value is set correctly.
point: this.inputs.originScreenPoint.clone().addXY(vsb.x, vsb.y),
name: "long_press"
});
}, this.options.longPressDurationMs);
}
this._selectedShapeIdsAtPointerDown = this.getSelectedShapeIds();
if (info.button === LEFT_MOUSE_BUTTON) this.capturedPointerId = info.pointerId;
inputs.buttons.add(info.button);
inputs.isPointing = true;
inputs.isDragging = false;
if (!isPenMode && isPen) this.updateInstanceState({ isPenMode: true });
if (info.button === STYLUS_ERASER_BUTTON) {
this._restoreToolId = this.getCurrentToolId();
this.complete();
this.setCurrentTool("eraser");
} else if (info.button === MIDDLE_MOUSE_BUTTON) {
if (!this.inputs.isPanning) {
this._prevCursor = this.getInstanceState().cursor.type;
}
this.inputs.isPanning = true;
clearTimeout(this._longPressTimeout);
}
if (this.inputs.isPanning) {
this.stopCameraAnimation();
this.setCursor({ type: "grabbing", rotation: 0 });
return this;
}
break;
}
case "pointer_move": {
if (!isPen && isPenMode) return;
const { x: cx, y: cy, z: cz } = unsafe__withoutCapture(() => this.getCamera());
if (this.inputs.isPanning && this.inputs.isPointing) {
const { currentScreenPoint, previousScreenPoint } = this.inputs;
const { panSpeed } = cameraOptions;
const offset = Vec.Sub(currentScreenPoint, previousScreenPoint);
this.setCamera(
new Vec(cx + offset.x * panSpeed / cz, cy + offset.y * panSpeed / cz, cz),
{ immediate: true }
);
this.maybeTrackPerformance("Panning");
return;
}
if (inputs.isPointing && !inputs.isDragging && Vec.Dist2(originPagePoint, currentPagePoint) * this.getZoomLevel() > (instanceState.isCoarsePointer ? this.options.coarseDragDistanceSquared : this.options.dragDistanceSquared) / cz) {
inputs.isDragging = true;
clearTimeout(this._longPressTimeout);
}
break;
}
case "pointer_up": {
inputs.isDragging = false;
inputs.isPointing = false;
clearTimeout(this._longPressTimeout);
inputs.buttons.delete(info.button);
if (instanceState.isPenMode && !isPen) return;
if (this.capturedPointerId === info.pointerId) {
this.capturedPointerId = null;
info.button = 0;
}
if (inputs.isPanning) {
if (!inputs.keys.has("Space")) {
inputs.isPanning = false;
inputs.isSpacebarPanning = false;
}
const slideDirection = this.inputs.pointerVelocity;
const slideSpeed = Math.min(2, slideDirection.len());
switch (info.button) {
case LEFT_MOUSE_BUTTON: {
this.setCursor({ type: "grab", rotation: 0 });
break;
}
case MIDDLE_MOUSE_BUTTON: {
if (this.inputs.keys.has(" ")) {
this.setCursor({ type: "grab", rotation: 0 });
} else {
this.setCursor({ type: this._prevCursor, rotation: 0 });
}
}
}
if (slideSpeed > 0) {
this.slideCamera({ speed: slideSpeed, direction: slideDirection });
}
} else {
if (info.button === STYLUS_ERASER_BUTTON) {
this.complete();
this.setCurrentTool(this._restoreToolId);
}
}
break;
}
}
break;
}
case "keyboard": {
if (info.key === "ShiftRight") info.key = "ShiftLeft";
if (info.key === "AltRight") info.key = "AltLeft";
if (info.code === "ControlRight") info.code = "ControlLeft";
if (info.code === "MetaRight") info.code = "MetaLeft";
switch (info.name) {
case "key_down": {
inputs.keys.add(info.code);
if (info.code === "Space" && !info.ctrlKey) {
if (!this.inputs.isPanning) {
this._prevCursor = instanceState.cursor.type;
}
this.inputs.isPanning = true;
this.inputs.isSpacebarPanning = true;
clearTimeout(this._longPressTimeout);
this.setCursor({ type: this.inputs.isPointing ? "grabbing" : "grab", rotation: 0 });
}
if (this.inputs.isSpacebarPanning) {
let offset;
switch (info.code) {
case "ArrowUp": {
offset = new Vec(0, -1);
break;
}
case "ArrowRight": {
offset = new Vec(1, 0);
break;
}
case "ArrowDown": {
offset = new Vec(0, 1);
break;
}
case "ArrowLeft": {
offset = new Vec(-1, 0);
break;
}
}
if (offset) {
const bounds = this.getViewportPageBounds();
const next = bounds.clone().translate(offset.mulV({ x: bounds.w, y: bounds.h }));
this._animateToViewport(next, { animation: { duration: 320 } });
}
}
break;
}
case "key_up": {
inputs.keys.delete(info.code);
if (info.code === "Space") {
if (this.inputs.buttons.has(MIDDLE_MOUSE_BUTTON)) ; else {
this.inputs.isPanning = false;
this.inputs.isSpacebarPanning = false;
this.setCursor({ type: this._prevCursor, rotation: 0 });
}
}
break;
}
}
break;
}
}
if (info.type === "pointer") {
if (info.button === MIDDLE_MOUSE_BUTTON) {
info.name = "middle_click";
} else if (info.button === RIGHT_MOUSE_BUTTON) {
info.name = "right_click";
}
const { isPenMode } = this.store.unsafeGetWithoutCapture(TLINSTANCE_ID);
if (info.isPen === isPenMode) {
const clickInfo = this._clickManager.handlePointerEvent(info);
if (info.name !== clickInfo.name) {
this.root.handleEvent(info);
this.emit("event", info);
this.root.handleEvent(clickInfo);
this.emit("event", clickInfo);
return;
}
}
}
this.root.handleEvent(info);
this.emit("event", info);
if (info.type === "pointer" && info.name === "pointer_down") {
this.menus.clearOpenMenus();
}
return this;
}
/** @internal */
maybeTrackPerformance(name) {
if (debugFlags.measurePerformance.get()) {
if (this.performanceTracker.isStarted()) {
clearTimeout(this.performanceTrackerTimeout);
} else {
this.performanceTracker.start(name);
}
this.performanceTrackerTimeout = this.timers.setTimeout(() => {
this.performanceTracker.stop();
}, 50);
}
}
}
_init$3 = __decoratorStart$3(_a$1);
__decorateElement$3(_init$3, 1, "getIsShapeHiddenCache", _getIsShapeHiddenCache_dec, Editor);
__decorateElement$3(_init$3, 1, "getCanUndo", _getCanUndo_dec, Editor);
__decorateElement$3(_init$3, 1, "getCanRedo", _getCanRedo_dec, Editor);
__decorateElement$3(_init$3, 1, "getPath", _getPath_dec, Editor);
__decorateElement$3(_init$3, 1, "getCurrentTool", _getCurrentTool_dec, Editor);
__decorateElement$3(_init$3, 1, "getCurrentToolId", _getCurrentToolId_dec, Editor);
__decorateElement$3(_init$3, 1, "getDocumentSettings", _getDocumentSettings_dec, Editor);
__decorateElement$3(_init$3, 1, "getInstanceState", _getInstanceState_dec, Editor);
__decorateElement$3(_init$3, 1, "getOpenMenus", _getOpenMenus_dec, Editor);
__decorateElement$3(_init$3, 1, "getIsMenuOpen", _getIsMenuOpen_dec, Editor);
__decorateElement$3(_init$3, 1, "getPageStates", _getPageStates_dec, Editor);
__decorateElement$3(_init$3, 1, "_getPageStatesQuery", __getPageStatesQuery_dec, Editor);
__decorateElement$3(_init$3, 1, "getCurrentPageState", _getCurrentPageState_dec, Editor);
__decorateElement$3(_init$3, 1, "_getCurrentPageStateId", __getCurrentPageStateId_dec, Editor);
__decorateElement$3(_init$3, 1, "getSelectedShapeIds", _getSelectedShapeIds_dec, Editor);
__decorateElement$3(_init$3, 1, "getSelectedShapes", _getSelectedShapes_dec, Editor);
__decorateElement$3(_init$3, 1, "getOnlySelectedShapeId", _getOnlySelectedShapeId_dec, Editor);
__decorateElement$3(_init$3, 1, "getOnlySelectedShape", _getOnlySelectedShape_dec, Editor);
__decorateElement$3(_init$3, 1, "getSelectionPageBounds", _getSelectionPageBounds_dec, Editor);
__decorateElement$3(_init$3, 1, "getSelectionRotation", _getSelectionRotation_dec, Editor);
__decorateElement$3(_init$3, 1, "getSelectionRotatedPageBounds", _getSelectionRotatedPageBounds_dec, Editor);
__decorateElement$3(_init$3, 1, "getSelectionRotatedScreenBounds", _getSelectionRotatedScreenBounds_dec, Editor);
__decorateElement$3(_init$3, 1, "getFocusedGroupId", _getFocusedGroupId_dec, Editor);
__decorateElement$3(_init$3, 1, "getFocusedGroup", _getFocusedGroup_dec, Editor);
__decorateElement$3(_init$3, 1, "getEditingShapeId", _getEditingShapeId_dec, Editor);
__decorateElement$3(_init$3, 1, "getEditingShape", _getEditingShape_dec, Editor);
__decorateElement$3(_init$3, 1, "getHoveredShapeId", _getHoveredShapeId_dec, Editor);
__decorateElement$3(_init$3, 1, "getHoveredShape", _getHoveredShape_dec, Editor);
__decorateElement$3(_init$3, 1, "getHintingShapeIds", _getHintingShapeIds_dec, Editor);
__decorateElement$3(_init$3, 1, "getHintingShape", _getHintingShape_dec, Editor);
__decorateElement$3(_init$3, 1, "getErasingShapeIds", _getErasingShapeIds_dec, Editor);
__decorateElement$3(_init$3, 1, "getErasingShapes", _getErasingShapes_dec, Editor);
__decorateElement$3(_init$3, 1, "_unsafe_getCameraId", __unsafe_getCameraId_dec, Editor);
__decorateElement$3(_init$3, 1, "getCamera", _getCamera_dec, Editor);
__decorateElement$3(_init$3, 1, "getViewportPageBoundsForFollowing", _getViewportPageBoundsForFollowing_dec, Editor);
__decorateElement$3(_init$3, 1, "getCameraForFollowing", _getCameraForFollowing_dec, Editor);
__decorateElement$3(_init$3, 1, "getZoomLevel", _getZoomLevel_dec, Editor);
__decorateElement$3(_init$3, 1, "getViewportScreenBounds", _getViewportScreenBounds_dec, Editor);
__decorateElement$3(_init$3, 1, "getViewportScreenCenter", _getViewportScreenCenter_dec, Editor);
__decorateElement$3(_init$3, 1, "getViewportPageBounds", _getViewportPageBounds_dec, Editor);
__decorateElement$3(_init$3, 1, "_getCollaboratorsQuery", __getCollaboratorsQuery_dec, Editor);
__decorateElement$3(_init$3, 1, "getCollaborators", _getCollaborators_dec, Editor);
__decorateElement$3(_init$3, 1, "getCollaboratorsOnCurrentPage", _getCollaboratorsOnCurrentPage_dec, Editor);
__decorateElement$3(_init$3, 1, "getRenderingShapes", _getRenderingShapes_dec, Editor);
__decorateElement$3(_init$3, 1, "_getAllPagesQuery", __getAllPagesQuery_dec, Editor);
__decorateElement$3(_init$3, 1, "getPages", _getPages_dec, Editor);
__decorateElement$3(_init$3, 1, "getCurrentPageId", _getCurrentPageId_dec, Editor);
__decorateElement$3(_init$3, 1, "getCurrentPageShapeIdsSorted", _getCurrentPageShapeIdsSorted_dec, Editor);
__decorateElement$3(_init$3, 1, "_getAllAssetsQuery", __getAllAssetsQuery_dec, Editor);
__decorateElement$3(_init$3, 1, "_getShapeGeometryCache", __getShapeGeometryCache_dec, Editor);
__decorateElement$3(_init$3, 1, "_getShapeHandlesCache", __getShapeHandlesCache_dec, Editor);
__decorateElement$3(_init$3, 1, "_getShapePageTransformCache", __getShapePageTransformCache_dec, Editor);
__decorateElement$3(_init$3, 1, "_getShapePageBoundsCache", __getShapePageBoundsCache_dec, Editor);
__decorateElement$3(_init$3, 1, "_getShapeClipPathCache", __getShapeClipPathCache_dec, Editor);
__decorateElement$3(_init$3, 1, "_getShapeMaskCache", __getShapeMaskCache_dec, Editor);
__decorateElement$3(_init$3, 1, "_getShapeMaskedPageBoundsCache", __getShapeMaskedPageBoundsCache_dec, Editor);
__decorateElement$3(_init$3, 1, "_notVisibleShapes", __notVisibleShapes_dec, Editor);
__decorateElement$3(_init$3, 1, "getCulledShapes", _getCulledShapes_dec, Editor);
__decorateElement$3(_init$3, 1, "getCurrentPageBounds", _getCurrentPageBounds_dec, Editor);
__decorateElement$3(_init$3, 1, "getCurrentPageShapes", _getCurrentPageShapes_dec, Editor);
__decorateElement$3(_init$3, 1, "getCurrentPageShapesSorted", _getCurrentPageShapesSorted_dec, Editor);
__decorateElement$3(_init$3, 1, "getCurrentPageRenderingShapesSorted", _getCurrentPageRenderingShapesSorted_dec, Editor);
__decorateElement$3(_init$3, 1, "_getBindingsIndexCache", __getBindingsIndexCache_dec, Editor);
__decorateElement$3(_init$3, 1, "_getSelectionSharedStyles", __getSelectionSharedStyles_dec, Editor);
__decorateElement$3(_init$3, 1, "getSharedStyles", _getSharedStyles_dec, Editor);
__decorateElement$3(_init$3, 1, "getSharedOpacity", _getSharedOpacity_dec, Editor);
__decorateElement$3(_init$3, 1, "getIsFocused", _getIsFocused_dec, Editor);
__decorateElement$3(_init$3, 1, "getIsReadonly", _getIsReadonly_dec, Editor);
__decorateElement$3(_init$3, 1, "_setShiftKeyTimeout", __setShiftKeyTimeout_dec, Editor);
__decorateElement$3(_init$3, 1, "_setAltKeyTimeout", __setAltKeyTimeout_dec, Editor);
__decorateElement$3(_init$3, 1, "_setCtrlKeyTimeout", __setCtrlKeyTimeout_dec, Editor);
__decorateElement$3(_init$3, 1, "_setMetaKeyTimeout", __setMetaKeyTimeout_dec, Editor);
__decoratorMetadata$3(_init$3, Editor);
function alertMaxShapes(editor, pageId = editor.getCurrentPageId()) {
const name = editor.getPage(pageId).name;
editor.emit("max-shapes", { name, pageId, count: editor.options.maxShapesPerPage });
}
function applyPartialToRecordWithProps(prev, partial) {
if (!partial) return prev;
let next = null;
const entries = Object.entries(partial);
for (let i = 0, n = entries.length; i < n; i++) {
const [k, v] = entries[i];
if (v === void 0) continue;
if (k === "id" || k === "type" || k === "typeName") continue;
if (v === prev[k]) continue;
if (!next) next = { ...prev };
if (k === "props" || k === "meta") {
next[k] = { ...prev[k] };
for (const [nextKey, nextValue] of Object.entries(v)) {
next[k][nextKey] = nextValue;
}
continue;
}
next[k] = v;
}
if (!next) return prev;
return next;
}
function pushShapeWithDescendants(editor, id, result) {
const shape = editor.getShape(id);
if (!shape) return;
result.push(shape);
const childIds = editor.getSortedChildIdsForParent(id);
for (let i = 0, n = childIds.length; i < n; i++) {
pushShapeWithDescendants(editor, childIds[i], result);
}
}
function withIsolatedShapes(editor, shapeIds, callback) {
let result;
editor.run(
() => {
const changes = editor.store.extractingChanges(() => {
const bindingsWithBoth = /* @__PURE__ */ new Set();
const bindingsToRemove = /* @__PURE__ */ new Set();
for (const shapeId of shapeIds) {
const shape = editor.getShape(shapeId);
if (!shape) continue;
for (const binding of editor.getBindingsInvolvingShape(shapeId)) {
const hasFrom = shapeIds.has(binding.fromId);
const hasTo = shapeIds.has(binding.toId);
if (hasFrom && hasTo) {
bindingsWithBoth.add(binding.id);
continue;
}
if (!hasFrom || !hasTo) {
bindingsToRemove.add(binding.id);
}
}
}
editor.deleteBindings([...bindingsToRemove], { isolateShapes: true });
try {
result = Result.ok(callback(bindingsWithBoth));
} catch (error) {
result = Result.err(error);
}
});
editor.store.applyDiff(reverseRecordsDiff(changes));
},
{ history: "ignore" }
);
if (result.ok) {
return result.value;
} else {
throw result.error;
}
}
function getCameraFitXFitY(editor, cameraOptions) {
if (!cameraOptions.constraints) throw Error("Should have constraints here");
const {
padding: { x: px, y: py }
} = cameraOptions.constraints;
const vsb = editor.getViewportScreenBounds();
const bounds = Box.From(cameraOptions.constraints.bounds);
const zx = (vsb.w - px * 2) / bounds.w;
const zy = (vsb.h - py * 2) / bounds.h;
return { zx, zy };
}
function useIsDarkMode() {
const editor = useEditor();
const exportContext = useSvgExportContext();
return useValue("isDarkMode", () => exportContext?.isDarkMode ?? editor.user.getIsDarkMode(), [
exportContext,
editor
]);
}
const CORNER_SVG = ``;
const EDGE_SVG = ``;
const ROTATE_CORNER_SVG = ``;
function getCursorCss(svg, r, tr, f, color, hotspotX = 16, hotspotY = 16) {
const a = (-tr - r) * (PI$1 / 180);
const s = Math.sin(a);
const c = Math.cos(a);
const dx = 1 * c - 1 * s;
const dy = 1 * s + 1 * c;
return `url("data:image/svg+xml,") ${hotspotX} ${hotspotY}, pointer`;
}
const STATIC_CURSORS = [
"default",
"pointer",
"cross",
"move",
"grab",
"grabbing",
"text",
"zoom-in",
"zoom-out"
];
const CURSORS = {
none: () => "none",
"ew-resize": (r, f, c) => getCursorCss(EDGE_SVG, r, 0, f, c),
"ns-resize": (r, f, c) => getCursorCss(EDGE_SVG, r, 90, f, c),
"nesw-resize": (r, f, c) => getCursorCss(CORNER_SVG, r, 0, f, c),
"nwse-resize": (r, f, c) => getCursorCss(CORNER_SVG, r, 90, f, c),
"nwse-rotate": (r, f, c) => getCursorCss(ROTATE_CORNER_SVG, r, 0, f, c),
"nesw-rotate": (r, f, c) => getCursorCss(ROTATE_CORNER_SVG, r, 90, f, c),
"senw-rotate": (r, f, c) => getCursorCss(ROTATE_CORNER_SVG, r, 180, f, c),
"swne-rotate": (r, f, c) => getCursorCss(ROTATE_CORNER_SVG, r, 270, f, c)
};
function getCursor(cursor, rotation = 0, color = "black") {
return CURSORS[cursor](radiansToDegrees(rotation), false, color);
}
function useCursor() {
const editor = useEditor();
const container = useContainer();
const isDarkMode = useIsDarkMode();
useQuickReactor(
"useCursor",
() => {
const { type, rotation } = editor.getInstanceState().cursor;
if (STATIC_CURSORS.includes(type)) {
container.style.setProperty("--tl-cursor", `var(--tl-cursor-${type})`);
return;
}
container.style.setProperty(
"--tl-cursor",
getCursor(type, rotation, isDarkMode ? "white" : "black")
);
},
[editor, container, isDarkMode]
);
}
function useDarkMode() {
const editor = useEditor();
const container = useContainer();
const isDarkMode = useIsDarkMode();
const forceSrgb = useValue(debugFlags.forceSrgb);
React.useEffect(() => {
if (isDarkMode) {
container.setAttribute("data-color-mode", "dark");
container.classList.remove("tl-theme__light");
container.classList.add("tl-theme__dark");
} else {
container.setAttribute("data-color-mode", "light");
container.classList.remove("tl-theme__dark");
container.classList.add("tl-theme__light");
}
if (forceSrgb) {
container.classList.add("tl-theme__force-sRGB");
} else {
container.classList.remove("tl-theme__force-sRGB");
}
}, [editor, container, forceSrgb, isDarkMode]);
}
function useForceUpdate() {
const [_, ss] = reactExports.useState(0);
reactExports.useEffect(() => ss((s) => s + 1), []);
}
const defaultAssetResolve = (asset) => asset.props.src;
const inlineBase64AssetStore = {
upload: (_, file) => FileHelpers.blobToDataUrl(file)
};
function createTLSchemaFromUtils(opts) {
if ("schema" in opts && opts.schema) return opts.schema;
return createTLSchema({
shapes: "shapeUtils" in opts && opts.shapeUtils ? utilsToMap(checkShapesAndAddCore(opts.shapeUtils)) : void 0,
bindings: "bindingUtils" in opts && opts.bindingUtils ? utilsToMap(checkBindings(opts.bindingUtils)) : void 0,
migrations: "migrations" in opts ? opts.migrations : void 0
});
}
function createTLStore({
initialData,
defaultName = "",
id,
assets = inlineBase64AssetStore,
onMount,
collaboration,
...rest
} = {}) {
const schema = createTLSchemaFromUtils(rest);
const store = new Store({
id,
schema,
initialData,
props: {
defaultName,
assets: {
upload: assets.upload,
resolve: assets.resolve ?? defaultAssetResolve
},
onMount: (editor) => {
assert(editor instanceof Editor);
onMount?.(editor);
},
collaboration
}
});
if (rest.snapshot) {
if (initialData) throw new Error("Cannot provide both initialData and snapshot");
loadSnapshot(store, rest.snapshot);
}
return store;
}
function utilsToMap(utils) {
return Object.fromEntries(
utils.map((s) => [
s.type,
{
props: s.props,
migrations: s.migrations
}
])
);
}
const instanceOfAny = (object, constructors) => constructors.some((c) => object instanceof c);
let idbProxyableTypes;
let cursorAdvanceMethods;
// This is a function to prevent it throwing up in node environments.
function getIdbProxyableTypes() {
return (idbProxyableTypes ||
(idbProxyableTypes = [
IDBDatabase,
IDBObjectStore,
IDBIndex,
IDBCursor,
IDBTransaction,
]));
}
// This is a function to prevent it throwing up in node environments.
function getCursorAdvanceMethods() {
return (cursorAdvanceMethods ||
(cursorAdvanceMethods = [
IDBCursor.prototype.advance,
IDBCursor.prototype.continue,
IDBCursor.prototype.continuePrimaryKey,
]));
}
const cursorRequestMap = new WeakMap();
const transactionDoneMap = new WeakMap();
const transactionStoreNamesMap = new WeakMap();
const transformCache = new WeakMap();
const reverseTransformCache = new WeakMap();
function promisifyRequest(request) {
const promise = new Promise((resolve, reject) => {
const unlisten = () => {
request.removeEventListener('success', success);
request.removeEventListener('error', error);
};
const success = () => {
resolve(wrap(request.result));
unlisten();
};
const error = () => {
reject(request.error);
unlisten();
};
request.addEventListener('success', success);
request.addEventListener('error', error);
});
promise
.then((value) => {
// Since cursoring reuses the IDBRequest (*sigh*), we cache it for later retrieval
// (see wrapFunction).
if (value instanceof IDBCursor) {
cursorRequestMap.set(value, request);
}
// Catching to avoid "Uncaught Promise exceptions"
})
.catch(() => { });
// This mapping exists in reverseTransformCache but doesn't doesn't exist in transformCache. This
// is because we create many promises from a single IDBRequest.
reverseTransformCache.set(promise, request);
return promise;
}
function cacheDonePromiseForTransaction(tx) {
// Early bail if we've already created a done promise for this transaction.
if (transactionDoneMap.has(tx))
return;
const done = new Promise((resolve, reject) => {
const unlisten = () => {
tx.removeEventListener('complete', complete);
tx.removeEventListener('error', error);
tx.removeEventListener('abort', error);
};
const complete = () => {
resolve();
unlisten();
};
const error = () => {
reject(tx.error || new DOMException('AbortError', 'AbortError'));
unlisten();
};
tx.addEventListener('complete', complete);
tx.addEventListener('error', error);
tx.addEventListener('abort', error);
});
// Cache it for later retrieval.
transactionDoneMap.set(tx, done);
}
let idbProxyTraps = {
get(target, prop, receiver) {
if (target instanceof IDBTransaction) {
// Special handling for transaction.done.
if (prop === 'done')
return transactionDoneMap.get(target);
// Polyfill for objectStoreNames because of Edge.
if (prop === 'objectStoreNames') {
return target.objectStoreNames || transactionStoreNamesMap.get(target);
}
// Make tx.store return the only store in the transaction, or undefined if there are many.
if (prop === 'store') {
return receiver.objectStoreNames[1]
? undefined
: receiver.objectStore(receiver.objectStoreNames[0]);
}
}
// Else transform whatever we get back.
return wrap(target[prop]);
},
set(target, prop, value) {
target[prop] = value;
return true;
},
has(target, prop) {
if (target instanceof IDBTransaction &&
(prop === 'done' || prop === 'store')) {
return true;
}
return prop in target;
},
};
function replaceTraps(callback) {
idbProxyTraps = callback(idbProxyTraps);
}
function wrapFunction(func) {
// Due to expected object equality (which is enforced by the caching in `wrap`), we
// only create one new func per func.
// Edge doesn't support objectStoreNames (booo), so we polyfill it here.
if (func === IDBDatabase.prototype.transaction &&
!('objectStoreNames' in IDBTransaction.prototype)) {
return function (storeNames, ...args) {
const tx = func.call(unwrap(this), storeNames, ...args);
transactionStoreNamesMap.set(tx, storeNames.sort ? storeNames.sort() : [storeNames]);
return wrap(tx);
};
}
// Cursor methods are special, as the behaviour is a little more different to standard IDB. In
// IDB, you advance the cursor and wait for a new 'success' on the IDBRequest that gave you the
// cursor. It's kinda like a promise that can resolve with many values. That doesn't make sense
// with real promises, so each advance methods returns a new promise for the cursor object, or
// undefined if the end of the cursor has been reached.
if (getCursorAdvanceMethods().includes(func)) {
return function (...args) {
// Calling the original function with the proxy as 'this' causes ILLEGAL INVOCATION, so we use
// the original object.
func.apply(unwrap(this), args);
return wrap(cursorRequestMap.get(this));
};
}
return function (...args) {
// Calling the original function with the proxy as 'this' causes ILLEGAL INVOCATION, so we use
// the original object.
return wrap(func.apply(unwrap(this), args));
};
}
function transformCachableValue(value) {
if (typeof value === 'function')
return wrapFunction(value);
// This doesn't return, it just creates a 'done' promise for the transaction,
// which is later returned for transaction.done (see idbObjectHandler).
if (value instanceof IDBTransaction)
cacheDonePromiseForTransaction(value);
if (instanceOfAny(value, getIdbProxyableTypes()))
return new Proxy(value, idbProxyTraps);
// Return the same value back if we're not going to transform it.
return value;
}
function wrap(value) {
// We sometimes generate multiple promises from a single IDBRequest (eg when cursoring), because
// IDB is weird and a single IDBRequest can yield many responses, so these can't be cached.
if (value instanceof IDBRequest)
return promisifyRequest(value);
// If we've already transformed this value before, reuse the transformed value.
// This is faster, but it also provides object equality.
if (transformCache.has(value))
return transformCache.get(value);
const newValue = transformCachableValue(value);
// Not all types are transformed.
// These may be primitive types, so they can't be WeakMap keys.
if (newValue !== value) {
transformCache.set(value, newValue);
reverseTransformCache.set(newValue, value);
}
return newValue;
}
const unwrap = (value) => reverseTransformCache.get(value);
/**
* Open a database.
*
* @param name Name of the database.
* @param version Schema version.
* @param callbacks Additional callbacks.
*/
function openDB(name, version, { blocked, upgrade, blocking, terminated } = {}) {
const request = indexedDB.open(name, version);
const openPromise = wrap(request);
if (upgrade) {
request.addEventListener('upgradeneeded', (event) => {
upgrade(wrap(request.result), event.oldVersion, event.newVersion, wrap(request.transaction), event);
});
}
if (blocked) {
request.addEventListener('blocked', (event) => blocked(
// Casting due to https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/1405
event.oldVersion, event.newVersion, event));
}
openPromise
.then((db) => {
if (terminated)
db.addEventListener('close', () => terminated());
if (blocking) {
db.addEventListener('versionchange', (event) => blocking(event.oldVersion, event.newVersion, event));
}
})
.catch(() => { });
return openPromise;
}
/**
* Delete a database.
*
* @param name Name of the database.
*/
function deleteDB(name, { blocked } = {}) {
const request = indexedDB.deleteDatabase(name);
if (blocked) {
request.addEventListener('blocked', (event) => blocked(
// Casting due to https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/1405
event.oldVersion, event));
}
return wrap(request).then(() => undefined);
}
const readMethods = ['get', 'getKey', 'getAll', 'getAllKeys', 'count'];
const writeMethods = ['put', 'add', 'delete', 'clear'];
const cachedMethods = new Map();
function getMethod(target, prop) {
if (!(target instanceof IDBDatabase &&
!(prop in target) &&
typeof prop === 'string')) {
return;
}
if (cachedMethods.get(prop))
return cachedMethods.get(prop);
const targetFuncName = prop.replace(/FromIndex$/, '');
const useIndex = prop !== targetFuncName;
const isWrite = writeMethods.includes(targetFuncName);
if (
// Bail if the target doesn't exist on the target. Eg, getAll isn't in Edge.
!(targetFuncName in (useIndex ? IDBIndex : IDBObjectStore).prototype) ||
!(isWrite || readMethods.includes(targetFuncName))) {
return;
}
const method = async function (storeName, ...args) {
// isWrite ? 'readwrite' : undefined gzipps better, but fails in Edge :(
const tx = this.transaction(storeName, isWrite ? 'readwrite' : 'readonly');
let target = tx.store;
if (useIndex)
target = target.index(args.shift());
// Must reject if op rejects.
// If it's a write operation, must reject if tx.done rejects.
// Must reject with op rejection first.
// Must resolve with op value.
// Must handle both promises (no unhandled rejections)
return (await Promise.all([
target[targetFuncName](...args),
isWrite && tx.done,
]))[0];
};
cachedMethods.set(prop, method);
return method;
}
replaceTraps((oldTraps) => ({
...oldTraps,
get: (target, prop, receiver) => getMethod(target, prop) || oldTraps.get(target, prop, receiver),
has: (target, prop) => !!getMethod(target, prop) || oldTraps.has(target, prop),
}));
const STORE_PREFIX = "TLDRAW_DOCUMENT_v2";
const LEGACY_ASSET_STORE_PREFIX = "TLDRAW_ASSET_STORE_v1";
const dbNameIndexKey = "TLDRAW_DB_NAME_INDEX_v2";
const Table = {
Records: "records",
Schema: "schema",
SessionState: "session_state",
Assets: "assets"
};
async function openLocalDb(persistenceKey) {
const storeId = STORE_PREFIX + persistenceKey;
addDbName(storeId);
return await openDB(storeId, 4, {
upgrade(database) {
if (!database.objectStoreNames.contains(Table.Records)) {
database.createObjectStore(Table.Records);
}
if (!database.objectStoreNames.contains(Table.Schema)) {
database.createObjectStore(Table.Schema);
}
if (!database.objectStoreNames.contains(Table.SessionState)) {
database.createObjectStore(Table.SessionState);
}
if (!database.objectStoreNames.contains(Table.Assets)) {
database.createObjectStore(Table.Assets);
}
}
});
}
async function migrateLegacyAssetDbIfNeeded(persistenceKey) {
const databases = window.indexedDB.databases ? (await window.indexedDB.databases()).map((db) => db.name) : getAllIndexDbNames();
const oldStoreId = LEGACY_ASSET_STORE_PREFIX + persistenceKey;
const existing = databases.find((dbName) => dbName === oldStoreId);
if (!existing) return;
const oldAssetDb = await openDB(oldStoreId, 1, {
upgrade(database) {
if (!database.objectStoreNames.contains("assets")) {
database.createObjectStore("assets");
}
}
});
if (!oldAssetDb.objectStoreNames.contains("assets")) return;
const oldTx = oldAssetDb.transaction(["assets"], "readonly");
const oldAssetStore = oldTx.objectStore("assets");
const oldAssetsKeys = await oldAssetStore.getAllKeys();
const oldAssets = await Promise.all(
oldAssetsKeys.map(async (key) => [key, await oldAssetStore.get(key)])
);
await oldTx.done;
const newDb = await openLocalDb(persistenceKey);
const newTx = newDb.transaction([Table.Assets], "readwrite");
const newAssetTable = newTx.objectStore(Table.Assets);
for (const [key, value] of oldAssets) {
newAssetTable.put(value, key);
}
await newTx.done;
oldAssetDb.close();
newDb.close();
await deleteDB(oldStoreId);
}
class LocalIndexedDb {
getDbPromise;
isClosed = false;
pendingTransactionSet = /* @__PURE__ */ new Set();
/** @internal */
static connectedInstances = /* @__PURE__ */ new Set();
constructor(persistenceKey) {
LocalIndexedDb.connectedInstances.add(this);
this.getDbPromise = (async () => {
await migrateLegacyAssetDbIfNeeded(persistenceKey);
return await openLocalDb(persistenceKey);
})();
}
getDb() {
return this.getDbPromise;
}
/**
* Wait for any pending transactions to be completed. Useful for tests.
*
* @internal
*/
pending() {
return Promise.allSettled([this.getDbPromise, ...this.pendingTransactionSet]).then(noop$2);
}
async close() {
if (this.isClosed) return;
this.isClosed = true;
await this.pending();
(await this.getDb()).close();
LocalIndexedDb.connectedInstances.delete(this);
}
tx(mode, names, cb) {
const txPromise = (async () => {
assert(!this.isClosed, "db is closed");
const db = await this.getDb();
const tx = db.transaction(names, mode);
const done = tx.done.catch((e) => {
if (!this.isClosed) {
throw e;
}
});
try {
return await cb(tx);
} finally {
if (!this.isClosed) {
await done;
} else {
tx.abort();
}
}
})();
this.pendingTransactionSet.add(txPromise);
txPromise.finally(() => this.pendingTransactionSet.delete(txPromise));
return txPromise;
}
async load({ sessionId } = {}) {
return await this.tx(
"readonly",
[Table.Records, Table.Schema, Table.SessionState],
async (tx) => {
const recordsStore = tx.objectStore(Table.Records);
const schemaStore = tx.objectStore(Table.Schema);
const sessionStateStore = tx.objectStore(Table.SessionState);
let sessionStateSnapshot = sessionId ? (await sessionStateStore.get(sessionId))?.snapshot : null;
if (!sessionStateSnapshot) {
const all = await sessionStateStore.getAll();
sessionStateSnapshot = all.sort((a, b) => a.updatedAt - b.updatedAt).pop()?.snapshot;
}
const result = {
records: await recordsStore.getAll(),
schema: await schemaStore.get(Table.Schema),
sessionStateSnapshot
};
return result;
}
);
}
async storeChanges({
schema,
changes,
sessionId,
sessionStateSnapshot
}) {
await this.tx("readwrite", [Table.Records, Table.Schema, Table.SessionState], async (tx) => {
const recordsStore = tx.objectStore(Table.Records);
const schemaStore = tx.objectStore(Table.Schema);
const sessionStateStore = tx.objectStore(Table.SessionState);
for (const [id, record] of Object.entries(changes.added)) {
await recordsStore.put(record, id);
}
for (const [_prev, updated] of Object.values(changes.updated)) {
await recordsStore.put(updated, updated.id);
}
for (const id of Object.keys(changes.removed)) {
await recordsStore.delete(id);
}
schemaStore.put(schema.serialize(), Table.Schema);
if (sessionStateSnapshot && sessionId) {
sessionStateStore.put(
{
snapshot: sessionStateSnapshot,
updatedAt: Date.now(),
id: sessionId
},
sessionId
);
} else if (sessionStateSnapshot || sessionId) {
console.error("sessionStateSnapshot and instanceId must be provided together");
}
});
}
async storeSnapshot({
schema,
snapshot,
sessionId,
sessionStateSnapshot
}) {
await this.tx("readwrite", [Table.Records, Table.Schema, Table.SessionState], async (tx) => {
const recordsStore = tx.objectStore(Table.Records);
const schemaStore = tx.objectStore(Table.Schema);
const sessionStateStore = tx.objectStore(Table.SessionState);
await recordsStore.clear();
for (const [id, record] of Object.entries(snapshot)) {
await recordsStore.put(record, id);
}
schemaStore.put(schema.serialize(), Table.Schema);
if (sessionStateSnapshot && sessionId) {
sessionStateStore.put(
{
snapshot: sessionStateSnapshot,
updatedAt: Date.now(),
id: sessionId
},
sessionId
);
} else if (sessionStateSnapshot || sessionId) {
console.error("sessionStateSnapshot and instanceId must be provided together");
}
});
}
async pruneSessions() {
await this.tx("readwrite", [Table.SessionState], async (tx) => {
const sessionStateStore = tx.objectStore(Table.SessionState);
const all = (await sessionStateStore.getAll()).sort((a, b) => a.updatedAt - b.updatedAt);
if (all.length < 10) {
await tx.done;
return;
}
const toDelete = all.slice(0, all.length - 10);
for (const { id } of toDelete) {
await sessionStateStore.delete(id);
}
});
}
async getAsset(assetId) {
return await this.tx("readonly", [Table.Assets], async (tx) => {
const assetsStore = tx.objectStore(Table.Assets);
return await assetsStore.get(assetId);
});
}
async storeAsset(assetId, blob) {
await this.tx("readwrite", [Table.Assets], async (tx) => {
const assetsStore = tx.objectStore(Table.Assets);
await assetsStore.put(blob, assetId);
});
}
}
function getAllIndexDbNames() {
const result = JSON.parse(getFromLocalStorage(dbNameIndexKey) || "[]") ?? [];
if (!Array.isArray(result)) {
return [];
}
return result;
}
function addDbName(name) {
const all = new Set(getAllIndexDbNames());
all.add(name);
setInLocalStorage(dbNameIndexKey, JSON.stringify([...all]));
}
function showCantWriteToIndexDbAlert() {
window.alert(
`Oops! We could not save changes to your browser's storage. We now need to reload the page and try again.
Keep seeing this message?
\u2022 If you're using tldraw in a private or "incognito" window, try loading tldraw in a regular window or in a different browser.
\u2022 If your hard disk is full, try clearing up some space and then reload the page.`
);
}
function showCantReadFromIndexDbAlert() {
window.alert(
`Oops! We could not access your browser's storage\u2014and the app won't work correctly without that. We now need to reload the page and try again.
Keep seeing this message?
\u2022 If you're using tldraw in a private or "incognito" window, try loading tldraw in a regular window or in a different browser.`
);
}
const PERSIST_THROTTLE_MS = 350;
const PERSIST_RETRY_THROTTLE_MS = 1e4;
const UPDATE_INSTANCE_STATE = Symbol("UPDATE_INSTANCE_STATE");
const msg = (msg2) => msg2;
class BroadcastChannelMock {
onmessage;
constructor(_name) {
}
postMessage(_msg) {
}
close() {
}
}
const BC = typeof BroadcastChannel === "undefined" ? BroadcastChannelMock : BroadcastChannel;
class TLLocalSyncClient {
constructor(store, {
persistenceKey,
sessionId = TAB_ID,
onLoad,
onLoadError
}, channel = new BC(`tldraw-tab-sync-${persistenceKey}`)) {
this.store = store;
this.channel = channel;
if (typeof window !== "undefined") {
window.tlsync = this;
}
this.persistenceKey = persistenceKey;
this.sessionId = sessionId;
this.db = new LocalIndexedDb(persistenceKey);
this.disposables.add(() => this.db.close());
this.serializedSchema = this.store.schema.serialize();
this.$sessionStateSnapshot = createSessionStateSnapshotSignal(this.store);
this.disposables.add(
// Set up a subscription to changes from the store: When
// the store changes (and if the change was made by the user)
// then immediately send the diff to other tabs via postMessage
// and schedule a persist.
store.listen(
({ changes }) => {
this.diffQueue.push(changes);
this.channel.postMessage(
msg({
type: "diff",
storeId: this.store.id,
changes,
schema: this.serializedSchema
})
);
this.schedulePersist();
},
{ source: "user", scope: "document" }
)
);
this.disposables.add(
store.listen(
() => {
this.diffQueue.push(UPDATE_INSTANCE_STATE);
this.schedulePersist();
},
{ scope: "session" }
)
);
this.connect(onLoad, onLoadError);
this.documentTypes = new Set(
Object.values(this.store.schema.types).filter((t) => t.scope === "document").map((t) => t.typeName)
);
}
disposables = /* @__PURE__ */ new Set();
diffQueue = [];
didDispose = false;
shouldDoFullDBWrite = true;
isReloading = false;
persistenceKey;
sessionId;
serializedSchema;
isDebugging = false;
documentTypes;
$sessionStateSnapshot;
/** @internal */
db;
initTime = Date.now();
debug(...args) {
if (this.isDebugging) {
console.debug(...args);
}
}
async connect(onLoad, onLoadError) {
this.debug("connecting");
let data;
try {
data = await this.db.load({ sessionId: this.sessionId });
} catch (error) {
onLoadError(error);
showCantReadFromIndexDbAlert();
return;
}
this.debug("loaded data from store", data, "didDispose", this.didDispose);
if (this.didDispose) return;
try {
if (data) {
const documentSnapshot = Object.fromEntries(data.records.map((r) => [r.id, r]));
const sessionStateSnapshot = data.sessionStateSnapshot ?? extractSessionStateFromLegacySnapshot(documentSnapshot);
const migrationResult = this.store.schema.migrateStoreSnapshot({
store: documentSnapshot,
// eslint-disable-next-line @typescript-eslint/no-deprecated
schema: data.schema ?? this.store.schema.serializeEarliestVersion()
});
if (migrationResult.type === "error") {
console.error("failed to migrate store", migrationResult);
onLoadError(new Error(`Failed to migrate store: ${migrationResult.reason}`));
return;
}
const records = Object.values(migrationResult.value).filter(
(r) => this.documentTypes.has(r.typeName)
);
if (records.length > 0) {
this.store.mergeRemoteChanges(() => {
this.store.put(records, "initialize");
});
}
if (sessionStateSnapshot) {
loadSessionStateSnapshotIntoStore(this.store, sessionStateSnapshot);
}
}
this.channel.onmessage = ({ data: data2 }) => {
this.debug("got message", data2);
const msg2 = data2;
const res = this.store.schema.getMigrationsSince(msg2.schema);
if (!res.ok) {
const timeSinceInit = Date.now() - this.initTime;
if (timeSinceInit < 5e3) {
onLoadError(new Error("Schema mismatch, please close other tabs and reload the page"));
return;
}
this.debug("reloading");
this.isReloading = true;
window?.location?.reload?.();
return;
} else if (res.value.length > 0) {
this.debug("telling them to reload");
this.channel.postMessage({ type: "announce", schema: this.serializedSchema });
this.shouldDoFullDBWrite = true;
this.persistIfNeeded();
return;
}
if (msg2.type === "diff") {
this.debug("applying diff");
transact(() => {
this.store.mergeRemoteChanges(() => {
this.store.applyDiff(msg2.changes);
});
});
}
};
this.channel.postMessage({ type: "announce", schema: this.serializedSchema });
this.disposables.add(() => {
this.channel.close();
});
onLoad(this);
} catch (e) {
this.debug("error loading data from store", e);
if (this.didDispose) return;
onLoadError(e);
return;
}
}
close() {
this.debug("closing");
this.didDispose = true;
this.disposables.forEach((d) => d());
}
isPersisting = false;
didLastWriteError = false;
// eslint-disable-next-line no-restricted-globals
scheduledPersistTimeout = null;
/**
* Schedule a persist. Persists don't happen immediately: they are throttled to avoid writing too
* often, and will retry if failed.
*
* @internal
*/
schedulePersist() {
this.debug("schedulePersist", this.scheduledPersistTimeout);
if (this.scheduledPersistTimeout) return;
this.scheduledPersistTimeout = setTimeout(
() => {
this.scheduledPersistTimeout = null;
this.persistIfNeeded();
},
this.didLastWriteError ? PERSIST_RETRY_THROTTLE_MS : PERSIST_THROTTLE_MS
);
}
/**
* Persist to IndexedDB only under certain circumstances:
*
* - If we're not already persisting
* - If we're not reloading the page
* - And we have something to persist (a full db write scheduled or changes in the diff queue)
*
* @internal
*/
persistIfNeeded() {
this.debug("persistIfNeeded", {
isPersisting: this.isPersisting,
isReloading: this.isReloading,
shouldDoFullDBWrite: this.shouldDoFullDBWrite,
diffQueueLength: this.diffQueue.length,
storeIsPossiblyCorrupt: this.store.isPossiblyCorrupted()
});
if (this.scheduledPersistTimeout) {
clearTimeout(this.scheduledPersistTimeout);
this.scheduledPersistTimeout = null;
}
if (this.isPersisting) return;
if (this.isReloading) return;
if (this.store.isPossiblyCorrupted()) return;
if (this.shouldDoFullDBWrite || this.diffQueue.length > 0) {
this.doPersist();
}
}
/**
* Actually persist to IndexedDB. If the write fails, then we'll retry with a full db write after
* a short delay.
*/
async doPersist() {
assert(!this.isPersisting, "persist already in progress");
if (this.didDispose) return;
this.isPersisting = true;
this.debug("doPersist start");
const diffQueue = this.diffQueue;
this.diffQueue = [];
try {
if (this.shouldDoFullDBWrite) {
this.shouldDoFullDBWrite = false;
await this.db.storeSnapshot({
schema: this.store.schema,
snapshot: this.store.serialize(),
sessionId: this.sessionId,
sessionStateSnapshot: this.$sessionStateSnapshot.get()
});
} else {
const diffs = squashRecordDiffs(
diffQueue.filter((d) => d !== UPDATE_INSTANCE_STATE)
);
await this.db.storeChanges({
changes: diffs,
schema: this.store.schema,
sessionId: this.sessionId,
sessionStateSnapshot: this.$sessionStateSnapshot.get()
});
}
this.didLastWriteError = false;
} catch (e) {
this.shouldDoFullDBWrite = true;
this.didLastWriteError = true;
console.error("failed to store changes in indexed db", e);
showCantWriteToIndexDbAlert();
if (typeof window !== "undefined") {
window.location.reload();
}
}
this.isPersisting = false;
this.debug("doPersist end");
this.schedulePersist();
}
}
function useRefState(initialValue) {
const ref = reactExports.useRef(initialValue);
const [state, setState] = reactExports.useState(initialValue);
if (state !== ref.current) {
setState(ref.current);
}
const update = reactExports.useCallback((value) => {
if (typeof value === "function") {
ref.current = value(ref.current);
} else {
ref.current = value;
}
setState(ref.current);
}, []);
return [state, update];
}
function useLocalStore(options) {
const [state, setState] = useRefState({ status: "loading" });
options = useShallowObjectIdentity(options);
reactExports.useEffect(() => {
const { persistenceKey, sessionId, ...rest } = options;
if (!persistenceKey) {
setState({
status: "not-synced",
store: createTLStore(rest)
});
return;
}
setState({ status: "loading" });
const objectURLCache = new WeakCache();
const assets = {
upload: async (asset, file) => {
await client.db.storeAsset(asset.id, file);
return asset.id;
},
resolve: async (asset) => {
if (!asset.props.src) return null;
if (asset.props.src.startsWith("asset:")) {
return await objectURLCache.get(asset, async () => {
const blob = await client.db.getAsset(asset.id);
if (!blob) return null;
return URL.createObjectURL(blob);
});
}
return asset.props.src;
},
...rest.assets
};
const store = createTLStore({ ...rest, assets });
let isClosed = false;
const client = new TLLocalSyncClient(store, {
sessionId,
persistenceKey,
onLoad() {
if (isClosed) return;
setState({ store, status: "synced-local" });
},
onLoadError(err) {
if (isClosed) return;
setState({ status: "error", error: err });
}
});
return () => {
isClosed = true;
client.close();
};
}, [options, setState]);
return state;
}
function useZoomCss() {
const editor = useEditor();
const container = useContainer();
reactExports.useEffect(() => {
const setScale = (s) => container.style.setProperty("--tl-zoom", s.toString());
const setScaleDebounced = debounce(setScale, 100);
const scheduler = new EffectScheduler("useZoomCss", () => {
const numShapes = editor.getCurrentPageShapeIds().size;
if (numShapes < 300) {
setScale(editor.getZoomLevel());
} else {
setScaleDebounced(editor.getZoomLevel());
}
});
scheduler.attach();
scheduler.execute();
return () => {
scheduler.detach();
setScaleDebounced.cancel();
};
}, [editor, container]);
}
function str2ab(str) {
const buf = new ArrayBuffer(str.length);
const bufView = new Uint8Array(buf);
for (let i = 0, strLen = str.length; i < strLen; i++) {
bufView[i] = str.charCodeAt(i);
}
return buf;
}
function importPublicKey(pemContents) {
const binaryDerString = atob(pemContents);
const binaryDer = str2ab(binaryDerString);
return crypto.subtle.importKey(
"spki",
new Uint8Array(binaryDer),
{
name: "ECDSA",
namedCurve: "P-256"
},
true,
["verify"]
);
}
const GRACE_PERIOD_DAYS = 5;
const FLAGS = {
ANNUAL_LICENSE: 1,
PERPETUAL_LICENSE: 2,
INTERNAL_LICENSE: 4,
WITH_WATERMARK: 8
};
const HIGHEST_FLAG = Math.max(...Object.values(FLAGS));
const PROPERTIES = {
ID: 0,
HOSTS: 1,
FLAGS: 2,
EXPIRY_DATE: 3
};
const NUMBER_OF_KNOWN_PROPERTIES = Object.keys(PROPERTIES).length;
const LICENSE_EMAIL = "sales@tldraw.com";
const WATERMARK_TRACK_SRC = `${getDefaultCdnBaseUrl()}/watermarks/watermark-track.svg`;
class LicenseManager {
publicKey = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEHJh0uUfxHtCGyerXmmatE368Hd9rI6LH9oPDQihnaCryRFWEVeOvf9U/SPbyxX74LFyJs5tYeAHq5Nc0Ax25LQ";
isDevelopment;
isTest;
isCryptoAvailable;
state = atom(
"license state",
"pending"
);
verbose = true;
constructor(licenseKey, testPublicKey, testEnvironment) {
this.isTest = false;
this.isDevelopment = this.getIsDevelopment(testEnvironment);
this.publicKey = testPublicKey || this.publicKey;
this.isCryptoAvailable = !!crypto.subtle;
this.getLicenseFromKey(licenseKey).then((result) => {
const isUnlicensed = isEditorUnlicensed(result);
if (!this.isDevelopment && isUnlicensed) {
fetch(WATERMARK_TRACK_SRC);
}
if (isUnlicensed) {
this.state.set("unlicensed");
} else if (result.isLicensedWithWatermark) {
this.state.set("licensed-with-watermark");
} else {
this.state.set("licensed");
}
});
}
getIsDevelopment(testEnvironment) {
if (testEnvironment === "development") return true;
if (testEnvironment === "production") return false;
return window.location.protocol !== "https:";
}
async extractLicenseKey(licenseKey) {
const [data, signature] = licenseKey.split(".");
const [prefix, encodedData] = data.split("/");
if (!prefix.startsWith("tldraw-")) {
throw new Error(`Unsupported prefix '${prefix}'`);
}
const publicCryptoKey = await importPublicKey(this.publicKey);
let isVerified;
try {
isVerified = await crypto.subtle.verify(
{
name: "ECDSA",
hash: { name: "SHA-256" }
},
publicCryptoKey,
new Uint8Array(str2ab(atob(signature))),
new Uint8Array(str2ab(atob(encodedData)))
);
} catch (e) {
console.error(e);
throw new Error("Could not perform signature validation");
}
if (!isVerified) {
throw new Error("Invalid signature");
}
let decodedData;
try {
decodedData = JSON.parse(atob(encodedData));
} catch {
throw new Error("Could not parse object");
}
if (decodedData.length > NUMBER_OF_KNOWN_PROPERTIES) {
this.outputMessages([
"License key contains some unknown properties.",
"You may want to update tldraw packages to a newer version to get access to new functionality."
]);
}
return {
id: decodedData[PROPERTIES.ID],
hosts: decodedData[PROPERTIES.HOSTS],
flags: decodedData[PROPERTIES.FLAGS],
expiryDate: decodedData[PROPERTIES.EXPIRY_DATE]
};
}
async getLicenseFromKey(licenseKey) {
if (!licenseKey) {
if (!this.isDevelopment) {
this.outputNoLicenseKeyProvided();
}
return { isLicenseParseable: false, reason: "no-key-provided" };
}
if (this.isDevelopment && !this.isCryptoAvailable) {
if (this.verbose) {
console.log(
"tldraw: you seem to be in a development environment that does not support crypto. License not verified."
);
console.log("You should check that this works in production separately.");
}
return { isLicenseParseable: false, reason: "has-key-development-mode" };
}
let cleanedLicenseKey = licenseKey.replace(/[\u200B-\u200D\uFEFF]/g, "");
cleanedLicenseKey = cleanedLicenseKey.replace(/\r?\n|\r/g, "");
try {
const licenseInfo = await this.extractLicenseKey(cleanedLicenseKey);
const expiryDate = new Date(licenseInfo.expiryDate);
const isAnnualLicense = this.isFlagEnabled(licenseInfo.flags, FLAGS.ANNUAL_LICENSE);
const isPerpetualLicense = this.isFlagEnabled(licenseInfo.flags, FLAGS.PERPETUAL_LICENSE);
const result = {
license: licenseInfo,
isLicenseParseable: true,
isDevelopment: this.isDevelopment,
isDomainValid: this.isDomainValid(licenseInfo),
expiryDate,
isAnnualLicense,
isAnnualLicenseExpired: isAnnualLicense && this.isAnnualLicenseExpired(expiryDate),
isPerpetualLicense,
isPerpetualLicenseExpired: isPerpetualLicense && this.isPerpetualLicenseExpired(expiryDate),
isInternalLicense: this.isFlagEnabled(licenseInfo.flags, FLAGS.INTERNAL_LICENSE),
isLicensedWithWatermark: this.isFlagEnabled(licenseInfo.flags, FLAGS.WITH_WATERMARK)
};
this.outputLicenseInfoIfNeeded(result);
return result;
} catch (e) {
this.outputInvalidLicenseKey(e.message);
return { isLicenseParseable: false, reason: "invalid-license-key" };
}
}
isDomainValid(licenseInfo) {
const currentHostname = window.location.hostname.toLowerCase();
return licenseInfo.hosts.some((host) => {
const normalizedHost = host.toLowerCase().trim();
if (normalizedHost === currentHostname || `www.${normalizedHost}` === currentHostname || normalizedHost === `www.${currentHostname}`) {
return true;
}
if (host === "*") {
return true;
}
if (host.includes("*")) {
const globToRegex = new RegExp(host.replace(/\*/g, ".*?"));
return globToRegex.test(currentHostname) || globToRegex.test(`www.${currentHostname}`);
}
return false;
});
}
getExpirationDateWithoutGracePeriod(expiryDate) {
return new Date(expiryDate.getFullYear(), expiryDate.getMonth(), expiryDate.getDate());
}
getExpirationDateWithGracePeriod(expiryDate) {
return new Date(
expiryDate.getFullYear(),
expiryDate.getMonth(),
expiryDate.getDate() + GRACE_PERIOD_DAYS + 1
// Add 1 day to include the expiration day
);
}
isAnnualLicenseExpired(expiryDate) {
const expiration = this.getExpirationDateWithGracePeriod(expiryDate);
const isExpired = /* @__PURE__ */ new Date() >= expiration;
if (!isExpired && /* @__PURE__ */ new Date() >= this.getExpirationDateWithoutGracePeriod(expiryDate)) {
this.outputMessages([
"tldraw license is about to expire, you are in a grace period.",
`Please reach out to ${LICENSE_EMAIL} if you would like to renew your license.`
]);
}
return isExpired;
}
isPerpetualLicenseExpired(expiryDate) {
const expiration = this.getExpirationDateWithGracePeriod(expiryDate);
const dates = {
major: new Date(publishDates.major),
minor: new Date(publishDates.minor)
};
return dates.major >= expiration || dates.minor >= expiration;
}
isFlagEnabled(flags, flag) {
return (flags & flag) === flag;
}
outputNoLicenseKeyProvided() {
}
outputInvalidLicenseKey(msg) {
this.outputMessages(["Invalid tldraw license key", `Reason: ${msg}`]);
}
outputLicenseInfoIfNeeded(result) {
if (result.isAnnualLicenseExpired) {
this.outputMessages([
"Your tldraw license has expired!",
`Please reach out to ${LICENSE_EMAIL} to renew.`
]);
}
if (!result.isDomainValid && !result.isDevelopment) {
this.outputMessages([
"This tldraw license key is not valid for this domain!",
`Please reach out to ${LICENSE_EMAIL} if you would like to use tldraw on other domains.`
]);
}
if (result.license.flags >= HIGHEST_FLAG * 2) {
this.outputMessages([
"This tldraw license contains some unknown flags.",
"You may want to update tldraw packages to a newer version to get access to new functionality."
]);
}
}
outputMessages(messages) {
if (this.isTest) return;
if (this.verbose) {
this.outputDelimiter();
for (const message of messages) {
console.log(
`%c${message}`,
`color: white; background: crimson; padding: 2px; border-radius: 3px;`
);
}
this.outputDelimiter();
}
}
outputDelimiter() {
console.log(
"%c-------------------------------------------------------------------",
`color: white; background: crimson; padding: 2px; border-radius: 3px;`
);
}
static className = "tl-watermark_SEE-LICENSE";
}
function isEditorUnlicensed(result) {
if (!result.isLicenseParseable) return true;
if (!result.isDomainValid && !result.isDevelopment) return true;
if (result.isPerpetualLicenseExpired || result.isAnnualLicenseExpired) {
if (result.isInternalLicense) {
throw new Error("License: Internal license expired.");
}
return true;
}
return false;
}
const LicenseContext = reactExports.createContext({});
const useLicenseContext = () => reactExports.useContext(LicenseContext);
function LicenseProvider({
licenseKey,
children
}) {
const [licenseManager] = reactExports.useState(() => new LicenseManager(licenseKey));
return /* @__PURE__ */ jsxRuntimeExports.jsx(LicenseContext.Provider, { value: licenseManager, children });
}
function usePassThroughWheelEvents(ref) {
if (!ref) throw Error("usePassThroughWheelEvents must be passed a ref");
const container = useContainer();
reactExports.useEffect(() => {
function onWheel(e) {
if (e.isSpecialRedispatchedEvent) return;
preventDefault(e);
const cvs = container.querySelector(".tl-canvas");
if (!cvs) return;
const newEvent = new WheelEvent("wheel", e);
newEvent.isSpecialRedispatchedEvent = true;
cvs.dispatchEvent(newEvent);
}
const elm = ref.current;
if (!elm) return;
elm.addEventListener("wheel", onWheel, { passive: false });
return () => {
elm.removeEventListener("wheel", onWheel);
};
}, [container, ref]);
}
const watermarkDesktopSvg = '';
const watermarkMobileSvg = '';
function useLicenseManagerState(licenseManager) {
return useValue("watermarkState", () => licenseManager.state.get(), [licenseManager]);
}
const WATERMARK_DESKTOP_LOCAL_SRC = `data:image/svg+xml;utf8,${encodeURIComponent(watermarkDesktopSvg)}`;
const WATERMARK_MOBILE_LOCAL_SRC = `data:image/svg+xml;utf8,${encodeURIComponent(watermarkMobileSvg)}`;
const Watermark = reactExports.memo(function Watermark2() {
const licenseManager = useLicenseContext();
const editor = useEditor();
const isMobile = useValue("is mobile", () => editor.getViewportScreenBounds().width < 700, [
editor
]);
const licenseManagerState = useLicenseManagerState(licenseManager);
if (!["licensed-with-watermark", "unlicensed"].includes(licenseManagerState)) return null;
return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(LicenseStyles, {}),
/* @__PURE__ */ jsxRuntimeExports.jsx(WatermarkInner, { src: isMobile ? WATERMARK_MOBILE_LOCAL_SRC : WATERMARK_DESKTOP_LOCAL_SRC })
] });
});
const WatermarkInner = reactExports.memo(function WatermarkInner2({ src }) {
const editor = useEditor();
const isDebugMode = useValue("debug mode", () => editor.getInstanceState().isDebugMode, [editor]);
const isMobile = useValue("is mobile", () => editor.getViewportScreenBounds().width < 700, [
editor
]);
const events = useCanvasEvents();
const ref = reactExports.useRef(null);
usePassThroughWheelEvents(ref);
const maskCss = `url('${src}') center 100% / 100% no-repeat`;
const url = "https://tldraw.dev";
return /* @__PURE__ */ jsxRuntimeExports.jsx(
"div",
{
ref,
className: LicenseManager.className,
"data-debug": isDebugMode,
"data-mobile": isMobile,
draggable: false,
...events,
children: tlenv.isWebview ? /* @__PURE__ */ jsxRuntimeExports.jsx(
"a",
{
draggable: false,
role: "button",
onPointerDown: (e) => {
stopEventPropagation(e);
preventDefault(e);
},
onClick: () => runtime.openWindow(url, "_blank"),
style: { mask: maskCss, WebkitMask: maskCss }
}
) : /* @__PURE__ */ jsxRuntimeExports.jsx(
"a",
{
href: url,
target: "_blank",
rel: "noreferrer",
draggable: false,
onPointerDown: (e) => {
stopEventPropagation(e);
},
style: { mask: maskCss, WebkitMask: maskCss }
}
)
}
);
});
const LicenseStyles = reactExports.memo(function LicenseStyles2() {
const className = LicenseManager.className;
const CSS = `/* ------------------- SEE LICENSE -------------------
The tldraw watermark is part of tldraw's license. It is shown for unlicensed
or "licensed-with-watermark" users. By using this library, you agree to
preserve the watermark's behavior, keeping it visible, unobscured, and
available to user-interaction.
To remove the watermark, please purchase a license at tldraw.dev.
*/
.${className} {
position: absolute;
bottom: var(--space-2);
right: var(--space-2);
width: 96px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
z-index: var(--layer-watermark) !important;
background-color: color-mix(in srgb, var(--color-background) 62%, transparent);
opacity: 1;
border-radius: 5px;
pointer-events: all;
padding: 2px;
box-sizing: content-box;
}
.${className} > a {
position: absolute;
width: 96px;
height: 32px;
pointer-events: all;
cursor: inherit;
color: var(--color-text);
opacity: .38;
background-color: currentColor;
}
.${className}[data-debug='true'] {
bottom: 46px;
}
.${className}[data-mobile='true'] {
border-radius: 4px 0px 0px 4px;
right: -2px;
width: 8px;
height: 48px;
}
.${className}[data-mobile='true'] > a {
width: 8px;
height: 32px;
}
@media (hover: hover) {
.${className} > a {
pointer-events: none;
}
.${className}:hover {
background-color: var(--color-background);
transition: background-color 0.2s ease-in-out;
transition-delay: 0.32s;
}
.${className}:hover > a {
animation: delayed_link 0.2s forwards ease-in-out;
animation-delay: 0.32s;
}
}
@keyframes delayed_link {
0% {
cursor: inherit;
opacity: .38;
pointer-events: none;
}
100% {
cursor: pointer;
opacity: 1;
pointer-events: all;
}
}`;
return /* @__PURE__ */ jsxRuntimeExports.jsx("style", { children: CSS });
});
const EMPTY_SHAPE_UTILS_ARRAY = [];
const EMPTY_BINDING_UTILS_ARRAY = [];
const EMPTY_TOOLS_ARRAY = [];
const TL_CONTAINER_CLASS = "tl-container";
const TldrawEditor = reactExports.memo(function TldrawEditor2({
store,
components,
className,
user: _user,
options: _options,
...rest
}) {
const [container, setContainer] = reactExports.useState(null);
const user = reactExports.useMemo(() => _user ?? createTLUser(), [_user]);
const ErrorFallback = components?.ErrorFallback === void 0 ? DefaultErrorFallback : components?.ErrorFallback;
const withDefaults = {
...rest,
shapeUtils: rest.shapeUtils ?? EMPTY_SHAPE_UTILS_ARRAY,
bindingUtils: rest.bindingUtils ?? EMPTY_BINDING_UTILS_ARRAY,
tools: rest.tools ?? EMPTY_TOOLS_ARRAY,
components,
options: useShallowObjectIdentity(_options)
};
return (
/* @__PURE__ */ jsxRuntimeExports.jsx(
"div",
{
ref: setContainer,
"data-tldraw": version,
draggable: false,
className: classNames(`${TL_CONTAINER_CLASS} tl-theme__light`, className),
onPointerDown: stopEventPropagation,
tabIndex: -1,
children: /* @__PURE__ */ jsxRuntimeExports.jsx(
OptionalErrorBoundary,
{
fallback: ErrorFallback,
onError: (error) => annotateError(error, { tags: { origin: "react.tldraw-before-app" } }),
children: container && /* @__PURE__ */ jsxRuntimeExports.jsx(LicenseProvider, { licenseKey: rest.licenseKey, children: /* @__PURE__ */ jsxRuntimeExports.jsx(ContainerProvider, { container, children: /* @__PURE__ */ jsxRuntimeExports.jsx(EditorComponentsProvider, { overrides: components, children: store ? store instanceof Store ? (
// Store is ready to go, whether externally synced or not
/* @__PURE__ */ (jsxRuntimeExports.jsx(TldrawEditorWithReadyStore, { ...withDefaults, store, user }))
) : (
// Store is a synced store, so handle syncing stages internally
/* @__PURE__ */ (jsxRuntimeExports.jsx(TldrawEditorWithLoadingStore, { ...withDefaults, store, user }))
) : (
// We have no store (it's undefined) so create one and possibly sync it
/* @__PURE__ */ (jsxRuntimeExports.jsx(TldrawEditorWithOwnStore, { ...withDefaults, store, user }))
) }) }) })
}
)
}
)
);
});
function TldrawEditorWithOwnStore(props) {
const {
defaultName,
snapshot,
initialData,
shapeUtils,
bindingUtils,
persistenceKey,
sessionId,
user,
assets
} = props;
const syncedStore = useLocalStore({
shapeUtils,
bindingUtils,
initialData,
persistenceKey,
sessionId,
defaultName,
snapshot,
assets
});
return /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawEditorWithLoadingStore, { ...props, store: syncedStore, user });
}
const TldrawEditorWithLoadingStore = reactExports.memo(function TldrawEditorBeforeLoading({
store,
user,
...rest
}) {
const container = useContainer();
reactExports.useLayoutEffect(() => {
if (user.userPreferences.get().colorScheme === "dark") {
container.classList.remove("tl-theme__light");
container.classList.add("tl-theme__dark");
}
}, [container, user]);
const { LoadingScreen: LoadingScreen2 } = useEditorComponents();
switch (store.status) {
case "error": {
throw store.error;
}
case "loading": {
return LoadingScreen2 ? /* @__PURE__ */ jsxRuntimeExports.jsx(LoadingScreen2, {}) : null;
}
}
return /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawEditorWithReadyStore, { ...rest, store: store.store, user });
});
const noAutoFocus = () => document.location.search.includes("tldraw_preserve_focus");
function TldrawEditorWithReadyStore({
onMount,
children,
store,
tools,
shapeUtils,
bindingUtils,
user,
initialState,
autoFocus = true,
inferDarkMode,
cameraOptions,
options,
licenseKey,
deepLinks: _deepLinks,
isShapeHidden
}) {
const { ErrorFallback } = useEditorComponents();
const container = useContainer();
const [editor, setEditor] = useRefState(null);
const canvasRef = reactExports.useRef(null);
const deepLinks = useShallowObjectIdentity(_deepLinks === true ? {} : _deepLinks);
const editorOptionsRef = reactExports.useRef({
// for these, it's because they're only used when the editor first mounts:
autoFocus: autoFocus && !noAutoFocus(),
inferDarkMode,
initialState,
// for these, it's because we keep them up to date in a separate effect:
cameraOptions,
deepLinks
});
reactExports.useLayoutEffect(() => {
editorOptionsRef.current = {
autoFocus: autoFocus && !noAutoFocus(),
inferDarkMode,
initialState,
cameraOptions,
deepLinks
};
}, [autoFocus, inferDarkMode, initialState, cameraOptions, deepLinks]);
reactExports.useLayoutEffect(
() => {
const { autoFocus: autoFocus2, inferDarkMode: inferDarkMode2, initialState: initialState2, cameraOptions: cameraOptions2, deepLinks: deepLinks2 } = editorOptionsRef.current;
const editor2 = new Editor({
store,
shapeUtils,
bindingUtils,
tools,
getContainer: () => container,
user,
initialState: initialState2,
// we should check for some kind of query parameter that turns off autofocus
autoFocus: autoFocus2,
inferDarkMode: inferDarkMode2,
cameraOptions: cameraOptions2,
options,
licenseKey,
isShapeHidden
});
editor2.updateViewportScreenBounds(canvasRef.current ?? container);
if (deepLinks2) {
if (!deepLinks2?.getUrl) {
editor2.navigateToDeepLink(deepLinks2);
} else {
editor2.navigateToDeepLink({ ...deepLinks2, url: deepLinks2.getUrl(editor2) });
}
}
setEditor(editor2);
return () => {
editor2.dispose();
};
},
// if any of these change, we need to recreate the editor.
[
bindingUtils,
container,
options,
shapeUtils,
store,
tools,
user,
setEditor,
licenseKey,
isShapeHidden
]
);
reactExports.useLayoutEffect(() => {
if (!editor) return;
if (deepLinks) {
return editor.registerDeepLinkListener(deepLinks);
}
}, [editor, deepLinks]);
reactExports.useLayoutEffect(() => {
if (editor && cameraOptions) {
editor.setCameraOptions(cameraOptions);
}
}, [editor, cameraOptions]);
const crashingError = reactExports.useSyncExternalStore(
reactExports.useCallback(
(onStoreChange) => {
if (editor) {
editor.on("crash", onStoreChange);
return () => editor.off("crash", onStoreChange);
}
return () => {
};
},
[editor]
),
() => editor?.getCrashingError() ?? null
);
reactExports.useEffect(
function handleFocusOnPointerDownForPreserveFocusMode() {
if (!editor) return;
function handleFocusOnPointerDown() {
if (!editor) return;
editor.focus();
}
function handleBlurOnPointerDown() {
if (!editor) return;
editor.blur();
}
if (autoFocus && noAutoFocus()) {
editor.getContainer().addEventListener("pointerdown", handleFocusOnPointerDown);
document.body.addEventListener("pointerdown", handleBlurOnPointerDown);
return () => {
editor.getContainer()?.removeEventListener("pointerdown", handleFocusOnPointerDown);
document.body.removeEventListener("pointerdown", handleBlurOnPointerDown);
};
}
},
[editor, autoFocus]
);
const { Canvas } = useEditorComponents();
if (!editor) {
return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "tl-canvas", ref: canvasRef });
}
return (
// the top-level tldraw component also renders an error boundary almost
// identical to this one. the reason we have two is because this one has
// access to `App`, which means that here we can enrich errors with data
// from app for reporting, and also still attempt to render the user's
// document in the event of an error to reassure them that their work is
// not lost.
/* @__PURE__ */ (jsxRuntimeExports.jsx(OptionalErrorBoundary, {
fallback: ErrorFallback,
onError: (error) => editor.annotateError(error, { origin: "react.tldraw", willCrashApp: true }),
children: crashingError ? /* @__PURE__ */ jsxRuntimeExports.jsx(Crash, { crashingError }) : /* @__PURE__ */ jsxRuntimeExports.jsx(EditorProvider, { editor, children: /* @__PURE__ */ jsxRuntimeExports.jsxs(Layout, { onMount, children: [
children ?? (Canvas ? /* @__PURE__ */ jsxRuntimeExports.jsx(Canvas, {}, editor.contextId) : null),
/* @__PURE__ */ jsxRuntimeExports.jsx(Watermark, {})
] }) })
}))
);
}
function Layout({ children, onMount }) {
useZoomCss();
useCursor();
useDarkMode();
useForceUpdate();
useOnMount((editor) => {
const teardownStore = editor.store.props.onMount(editor);
const teardownCallback = onMount?.(editor);
return () => {
teardownStore?.();
teardownCallback?.();
};
});
return children;
}
function Crash({ crashingError }) {
throw crashingError;
}
function LoadingScreen({ children }) {
return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "tl-loading", children });
}
function ErrorScreen({ children }) {
return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "tl-loading", children });
}
function useOnMount(onMount) {
const editor = useEditor();
const onMountEvent = useEvent((editor2) => {
let teardown = void 0;
editor2.run(
() => {
teardown = onMount?.(editor2);
editor2.emit("mount");
},
{ history: "ignore" }
);
window.tldrawReady = true;
return teardown;
});
React.useLayoutEffect(() => {
if (editor) return onMountEvent?.(editor);
}, [editor, onMountEvent]);
}
function HTMLContainer({ children, className = "", ...rest }) {
return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { ...rest, className: classNames("tl-html-container", className), children });
}
class BindingUtil {
constructor(editor) {
this.editor = editor;
}
static props;
static migrations;
/**
* The type of the binding util, which should match the binding's type.
*
* @public
*/
static type;
}
function resizeBox(shape, info, opts = {}) {
const { newPoint, handle, scaleX, scaleY } = info;
const { minWidth = 1, maxWidth = Infinity, minHeight = 1, maxHeight = Infinity } = opts;
let w = shape.props.w * scaleX;
let h = shape.props.h * scaleY;
const offset = new Vec(0, 0);
if (w > 0) {
if (w < minWidth) {
switch (handle) {
case "top_left":
case "left":
case "bottom_left": {
offset.x = w - minWidth;
break;
}
case "top":
case "bottom": {
offset.x = (w - minWidth) / 2;
break;
}
default: {
offset.x = 0;
}
}
w = minWidth;
}
} else {
offset.x = w;
w = -w;
if (w < minWidth) {
switch (handle) {
case "top_left":
case "left":
case "bottom_left": {
offset.x = -w;
break;
}
default: {
offset.x = -minWidth;
}
}
w = minWidth;
}
}
if (h > 0) {
if (h < minHeight) {
switch (handle) {
case "top_left":
case "top":
case "top_right": {
offset.y = h - minHeight;
break;
}
case "right":
case "left": {
offset.y = (h - minHeight) / 2;
break;
}
default: {
offset.y = 0;
}
}
h = minHeight;
}
} else {
offset.y = h;
h = -h;
if (h < minHeight) {
switch (handle) {
case "top_left":
case "top":
case "top_right": {
offset.y = -h;
break;
}
default: {
offset.y = -minHeight;
}
}
h = minHeight;
}
}
const { x, y } = offset.rot(shape.rotation).add(newPoint);
return {
...shape,
x,
y,
props: {
w: Math.min(maxWidth, w),
h: Math.min(maxHeight, h)
}
};
}
class BaseBoxShapeUtil extends ShapeUtil {
getGeometry(shape) {
return new Rectangle2d({
width: shape.props.w,
height: shape.props.h,
isFilled: true
});
}
onResize(shape, info) {
return resizeBox(shape, info);
}
getHandleSnapGeometry(shape) {
return {
points: this.getGeometry(shape).bounds.cornersAndCenter
};
}
getInterpolatedProps(startShape, endShape, t) {
return {
...endShape.props,
w: lerp(startShape.props.w, endShape.props.w, t),
h: lerp(startShape.props.h, endShape.props.h, t)
};
}
}
let Idle$c = class Idle extends StateNode {
static id = "idle";
onPointerDown(info) {
this.parent.transition("pointing", info);
}
onEnter() {
this.editor.setCursor({ type: "cross", rotation: 0 });
}
onCancel() {
this.editor.setCurrentTool("select");
}
};
let Pointing$8 = class Pointing extends StateNode {
static id = "pointing";
onPointerMove(info) {
if (this.editor.inputs.isDragging) {
const { originPagePoint } = this.editor.inputs;
const shapeType = this.parent.shapeType;
const id = createShapeId();
const creatingMarkId = this.editor.markHistoryStoppingPoint(`creating_box:${id}`);
const newPoint = maybeSnapToGrid(originPagePoint, this.editor);
this.editor.createShapes([
{
id,
type: shapeType,
x: newPoint.x,
y: newPoint.y,
props: {
w: 1,
h: 1
}
}
]).select(id);
const parent = this.parent;
this.editor.setCurrentTool(
"select.resizing",
{
...info,
target: "selection",
handle: "bottom_right",
isCreating: true,
creatingMarkId,
creationCursorOffset: { x: 1, y: 1 },
onInteractionEnd: this.parent.id,
onCreate: parent.onCreate ? (shape) => parent.onCreate?.(shape) : void 0
}
/** satisfies ResizingInfo, defined in main tldraw package 😧 */
);
}
}
onPointerUp() {
this.complete();
}
onCancel() {
this.cancel();
}
onComplete() {
this.complete();
}
onInterrupt() {
this.cancel();
}
complete() {
const { originPagePoint } = this.editor.inputs;
const shapeType = this.parent.shapeType;
const id = createShapeId();
this.editor.markHistoryStoppingPoint(`creating_box:${id}`);
this.editor.createShapes([
{
id,
type: shapeType,
x: originPagePoint.x,
y: originPagePoint.y
}
]);
const shape = this.editor.getShape(id);
if (!shape) {
this.cancel();
return;
}
let { w, h } = shape.props;
const delta = new Vec(w / 2, h / 2);
const parentTransform = this.editor.getShapeParentTransform(shape);
if (parentTransform) delta.rot(-parentTransform.rotation());
let scale = 1;
if (this.editor.user.getIsDynamicResizeMode()) {
scale = 1 / this.editor.getZoomLevel();
w *= scale;
h *= scale;
delta.mul(scale);
}
const next = structuredClone(shape);
const newPoint = maybeSnapToGrid(new Vec(shape.x - delta.x, shape.y - delta.y), this.editor);
next.x = newPoint.x;
next.y = newPoint.y;
next.props.w = w;
next.props.h = h;
if ("scale" in shape.props) {
next.props.scale = scale;
}
this.editor.updateShape(next);
this.editor.setSelectedShapes([id]);
if (this.editor.getInstanceState().isToolLocked) {
this.parent.transition("idle");
} else {
this.editor.setCurrentTool("select.idle");
}
}
cancel() {
this.parent.transition("idle");
}
};
function maybeSnapToGrid(point, editor) {
const isGridMode = editor.getInstanceState().isGridMode;
const gridSize = editor.getDocumentSettings().gridSize;
if (isGridMode) return point.clone().snapToGrid(gridSize);
return point.clone();
}
class BaseBoxShapeTool extends StateNode {
static id = "box";
static initial = "idle";
static children() {
return [Idle$c, Pointing$8];
}
}
function useGlobalMenuIsOpen(id, onChange, onEvent) {
const rIsOpen = reactExports.useRef(false);
const onOpenChange = reactExports.useCallback(
(isOpen2) => {
rIsOpen.current = isOpen2;
if (isOpen2) {
tlmenus.addOpenMenu(id);
} else {
tlmenus.deleteOpenMenu(id);
}
onChange?.(isOpen2);
},
[id, onChange]
);
const isOpen = useValue("is menu open", () => tlmenus.getOpenMenus().includes(id), [id]);
reactExports.useEffect(() => {
if (rIsOpen.current) {
onEvent?.("open-menu");
tlmenus.addOpenMenu(id);
}
return () => {
if (rIsOpen.current) {
tlmenus.deleteOpenMenu(id);
tlmenus.getOpenMenus().forEach((menuId) => {
if (menuId.startsWith(id)) {
onEvent?.("close-menu");
tlmenus.deleteOpenMenu(menuId);
}
});
rIsOpen.current = false;
}
};
}, [id, onEvent]);
return [isOpen, onOpenChange];
}
function useIsEditing(shapeId) {
const editor = useEditor();
return useValue("isEditing", () => editor.getEditingShapeId() === shapeId, [editor, shapeId]);
}
function useSelectionEvents(handle) {
const editor = useEditor();
const events = reactExports.useMemo(
function selectionEvents() {
const onPointerDown = (e) => {
if (e.isKilled) return;
if (e.button === RIGHT_MOUSE_BUTTON) {
editor.dispatch({
type: "pointer",
target: "selection",
handle,
name: "right_click",
...getPointerInfo(e)
});
return;
}
if (e.button !== 0) return;
const elm = loopToHtmlElement(e.currentTarget);
function releaseCapture() {
elm.removeEventListener("pointerup", releaseCapture);
releasePointerCapture(elm, e);
}
setPointerCapture(elm, e);
elm.addEventListener("pointerup", releaseCapture);
editor.dispatch({
name: "pointer_down",
type: "pointer",
target: "selection",
handle,
...getPointerInfo(e)
});
stopEventPropagation(e);
};
let lastX, lastY;
function onPointerMove(e) {
if (e.isKilled) return;
if (e.button !== 0) return;
if (e.clientX === lastX && e.clientY === lastY) return;
lastX = e.clientX;
lastY = e.clientY;
editor.dispatch({
name: "pointer_move",
type: "pointer",
target: "selection",
handle,
...getPointerInfo(e)
});
}
const onPointerUp = (e) => {
if (e.isKilled) return;
if (e.button !== 0) return;
editor.dispatch({
name: "pointer_up",
type: "pointer",
target: "selection",
handle,
...getPointerInfo(e)
});
};
return {
onPointerDown,
onPointerMove,
onPointerUp
};
},
[editor, handle]
);
return events;
}
function useTLStore(opts) {
const [current, setCurrent] = reactExports.useState(() => ({ store: createTLStore(opts), opts }));
if (!areObjectsShallowEqual(current.opts, opts)) {
const next = { store: createTLStore(opts), opts };
setCurrent(next);
return next.store;
}
return current.store;
}
function useTLSchemaFromUtils(opts) {
const [current, setCurrent] = reactExports.useState(() => ({ opts, schema: createTLSchemaFromUtils(opts) }));
if (!areObjectsShallowEqual(current.opts, opts)) {
const next = createTLSchemaFromUtils(opts);
setCurrent({ opts, schema: next });
return next;
}
return current.schema;
}
const SPACING = 20;
const MIN_COUNT = 8;
function getVerticesCountForLength(length, spacing = SPACING) {
return Math.max(MIN_COUNT, Math.ceil(length / spacing));
}
class Arc2d extends Geometry2d {
_center;
radius;
start;
end;
largeArcFlag;
sweepFlag;
measure;
angleStart;
angleEnd;
constructor(config) {
super({ ...config, isFilled: false, isClosed: false });
const { center, sweepFlag, largeArcFlag, start, end } = config;
if (start.equals(end)) throw Error(`Arc must have different start and end points.`);
this.angleStart = Vec.Angle(center, start);
this.angleEnd = Vec.Angle(center, end);
this.radius = Vec.Dist(center, start);
this.measure = getArcMeasure(this.angleStart, this.angleEnd, sweepFlag, largeArcFlag);
this.start = start;
this.end = end;
this.sweepFlag = sweepFlag;
this.largeArcFlag = largeArcFlag;
this._center = center;
}
nearestPoint(point) {
const { _center, measure, radius, angleEnd, angleStart, start: A, end: B } = this;
const t = getPointInArcT(measure, angleStart, angleEnd, _center.angle(point));
if (t <= 0) return A;
if (t >= 1) return B;
const P = _center.clone().add(point.clone().sub(_center).uni().mul(radius));
let nearest;
let dist = Infinity;
let d;
for (const p of [A, B, P]) {
d = Vec.Dist2(point, p);
if (d < dist) {
nearest = p;
dist = d;
}
}
if (!nearest) throw Error("nearest point not found");
return nearest;
}
hitTestLineSegment(A, B) {
const { _center, radius, measure, angleStart, angleEnd } = this;
const intersection = intersectLineSegmentCircle(A, B, _center, radius);
if (intersection === null) return false;
return intersection.some((p) => {
const result = getPointInArcT(measure, angleStart, angleEnd, _center.angle(p));
return result >= 0 && result <= 1;
});
}
getVertices() {
const { _center, measure, length, radius, angleStart } = this;
const vertices = [];
for (let i = 0, n = getVerticesCountForLength(Math.abs(length)); i < n + 1; i++) {
const t = i / n * measure;
const angle = angleStart + t;
vertices.push(getPointOnCircle(_center, radius, angle));
}
return vertices;
}
getSvgPathData(first = true) {
const { start, end, radius, largeArcFlag, sweepFlag } = this;
return `${first ? `M${start.toFixed()}` : ``} A${radius} ${radius} 0 ${largeArcFlag} ${sweepFlag} ${end.toFixed()}`;
}
getLength() {
return this.measure * this.radius;
}
}
class Circle2d extends Geometry2d {
constructor(config) {
super({ isClosed: true, ...config });
this.config = config;
const { x = 0, y = 0, radius } = config;
this.x = x;
this.y = y;
this._center = new Vec(radius + x, radius + y);
this.radius = radius;
}
_center;
radius;
x;
y;
getBounds() {
return new Box(this.x, this.y, this.radius * 2, this.radius * 2);
}
getVertices() {
const { _center, radius } = this;
const perimeter = PI2 * radius;
const vertices = [];
for (let i = 0, n = getVerticesCountForLength(perimeter); i < n; i++) {
const angle = i / n * PI2;
vertices.push(getPointOnCircle(_center, radius, angle));
}
return vertices;
}
nearestPoint(point) {
const { _center, radius } = this;
if (_center.equals(point)) return Vec.AddXY(_center, radius, 0);
return _center.clone().add(point.clone().sub(_center).uni().mul(radius));
}
hitTestLineSegment(A, B, distance = 0) {
const { _center, radius } = this;
return intersectLineSegmentCircle(A, B, _center, radius + distance) !== null;
}
getSvgPathData() {
const { _center, radius } = this;
return `M${_center.x + radius},${_center.y} a${radius},${radius} 0 1,0 ${radius * 2},0a${radius},${radius} 0 1,0 -${radius * 2},0`;
}
}
class CubicBezier2d extends Polyline2d {
a;
b;
c;
d;
constructor(config) {
const { start: a, cp1: b, cp2: c, end: d } = config;
super({ ...config, points: [a, d] });
this.a = a;
this.b = b;
this.c = c;
this.d = d;
}
getVertices() {
const vertices = [];
const { a, b, c, d } = this;
for (let i = 0, n = 10; i <= n; i++) {
const t = i / n;
vertices.push(
new Vec(
(1 - t) * (1 - t) * (1 - t) * a.x + 3 * ((1 - t) * (1 - t)) * t * b.x + 3 * (1 - t) * (t * t) * c.x + t * t * t * d.x,
(1 - t) * (1 - t) * (1 - t) * a.y + 3 * ((1 - t) * (1 - t)) * t * b.y + 3 * (1 - t) * (t * t) * c.y + t * t * t * d.y
)
);
}
return vertices;
}
midPoint() {
return CubicBezier2d.GetAtT(this, 0.5);
}
nearestPoint(A) {
let nearest;
let dist = Infinity;
let d;
let p;
for (const edge of this.segments) {
p = edge.nearestPoint(A);
d = Vec.Dist2(p, A);
if (d < dist) {
nearest = p;
dist = d;
}
}
if (!nearest) throw Error("nearest point not found");
return nearest;
}
getSvgPathData(first = true) {
const { a, b, c, d } = this;
return `${first ? `M ${a.toFixed()} ` : ``} C${b.toFixed()} ${c.toFixed()} ${d.toFixed()}`;
}
static GetAtT(segment, t) {
const { a, b, c, d } = segment;
return new Vec(
(1 - t) * (1 - t) * (1 - t) * a.x + 3 * ((1 - t) * (1 - t)) * t * b.x + 3 * (1 - t) * (t * t) * c.x + t * t * t * d.x,
(1 - t) * (1 - t) * (1 - t) * a.y + 3 * ((1 - t) * (1 - t)) * t * b.y + 3 * (1 - t) * (t * t) * c.y + t * t * t * d.y
);
}
getLength(precision = 32) {
let n1, p1 = this.a, length = 0;
for (let i = 1; i <= precision; i++) {
n1 = CubicBezier2d.GetAtT(this, i / precision);
length += Vec.Dist(p1, n1);
p1 = n1;
}
return length;
}
}
class CubicSpline2d extends Geometry2d {
points;
constructor(config) {
super({ ...config, isClosed: false, isFilled: false });
const { points } = config;
this.points = points;
}
_segments;
// eslint-disable-next-line no-restricted-syntax
get segments() {
if (!this._segments) {
this._segments = [];
const { points } = this;
const len = points.length;
const last = len - 2;
const k = 1.25;
for (let i = 0; i < len - 1; i++) {
const p0 = i === 0 ? points[0] : points[i - 1];
const p1 = points[i];
const p2 = points[i + 1];
const p3 = i === last ? p2 : points[i + 2];
const start = p1, cp1 = i === 0 ? p0 : new Vec(p1.x + (p2.x - p0.x) / 6 * k, p1.y + (p2.y - p0.y) / 6 * k), cp2 = i === last ? p2 : new Vec(p2.x - (p3.x - p1.x) / 6 * k, p2.y - (p3.y - p1.y) / 6 * k), end = p2;
this._segments.push(new CubicBezier2d({ start, cp1, cp2, end }));
}
}
return this._segments;
}
getLength() {
return this.segments.reduce((acc, segment) => acc + segment.length, 0);
}
getVertices() {
const vertices = this.segments.reduce((acc, segment) => {
return acc.concat(segment.vertices);
}, []);
vertices.push(this.points[this.points.length - 1]);
return vertices;
}
nearestPoint(A) {
let nearest;
let dist = Infinity;
let d;
let p;
for (const segment of this.segments) {
p = segment.nearestPoint(A);
d = Vec.Dist2(p, A);
if (d < dist) {
nearest = p;
dist = d;
}
}
if (!nearest) throw Error("nearest point not found");
return nearest;
}
hitTestLineSegment(A, B) {
return this.segments.some((segment) => segment.hitTestLineSegment(A, B));
}
getSvgPathData() {
let d = this.segments.reduce((d2, segment, i) => {
return d2 + segment.getSvgPathData(i === 0);
}, "");
if (this.isClosed) {
d += "Z";
}
return d;
}
}
class Ellipse2d extends Geometry2d {
constructor(config) {
super({ ...config, isClosed: true });
this.config = config;
const { width, height } = config;
this.w = width;
this.h = height;
}
w;
h;
_edges;
// eslint-disable-next-line no-restricted-syntax
get edges() {
if (!this._edges) {
const { vertices } = this;
this._edges = [];
for (let i = 0, n = vertices.length; i < n; i++) {
const start = vertices[i];
const end = vertices[(i + 1) % n];
this._edges.push(new Edge2d({ start, end }));
}
}
return this._edges;
}
getVertices() {
const w = Math.max(1, this.w);
const h = Math.max(1, this.h);
const cx = w / 2;
const cy = h / 2;
const q = Math.pow(cx - cy, 2) / Math.pow(cx + cy, 2);
const p = PI$1 * (cx + cy) * (1 + 3 * q / (10 + Math.sqrt(4 - 3 * q)));
const len = getVerticesCountForLength(p);
const step = PI2 / len;
const a = Math.cos(step);
const b = Math.sin(step);
let sin = 0;
let cos = 1;
let ts = 0;
let tc = 1;
const vertices = Array(len);
for (let i = 0; i < len; i++) {
vertices[i] = new Vec(cx + cx * cos, cy + cy * sin);
ts = b * cos + a * sin;
tc = a * cos - b * sin;
sin = ts;
cos = tc;
}
return vertices;
}
nearestPoint(A) {
let nearest;
let dist = Infinity;
let d;
let p;
for (const edge of this.edges) {
p = edge.nearestPoint(A);
d = Vec.Dist2(p, A);
if (d < dist) {
nearest = p;
dist = d;
}
}
if (!nearest) throw Error("nearest point not found");
return nearest;
}
hitTestLineSegment(A, B) {
return this.edges.some((edge) => edge.hitTestLineSegment(A, B));
}
getBounds() {
return new Box(0, 0, this.w, this.h);
}
getLength() {
const { w, h } = this;
const cx = w / 2;
const cy = h / 2;
const rx = Math.max(0, cx);
const ry = Math.max(0, cy);
return perimeterOfEllipse(rx, ry);
}
getSvgPathData(first = false) {
const { w, h } = this;
const cx = w / 2;
const cy = h / 2;
const rx = Math.max(0, cx);
const ry = Math.max(0, cy);
return `${first ? `M${cx - rx},${cy}` : ``} a${rx},${ry},0,1,1,${rx * 2},0a${rx},${ry},0,1,1,-${rx * 2},0`;
}
}
class Stadium2d extends Geometry2d {
constructor(config) {
super({ ...config, isClosed: true });
this.config = config;
const { width: w, height: h } = config;
this.w = w;
this.h = h;
if (h > w) {
const r = w / 2;
this.a = new Arc2d({
start: new Vec(0, r),
end: new Vec(w, r),
center: new Vec(w / 2, r),
sweepFlag: 1,
largeArcFlag: 1
});
this.b = new Edge2d({ start: new Vec(w, r), end: new Vec(w, h - r) });
this.c = new Arc2d({
start: new Vec(w, h - r),
end: new Vec(0, h - r),
center: new Vec(w / 2, h - r),
sweepFlag: 1,
largeArcFlag: 1
});
this.d = new Edge2d({ start: new Vec(0, h - r), end: new Vec(0, r) });
} else {
const r = h / 2;
this.a = new Arc2d({
start: new Vec(r, h),
end: new Vec(r, 0),
center: new Vec(r, r),
sweepFlag: 1,
largeArcFlag: 1
});
this.b = new Edge2d({ start: new Vec(r, 0), end: new Vec(w - r, 0) });
this.c = new Arc2d({
start: new Vec(w - r, 0),
end: new Vec(w - r, h),
center: new Vec(w - r, r),
sweepFlag: 1,
largeArcFlag: 1
});
this.d = new Edge2d({ start: new Vec(w - r, h), end: new Vec(r, h) });
}
}
w;
h;
a;
b;
c;
d;
nearestPoint(A) {
let nearest;
let dist = Infinity;
let _d;
let p;
const { a, b, c, d } = this;
for (const part of [a, b, c, d]) {
p = part.nearestPoint(A);
_d = Vec.Dist2(p, A);
if (_d < dist) {
nearest = p;
dist = _d;
}
}
if (!nearest) throw Error("nearest point not found");
return nearest;
}
hitTestLineSegment(A, B) {
const { a, b, c, d } = this;
return [a, b, c, d].some((edge) => edge.hitTestLineSegment(A, B));
}
getVertices() {
const { a, b, c, d } = this;
return [a, b, c, d].reduce((a2, p) => {
a2.push(...p.vertices);
return a2;
}, []);
}
getBounds() {
return new Box(0, 0, this.w, this.h);
}
getLength() {
const { h, w } = this;
if (h > w) return (PI$1 * (w / 2) + (h - w)) * 2;
else return (PI$1 * (h / 2) + (w - h)) * 2;
}
getSvgPathData() {
const { a, b, c, d } = this;
return [a, b, c, d].map((p, i) => p.getSvgPathData(i === 0)).join(" ") + " Z";
}
}
async function hardReset({ shouldReload = true } = {}) {
clearSessionStorage();
for (const instance of LocalIndexedDb.connectedInstances) {
await instance.close();
}
await Promise.all(getAllIndexDbNames().map((db) => deleteDB(db)));
clearLocalStorage();
if (shouldReload) {
window.location.reload();
}
}
if (typeof window !== "undefined") {
window.__tldraw__hardReset = hardReset;
}
function openWindow(url, target = "_blank") {
runtime.openWindow(url, target);
}
registerTldrawLibraryVersion(
"@tldraw/editor",
"3.6.1",
"esm"
);
const defaultEventHandler = () => void 0;
const EventsContext = reactExports.createContext(null);
function TldrawUiEventsProvider({ onEvent, children }) {
return /* @__PURE__ */ jsxRuntimeExports.jsx(EventsContext.Provider, { value: onEvent ?? defaultEventHandler, children });
}
function useUiEvents() {
const eventHandler = reactExports.useContext(EventsContext);
return eventHandler ?? defaultEventHandler;
}
// src/primitive.tsx
function composeEventHandlers(originalEventHandler, ourEventHandler, { checkForDefaultPrevented = true } = {}) {
return function handleEvent(event) {
originalEventHandler?.(event);
if (checkForDefaultPrevented === false || !event.defaultPrevented) {
return ourEventHandler?.(event);
}
};
}
// packages/react/context/src/create-context.tsx
function createContext2(rootComponentName, defaultContext) {
const Context = reactExports.createContext(defaultContext);
const Provider = (props) => {
const { children, ...context } = props;
const value = reactExports.useMemo(() => context, Object.values(context));
return /* @__PURE__ */ jsxRuntimeExports.jsx(Context.Provider, { value, children });
};
Provider.displayName = rootComponentName + "Provider";
function useContext2(consumerName) {
const context = reactExports.useContext(Context);
if (context) return context;
if (defaultContext !== void 0) return defaultContext;
throw new Error(`\`${consumerName}\` must be used within \`${rootComponentName}\``);
}
return [Provider, useContext2];
}
function createContextScope(scopeName, createContextScopeDeps = []) {
let defaultContexts = [];
function createContext3(rootComponentName, defaultContext) {
const BaseContext = reactExports.createContext(defaultContext);
const index = defaultContexts.length;
defaultContexts = [...defaultContexts, defaultContext];
const Provider = (props) => {
const { scope, children, ...context } = props;
const Context = scope?.[scopeName]?.[index] || BaseContext;
const value = reactExports.useMemo(() => context, Object.values(context));
return /* @__PURE__ */ jsxRuntimeExports.jsx(Context.Provider, { value, children });
};
Provider.displayName = rootComponentName + "Provider";
function useContext2(consumerName, scope) {
const Context = scope?.[scopeName]?.[index] || BaseContext;
const context = reactExports.useContext(Context);
if (context) return context;
if (defaultContext !== void 0) return defaultContext;
throw new Error(`\`${consumerName}\` must be used within \`${rootComponentName}\``);
}
return [Provider, useContext2];
}
const createScope = () => {
const scopeContexts = defaultContexts.map((defaultContext) => {
return reactExports.createContext(defaultContext);
});
return function useScope(scope) {
const contexts = scope?.[scopeName] || scopeContexts;
return reactExports.useMemo(
() => ({ [`__scope${scopeName}`]: { ...scope, [scopeName]: contexts } }),
[scope, contexts]
);
};
};
createScope.scopeName = scopeName;
return [createContext3, composeContextScopes(createScope, ...createContextScopeDeps)];
}
function composeContextScopes(...scopes) {
const baseScope = scopes[0];
if (scopes.length === 1) return baseScope;
const createScope = () => {
const scopeHooks = scopes.map((createScope2) => ({
useScope: createScope2(),
scopeName: createScope2.scopeName
}));
return function useComposedScopes(overrideScopes) {
const nextScopes = scopeHooks.reduce((nextScopes2, { useScope, scopeName }) => {
const scopeProps = useScope(overrideScopes);
const currentScope = scopeProps[`__scope${scopeName}`];
return { ...nextScopes2, ...currentScope };
}, {});
return reactExports.useMemo(() => ({ [`__scope${baseScope.scopeName}`]: nextScopes }), [nextScopes]);
};
};
createScope.scopeName = baseScope.scopeName;
return createScope;
}
// packages/react/compose-refs/src/compose-refs.tsx
function setRef(ref, value) {
if (typeof ref === "function") {
return ref(value);
} else if (ref !== null && ref !== void 0) {
ref.current = value;
}
}
function composeRefs(...refs) {
return (node) => {
let hasCleanup = false;
const cleanups = refs.map((ref) => {
const cleanup = setRef(ref, node);
if (!hasCleanup && typeof cleanup == "function") {
hasCleanup = true;
}
return cleanup;
});
if (hasCleanup) {
return () => {
for (let i = 0; i < cleanups.length; i++) {
const cleanup = cleanups[i];
if (typeof cleanup == "function") {
cleanup();
} else {
setRef(refs[i], null);
}
}
};
}
};
}
function useComposedRefs(...refs) {
return reactExports.useCallback(composeRefs(...refs), refs);
}
// src/slot.tsx
// @__NO_SIDE_EFFECTS__
function createSlot(ownerName) {
const SlotClone = /* @__PURE__ */ createSlotClone(ownerName);
const Slot2 = reactExports.forwardRef((props, forwardedRef) => {
const { children, ...slotProps } = props;
const childrenArray = reactExports.Children.toArray(children);
const slottable = childrenArray.find(isSlottable);
if (slottable) {
const newElement = slottable.props.children;
const newChildren = childrenArray.map((child) => {
if (child === slottable) {
if (reactExports.Children.count(newElement) > 1) return reactExports.Children.only(null);
return reactExports.isValidElement(newElement) ? newElement.props.children : null;
} else {
return child;
}
});
return /* @__PURE__ */ jsxRuntimeExports.jsx(SlotClone, { ...slotProps, ref: forwardedRef, children: reactExports.isValidElement(newElement) ? reactExports.cloneElement(newElement, void 0, newChildren) : null });
}
return /* @__PURE__ */ jsxRuntimeExports.jsx(SlotClone, { ...slotProps, ref: forwardedRef, children });
});
Slot2.displayName = `${ownerName}.Slot`;
return Slot2;
}
// @__NO_SIDE_EFFECTS__
function createSlotClone(ownerName) {
const SlotClone = reactExports.forwardRef((props, forwardedRef) => {
const { children, ...slotProps } = props;
if (reactExports.isValidElement(children)) {
const childrenRef = getElementRef$1(children);
const props2 = mergeProps(slotProps, children.props);
if (children.type !== reactExports.Fragment) {
props2.ref = forwardedRef ? composeRefs(forwardedRef, childrenRef) : childrenRef;
}
return reactExports.cloneElement(children, props2);
}
return reactExports.Children.count(children) > 1 ? reactExports.Children.only(null) : null;
});
SlotClone.displayName = `${ownerName}.SlotClone`;
return SlotClone;
}
var SLOTTABLE_IDENTIFIER = Symbol("radix.slottable");
function isSlottable(child) {
return reactExports.isValidElement(child) && typeof child.type === "function" && "__radixId" in child.type && child.type.__radixId === SLOTTABLE_IDENTIFIER;
}
function mergeProps(slotProps, childProps) {
const overrideProps = { ...childProps };
for (const propName in childProps) {
const slotPropValue = slotProps[propName];
const childPropValue = childProps[propName];
const isHandler = /^on[A-Z]/.test(propName);
if (isHandler) {
if (slotPropValue && childPropValue) {
overrideProps[propName] = (...args) => {
const result = childPropValue(...args);
slotPropValue(...args);
return result;
};
} else if (slotPropValue) {
overrideProps[propName] = slotPropValue;
}
} else if (propName === "style") {
overrideProps[propName] = { ...slotPropValue, ...childPropValue };
} else if (propName === "className") {
overrideProps[propName] = [slotPropValue, childPropValue].filter(Boolean).join(" ");
}
}
return { ...slotProps, ...overrideProps };
}
function getElementRef$1(element) {
let getter = Object.getOwnPropertyDescriptor(element.props, "ref")?.get;
let mayWarn = getter && "isReactWarning" in getter && getter.isReactWarning;
if (mayWarn) {
return element.ref;
}
getter = Object.getOwnPropertyDescriptor(element, "ref")?.get;
mayWarn = getter && "isReactWarning" in getter && getter.isReactWarning;
if (mayWarn) {
return element.props.ref;
}
return element.props.ref || element.ref;
}
// src/primitive.tsx
var NODES = [
"a",
"button",
"div",
"form",
"h2",
"h3",
"img",
"input",
"label",
"li",
"nav",
"ol",
"p",
"select",
"span",
"svg",
"ul"
];
var Primitive = NODES.reduce((primitive, node) => {
const Slot = createSlot(`Primitive.${node}`);
const Node = reactExports.forwardRef((props, forwardedRef) => {
const { asChild, ...primitiveProps } = props;
const Comp = asChild ? Slot : node;
if (typeof window !== "undefined") {
window[Symbol.for("radix-ui")] = true;
}
return /* @__PURE__ */ jsxRuntimeExports.jsx(Comp, { ...primitiveProps, ref: forwardedRef });
});
Node.displayName = `Primitive.${node}`;
return { ...primitive, [node]: Node };
}, {});
function dispatchDiscreteCustomEvent(target, event) {
if (target) reactDomExports.flushSync(() => target.dispatchEvent(event));
}
function createCollection(name) {
const PROVIDER_NAME = name + "CollectionProvider";
const [createCollectionContext, createCollectionScope] = createContextScope(PROVIDER_NAME);
const [CollectionProviderImpl, useCollectionContext] = createCollectionContext(
PROVIDER_NAME,
{ collectionRef: { current: null }, itemMap: /* @__PURE__ */ new Map() }
);
const CollectionProvider = (props) => {
const { scope, children } = props;
const ref = React.useRef(null);
const itemMap = React.useRef(/* @__PURE__ */ new Map()).current;
return /* @__PURE__ */ jsxRuntimeExports.jsx(CollectionProviderImpl, { scope, itemMap, collectionRef: ref, children });
};
CollectionProvider.displayName = PROVIDER_NAME;
const COLLECTION_SLOT_NAME = name + "CollectionSlot";
const CollectionSlotImpl = createSlot(COLLECTION_SLOT_NAME);
const CollectionSlot = React.forwardRef(
(props, forwardedRef) => {
const { scope, children } = props;
const context = useCollectionContext(COLLECTION_SLOT_NAME, scope);
const composedRefs = useComposedRefs(forwardedRef, context.collectionRef);
return /* @__PURE__ */ jsxRuntimeExports.jsx(CollectionSlotImpl, { ref: composedRefs, children });
}
);
CollectionSlot.displayName = COLLECTION_SLOT_NAME;
const ITEM_SLOT_NAME = name + "CollectionItemSlot";
const ITEM_DATA_ATTR = "data-radix-collection-item";
const CollectionItemSlotImpl = createSlot(ITEM_SLOT_NAME);
const CollectionItemSlot = React.forwardRef(
(props, forwardedRef) => {
const { scope, children, ...itemData } = props;
const ref = React.useRef(null);
const composedRefs = useComposedRefs(forwardedRef, ref);
const context = useCollectionContext(ITEM_SLOT_NAME, scope);
React.useEffect(() => {
context.itemMap.set(ref, { ref, ...itemData });
return () => void context.itemMap.delete(ref);
});
return /* @__PURE__ */ jsxRuntimeExports.jsx(CollectionItemSlotImpl, { ...{ [ITEM_DATA_ATTR]: "" }, ref: composedRefs, children });
}
);
CollectionItemSlot.displayName = ITEM_SLOT_NAME;
function useCollection(scope) {
const context = useCollectionContext(name + "CollectionConsumer", scope);
const getItems = React.useCallback(() => {
const collectionNode = context.collectionRef.current;
if (!collectionNode) return [];
const orderedNodes = Array.from(collectionNode.querySelectorAll(`[${ITEM_DATA_ATTR}]`));
const items = Array.from(context.itemMap.values());
const orderedItems = items.sort(
(a, b) => orderedNodes.indexOf(a.ref.current) - orderedNodes.indexOf(b.ref.current)
);
return orderedItems;
}, [context.collectionRef, context.itemMap]);
return getItems;
}
return [
{ Provider: CollectionProvider, Slot: CollectionSlot, ItemSlot: CollectionItemSlot },
useCollection,
createCollectionScope
];
}
// packages/react/direction/src/direction.tsx
var DirectionContext = reactExports.createContext(void 0);
function useDirection(localDir) {
const globalDir = reactExports.useContext(DirectionContext);
return localDir || globalDir || "ltr";
}
// packages/react/use-callback-ref/src/use-callback-ref.tsx
function useCallbackRef$1(callback) {
const callbackRef = reactExports.useRef(callback);
reactExports.useEffect(() => {
callbackRef.current = callback;
});
return reactExports.useMemo(() => (...args) => callbackRef.current?.(...args), []);
}
// packages/react/use-escape-keydown/src/use-escape-keydown.tsx
function useEscapeKeydown(onEscapeKeyDownProp, ownerDocument = globalThis?.document) {
const onEscapeKeyDown = useCallbackRef$1(onEscapeKeyDownProp);
reactExports.useEffect(() => {
const handleKeyDown = (event) => {
if (event.key === "Escape") {
onEscapeKeyDown(event);
}
};
ownerDocument.addEventListener("keydown", handleKeyDown, { capture: true });
return () => ownerDocument.removeEventListener("keydown", handleKeyDown, { capture: true });
}, [onEscapeKeyDown, ownerDocument]);
}
var DISMISSABLE_LAYER_NAME = "DismissableLayer";
var CONTEXT_UPDATE = "dismissableLayer.update";
var POINTER_DOWN_OUTSIDE = "dismissableLayer.pointerDownOutside";
var FOCUS_OUTSIDE = "dismissableLayer.focusOutside";
var originalBodyPointerEvents;
var DismissableLayerContext = reactExports.createContext({
layers: /* @__PURE__ */ new Set(),
layersWithOutsidePointerEventsDisabled: /* @__PURE__ */ new Set(),
branches: /* @__PURE__ */ new Set()
});
var DismissableLayer = reactExports.forwardRef(
(props, forwardedRef) => {
const {
disableOutsidePointerEvents = false,
onEscapeKeyDown,
onPointerDownOutside,
onFocusOutside,
onInteractOutside,
onDismiss,
...layerProps
} = props;
const context = reactExports.useContext(DismissableLayerContext);
const [node, setNode] = reactExports.useState(null);
const ownerDocument = node?.ownerDocument ?? globalThis?.document;
const [, force] = reactExports.useState({});
const composedRefs = useComposedRefs(forwardedRef, (node2) => setNode(node2));
const layers = Array.from(context.layers);
const [highestLayerWithOutsidePointerEventsDisabled] = [...context.layersWithOutsidePointerEventsDisabled].slice(-1);
const highestLayerWithOutsidePointerEventsDisabledIndex = layers.indexOf(highestLayerWithOutsidePointerEventsDisabled);
const index = node ? layers.indexOf(node) : -1;
const isBodyPointerEventsDisabled = context.layersWithOutsidePointerEventsDisabled.size > 0;
const isPointerEventsEnabled = index >= highestLayerWithOutsidePointerEventsDisabledIndex;
const pointerDownOutside = usePointerDownOutside((event) => {
const target = event.target;
const isPointerDownOnBranch = [...context.branches].some((branch) => branch.contains(target));
if (!isPointerEventsEnabled || isPointerDownOnBranch) return;
onPointerDownOutside?.(event);
onInteractOutside?.(event);
if (!event.defaultPrevented) onDismiss?.();
}, ownerDocument);
const focusOutside = useFocusOutside((event) => {
const target = event.target;
const isFocusInBranch = [...context.branches].some((branch) => branch.contains(target));
if (isFocusInBranch) return;
onFocusOutside?.(event);
onInteractOutside?.(event);
if (!event.defaultPrevented) onDismiss?.();
}, ownerDocument);
useEscapeKeydown((event) => {
const isHighestLayer = index === context.layers.size - 1;
if (!isHighestLayer) return;
onEscapeKeyDown?.(event);
if (!event.defaultPrevented && onDismiss) {
event.preventDefault();
onDismiss();
}
}, ownerDocument);
reactExports.useEffect(() => {
if (!node) return;
if (disableOutsidePointerEvents) {
if (context.layersWithOutsidePointerEventsDisabled.size === 0) {
originalBodyPointerEvents = ownerDocument.body.style.pointerEvents;
ownerDocument.body.style.pointerEvents = "none";
}
context.layersWithOutsidePointerEventsDisabled.add(node);
}
context.layers.add(node);
dispatchUpdate();
return () => {
if (disableOutsidePointerEvents && context.layersWithOutsidePointerEventsDisabled.size === 1) {
ownerDocument.body.style.pointerEvents = originalBodyPointerEvents;
}
};
}, [node, ownerDocument, disableOutsidePointerEvents, context]);
reactExports.useEffect(() => {
return () => {
if (!node) return;
context.layers.delete(node);
context.layersWithOutsidePointerEventsDisabled.delete(node);
dispatchUpdate();
};
}, [node, context]);
reactExports.useEffect(() => {
const handleUpdate = () => force({});
document.addEventListener(CONTEXT_UPDATE, handleUpdate);
return () => document.removeEventListener(CONTEXT_UPDATE, handleUpdate);
}, []);
return /* @__PURE__ */ jsxRuntimeExports.jsx(
Primitive.div,
{
...layerProps,
ref: composedRefs,
style: {
pointerEvents: isBodyPointerEventsDisabled ? isPointerEventsEnabled ? "auto" : "none" : void 0,
...props.style
},
onFocusCapture: composeEventHandlers(props.onFocusCapture, focusOutside.onFocusCapture),
onBlurCapture: composeEventHandlers(props.onBlurCapture, focusOutside.onBlurCapture),
onPointerDownCapture: composeEventHandlers(
props.onPointerDownCapture,
pointerDownOutside.onPointerDownCapture
)
}
);
}
);
DismissableLayer.displayName = DISMISSABLE_LAYER_NAME;
var BRANCH_NAME = "DismissableLayerBranch";
var DismissableLayerBranch = reactExports.forwardRef((props, forwardedRef) => {
const context = reactExports.useContext(DismissableLayerContext);
const ref = reactExports.useRef(null);
const composedRefs = useComposedRefs(forwardedRef, ref);
reactExports.useEffect(() => {
const node = ref.current;
if (node) {
context.branches.add(node);
return () => {
context.branches.delete(node);
};
}
}, [context.branches]);
return /* @__PURE__ */ jsxRuntimeExports.jsx(Primitive.div, { ...props, ref: composedRefs });
});
DismissableLayerBranch.displayName = BRANCH_NAME;
function usePointerDownOutside(onPointerDownOutside, ownerDocument = globalThis?.document) {
const handlePointerDownOutside = useCallbackRef$1(onPointerDownOutside);
const isPointerInsideReactTreeRef = reactExports.useRef(false);
const handleClickRef = reactExports.useRef(() => {
});
reactExports.useEffect(() => {
const handlePointerDown = (event) => {
if (event.target && !isPointerInsideReactTreeRef.current) {
let handleAndDispatchPointerDownOutsideEvent2 = function() {
handleAndDispatchCustomEvent$1(
POINTER_DOWN_OUTSIDE,
handlePointerDownOutside,
eventDetail,
{ discrete: true }
);
};
const eventDetail = { originalEvent: event };
if (event.pointerType === "touch") {
ownerDocument.removeEventListener("click", handleClickRef.current);
handleClickRef.current = handleAndDispatchPointerDownOutsideEvent2;
ownerDocument.addEventListener("click", handleClickRef.current, { once: true });
} else {
handleAndDispatchPointerDownOutsideEvent2();
}
} else {
ownerDocument.removeEventListener("click", handleClickRef.current);
}
isPointerInsideReactTreeRef.current = false;
};
const timerId = window.setTimeout(() => {
ownerDocument.addEventListener("pointerdown", handlePointerDown);
}, 0);
return () => {
window.clearTimeout(timerId);
ownerDocument.removeEventListener("pointerdown", handlePointerDown);
ownerDocument.removeEventListener("click", handleClickRef.current);
};
}, [ownerDocument, handlePointerDownOutside]);
return {
// ensures we check React component tree (not just DOM tree)
onPointerDownCapture: () => isPointerInsideReactTreeRef.current = true
};
}
function useFocusOutside(onFocusOutside, ownerDocument = globalThis?.document) {
const handleFocusOutside = useCallbackRef$1(onFocusOutside);
const isFocusInsideReactTreeRef = reactExports.useRef(false);
reactExports.useEffect(() => {
const handleFocus = (event) => {
if (event.target && !isFocusInsideReactTreeRef.current) {
const eventDetail = { originalEvent: event };
handleAndDispatchCustomEvent$1(FOCUS_OUTSIDE, handleFocusOutside, eventDetail, {
discrete: false
});
}
};
ownerDocument.addEventListener("focusin", handleFocus);
return () => ownerDocument.removeEventListener("focusin", handleFocus);
}, [ownerDocument, handleFocusOutside]);
return {
onFocusCapture: () => isFocusInsideReactTreeRef.current = true,
onBlurCapture: () => isFocusInsideReactTreeRef.current = false
};
}
function dispatchUpdate() {
const event = new CustomEvent(CONTEXT_UPDATE);
document.dispatchEvent(event);
}
function handleAndDispatchCustomEvent$1(name, handler, detail, { discrete }) {
const target = detail.originalEvent.target;
const event = new CustomEvent(name, { bubbles: false, cancelable: true, detail });
if (handler) target.addEventListener(name, handler, { once: true });
if (discrete) {
dispatchDiscreteCustomEvent(target, event);
} else {
target.dispatchEvent(event);
}
}
var Root$4 = DismissableLayer;
var Branch = DismissableLayerBranch;
var count$1 = 0;
function useFocusGuards() {
reactExports.useEffect(() => {
const edgeGuards = document.querySelectorAll("[data-radix-focus-guard]");
document.body.insertAdjacentElement("afterbegin", edgeGuards[0] ?? createFocusGuard());
document.body.insertAdjacentElement("beforeend", edgeGuards[1] ?? createFocusGuard());
count$1++;
return () => {
if (count$1 === 1) {
document.querySelectorAll("[data-radix-focus-guard]").forEach((node) => node.remove());
}
count$1--;
};
}, []);
}
function createFocusGuard() {
const element = document.createElement("span");
element.setAttribute("data-radix-focus-guard", "");
element.tabIndex = 0;
element.style.outline = "none";
element.style.opacity = "0";
element.style.position = "fixed";
element.style.pointerEvents = "none";
return element;
}
var AUTOFOCUS_ON_MOUNT = "focusScope.autoFocusOnMount";
var AUTOFOCUS_ON_UNMOUNT = "focusScope.autoFocusOnUnmount";
var EVENT_OPTIONS$1 = { bubbles: false, cancelable: true };
var FOCUS_SCOPE_NAME = "FocusScope";
var FocusScope = reactExports.forwardRef((props, forwardedRef) => {
const {
loop = false,
trapped = false,
onMountAutoFocus: onMountAutoFocusProp,
onUnmountAutoFocus: onUnmountAutoFocusProp,
...scopeProps
} = props;
const [container, setContainer] = reactExports.useState(null);
const onMountAutoFocus = useCallbackRef$1(onMountAutoFocusProp);
const onUnmountAutoFocus = useCallbackRef$1(onUnmountAutoFocusProp);
const lastFocusedElementRef = reactExports.useRef(null);
const composedRefs = useComposedRefs(forwardedRef, (node) => setContainer(node));
const focusScope = reactExports.useRef({
paused: false,
pause() {
this.paused = true;
},
resume() {
this.paused = false;
}
}).current;
reactExports.useEffect(() => {
if (trapped) {
let handleFocusIn2 = function(event) {
if (focusScope.paused || !container) return;
const target = event.target;
if (container.contains(target)) {
lastFocusedElementRef.current = target;
} else {
focus(lastFocusedElementRef.current, { select: true });
}
}, handleFocusOut2 = function(event) {
if (focusScope.paused || !container) return;
const relatedTarget = event.relatedTarget;
if (relatedTarget === null) return;
if (!container.contains(relatedTarget)) {
focus(lastFocusedElementRef.current, { select: true });
}
}, handleMutations2 = function(mutations) {
const focusedElement = document.activeElement;
if (focusedElement !== document.body) return;
for (const mutation of mutations) {
if (mutation.removedNodes.length > 0) focus(container);
}
};
document.addEventListener("focusin", handleFocusIn2);
document.addEventListener("focusout", handleFocusOut2);
const mutationObserver = new MutationObserver(handleMutations2);
if (container) mutationObserver.observe(container, { childList: true, subtree: true });
return () => {
document.removeEventListener("focusin", handleFocusIn2);
document.removeEventListener("focusout", handleFocusOut2);
mutationObserver.disconnect();
};
}
}, [trapped, container, focusScope.paused]);
reactExports.useEffect(() => {
if (container) {
focusScopesStack.add(focusScope);
const previouslyFocusedElement = document.activeElement;
const hasFocusedCandidate = container.contains(previouslyFocusedElement);
if (!hasFocusedCandidate) {
const mountEvent = new CustomEvent(AUTOFOCUS_ON_MOUNT, EVENT_OPTIONS$1);
container.addEventListener(AUTOFOCUS_ON_MOUNT, onMountAutoFocus);
container.dispatchEvent(mountEvent);
if (!mountEvent.defaultPrevented) {
focusFirst$3(removeLinks(getTabbableCandidates$1(container)), { select: true });
if (document.activeElement === previouslyFocusedElement) {
focus(container);
}
}
}
return () => {
container.removeEventListener(AUTOFOCUS_ON_MOUNT, onMountAutoFocus);
setTimeout(() => {
const unmountEvent = new CustomEvent(AUTOFOCUS_ON_UNMOUNT, EVENT_OPTIONS$1);
container.addEventListener(AUTOFOCUS_ON_UNMOUNT, onUnmountAutoFocus);
container.dispatchEvent(unmountEvent);
if (!unmountEvent.defaultPrevented) {
focus(previouslyFocusedElement ?? document.body, { select: true });
}
container.removeEventListener(AUTOFOCUS_ON_UNMOUNT, onUnmountAutoFocus);
focusScopesStack.remove(focusScope);
}, 0);
};
}
}, [container, onMountAutoFocus, onUnmountAutoFocus, focusScope]);
const handleKeyDown = reactExports.useCallback(
(event) => {
if (!loop && !trapped) return;
if (focusScope.paused) return;
const isTabKey = event.key === "Tab" && !event.altKey && !event.ctrlKey && !event.metaKey;
const focusedElement = document.activeElement;
if (isTabKey && focusedElement) {
const container2 = event.currentTarget;
const [first, last] = getTabbableEdges(container2);
const hasTabbableElementsInside = first && last;
if (!hasTabbableElementsInside) {
if (focusedElement === container2) event.preventDefault();
} else {
if (!event.shiftKey && focusedElement === last) {
event.preventDefault();
if (loop) focus(first, { select: true });
} else if (event.shiftKey && focusedElement === first) {
event.preventDefault();
if (loop) focus(last, { select: true });
}
}
}
},
[loop, trapped, focusScope.paused]
);
return /* @__PURE__ */ jsxRuntimeExports.jsx(Primitive.div, { tabIndex: -1, ...scopeProps, ref: composedRefs, onKeyDown: handleKeyDown });
});
FocusScope.displayName = FOCUS_SCOPE_NAME;
function focusFirst$3(candidates, { select = false } = {}) {
const previouslyFocusedElement = document.activeElement;
for (const candidate of candidates) {
focus(candidate, { select });
if (document.activeElement !== previouslyFocusedElement) return;
}
}
function getTabbableEdges(container) {
const candidates = getTabbableCandidates$1(container);
const first = findVisible(candidates, container);
const last = findVisible(candidates.reverse(), container);
return [first, last];
}
function getTabbableCandidates$1(container) {
const nodes = [];
const walker = document.createTreeWalker(container, NodeFilter.SHOW_ELEMENT, {
acceptNode: (node) => {
const isHiddenInput = node.tagName === "INPUT" && node.type === "hidden";
if (node.disabled || node.hidden || isHiddenInput) return NodeFilter.FILTER_SKIP;
return node.tabIndex >= 0 ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
}
});
while (walker.nextNode()) nodes.push(walker.currentNode);
return nodes;
}
function findVisible(elements, container) {
for (const element of elements) {
if (!isHidden(element, { upTo: container })) return element;
}
}
function isHidden(node, { upTo }) {
if (getComputedStyle(node).visibility === "hidden") return true;
while (node) {
if (upTo !== void 0 && node === upTo) return false;
if (getComputedStyle(node).display === "none") return true;
node = node.parentElement;
}
return false;
}
function isSelectableInput(element) {
return element instanceof HTMLInputElement && "select" in element;
}
function focus(element, { select = false } = {}) {
if (element && element.focus) {
const previouslyFocusedElement = document.activeElement;
element.focus({ preventScroll: true });
if (element !== previouslyFocusedElement && isSelectableInput(element) && select)
element.select();
}
}
var focusScopesStack = createFocusScopesStack();
function createFocusScopesStack() {
let stack = [];
return {
add(focusScope) {
const activeFocusScope = stack[0];
if (focusScope !== activeFocusScope) {
activeFocusScope?.pause();
}
stack = arrayRemove(stack, focusScope);
stack.unshift(focusScope);
},
remove(focusScope) {
stack = arrayRemove(stack, focusScope);
stack[0]?.resume();
}
};
}
function arrayRemove(array, item) {
const updatedArray = [...array];
const index = updatedArray.indexOf(item);
if (index !== -1) {
updatedArray.splice(index, 1);
}
return updatedArray;
}
function removeLinks(items) {
return items.filter((item) => item.tagName !== "A");
}
// packages/react/use-layout-effect/src/use-layout-effect.tsx
var useLayoutEffect2 = globalThis?.document ? reactExports.useLayoutEffect : () => {
};
// packages/react/id/src/id.tsx
var useReactId = React$1[" useId ".trim().toString()] || (() => void 0);
var count = 0;
function useId(deterministicId) {
const [id, setId] = reactExports.useState(useReactId());
useLayoutEffect2(() => {
setId((reactId) => reactId ?? String(count++));
}, [deterministicId]);
return deterministicId || (id ? `radix-${id}` : "");
}
// src/arrow.tsx
var NAME$1 = "Arrow";
var Arrow$1 = reactExports.forwardRef((props, forwardedRef) => {
const { children, width = 10, height = 5, ...arrowProps } = props;
return /* @__PURE__ */ jsxRuntimeExports.jsx(
Primitive.svg,
{
...arrowProps,
ref: forwardedRef,
width,
height,
viewBox: "0 0 30 10",
preserveAspectRatio: "none",
children: props.asChild ? children : /* @__PURE__ */ jsxRuntimeExports.jsx("polygon", { points: "0,0 30,0 15,10" })
}
);
});
Arrow$1.displayName = NAME$1;
var Root$3 = Arrow$1;
// packages/react/use-size/src/use-size.tsx
function useSize(element) {
const [size, setSize] = reactExports.useState(void 0);
useLayoutEffect2(() => {
if (element) {
setSize({ width: element.offsetWidth, height: element.offsetHeight });
const resizeObserver = new ResizeObserver((entries) => {
if (!Array.isArray(entries)) {
return;
}
if (!entries.length) {
return;
}
const entry = entries[0];
let width;
let height;
if ("borderBoxSize" in entry) {
const borderSizeEntry = entry["borderBoxSize"];
const borderSize = Array.isArray(borderSizeEntry) ? borderSizeEntry[0] : borderSizeEntry;
width = borderSize["inlineSize"];
height = borderSize["blockSize"];
} else {
width = element.offsetWidth;
height = element.offsetHeight;
}
setSize({ width, height });
});
resizeObserver.observe(element, { box: "border-box" });
return () => resizeObserver.unobserve(element);
} else {
setSize(void 0);
}
}, [element]);
return size;
}
var POPPER_NAME = "Popper";
var [createPopperContext, createPopperScope] = createContextScope(POPPER_NAME);
var [PopperProvider, usePopperContext] = createPopperContext(POPPER_NAME);
var Popper = (props) => {
const { __scopePopper, children } = props;
const [anchor, setAnchor] = reactExports.useState(null);
return /* @__PURE__ */ jsxRuntimeExports.jsx(PopperProvider, { scope: __scopePopper, anchor, onAnchorChange: setAnchor, children });
};
Popper.displayName = POPPER_NAME;
var ANCHOR_NAME$2 = "PopperAnchor";
var PopperAnchor = reactExports.forwardRef(
(props, forwardedRef) => {
const { __scopePopper, virtualRef, ...anchorProps } = props;
const context = usePopperContext(ANCHOR_NAME$2, __scopePopper);
const ref = reactExports.useRef(null);
const composedRefs = useComposedRefs(forwardedRef, ref);
const anchorRef = reactExports.useRef(null);
reactExports.useEffect(() => {
const previousAnchor = anchorRef.current;
anchorRef.current = virtualRef?.current || ref.current;
if (previousAnchor !== anchorRef.current) {
context.onAnchorChange(anchorRef.current);
}
});
return virtualRef ? null : /* @__PURE__ */ jsxRuntimeExports.jsx(Primitive.div, { ...anchorProps, ref: composedRefs });
}
);
PopperAnchor.displayName = ANCHOR_NAME$2;
var CONTENT_NAME$5 = "PopperContent";
var [PopperContentProvider, useContentContext] = createPopperContext(CONTENT_NAME$5);
var PopperContent = reactExports.forwardRef(
(props, forwardedRef) => {
const {
__scopePopper,
side = "bottom",
sideOffset = 0,
align = "center",
alignOffset = 0,
arrowPadding = 0,
avoidCollisions = true,
collisionBoundary = [],
collisionPadding: collisionPaddingProp = 0,
sticky = "partial",
hideWhenDetached = false,
updatePositionStrategy = "optimized",
onPlaced,
...contentProps
} = props;
const context = usePopperContext(CONTENT_NAME$5, __scopePopper);
const [content, setContent] = reactExports.useState(null);
const composedRefs = useComposedRefs(forwardedRef, (node) => setContent(node));
const [arrow$1, setArrow] = reactExports.useState(null);
const arrowSize = useSize(arrow$1);
const arrowWidth = arrowSize?.width ?? 0;
const arrowHeight = arrowSize?.height ?? 0;
const desiredPlacement = side + (align !== "center" ? "-" + align : "");
const collisionPadding = typeof collisionPaddingProp === "number" ? collisionPaddingProp : { top: 0, right: 0, bottom: 0, left: 0, ...collisionPaddingProp };
const boundary = Array.isArray(collisionBoundary) ? collisionBoundary : [collisionBoundary];
const hasExplicitBoundaries = boundary.length > 0;
const detectOverflowOptions = {
padding: collisionPadding,
boundary: boundary.filter(isNotNull),
// with `strategy: 'fixed'`, this is the only way to get it to respect boundaries
altBoundary: hasExplicitBoundaries
};
const { refs, floatingStyles, placement, isPositioned, middlewareData } = useFloating({
// default to `fixed` strategy so users don't have to pick and we also avoid focus scroll issues
strategy: "fixed",
placement: desiredPlacement,
whileElementsMounted: (...args) => {
const cleanup = autoUpdate(...args, {
animationFrame: updatePositionStrategy === "always"
});
return cleanup;
},
elements: {
reference: context.anchor
},
middleware: [
offset({ mainAxis: sideOffset + arrowHeight, alignmentAxis: alignOffset }),
avoidCollisions && shift({
mainAxis: true,
crossAxis: false,
limiter: sticky === "partial" ? limitShift() : void 0,
...detectOverflowOptions
}),
avoidCollisions && flip({ ...detectOverflowOptions }),
size({
...detectOverflowOptions,
apply: ({ elements, rects, availableWidth, availableHeight }) => {
const { width: anchorWidth, height: anchorHeight } = rects.reference;
const contentStyle = elements.floating.style;
contentStyle.setProperty("--radix-popper-available-width", `${availableWidth}px`);
contentStyle.setProperty("--radix-popper-available-height", `${availableHeight}px`);
contentStyle.setProperty("--radix-popper-anchor-width", `${anchorWidth}px`);
contentStyle.setProperty("--radix-popper-anchor-height", `${anchorHeight}px`);
}
}),
arrow$1 && arrow({ element: arrow$1, padding: arrowPadding }),
transformOrigin({ arrowWidth, arrowHeight }),
hideWhenDetached && hide({ strategy: "referenceHidden", ...detectOverflowOptions })
]
});
const [placedSide, placedAlign] = getSideAndAlignFromPlacement(placement);
const handlePlaced = useCallbackRef$1(onPlaced);
useLayoutEffect2(() => {
if (isPositioned) {
handlePlaced?.();
}
}, [isPositioned, handlePlaced]);
const arrowX = middlewareData.arrow?.x;
const arrowY = middlewareData.arrow?.y;
const cannotCenterArrow = middlewareData.arrow?.centerOffset !== 0;
const [contentZIndex, setContentZIndex] = reactExports.useState();
useLayoutEffect2(() => {
if (content) setContentZIndex(window.getComputedStyle(content).zIndex);
}, [content]);
return /* @__PURE__ */ jsxRuntimeExports.jsx(
"div",
{
ref: refs.setFloating,
"data-radix-popper-content-wrapper": "",
style: {
...floatingStyles,
transform: isPositioned ? floatingStyles.transform : "translate(0, -200%)",
// keep off the page when measuring
minWidth: "max-content",
zIndex: contentZIndex,
["--radix-popper-transform-origin"]: [
middlewareData.transformOrigin?.x,
middlewareData.transformOrigin?.y
].join(" "),
// hide the content if using the hide middleware and should be hidden
// set visibility to hidden and disable pointer events so the UI behaves
// as if the PopperContent isn't there at all
...middlewareData.hide?.referenceHidden && {
visibility: "hidden",
pointerEvents: "none"
}
},
dir: props.dir,
children: /* @__PURE__ */ jsxRuntimeExports.jsx(
PopperContentProvider,
{
scope: __scopePopper,
placedSide,
onArrowChange: setArrow,
arrowX,
arrowY,
shouldHideArrow: cannotCenterArrow,
children: /* @__PURE__ */ jsxRuntimeExports.jsx(
Primitive.div,
{
"data-side": placedSide,
"data-align": placedAlign,
...contentProps,
ref: composedRefs,
style: {
...contentProps.style,
// if the PopperContent hasn't been placed yet (not all measurements done)
// we prevent animations so that users's animation don't kick in too early referring wrong sides
animation: !isPositioned ? "none" : void 0
}
}
)
}
)
}
);
}
);
PopperContent.displayName = CONTENT_NAME$5;
var ARROW_NAME$4 = "PopperArrow";
var OPPOSITE_SIDE = {
top: "bottom",
right: "left",
bottom: "top",
left: "right"
};
var PopperArrow = reactExports.forwardRef(function PopperArrow2(props, forwardedRef) {
const { __scopePopper, ...arrowProps } = props;
const contentContext = useContentContext(ARROW_NAME$4, __scopePopper);
const baseSide = OPPOSITE_SIDE[contentContext.placedSide];
return (
// we have to use an extra wrapper because `ResizeObserver` (used by `useSize`)
// doesn't report size as we'd expect on SVG elements.
// it reports their bounding box which is effectively the largest path inside the SVG.
/* @__PURE__ */ jsxRuntimeExports.jsx(
"span",
{
ref: contentContext.onArrowChange,
style: {
position: "absolute",
left: contentContext.arrowX,
top: contentContext.arrowY,
[baseSide]: 0,
transformOrigin: {
top: "",
right: "0 0",
bottom: "center 0",
left: "100% 0"
}[contentContext.placedSide],
transform: {
top: "translateY(100%)",
right: "translateY(50%) rotate(90deg) translateX(-50%)",
bottom: `rotate(180deg)`,
left: "translateY(50%) rotate(-90deg) translateX(50%)"
}[contentContext.placedSide],
visibility: contentContext.shouldHideArrow ? "hidden" : void 0
},
children: /* @__PURE__ */ jsxRuntimeExports.jsx(
Root$3,
{
...arrowProps,
ref: forwardedRef,
style: {
...arrowProps.style,
// ensures the element can be measured correctly (mostly for if SVG)
display: "block"
}
}
)
}
)
);
});
PopperArrow.displayName = ARROW_NAME$4;
function isNotNull(value) {
return value !== null;
}
var transformOrigin = (options) => ({
name: "transformOrigin",
options,
fn(data) {
const { placement, rects, middlewareData } = data;
const cannotCenterArrow = middlewareData.arrow?.centerOffset !== 0;
const isArrowHidden = cannotCenterArrow;
const arrowWidth = isArrowHidden ? 0 : options.arrowWidth;
const arrowHeight = isArrowHidden ? 0 : options.arrowHeight;
const [placedSide, placedAlign] = getSideAndAlignFromPlacement(placement);
const noArrowAlign = { start: "0%", center: "50%", end: "100%" }[placedAlign];
const arrowXCenter = (middlewareData.arrow?.x ?? 0) + arrowWidth / 2;
const arrowYCenter = (middlewareData.arrow?.y ?? 0) + arrowHeight / 2;
let x = "";
let y = "";
if (placedSide === "bottom") {
x = isArrowHidden ? noArrowAlign : `${arrowXCenter}px`;
y = `${-arrowHeight}px`;
} else if (placedSide === "top") {
x = isArrowHidden ? noArrowAlign : `${arrowXCenter}px`;
y = `${rects.floating.height + arrowHeight}px`;
} else if (placedSide === "right") {
x = `${-arrowHeight}px`;
y = isArrowHidden ? noArrowAlign : `${arrowYCenter}px`;
} else if (placedSide === "left") {
x = `${rects.floating.width + arrowHeight}px`;
y = isArrowHidden ? noArrowAlign : `${arrowYCenter}px`;
}
return { data: { x, y } };
}
});
function getSideAndAlignFromPlacement(placement) {
const [side, align = "center"] = placement.split("-");
return [side, align];
}
var Root2$4 = Popper;
var Anchor = PopperAnchor;
var Content$1 = PopperContent;
var Arrow = PopperArrow;
var PORTAL_NAME$5 = "Portal";
var Portal$3 = reactExports.forwardRef((props, forwardedRef) => {
const { container: containerProp, ...portalProps } = props;
const [mounted, setMounted] = reactExports.useState(false);
useLayoutEffect2(() => setMounted(true), []);
const container = containerProp || mounted && globalThis?.document?.body;
return container ? ReactDOM.createPortal(/* @__PURE__ */ jsxRuntimeExports.jsx(Primitive.div, { ...portalProps, ref: forwardedRef }), container) : null;
});
Portal$3.displayName = PORTAL_NAME$5;
function useStateMachine(initialState, machine) {
return reactExports.useReducer((state, event) => {
const nextState = machine[state][event];
return nextState ?? state;
}, initialState);
}
// src/presence.tsx
var Presence = (props) => {
const { present, children } = props;
const presence = usePresence(present);
const child = typeof children === "function" ? children({ present: presence.isPresent }) : reactExports.Children.only(children);
const ref = useComposedRefs(presence.ref, getElementRef(child));
const forceMount = typeof children === "function";
return forceMount || presence.isPresent ? reactExports.cloneElement(child, { ref }) : null;
};
Presence.displayName = "Presence";
function usePresence(present) {
const [node, setNode] = reactExports.useState();
const stylesRef = reactExports.useRef(null);
const prevPresentRef = reactExports.useRef(present);
const prevAnimationNameRef = reactExports.useRef("none");
const initialState = present ? "mounted" : "unmounted";
const [state, send] = useStateMachine(initialState, {
mounted: {
UNMOUNT: "unmounted",
ANIMATION_OUT: "unmountSuspended"
},
unmountSuspended: {
MOUNT: "mounted",
ANIMATION_END: "unmounted"
},
unmounted: {
MOUNT: "mounted"
}
});
reactExports.useEffect(() => {
const currentAnimationName = getAnimationName(stylesRef.current);
prevAnimationNameRef.current = state === "mounted" ? currentAnimationName : "none";
}, [state]);
useLayoutEffect2(() => {
const styles = stylesRef.current;
const wasPresent = prevPresentRef.current;
const hasPresentChanged = wasPresent !== present;
if (hasPresentChanged) {
const prevAnimationName = prevAnimationNameRef.current;
const currentAnimationName = getAnimationName(styles);
if (present) {
send("MOUNT");
} else if (currentAnimationName === "none" || styles?.display === "none") {
send("UNMOUNT");
} else {
const isAnimating = prevAnimationName !== currentAnimationName;
if (wasPresent && isAnimating) {
send("ANIMATION_OUT");
} else {
send("UNMOUNT");
}
}
prevPresentRef.current = present;
}
}, [present, send]);
useLayoutEffect2(() => {
if (node) {
let timeoutId;
const ownerWindow = node.ownerDocument.defaultView ?? window;
const handleAnimationEnd = (event) => {
const currentAnimationName = getAnimationName(stylesRef.current);
const isCurrentAnimation = currentAnimationName.includes(CSS.escape(event.animationName));
if (event.target === node && isCurrentAnimation) {
send("ANIMATION_END");
if (!prevPresentRef.current) {
const currentFillMode = node.style.animationFillMode;
node.style.animationFillMode = "forwards";
timeoutId = ownerWindow.setTimeout(() => {
if (node.style.animationFillMode === "forwards") {
node.style.animationFillMode = currentFillMode;
}
});
}
}
};
const handleAnimationStart = (event) => {
if (event.target === node) {
prevAnimationNameRef.current = getAnimationName(stylesRef.current);
}
};
node.addEventListener("animationstart", handleAnimationStart);
node.addEventListener("animationcancel", handleAnimationEnd);
node.addEventListener("animationend", handleAnimationEnd);
return () => {
ownerWindow.clearTimeout(timeoutId);
node.removeEventListener("animationstart", handleAnimationStart);
node.removeEventListener("animationcancel", handleAnimationEnd);
node.removeEventListener("animationend", handleAnimationEnd);
};
} else {
send("ANIMATION_END");
}
}, [node, send]);
return {
isPresent: ["mounted", "unmountSuspended"].includes(state),
ref: reactExports.useCallback((node2) => {
stylesRef.current = node2 ? getComputedStyle(node2) : null;
setNode(node2);
}, [])
};
}
function getAnimationName(styles) {
return styles?.animationName || "none";
}
function getElementRef(element) {
let getter = Object.getOwnPropertyDescriptor(element.props, "ref")?.get;
let mayWarn = getter && "isReactWarning" in getter && getter.isReactWarning;
if (mayWarn) {
return element.ref;
}
getter = Object.getOwnPropertyDescriptor(element, "ref")?.get;
mayWarn = getter && "isReactWarning" in getter && getter.isReactWarning;
if (mayWarn) {
return element.props.ref;
}
return element.props.ref || element.ref;
}
// src/use-controllable-state.tsx
var useInsertionEffect = React$1[" useInsertionEffect ".trim().toString()] || useLayoutEffect2;
function useControllableState({
prop,
defaultProp,
onChange = () => {
},
caller
}) {
const [uncontrolledProp, setUncontrolledProp, onChangeRef] = useUncontrolledState({
defaultProp,
onChange
});
const isControlled = prop !== void 0;
const value = isControlled ? prop : uncontrolledProp;
{
const isControlledRef = reactExports.useRef(prop !== void 0);
reactExports.useEffect(() => {
const wasControlled = isControlledRef.current;
if (wasControlled !== isControlled) {
const from = wasControlled ? "controlled" : "uncontrolled";
const to = isControlled ? "controlled" : "uncontrolled";
console.warn(
`${caller} is changing from ${from} to ${to}. Components should not switch from controlled to uncontrolled (or vice versa). Decide between using a controlled or uncontrolled value for the lifetime of the component.`
);
}
isControlledRef.current = isControlled;
}, [isControlled, caller]);
}
const setValue = reactExports.useCallback(
(nextValue) => {
if (isControlled) {
const value2 = isFunction(nextValue) ? nextValue(prop) : nextValue;
if (value2 !== prop) {
onChangeRef.current?.(value2);
}
} else {
setUncontrolledProp(nextValue);
}
},
[isControlled, prop, setUncontrolledProp, onChangeRef]
);
return [value, setValue];
}
function useUncontrolledState({
defaultProp,
onChange
}) {
const [value, setValue] = reactExports.useState(defaultProp);
const prevValueRef = reactExports.useRef(value);
const onChangeRef = reactExports.useRef(onChange);
useInsertionEffect(() => {
onChangeRef.current = onChange;
}, [onChange]);
reactExports.useEffect(() => {
if (prevValueRef.current !== value) {
onChangeRef.current?.(value);
prevValueRef.current = value;
}
}, [value, prevValueRef]);
return [value, setValue, onChangeRef];
}
function isFunction(value) {
return typeof value === "function";
}
var ENTRY_FOCUS = "rovingFocusGroup.onEntryFocus";
var EVENT_OPTIONS = { bubbles: false, cancelable: true };
var GROUP_NAME$3 = "RovingFocusGroup";
var [Collection$3, useCollection$3, createCollectionScope$3] = createCollection(GROUP_NAME$3);
var [createRovingFocusGroupContext, createRovingFocusGroupScope] = createContextScope(
GROUP_NAME$3,
[createCollectionScope$3]
);
var [RovingFocusProvider, useRovingFocusContext] = createRovingFocusGroupContext(GROUP_NAME$3);
var RovingFocusGroup = reactExports.forwardRef(
(props, forwardedRef) => {
return /* @__PURE__ */ jsxRuntimeExports.jsx(Collection$3.Provider, { scope: props.__scopeRovingFocusGroup, children: /* @__PURE__ */ jsxRuntimeExports.jsx(Collection$3.Slot, { scope: props.__scopeRovingFocusGroup, children: /* @__PURE__ */ jsxRuntimeExports.jsx(RovingFocusGroupImpl, { ...props, ref: forwardedRef }) }) });
}
);
RovingFocusGroup.displayName = GROUP_NAME$3;
var RovingFocusGroupImpl = reactExports.forwardRef((props, forwardedRef) => {
const {
__scopeRovingFocusGroup,
orientation,
loop = false,
dir,
currentTabStopId: currentTabStopIdProp,
defaultCurrentTabStopId,
onCurrentTabStopIdChange,
onEntryFocus,
preventScrollOnEntryFocus = false,
...groupProps
} = props;
const ref = reactExports.useRef(null);
const composedRefs = useComposedRefs(forwardedRef, ref);
const direction = useDirection(dir);
const [currentTabStopId, setCurrentTabStopId] = useControllableState({
prop: currentTabStopIdProp,
defaultProp: defaultCurrentTabStopId ?? null,
onChange: onCurrentTabStopIdChange,
caller: GROUP_NAME$3
});
const [isTabbingBackOut, setIsTabbingBackOut] = reactExports.useState(false);
const handleEntryFocus = useCallbackRef$1(onEntryFocus);
const getItems = useCollection$3(__scopeRovingFocusGroup);
const isClickFocusRef = reactExports.useRef(false);
const [focusableItemsCount, setFocusableItemsCount] = reactExports.useState(0);
reactExports.useEffect(() => {
const node = ref.current;
if (node) {
node.addEventListener(ENTRY_FOCUS, handleEntryFocus);
return () => node.removeEventListener(ENTRY_FOCUS, handleEntryFocus);
}
}, [handleEntryFocus]);
return /* @__PURE__ */ jsxRuntimeExports.jsx(
RovingFocusProvider,
{
scope: __scopeRovingFocusGroup,
orientation,
dir: direction,
loop,
currentTabStopId,
onItemFocus: reactExports.useCallback(
(tabStopId) => setCurrentTabStopId(tabStopId),
[setCurrentTabStopId]
),
onItemShiftTab: reactExports.useCallback(() => setIsTabbingBackOut(true), []),
onFocusableItemAdd: reactExports.useCallback(
() => setFocusableItemsCount((prevCount) => prevCount + 1),
[]
),
onFocusableItemRemove: reactExports.useCallback(
() => setFocusableItemsCount((prevCount) => prevCount - 1),
[]
),
children: /* @__PURE__ */ jsxRuntimeExports.jsx(
Primitive.div,
{
tabIndex: isTabbingBackOut || focusableItemsCount === 0 ? -1 : 0,
"data-orientation": orientation,
...groupProps,
ref: composedRefs,
style: { outline: "none", ...props.style },
onMouseDown: composeEventHandlers(props.onMouseDown, () => {
isClickFocusRef.current = true;
}),
onFocus: composeEventHandlers(props.onFocus, (event) => {
const isKeyboardFocus = !isClickFocusRef.current;
if (event.target === event.currentTarget && isKeyboardFocus && !isTabbingBackOut) {
const entryFocusEvent = new CustomEvent(ENTRY_FOCUS, EVENT_OPTIONS);
event.currentTarget.dispatchEvent(entryFocusEvent);
if (!entryFocusEvent.defaultPrevented) {
const items = getItems().filter((item) => item.focusable);
const activeItem = items.find((item) => item.active);
const currentItem = items.find((item) => item.id === currentTabStopId);
const candidateItems = [activeItem, currentItem, ...items].filter(
Boolean
);
const candidateNodes = candidateItems.map((item) => item.ref.current);
focusFirst$2(candidateNodes, preventScrollOnEntryFocus);
}
}
isClickFocusRef.current = false;
}),
onBlur: composeEventHandlers(props.onBlur, () => setIsTabbingBackOut(false))
}
)
}
);
});
var ITEM_NAME$3 = "RovingFocusGroupItem";
var RovingFocusGroupItem = reactExports.forwardRef(
(props, forwardedRef) => {
const {
__scopeRovingFocusGroup,
focusable = true,
active = false,
tabStopId,
children,
...itemProps
} = props;
const autoId = useId();
const id = tabStopId || autoId;
const context = useRovingFocusContext(ITEM_NAME$3, __scopeRovingFocusGroup);
const isCurrentTabStop = context.currentTabStopId === id;
const getItems = useCollection$3(__scopeRovingFocusGroup);
const { onFocusableItemAdd, onFocusableItemRemove, currentTabStopId } = context;
reactExports.useEffect(() => {
if (focusable) {
onFocusableItemAdd();
return () => onFocusableItemRemove();
}
}, [focusable, onFocusableItemAdd, onFocusableItemRemove]);
return /* @__PURE__ */ jsxRuntimeExports.jsx(
Collection$3.ItemSlot,
{
scope: __scopeRovingFocusGroup,
id,
focusable,
active,
children: /* @__PURE__ */ jsxRuntimeExports.jsx(
Primitive.span,
{
tabIndex: isCurrentTabStop ? 0 : -1,
"data-orientation": context.orientation,
...itemProps,
ref: forwardedRef,
onMouseDown: composeEventHandlers(props.onMouseDown, (event) => {
if (!focusable) event.preventDefault();
else context.onItemFocus(id);
}),
onFocus: composeEventHandlers(props.onFocus, () => context.onItemFocus(id)),
onKeyDown: composeEventHandlers(props.onKeyDown, (event) => {
if (event.key === "Tab" && event.shiftKey) {
context.onItemShiftTab();
return;
}
if (event.target !== event.currentTarget) return;
const focusIntent = getFocusIntent(event, context.orientation, context.dir);
if (focusIntent !== void 0) {
if (event.metaKey || event.ctrlKey || event.altKey || event.shiftKey) return;
event.preventDefault();
const items = getItems().filter((item) => item.focusable);
let candidateNodes = items.map((item) => item.ref.current);
if (focusIntent === "last") candidateNodes.reverse();
else if (focusIntent === "prev" || focusIntent === "next") {
if (focusIntent === "prev") candidateNodes.reverse();
const currentIndex = candidateNodes.indexOf(event.currentTarget);
candidateNodes = context.loop ? wrapArray$1(candidateNodes, currentIndex + 1) : candidateNodes.slice(currentIndex + 1);
}
setTimeout(() => focusFirst$2(candidateNodes));
}
}),
children: typeof children === "function" ? children({ isCurrentTabStop, hasTabStop: currentTabStopId != null }) : children
}
)
}
);
}
);
RovingFocusGroupItem.displayName = ITEM_NAME$3;
var MAP_KEY_TO_FOCUS_INTENT = {
ArrowLeft: "prev",
ArrowUp: "prev",
ArrowRight: "next",
ArrowDown: "next",
PageUp: "first",
Home: "first",
PageDown: "last",
End: "last"
};
function getDirectionAwareKey(key, dir) {
if (dir !== "rtl") return key;
return key === "ArrowLeft" ? "ArrowRight" : key === "ArrowRight" ? "ArrowLeft" : key;
}
function getFocusIntent(event, orientation, dir) {
const key = getDirectionAwareKey(event.key, dir);
if (orientation === "vertical" && ["ArrowLeft", "ArrowRight"].includes(key)) return void 0;
if (orientation === "horizontal" && ["ArrowUp", "ArrowDown"].includes(key)) return void 0;
return MAP_KEY_TO_FOCUS_INTENT[key];
}
function focusFirst$2(candidates, preventScroll = false) {
const PREVIOUSLY_FOCUSED_ELEMENT = document.activeElement;
for (const candidate of candidates) {
if (candidate === PREVIOUSLY_FOCUSED_ELEMENT) return;
candidate.focus({ preventScroll });
if (document.activeElement !== PREVIOUSLY_FOCUSED_ELEMENT) return;
}
}
function wrapArray$1(array, startIndex) {
return array.map((_, index) => array[(startIndex + index) % array.length]);
}
var Root$2 = RovingFocusGroup;
var Item = RovingFocusGroupItem;
var getDefaultParent = function (originalTarget) {
if (typeof document === 'undefined') {
return null;
}
var sampleTarget = Array.isArray(originalTarget) ? originalTarget[0] : originalTarget;
return sampleTarget.ownerDocument.body;
};
var counterMap = new WeakMap();
var uncontrolledNodes = new WeakMap();
var markerMap = {};
var lockCount = 0;
var unwrapHost = function (node) {
return node && (node.host || unwrapHost(node.parentNode));
};
var correctTargets = function (parent, targets) {
return targets
.map(function (target) {
if (parent.contains(target)) {
return target;
}
var correctedTarget = unwrapHost(target);
if (correctedTarget && parent.contains(correctedTarget)) {
return correctedTarget;
}
console.error('aria-hidden', target, 'in not contained inside', parent, '. Doing nothing');
return null;
})
.filter(function (x) { return Boolean(x); });
};
/**
* Marks everything except given node(or nodes) as aria-hidden
* @param {Element | Element[]} originalTarget - elements to keep on the page
* @param [parentNode] - top element, defaults to document.body
* @param {String} [markerName] - a special attribute to mark every node
* @param {String} [controlAttribute] - html Attribute to control
* @return {Undo} undo command
*/
var applyAttributeToOthers = function (originalTarget, parentNode, markerName, controlAttribute) {
var targets = correctTargets(parentNode, Array.isArray(originalTarget) ? originalTarget : [originalTarget]);
if (!markerMap[markerName]) {
markerMap[markerName] = new WeakMap();
}
var markerCounter = markerMap[markerName];
var hiddenNodes = [];
var elementsToKeep = new Set();
var elementsToStop = new Set(targets);
var keep = function (el) {
if (!el || elementsToKeep.has(el)) {
return;
}
elementsToKeep.add(el);
keep(el.parentNode);
};
targets.forEach(keep);
var deep = function (parent) {
if (!parent || elementsToStop.has(parent)) {
return;
}
Array.prototype.forEach.call(parent.children, function (node) {
if (elementsToKeep.has(node)) {
deep(node);
}
else {
try {
var attr = node.getAttribute(controlAttribute);
var alreadyHidden = attr !== null && attr !== 'false';
var counterValue = (counterMap.get(node) || 0) + 1;
var markerValue = (markerCounter.get(node) || 0) + 1;
counterMap.set(node, counterValue);
markerCounter.set(node, markerValue);
hiddenNodes.push(node);
if (counterValue === 1 && alreadyHidden) {
uncontrolledNodes.set(node, true);
}
if (markerValue === 1) {
node.setAttribute(markerName, 'true');
}
if (!alreadyHidden) {
node.setAttribute(controlAttribute, 'true');
}
}
catch (e) {
console.error('aria-hidden: cannot operate on ', node, e);
}
}
});
};
deep(parentNode);
elementsToKeep.clear();
lockCount++;
return function () {
hiddenNodes.forEach(function (node) {
var counterValue = counterMap.get(node) - 1;
var markerValue = markerCounter.get(node) - 1;
counterMap.set(node, counterValue);
markerCounter.set(node, markerValue);
if (!counterValue) {
if (!uncontrolledNodes.has(node)) {
node.removeAttribute(controlAttribute);
}
uncontrolledNodes.delete(node);
}
if (!markerValue) {
node.removeAttribute(markerName);
}
});
lockCount--;
if (!lockCount) {
// clear
counterMap = new WeakMap();
counterMap = new WeakMap();
uncontrolledNodes = new WeakMap();
markerMap = {};
}
};
};
/**
* Marks everything except given node(or nodes) as aria-hidden
* @param {Element | Element[]} originalTarget - elements to keep on the page
* @param [parentNode] - top element, defaults to document.body
* @param {String} [markerName] - a special attribute to mark every node
* @return {Undo} undo command
*/
var hideOthers = function (originalTarget, parentNode, markerName) {
if (markerName === void 0) { markerName = 'data-aria-hidden'; }
var targets = Array.from(Array.isArray(originalTarget) ? originalTarget : [originalTarget]);
var activeParentNode = getDefaultParent(originalTarget);
if (!activeParentNode) {
return function () { return null; };
}
// we should not hide aria-live elements - https://github.com/theKashey/aria-hidden/issues/10
// and script elements, as they have no impact on accessibility.
targets.push.apply(targets, Array.from(activeParentNode.querySelectorAll('[aria-live], script')));
return applyAttributeToOthers(targets, activeParentNode, markerName, 'aria-hidden');
};
/******************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
var __assign = function() {
__assign = Object.assign || function __assign(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
function __rest(s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
}
function __spreadArray(to, from, pack) {
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
if (ar || !(i in from)) {
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
ar[i] = from[i];
}
}
return to.concat(ar || Array.prototype.slice.call(from));
}
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
};
var zeroRightClassName = 'right-scroll-bar-position';
var fullWidthClassName = 'width-before-scroll-bar';
var noScrollbarsClassName = 'with-scroll-bars-hidden';
/**
* Name of a CSS variable containing the amount of "hidden" scrollbar
* ! might be undefined ! use will fallback!
*/
var removedBarSizeVariable = '--removed-body-scroll-bar-size';
/**
* Assigns a value for a given ref, no matter of the ref format
* @param {RefObject} ref - a callback function or ref object
* @param value - a new value
*
* @see https://github.com/theKashey/use-callback-ref#assignref
* @example
* const refObject = useRef();
* const refFn = (ref) => {....}
*
* assignRef(refObject, "refValue");
* assignRef(refFn, "refValue");
*/
function assignRef(ref, value) {
if (typeof ref === 'function') {
ref(value);
}
else if (ref) {
ref.current = value;
}
return ref;
}
/**
* creates a MutableRef with ref change callback
* @param initialValue - initial ref value
* @param {Function} callback - a callback to run when value changes
*
* @example
* const ref = useCallbackRef(0, (newValue, oldValue) => console.log(oldValue, '->', newValue);
* ref.current = 1;
* // prints 0 -> 1
*
* @see https://reactjs.org/docs/hooks-reference.html#useref
* @see https://github.com/theKashey/use-callback-ref#usecallbackref---to-replace-reactuseref
* @returns {MutableRefObject}
*/
function useCallbackRef(initialValue, callback) {
var ref = reactExports.useState(function () { return ({
// value
value: initialValue,
// last callback
callback: callback,
// "memoized" public interface
facade: {
get current() {
return ref.value;
},
set current(value) {
var last = ref.value;
if (last !== value) {
ref.value = value;
ref.callback(value, last);
}
},
},
}); })[0];
// update callback
ref.callback = callback;
return ref.facade;
}
var useIsomorphicLayoutEffect = typeof window !== 'undefined' ? reactExports.useLayoutEffect : reactExports.useEffect;
var currentValues = new WeakMap();
/**
* Merges two or more refs together providing a single interface to set their value
* @param {RefObject|Ref} refs
* @returns {MutableRefObject} - a new ref, which translates all changes to {refs}
*
* @see {@link mergeRefs} a version without buit-in memoization
* @see https://github.com/theKashey/use-callback-ref#usemergerefs
* @example
* const Component = React.forwardRef((props, ref) => {
* const ownRef = useRef();
* const domRef = useMergeRefs([ref, ownRef]); // 👈 merge together
* return ...
* }
*/
function useMergeRefs(refs, defaultValue) {
var callbackRef = useCallbackRef(null, function (newValue) {
return refs.forEach(function (ref) { return assignRef(ref, newValue); });
});
// handle refs changes - added or removed
useIsomorphicLayoutEffect(function () {
var oldValue = currentValues.get(callbackRef);
if (oldValue) {
var prevRefs_1 = new Set(oldValue);
var nextRefs_1 = new Set(refs);
var current_1 = callbackRef.current;
prevRefs_1.forEach(function (ref) {
if (!nextRefs_1.has(ref)) {
assignRef(ref, null);
}
});
nextRefs_1.forEach(function (ref) {
if (!prevRefs_1.has(ref)) {
assignRef(ref, current_1);
}
});
}
currentValues.set(callbackRef, refs);
}, [refs]);
return callbackRef;
}
function ItoI(a) {
return a;
}
function innerCreateMedium(defaults, middleware) {
if (middleware === void 0) { middleware = ItoI; }
var buffer = [];
var assigned = false;
var medium = {
read: function () {
if (assigned) {
throw new Error('Sidecar: could not `read` from an `assigned` medium. `read` could be used only with `useMedium`.');
}
if (buffer.length) {
return buffer[buffer.length - 1];
}
return defaults;
},
useMedium: function (data) {
var item = middleware(data, assigned);
buffer.push(item);
return function () {
buffer = buffer.filter(function (x) { return x !== item; });
};
},
assignSyncMedium: function (cb) {
assigned = true;
while (buffer.length) {
var cbs = buffer;
buffer = [];
cbs.forEach(cb);
}
buffer = {
push: function (x) { return cb(x); },
filter: function () { return buffer; },
};
},
assignMedium: function (cb) {
assigned = true;
var pendingQueue = [];
if (buffer.length) {
var cbs = buffer;
buffer = [];
cbs.forEach(cb);
pendingQueue = buffer;
}
var executeQueue = function () {
var cbs = pendingQueue;
pendingQueue = [];
cbs.forEach(cb);
};
var cycle = function () { return Promise.resolve().then(executeQueue); };
cycle();
buffer = {
push: function (x) {
pendingQueue.push(x);
cycle();
},
filter: function (filter) {
pendingQueue = pendingQueue.filter(filter);
return buffer;
},
};
},
};
return medium;
}
// eslint-disable-next-line @typescript-eslint/ban-types
function createSidecarMedium(options) {
if (options === void 0) { options = {}; }
var medium = innerCreateMedium(null);
medium.options = __assign({ async: true, ssr: false }, options);
return medium;
}
var SideCar$1 = function (_a) {
var sideCar = _a.sideCar, rest = __rest(_a, ["sideCar"]);
if (!sideCar) {
throw new Error('Sidecar: please provide `sideCar` property to import the right car');
}
var Target = sideCar.read();
if (!Target) {
throw new Error('Sidecar medium not found');
}
return reactExports.createElement(Target, __assign({}, rest));
};
SideCar$1.isSideCarExport = true;
function exportSidecar(medium, exported) {
medium.useMedium(exported);
return SideCar$1;
}
var effectCar = createSidecarMedium();
var nothing = function () {
return;
};
/**
* Removes scrollbar from the page and contain the scroll within the Lock
*/
var RemoveScroll = reactExports.forwardRef(function (props, parentRef) {
var ref = reactExports.useRef(null);
var _a = reactExports.useState({
onScrollCapture: nothing,
onWheelCapture: nothing,
onTouchMoveCapture: nothing,
}), callbacks = _a[0], setCallbacks = _a[1];
var forwardProps = props.forwardProps, children = props.children, className = props.className, removeScrollBar = props.removeScrollBar, enabled = props.enabled, shards = props.shards, sideCar = props.sideCar, noRelative = props.noRelative, noIsolation = props.noIsolation, inert = props.inert, allowPinchZoom = props.allowPinchZoom, _b = props.as, Container = _b === void 0 ? 'div' : _b, gapMode = props.gapMode, rest = __rest(props, ["forwardProps", "children", "className", "removeScrollBar", "enabled", "shards", "sideCar", "noRelative", "noIsolation", "inert", "allowPinchZoom", "as", "gapMode"]);
var SideCar = sideCar;
var containerRef = useMergeRefs([ref, parentRef]);
var containerProps = __assign(__assign({}, rest), callbacks);
return (reactExports.createElement(reactExports.Fragment, null,
enabled && (reactExports.createElement(SideCar, { sideCar: effectCar, removeScrollBar: removeScrollBar, shards: shards, noRelative: noRelative, noIsolation: noIsolation, inert: inert, setCallbacks: setCallbacks, allowPinchZoom: !!allowPinchZoom, lockRef: ref, gapMode: gapMode })),
forwardProps ? (reactExports.cloneElement(reactExports.Children.only(children), __assign(__assign({}, containerProps), { ref: containerRef }))) : (reactExports.createElement(Container, __assign({}, containerProps, { className: className, ref: containerRef }), children))));
});
RemoveScroll.defaultProps = {
enabled: true,
removeScrollBar: true,
inert: false,
};
RemoveScroll.classNames = {
fullWidth: fullWidthClassName,
zeroRight: zeroRightClassName,
};
var getNonce = function () {
if (typeof __webpack_nonce__ !== 'undefined') {
return __webpack_nonce__;
}
return undefined;
};
function makeStyleTag() {
if (!document)
return null;
var tag = document.createElement('style');
tag.type = 'text/css';
var nonce = getNonce();
if (nonce) {
tag.setAttribute('nonce', nonce);
}
return tag;
}
function injectStyles(tag, css) {
// @ts-ignore
if (tag.styleSheet) {
// @ts-ignore
tag.styleSheet.cssText = css;
}
else {
tag.appendChild(document.createTextNode(css));
}
}
function insertStyleTag(tag) {
var head = document.head || document.getElementsByTagName('head')[0];
head.appendChild(tag);
}
var stylesheetSingleton = function () {
var counter = 0;
var stylesheet = null;
return {
add: function (style) {
if (counter == 0) {
if ((stylesheet = makeStyleTag())) {
injectStyles(stylesheet, style);
insertStyleTag(stylesheet);
}
}
counter++;
},
remove: function () {
counter--;
if (!counter && stylesheet) {
stylesheet.parentNode && stylesheet.parentNode.removeChild(stylesheet);
stylesheet = null;
}
},
};
};
/**
* creates a hook to control style singleton
* @see {@link styleSingleton} for a safer component version
* @example
* ```tsx
* const useStyle = styleHookSingleton();
* ///
* useStyle('body { overflow: hidden}');
*/
var styleHookSingleton = function () {
var sheet = stylesheetSingleton();
return function (styles, isDynamic) {
reactExports.useEffect(function () {
sheet.add(styles);
return function () {
sheet.remove();
};
}, [styles && isDynamic]);
};
};
/**
* create a Component to add styles on demand
* - styles are added when first instance is mounted
* - styles are removed when the last instance is unmounted
* - changing styles in runtime does nothing unless dynamic is set. But with multiple components that can lead to the undefined behavior
*/
var styleSingleton = function () {
var useStyle = styleHookSingleton();
var Sheet = function (_a) {
var styles = _a.styles, dynamic = _a.dynamic;
useStyle(styles, dynamic);
return null;
};
return Sheet;
};
var zeroGap = {
left: 0,
top: 0,
right: 0,
gap: 0,
};
var parse = function (x) { return parseInt(x || '', 10) || 0; };
var getOffset = function (gapMode) {
var cs = window.getComputedStyle(document.body);
var left = cs[gapMode === 'padding' ? 'paddingLeft' : 'marginLeft'];
var top = cs[gapMode === 'padding' ? 'paddingTop' : 'marginTop'];
var right = cs[gapMode === 'padding' ? 'paddingRight' : 'marginRight'];
return [parse(left), parse(top), parse(right)];
};
var getGapWidth = function (gapMode) {
if (gapMode === void 0) { gapMode = 'margin'; }
if (typeof window === 'undefined') {
return zeroGap;
}
var offsets = getOffset(gapMode);
var documentWidth = document.documentElement.clientWidth;
var windowWidth = window.innerWidth;
return {
left: offsets[0],
top: offsets[1],
right: offsets[2],
gap: Math.max(0, windowWidth - documentWidth + offsets[2] - offsets[0]),
};
};
var Style = styleSingleton();
var lockAttribute = 'data-scroll-locked';
// important tip - once we measure scrollBar width and remove them
// we could not repeat this operation
// thus we are using style-singleton - only the first "yet correct" style will be applied.
var getStyles = function (_a, allowRelative, gapMode, important) {
var left = _a.left, top = _a.top, right = _a.right, gap = _a.gap;
if (gapMode === void 0) { gapMode = 'margin'; }
return "\n .".concat(noScrollbarsClassName, " {\n overflow: hidden ").concat(important, ";\n padding-right: ").concat(gap, "px ").concat(important, ";\n }\n body[").concat(lockAttribute, "] {\n overflow: hidden ").concat(important, ";\n overscroll-behavior: contain;\n ").concat([
allowRelative && "position: relative ".concat(important, ";"),
gapMode === 'margin' &&
"\n padding-left: ".concat(left, "px;\n padding-top: ").concat(top, "px;\n padding-right: ").concat(right, "px;\n margin-left:0;\n margin-top:0;\n margin-right: ").concat(gap, "px ").concat(important, ";\n "),
gapMode === 'padding' && "padding-right: ".concat(gap, "px ").concat(important, ";"),
]
.filter(Boolean)
.join(''), "\n }\n \n .").concat(zeroRightClassName, " {\n right: ").concat(gap, "px ").concat(important, ";\n }\n \n .").concat(fullWidthClassName, " {\n margin-right: ").concat(gap, "px ").concat(important, ";\n }\n \n .").concat(zeroRightClassName, " .").concat(zeroRightClassName, " {\n right: 0 ").concat(important, ";\n }\n \n .").concat(fullWidthClassName, " .").concat(fullWidthClassName, " {\n margin-right: 0 ").concat(important, ";\n }\n \n body[").concat(lockAttribute, "] {\n ").concat(removedBarSizeVariable, ": ").concat(gap, "px;\n }\n");
};
var getCurrentUseCounter = function () {
var counter = parseInt(document.body.getAttribute(lockAttribute) || '0', 10);
return isFinite(counter) ? counter : 0;
};
var useLockAttribute = function () {
reactExports.useEffect(function () {
document.body.setAttribute(lockAttribute, (getCurrentUseCounter() + 1).toString());
return function () {
var newCounter = getCurrentUseCounter() - 1;
if (newCounter <= 0) {
document.body.removeAttribute(lockAttribute);
}
else {
document.body.setAttribute(lockAttribute, newCounter.toString());
}
};
}, []);
};
/**
* Removes page scrollbar and blocks page scroll when mounted
*/
var RemoveScrollBar = function (_a) {
var noRelative = _a.noRelative, noImportant = _a.noImportant, _b = _a.gapMode, gapMode = _b === void 0 ? 'margin' : _b;
useLockAttribute();
/*
gap will be measured on every component mount
however it will be used only by the "first" invocation
due to singleton nature of