@tldraw/editor 5.3.2 → 5.4.0-canary.0063438d1c75

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist-cjs/index.d.ts +94 -52
  2. package/dist-cjs/index.js +3 -1
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/editor/Editor.js +86 -26
  5. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  6. package/dist-cjs/lib/editor/derivations/shapeIdsInCurrentPage.js +7 -3
  7. package/dist-cjs/lib/editor/derivations/shapeIdsInCurrentPage.js.map +2 -2
  8. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js +6 -3
  9. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +2 -2
  10. package/dist-cjs/lib/editor/managers/TickManager/TickManager.js +6 -3
  11. package/dist-cjs/lib/editor/managers/TickManager/TickManager.js.map +2 -2
  12. package/dist-cjs/lib/globals/editors.js +57 -0
  13. package/dist-cjs/lib/globals/editors.js.map +7 -0
  14. package/dist-cjs/lib/hooks/useViewportHeight.js.map +2 -2
  15. package/dist-cjs/lib/primitives/geometry/Group2d.js +63 -21
  16. package/dist-cjs/lib/primitives/geometry/Group2d.js.map +2 -2
  17. package/dist-cjs/lib/utils/normalizeWheel.js +2 -0
  18. package/dist-cjs/lib/utils/normalizeWheel.js.map +2 -2
  19. package/dist-cjs/version.js +3 -3
  20. package/dist-cjs/version.js.map +1 -1
  21. package/dist-esm/index.d.mts +94 -52
  22. package/dist-esm/index.mjs +3 -1
  23. package/dist-esm/index.mjs.map +2 -2
  24. package/dist-esm/lib/editor/Editor.mjs +86 -26
  25. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  26. package/dist-esm/lib/editor/derivations/shapeIdsInCurrentPage.mjs +7 -3
  27. package/dist-esm/lib/editor/derivations/shapeIdsInCurrentPage.mjs.map +2 -2
  28. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +7 -4
  29. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +2 -2
  30. package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs +6 -3
  31. package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs.map +2 -2
  32. package/dist-esm/lib/globals/editors.mjs +37 -0
  33. package/dist-esm/lib/globals/editors.mjs.map +7 -0
  34. package/dist-esm/lib/hooks/useViewportHeight.mjs.map +2 -2
  35. package/dist-esm/lib/primitives/geometry/Group2d.mjs +63 -21
  36. package/dist-esm/lib/primitives/geometry/Group2d.mjs.map +2 -2
  37. package/dist-esm/lib/utils/normalizeWheel.mjs +2 -0
  38. package/dist-esm/lib/utils/normalizeWheel.mjs.map +2 -2
  39. package/dist-esm/version.mjs +3 -3
  40. package/dist-esm/version.mjs.map +1 -1
  41. package/package.json +7 -7
  42. package/src/index.ts +2 -0
  43. package/src/lib/editor/Editor.ts +126 -37
  44. package/src/lib/editor/derivations/shapeIdsInCurrentPage.ts +7 -3
  45. package/src/lib/editor/managers/TextManager/TextManager.test.ts +42 -0
  46. package/src/lib/editor/managers/TextManager/TextManager.ts +10 -8
  47. package/src/lib/editor/managers/TickManager/TickManager.test.ts +13 -0
  48. package/src/lib/editor/managers/TickManager/TickManager.ts +9 -3
  49. package/src/lib/globals/editors.test.ts +76 -0
  50. package/src/lib/globals/editors.ts +53 -0
  51. package/src/lib/hooks/useViewportHeight.ts +9 -1
  52. package/src/lib/primitives/geometry/Group2d.ts +63 -27
  53. package/src/lib/utils/normalizeWheel.test.ts +39 -0
  54. package/src/lib/utils/normalizeWheel.ts +6 -0
  55. package/src/version.ts +3 -3
@@ -19,9 +19,13 @@ function deriveShapeIdsInCurrentPage(store, getCurrentPageId) {
19
19
  function fromScratch() {
20
20
  const currentPageId = getCurrentPageId();
21
21
  lastPageId = currentPageId;
22
- return new Set(
23
- [...shapesIndex.get()].filter((id) => isShapeInPage(store, currentPageId, store.get(id)))
24
- );
22
+ const result = /* @__PURE__ */ new Set();
23
+ for (const id of shapesIndex.get()) {
24
+ if (isShapeInPage(store, currentPageId, store.get(id))) {
25
+ result.add(id);
26
+ }
27
+ }
28
+ return result;
25
29
  }
26
30
  return computed("_shapeIdsInCurrentPage", (prevValue, lastComputedEpoch) => {
27
31
  if (isUninitialized(prevValue)) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/lib/editor/derivations/shapeIdsInCurrentPage.ts"],
4
- "sourcesContent": ["import { computed, isUninitialized, RESET_VALUE, withDiff } from '@tldraw/state'\nimport { IncrementalSetConstructor } from '@tldraw/store'\nimport {\n\tisPageId,\n\tisShape,\n\tisShapeId,\n\tTLPageId,\n\tTLShape,\n\tTLShapeId,\n\tTLStore,\n} from '@tldraw/tlschema'\n\n/**\n * Get whether a shape is in the current page.\n *\n * @param store - The tldraw store.\n * @param pageId - The id of the page to check.\n * @param shape - The the shape to check.\n */\nconst isShapeInPage = (store: TLStore, pageId: TLPageId, shape: TLShape): boolean => {\n\twhile (!isPageId(shape.parentId)) {\n\t\tconst parent = store.get(shape.parentId)\n\t\tif (!parent) return false\n\t\tshape = parent\n\t}\n\n\treturn shape.parentId === pageId\n}\n\n/**\n * A derivation that returns a list of shape ids in the current page.\n *\n * @param store - The tldraw store.\n * @param getCurrentPageId - A function that returns the current page id.\n */\nexport function deriveShapeIdsInCurrentPage(store: TLStore, getCurrentPageId: () => TLPageId) {\n\tconst shapesIndex = store.query.ids('shape')\n\tlet lastPageId: null | TLPageId = null\n\tfunction fromScratch() {\n\t\tconst currentPageId = getCurrentPageId()\n\t\tlastPageId = currentPageId\n\t\treturn new Set(\n\t\t\t[...shapesIndex.get()].filter((id) => isShapeInPage(store, currentPageId, store.get(id)!))\n\t\t)\n\t}\n\treturn computed<Set<TLShapeId>>('_shapeIdsInCurrentPage', (prevValue, lastComputedEpoch) => {\n\t\tif (isUninitialized(prevValue)) {\n\t\t\treturn fromScratch()\n\t\t}\n\n\t\tconst currentPageId = getCurrentPageId()\n\n\t\tif (currentPageId !== lastPageId) {\n\t\t\treturn fromScratch()\n\t\t}\n\n\t\tconst diff = store.history.getDiffSince(lastComputedEpoch)\n\n\t\tif (diff === RESET_VALUE) {\n\t\t\treturn fromScratch()\n\t\t}\n\n\t\tconst builder = new IncrementalSetConstructor<TLShapeId>(\n\t\t\tprevValue\n\t\t) as IncrementalSetConstructor<TLShapeId>\n\n\t\tfor (const changes of diff) {\n\t\t\tfor (const record of Object.values(changes.added)) {\n\t\t\t\tif (isShape(record) && isShapeInPage(store, currentPageId, record)) {\n\t\t\t\t\tbuilder.add(record.id)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfor (const [_from, to] of Object.values(changes.updated)) {\n\t\t\t\tif (isShape(to)) {\n\t\t\t\t\tif (isShapeInPage(store, currentPageId, to)) {\n\t\t\t\t\t\tbuilder.add(to.id)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tbuilder.remove(to.id)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfor (const id of Object.keys(changes.removed)) {\n\t\t\t\tif (isShapeId(id)) {\n\t\t\t\t\tbuilder.remove(id)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconst result = builder.get()\n\t\tif (!result) {\n\t\t\treturn prevValue\n\t\t}\n\n\t\treturn withDiff(result.value, result.diff)\n\t})\n}\n"],
5
- "mappings": "AAAA,SAAS,UAAU,iBAAiB,aAAa,gBAAgB;AACjE,SAAS,iCAAiC;AAC1C;AAAA,EACC;AAAA,EACA;AAAA,EACA;AAAA,OAKM;AASP,MAAM,gBAAgB,CAAC,OAAgB,QAAkB,UAA4B;AACpF,SAAO,CAAC,SAAS,MAAM,QAAQ,GAAG;AACjC,UAAM,SAAS,MAAM,IAAI,MAAM,QAAQ;AACvC,QAAI,CAAC,OAAQ,QAAO;AACpB,YAAQ;AAAA,EACT;AAEA,SAAO,MAAM,aAAa;AAC3B;AAQO,SAAS,4BAA4B,OAAgB,kBAAkC;AAC7F,QAAM,cAAc,MAAM,MAAM,IAAI,OAAO;AAC3C,MAAI,aAA8B;AAClC,WAAS,cAAc;AACtB,UAAM,gBAAgB,iBAAiB;AACvC,iBAAa;AACb,WAAO,IAAI;AAAA,MACV,CAAC,GAAG,YAAY,IAAI,CAAC,EAAE,OAAO,CAAC,OAAO,cAAc,OAAO,eAAe,MAAM,IAAI,EAAE,CAAE,CAAC;AAAA,IAC1F;AAAA,EACD;AACA,SAAO,SAAyB,0BAA0B,CAAC,WAAW,sBAAsB;AAC3F,QAAI,gBAAgB,SAAS,GAAG;AAC/B,aAAO,YAAY;AAAA,IACpB;AAEA,UAAM,gBAAgB,iBAAiB;AAEvC,QAAI,kBAAkB,YAAY;AACjC,aAAO,YAAY;AAAA,IACpB;AAEA,UAAM,OAAO,MAAM,QAAQ,aAAa,iBAAiB;AAEzD,QAAI,SAAS,aAAa;AACzB,aAAO,YAAY;AAAA,IACpB;AAEA,UAAM,UAAU,IAAI;AAAA,MACnB;AAAA,IACD;AAEA,eAAW,WAAW,MAAM;AAC3B,iBAAW,UAAU,OAAO,OAAO,QAAQ,KAAK,GAAG;AAClD,YAAI,QAAQ,MAAM,KAAK,cAAc,OAAO,eAAe,MAAM,GAAG;AACnE,kBAAQ,IAAI,OAAO,EAAE;AAAA,QACtB;AAAA,MACD;AAEA,iBAAW,CAAC,OAAO,EAAE,KAAK,OAAO,OAAO,QAAQ,OAAO,GAAG;AACzD,YAAI,QAAQ,EAAE,GAAG;AAChB,cAAI,cAAc,OAAO,eAAe,EAAE,GAAG;AAC5C,oBAAQ,IAAI,GAAG,EAAE;AAAA,UAClB,OAAO;AACN,oBAAQ,OAAO,GAAG,EAAE;AAAA,UACrB;AAAA,QACD;AAAA,MACD;AAEA,iBAAW,MAAM,OAAO,KAAK,QAAQ,OAAO,GAAG;AAC9C,YAAI,UAAU,EAAE,GAAG;AAClB,kBAAQ,OAAO,EAAE;AAAA,QAClB;AAAA,MACD;AAAA,IACD;AAEA,UAAM,SAAS,QAAQ,IAAI;AAC3B,QAAI,CAAC,QAAQ;AACZ,aAAO;AAAA,IACR;AAEA,WAAO,SAAS,OAAO,OAAO,OAAO,IAAI;AAAA,EAC1C,CAAC;AACF;",
4
+ "sourcesContent": ["import { computed, isUninitialized, RESET_VALUE, withDiff } from '@tldraw/state'\nimport { IncrementalSetConstructor } from '@tldraw/store'\nimport {\n\tisPageId,\n\tisShape,\n\tisShapeId,\n\tTLPageId,\n\tTLShape,\n\tTLShapeId,\n\tTLStore,\n} from '@tldraw/tlschema'\n\n/**\n * Get whether a shape is in the current page.\n *\n * @param store - The tldraw store.\n * @param pageId - The id of the page to check.\n * @param shape - The the shape to check.\n */\nconst isShapeInPage = (store: TLStore, pageId: TLPageId, shape: TLShape): boolean => {\n\twhile (!isPageId(shape.parentId)) {\n\t\tconst parent = store.get(shape.parentId)\n\t\tif (!parent) return false\n\t\tshape = parent\n\t}\n\n\treturn shape.parentId === pageId\n}\n\n/**\n * A derivation that returns a list of shape ids in the current page.\n *\n * @param store - The tldraw store.\n * @param getCurrentPageId - A function that returns the current page id.\n */\nexport function deriveShapeIdsInCurrentPage(store: TLStore, getCurrentPageId: () => TLPageId) {\n\tconst shapesIndex = store.query.ids('shape')\n\tlet lastPageId: null | TLPageId = null\n\tfunction fromScratch() {\n\t\tconst currentPageId = getCurrentPageId()\n\t\tlastPageId = currentPageId\n\t\tconst result = new Set<TLShapeId>()\n\t\tfor (const id of shapesIndex.get()) {\n\t\t\tif (isShapeInPage(store, currentPageId, store.get(id)!)) {\n\t\t\t\tresult.add(id)\n\t\t\t}\n\t\t}\n\t\treturn result\n\t}\n\treturn computed<Set<TLShapeId>>('_shapeIdsInCurrentPage', (prevValue, lastComputedEpoch) => {\n\t\tif (isUninitialized(prevValue)) {\n\t\t\treturn fromScratch()\n\t\t}\n\n\t\tconst currentPageId = getCurrentPageId()\n\n\t\tif (currentPageId !== lastPageId) {\n\t\t\treturn fromScratch()\n\t\t}\n\n\t\tconst diff = store.history.getDiffSince(lastComputedEpoch)\n\n\t\tif (diff === RESET_VALUE) {\n\t\t\treturn fromScratch()\n\t\t}\n\n\t\tconst builder = new IncrementalSetConstructor<TLShapeId>(\n\t\t\tprevValue\n\t\t) as IncrementalSetConstructor<TLShapeId>\n\n\t\tfor (const changes of diff) {\n\t\t\tfor (const record of Object.values(changes.added)) {\n\t\t\t\tif (isShape(record) && isShapeInPage(store, currentPageId, record)) {\n\t\t\t\t\tbuilder.add(record.id)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfor (const [_from, to] of Object.values(changes.updated)) {\n\t\t\t\tif (isShape(to)) {\n\t\t\t\t\tif (isShapeInPage(store, currentPageId, to)) {\n\t\t\t\t\t\tbuilder.add(to.id)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tbuilder.remove(to.id)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfor (const id of Object.keys(changes.removed)) {\n\t\t\t\tif (isShapeId(id)) {\n\t\t\t\t\tbuilder.remove(id)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconst result = builder.get()\n\t\tif (!result) {\n\t\t\treturn prevValue\n\t\t}\n\n\t\treturn withDiff(result.value, result.diff)\n\t})\n}\n"],
5
+ "mappings": "AAAA,SAAS,UAAU,iBAAiB,aAAa,gBAAgB;AACjE,SAAS,iCAAiC;AAC1C;AAAA,EACC;AAAA,EACA;AAAA,EACA;AAAA,OAKM;AASP,MAAM,gBAAgB,CAAC,OAAgB,QAAkB,UAA4B;AACpF,SAAO,CAAC,SAAS,MAAM,QAAQ,GAAG;AACjC,UAAM,SAAS,MAAM,IAAI,MAAM,QAAQ;AACvC,QAAI,CAAC,OAAQ,QAAO;AACpB,YAAQ;AAAA,EACT;AAEA,SAAO,MAAM,aAAa;AAC3B;AAQO,SAAS,4BAA4B,OAAgB,kBAAkC;AAC7F,QAAM,cAAc,MAAM,MAAM,IAAI,OAAO;AAC3C,MAAI,aAA8B;AAClC,WAAS,cAAc;AACtB,UAAM,gBAAgB,iBAAiB;AACvC,iBAAa;AACb,UAAM,SAAS,oBAAI,IAAe;AAClC,eAAW,MAAM,YAAY,IAAI,GAAG;AACnC,UAAI,cAAc,OAAO,eAAe,MAAM,IAAI,EAAE,CAAE,GAAG;AACxD,eAAO,IAAI,EAAE;AAAA,MACd;AAAA,IACD;AACA,WAAO;AAAA,EACR;AACA,SAAO,SAAyB,0BAA0B,CAAC,WAAW,sBAAsB;AAC3F,QAAI,gBAAgB,SAAS,GAAG;AAC/B,aAAO,YAAY;AAAA,IACpB;AAEA,UAAM,gBAAgB,iBAAiB;AAEvC,QAAI,kBAAkB,YAAY;AACjC,aAAO,YAAY;AAAA,IACpB;AAEA,UAAM,OAAO,MAAM,QAAQ,aAAa,iBAAiB;AAEzD,QAAI,SAAS,aAAa;AACzB,aAAO,YAAY;AAAA,IACpB;AAEA,UAAM,UAAU,IAAI;AAAA,MACnB;AAAA,IACD;AAEA,eAAW,WAAW,MAAM;AAC3B,iBAAW,UAAU,OAAO,OAAO,QAAQ,KAAK,GAAG;AAClD,YAAI,QAAQ,MAAM,KAAK,cAAc,OAAO,eAAe,MAAM,GAAG;AACnE,kBAAQ,IAAI,OAAO,EAAE;AAAA,QACtB;AAAA,MACD;AAEA,iBAAW,CAAC,OAAO,EAAE,KAAK,OAAO,OAAO,QAAQ,OAAO,GAAG;AACzD,YAAI,QAAQ,EAAE,GAAG;AAChB,cAAI,cAAc,OAAO,eAAe,EAAE,GAAG;AAC5C,oBAAQ,IAAI,GAAG,EAAE;AAAA,UAClB,OAAO;AACN,oBAAQ,OAAO,GAAG,EAAE;AAAA,UACrB;AAAA,QACD;AAAA,MACD;AAEA,iBAAW,MAAM,OAAO,KAAK,QAAQ,OAAO,GAAG;AAC9C,YAAI,UAAU,EAAE,GAAG;AAClB,kBAAQ,OAAO,EAAE;AAAA,QAClB;AAAA,MACD;AAAA,IACD;AAEA,UAAM,SAAS,QAAQ,IAAI;AAC3B,QAAI,CAAC,QAAQ;AACZ,aAAO;AAAA,IACR;AAEA,WAAO,SAAS,OAAO,OAAO,OAAO,IAAI;AAAA,EAC1C,CAAC;AACF;",
6
6
  "names": []
7
7
  }
@@ -1,4 +1,4 @@
1
- import { objectMapKeys } from "@tldraw/utils";
1
+ import { iterateGraphemes, objectMapKeys } from "@tldraw/utils";
2
2
  import { EditorManager } from "../EditorManager.mjs";
3
3
  function resolveLineHeightPx(fontSize, lineHeight) {
4
4
  return Math.round(fontSize * lineHeight);
@@ -16,7 +16,6 @@ const textAlignmentsForLtr = {
16
16
  "end-legacy": "right"
17
17
  };
18
18
  const spaceCharacterRegex = /\s/;
19
- const graphemeSegmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
20
19
  const initialDefaultStyles = Object.freeze({
21
20
  "overflow-wrap": "break-word",
22
21
  "word-break": "auto",
@@ -188,7 +187,7 @@ class TextManager extends EditorManager {
188
187
  let didTruncate = false;
189
188
  for (const childNode of element.childNodes) {
190
189
  if (childNode.nodeType !== Node.TEXT_NODE) continue;
191
- for (const { segment } of graphemeSegmenter.segment(childNode.textContent ?? "")) {
190
+ for (const segment of iterateGraphemes(childNode.textContent ?? "")) {
192
191
  range.setStart(textNode, idx);
193
192
  range.setEnd(textNode, idx + segment.length);
194
193
  const rects = range.getClientRects();
@@ -275,13 +274,17 @@ class TextManager extends EditorManager {
275
274
  });
276
275
  if (opts.overflow === "truncate-ellipsis" && didTruncate) {
277
276
  elm.textContent = "\u2026";
278
- const ellipsisWidth = Math.ceil(this.measureElementTextNodeSpans(elm).spans[0].box.w);
277
+ const ellipsisSpan = this.measureElementTextNodeSpans(elm).spans[0];
278
+ const ellipsisWidth = ellipsisSpan ? Math.ceil(ellipsisSpan.box.w) : 0;
279
279
  elm.style.setProperty("width", `${elementWidth - ellipsisWidth}px`);
280
280
  elm.textContent = normalizedText;
281
281
  const truncatedSpans = this.measureElementTextNodeSpans(elm, {
282
282
  shouldTruncateToFirstLine: true
283
283
  }).spans;
284
284
  const lastSpan = truncatedSpans[truncatedSpans.length - 1];
285
+ if (!lastSpan) {
286
+ return truncatedSpans;
287
+ }
285
288
  truncatedSpans.push({
286
289
  text: "\u2026",
287
290
  box: {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/lib/editor/managers/TextManager/TextManager.ts"],
4
- "sourcesContent": ["import { BoxModel, TLDefaultHorizontalAlignStyle } from '@tldraw/tlschema'\nimport { objectMapKeys } from '@tldraw/utils'\nimport type { Editor } from '../../Editor'\nimport { EditorManager } from '../EditorManager'\n\n/**\n * The whole-pixel line-height for a given font size and tldraw's unitless line-height\n * multiplier. tldraw's theme stores line-height as a multiplier (e.g. 1.35); resolving it\n * to a whole pixel keeps line spacing identical across rendering engines, which otherwise\n * disagree on fractional line boxes (WebKit snaps them to whole pixels, Blink keeps the\n * fraction) and let multi-line text drift apart. Apply it everywhere line-height is used \u2014\n * measurement, on-canvas render, and export \u2014 so geometry and rendering agree.\n * See https://github.com/tldraw/tldraw/issues/8970.\n *\n * @public\n */\nexport function resolveLineHeightPx(fontSize: number, lineHeight: number): number {\n\treturn Math.round(fontSize * lineHeight)\n}\n\nconst fixNewLines = /\\r?\\n|\\r/g\n\nfunction normalizeTextForDom(text: string) {\n\treturn text\n\t\t.replace(fixNewLines, '\\n')\n\t\t.split('\\n')\n\t\t.map((x) => x || ' ')\n\t\t.join('\\n')\n}\n\nconst textAlignmentsForLtr = {\n\tstart: 'left',\n\t'start-legacy': 'left',\n\tmiddle: 'center',\n\t'middle-legacy': 'center',\n\tend: 'right',\n\t'end-legacy': 'right',\n}\n\ninterface PoolItem {\n\tel: HTMLDivElement\n\thtml: string\n\tappliedStyleKeys: string[]\n}\n\n/** @public */\nexport interface BatchMeasurementRequest {\n\thtml: string\n\topts: TLMeasureTextOpts\n}\n\n/** @public */\nexport type TLMeasuredTextSize = BoxModel & {\n\tscrollWidth: number\n}\n\n/** @public */\nexport interface TLMeasureTextOpts {\n\tfontStyle: string\n\tfontWeight: string\n\tfontFamily: string\n\tfontSize: number\n\t/** This must be a number, e.g. 1.35, not a pixel value. */\n\tlineHeight: number\n\t/**\n\t * When maxWidth is a number, the text will be wrapped to that maxWidth. When maxWidth\n\t * is null, the text will be measured without wrapping, but explicit line breaks and\n\t * space are preserved.\n\t */\n\tmaxWidth: null | number\n\tminWidth?: null | number\n\t// todo: make this a number so that it is consistent with other TLMeasureTextSpanOpts\n\tpadding: string\n\totherStyles?: Record<string, string>\n\tdisableOverflowWrapBreaking?: boolean\n\tmeasureScrollWidth?: boolean\n}\n\n/** @public */\nexport interface TLMeasureTextSpanOpts {\n\toverflow: 'wrap' | 'truncate-ellipsis' | 'truncate-clip'\n\twidth: number\n\theight: number\n\tpadding: number\n\tfontSize: number\n\tfontWeight: string\n\tfontFamily: string\n\tfontStyle: string\n\tlineHeight: number\n\ttextAlign: TLDefaultHorizontalAlignStyle\n\totherStyles?: Record<string, string>\n\tmeasureScrollWidth?: boolean\n}\n\nconst spaceCharacterRegex = /\\s/\n\n// Iterate by grapheme cluster (e.g. emoji sequences, flags, accented letters)\n// rather than by code point, so a single glyph is measured as one unit.\nconst graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' })\n\nconst initialDefaultStyles = Object.freeze({\n\t'overflow-wrap': 'break-word',\n\t'word-break': 'auto',\n\twidth: null,\n\theight: null,\n\t'max-width': null,\n\t'min-width': null,\n})\n\n/** @public */\nexport class TextManager extends EditorManager {\n\tprivate elm: HTMLDivElement\n\tprivate poolElms: PoolItem[] = []\n\n\tconstructor(editor: Editor) {\n\t\tsuper(editor)\n\t\tthis.elm = this.createMeasurementEl()\n\t\tthis.editor.getContainer().appendChild(this.elm)\n\t\tthis.register(() => {\n\t\t\tthis.elm.remove()\n\t\t\tfor (const { el } of this.poolElms) {\n\t\t\t\tel.remove()\n\t\t\t}\n\t\t\tthis.poolElms.length = 0\n\t\t})\n\t}\n\n\tprivate createMeasurementEl(): HTMLDivElement {\n\t\tconst elm = this.editor.getContainerDocument().createElement('div')\n\t\telm.classList.add('tl-text')\n\t\telm.classList.add('tl-text-measure')\n\t\telm.setAttribute('dir', 'auto')\n\t\telm.tabIndex = -1\n\t\tfor (const key of objectMapKeys(initialDefaultStyles)) {\n\t\t\telm.style.setProperty(key, initialDefaultStyles[key])\n\t\t}\n\n\t\treturn elm\n\t}\n\n\tprivate resetElementStyles(el: HTMLElement, appliedStyleKeys: string[]) {\n\t\tfor (const key of appliedStyleKeys) {\n\t\t\tif (key in initialDefaultStyles) {\n\t\t\t\tel.style.setProperty(key, initialDefaultStyles[key as keyof typeof initialDefaultStyles])\n\t\t\t} else {\n\t\t\t\tel.style.removeProperty(key)\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate setElementStyles(el: HTMLElement, styles: Record<string, string | undefined | null>) {\n\t\ttype StyleValue = string | null\n\t\ttype RestoreEntry = [prop: string, value: StyleValue]\n\n\t\tconst restore: RestoreEntry[] = []\n\n\t\tfor (const [key, nextValue] of Object.entries(styles)) {\n\t\t\tconst oldValue = el.style.getPropertyValue(key)\n\n\t\t\tif (typeof nextValue === 'string') {\n\t\t\t\tif (oldValue === nextValue) continue\n\t\t\t\trestore.push([key, oldValue || null])\n\t\t\t\tel.style.setProperty(key, nextValue)\n\t\t\t} else {\n\t\t\t\tif (!oldValue) continue\n\t\t\t\trestore.push([key, oldValue])\n\t\t\t\tel.style.removeProperty(key)\n\t\t\t}\n\t\t}\n\n\t\treturn () => {\n\t\t\tfor (const [key, value] of restore) {\n\t\t\t\tif (value === null || value === '') el.style.removeProperty(key)\n\t\t\t\telse el.style.setProperty(key, value)\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate getMeasureStyles(opts: TLMeasureTextOpts): Record<string, string | undefined> {\n\t\treturn {\n\t\t\t'font-family': opts.fontFamily,\n\t\t\t'font-style': opts.fontStyle,\n\t\t\t'font-weight': opts.fontWeight,\n\t\t\t'font-size': opts.fontSize + 'px',\n\t\t\t'line-height': `${resolveLineHeightPx(opts.fontSize, opts.lineHeight)}px`,\n\t\t\t// Unitless multiplier consumed by the .tl-rich-text h1\u2013h6 rule (see editor.css),\n\t\t\t// so heading measurement matches on-canvas rendering.\n\t\t\t'--tl-rich-text-heading-line-height': `${opts.lineHeight}`,\n\t\t\tpadding: opts.padding,\n\t\t\t'max-width': opts.maxWidth ? opts.maxWidth + 'px' : undefined,\n\t\t\t'min-width': opts.minWidth ? opts.minWidth + 'px' : undefined,\n\t\t\t'overflow-wrap': opts.disableOverflowWrapBreaking ? 'normal' : 'break-word',\n\t\t\t...opts.otherStyles,\n\t\t}\n\t}\n\n\tprivate ensurePoolSize(size: number) {\n\t\tif (this.poolElms.length >= size) return\n\n\t\tconst fragment = this.editor.getContainerDocument().createDocumentFragment()\n\t\twhile (this.poolElms.length < size) {\n\t\t\tconst el = this.createMeasurementEl()\n\t\t\tthis.poolElms.push({ el, html: '', appliedStyleKeys: [] })\n\t\t\tfragment.appendChild(el)\n\t\t}\n\t\tthis.editor.getContainer().appendChild(fragment)\n\t}\n\n\tprivate getPoolItem(index: number): PoolItem {\n\t\tthis.ensurePoolSize(index + 1)\n\t\treturn this.poolElms[index]\n\t}\n\n\tmeasureHtmlBatch(requests: BatchMeasurementRequest[]): TLMeasuredTextSize[] {\n\t\tif (requests.length === 0) return []\n\n\t\twhile (this.poolElms.length > requests.length) {\n\t\t\tconst { el } = this.poolElms.pop()!\n\t\t\tel.remove()\n\t\t}\n\n\t\tfor (let i = 0; i < requests.length; i++) {\n\t\t\tconst { html, opts } = requests[i]\n\t\t\tconst poolItem = this.getPoolItem(i)\n\n\t\t\tconst { el } = poolItem\n\t\t\tthis.resetElementStyles(el, poolItem.appliedStyleKeys)\n\t\t\tconst styles = this.getMeasureStyles(opts)\n\t\t\tthis.setElementStyles(el, styles)\n\t\t\tpoolItem.appliedStyleKeys = Object.keys(styles)\n\t\t\t// Skip innerHTML parsing if the content hasn't changed\n\t\t\tif (poolItem.html !== html) {\n\t\t\t\tel.innerHTML = html\n\t\t\t\tpoolItem.html = html\n\t\t\t}\n\t\t}\n\n\t\tconst results: TLMeasuredTextSize[] = []\n\t\tfor (let i = 0; i < requests.length; i++) {\n\t\t\tconst el = this.getPoolItem(i).el\n\t\t\tconst scrollWidth = requests[i].opts.measureScrollWidth ? el.scrollWidth : 0\n\t\t\tconst rect = el.getBoundingClientRect()\n\t\t\tresults.push({\n\t\t\t\tx: 0,\n\t\t\t\ty: 0,\n\t\t\t\tw: rect.width,\n\t\t\t\th: rect.height,\n\t\t\t\tscrollWidth,\n\t\t\t})\n\t\t}\n\n\t\treturn results\n\t}\n\n\tmeasureText(textToMeasure: string, opts: TLMeasureTextOpts): TLMeasuredTextSize {\n\t\tconst div = this.editor.getContainerDocument().createElement('div')\n\t\tdiv.textContent = normalizeTextForDom(textToMeasure)\n\t\treturn this.measureHtml(div.innerHTML, opts)\n\t}\n\n\tmeasureHtml(html: string, opts: TLMeasureTextOpts): TLMeasuredTextSize {\n\t\tconst { elm } = this\n\n\t\tconst restoreStyles = this.setElementStyles(elm, this.getMeasureStyles(opts))\n\n\t\ttry {\n\t\t\telm.innerHTML = html\n\n\t\t\tconst scrollWidth = opts.measureScrollWidth ? elm.scrollWidth : 0\n\t\t\tconst rect = elm.getBoundingClientRect()\n\n\t\t\treturn {\n\t\t\t\tx: 0,\n\t\t\t\ty: 0,\n\t\t\t\tw: rect.width,\n\t\t\t\th: rect.height,\n\t\t\t\tscrollWidth,\n\t\t\t}\n\t\t} finally {\n\t\t\trestoreStyles()\n\t\t}\n\t}\n\n\t/**\n\t * Given an html element, measure the position of each span of unbroken\n\t * word/white-space characters within any text nodes it contains.\n\t */\n\tmeasureElementTextNodeSpans(\n\t\telement: HTMLElement,\n\t\t{ shouldTruncateToFirstLine = false }: { shouldTruncateToFirstLine?: boolean } = {}\n\t): { spans: { box: BoxModel; text: string }[]; didTruncate: boolean } {\n\t\tconst spans = []\n\n\t\t// Measurements of individual spans are relative to the containing element\n\t\tconst elmBounds = element.getBoundingClientRect()\n\t\tconst offsetX = -elmBounds.left\n\t\tconst offsetY = -elmBounds.top\n\n\t\t// we measure by creating a range that spans each character in the elements text node\n\t\tconst range = new Range()\n\t\tconst textNode = element.childNodes[0]\n\t\tlet idx = 0\n\n\t\tlet currentSpan = null\n\t\tlet prevCharWasSpaceCharacter = null\n\t\tlet prevCharTop = 0\n\t\tlet prevCharLeftForRTLTest = 0\n\t\tlet didTruncate = false\n\t\tfor (const childNode of element.childNodes) {\n\t\t\tif (childNode.nodeType !== Node.TEXT_NODE) continue\n\n\t\t\tfor (const { segment } of graphemeSegmenter.segment(childNode.textContent ?? '')) {\n\t\t\t\t// place the range around the grapheme we're interested in\n\t\t\t\trange.setStart(textNode, idx)\n\t\t\t\trange.setEnd(textNode, idx + segment.length)\n\t\t\t\t// measure the range. some browsers return multiple rects for the\n\t\t\t\t// first char in a new line - one for the line break, and one for\n\t\t\t\t// the character itself. we're only interested in the character.\n\t\t\t\tconst rects = range.getClientRects()\n\t\t\t\tconst rect = rects[rects.length - 1]\n\n\t\t\t\t// some graphemes produce no layout rectangle (e.g. zero-width\n\t\t\t\t// spaces or newlines). skip them rather than crashing, but keep\n\t\t\t\t// advancing the index so later graphemes stay aligned.\n\t\t\t\t// See https://github.com/tldraw/tldraw/issues/9112.\n\t\t\t\tif (!rect) {\n\t\t\t\t\tidx += segment.length\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\t// calculate the position of the character relative to the element\n\t\t\t\tconst top = rect.top + offsetY\n\t\t\t\tconst left = rect.left + offsetX\n\t\t\t\tconst right = rect.right + offsetX\n\t\t\t\tconst isRTL = left < prevCharLeftForRTLTest\n\n\t\t\t\tconst isSpaceCharacter = spaceCharacterRegex.test(segment)\n\t\t\t\tif (\n\t\t\t\t\t// If we're at a word boundary...\n\t\t\t\t\tisSpaceCharacter !== prevCharWasSpaceCharacter ||\n\t\t\t\t\t// ...or we're on a different line...\n\t\t\t\t\ttop !== prevCharTop ||\n\t\t\t\t\t// ...or we're at the start of the text and haven't created a span yet...\n\t\t\t\t\t!currentSpan\n\t\t\t\t) {\n\t\t\t\t\t// ...then we're at a span boundary!\n\n\t\t\t\t\tif (currentSpan) {\n\t\t\t\t\t\t// if we're truncating to a single line & we just finished the first line, stop there\n\t\t\t\t\t\tif (shouldTruncateToFirstLine && top !== prevCharTop) {\n\t\t\t\t\t\t\tdidTruncate = true\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// otherwise add the span to the list ready to start a new one\n\t\t\t\t\t\tspans.push(currentSpan)\n\t\t\t\t\t}\n\n\t\t\t\t\t// start a new span\n\t\t\t\t\tcurrentSpan = {\n\t\t\t\t\t\tbox: { x: left, y: top, w: rect.width, h: rect.height },\n\t\t\t\t\t\ttext: segment,\n\t\t\t\t\t}\n\t\t\t\t\tprevCharLeftForRTLTest = left\n\t\t\t\t} else {\n\t\t\t\t\t// Looks like we're in RTL mode, so we need to adjust the left position.\n\t\t\t\t\tif (isRTL) {\n\t\t\t\t\t\tcurrentSpan.box.x = left\n\t\t\t\t\t}\n\n\t\t\t\t\t// otherwise we just need to extend the current span with the next character\n\t\t\t\t\tcurrentSpan.box.w = isRTL ? currentSpan.box.w + rect.width : right - currentSpan.box.x\n\t\t\t\t\tcurrentSpan.text += segment\n\t\t\t\t}\n\n\t\t\t\tif (segment === '\\n') {\n\t\t\t\t\tprevCharLeftForRTLTest = 0\n\t\t\t\t}\n\n\t\t\t\tprevCharWasSpaceCharacter = isSpaceCharacter\n\t\t\t\tprevCharTop = top\n\t\t\t\tidx += segment.length\n\t\t\t}\n\t\t}\n\n\t\t// Add the last span\n\t\tif (currentSpan) {\n\t\t\tspans.push(currentSpan)\n\t\t}\n\n\t\treturn { spans, didTruncate }\n\t}\n\n\t/**\n\t * Measure text into individual spans. Spans are created by rendering the\n\t * text, then dividing it up according to line breaks and word boundaries.\n\t *\n\t * It works by having the browser render the text, then measuring the\n\t * position of each character. You can use this to replicate the text-layout\n\t * algorithm of the current browser in e.g. an SVG export.\n\t */\n\tmeasureTextSpans(\n\t\ttextToMeasure: string,\n\t\topts: TLMeasureTextSpanOpts\n\t): { text: string; box: BoxModel }[] {\n\t\tif (textToMeasure === '') return []\n\n\t\tconst { elm } = this\n\n\t\tconst shouldTruncateToFirstLine =\n\t\t\topts.overflow === 'truncate-ellipsis' || opts.overflow === 'truncate-clip'\n\t\tconst elementWidth = Math.ceil(opts.width - opts.padding * 2)\n\t\tconst newStyles = {\n\t\t\t'font-family': opts.fontFamily,\n\t\t\t'font-style': opts.fontStyle,\n\t\t\t'font-weight': opts.fontWeight,\n\t\t\t'font-size': opts.fontSize + 'px',\n\t\t\t'line-height': `${resolveLineHeightPx(opts.fontSize, opts.lineHeight)}px`,\n\t\t\twidth: `${elementWidth}px`,\n\t\t\theight: 'min-content',\n\t\t\t'text-align': textAlignmentsForLtr[opts.textAlign],\n\t\t\t'overflow-wrap': shouldTruncateToFirstLine ? 'anywhere' : 'break-word',\n\t\t\t'word-break': shouldTruncateToFirstLine ? 'break-all' : 'normal',\n\t\t\t...opts.otherStyles,\n\t\t}\n\t\tconst restoreStyles = this.setElementStyles(elm, newStyles)\n\n\t\ttry {\n\t\t\tconst normalizedText = normalizeTextForDom(textToMeasure)\n\n\t\t\t// Render the text into the measurement element:\n\t\t\telm.textContent = normalizedText\n\n\t\t\t// actually measure the text:\n\t\t\tconst { spans, didTruncate } = this.measureElementTextNodeSpans(elm, {\n\t\t\t\tshouldTruncateToFirstLine,\n\t\t\t})\n\n\t\t\tif (opts.overflow === 'truncate-ellipsis' && didTruncate) {\n\t\t\t\t// we need to measure the ellipsis to know how much space it takes up\n\t\t\t\telm.textContent = '\u2026'\n\t\t\t\tconst ellipsisWidth = Math.ceil(this.measureElementTextNodeSpans(elm).spans[0].box.w)\n\n\t\t\t\t// then, we need to subtract that space from the width we have and measure again:\n\t\t\t\telm.style.setProperty('width', `${elementWidth - ellipsisWidth}px`)\n\t\t\t\telm.textContent = normalizedText\n\t\t\t\tconst truncatedSpans = this.measureElementTextNodeSpans(elm, {\n\t\t\t\t\tshouldTruncateToFirstLine: true,\n\t\t\t\t}).spans\n\n\t\t\t\t// Finally, we add in our ellipsis at the end of the last span. We\n\t\t\t\t// have to do this after measuring, not before, because adding the\n\t\t\t\t// ellipsis changes how whitespace might be getting collapsed by the\n\t\t\t\t// browser.\n\t\t\t\tconst lastSpan = truncatedSpans[truncatedSpans.length - 1]!\n\t\t\t\ttruncatedSpans.push({\n\t\t\t\t\ttext: '\u2026',\n\t\t\t\t\tbox: {\n\t\t\t\t\t\tx: Math.min(lastSpan.box.x + lastSpan.box.w, opts.width - opts.padding - ellipsisWidth),\n\t\t\t\t\t\ty: lastSpan.box.y,\n\t\t\t\t\t\tw: ellipsisWidth,\n\t\t\t\t\t\th: lastSpan.box.h,\n\t\t\t\t\t},\n\t\t\t\t})\n\n\t\t\t\treturn truncatedSpans\n\t\t\t}\n\n\t\t\treturn spans\n\t\t} finally {\n\t\t\trestoreStyles()\n\t\t}\n\t}\n}\n"],
5
- "mappings": "AACA,SAAS,qBAAqB;AAE9B,SAAS,qBAAqB;AAavB,SAAS,oBAAoB,UAAkB,YAA4B;AACjF,SAAO,KAAK,MAAM,WAAW,UAAU;AACxC;AAEA,MAAM,cAAc;AAEpB,SAAS,oBAAoB,MAAc;AAC1C,SAAO,KACL,QAAQ,aAAa,IAAI,EACzB,MAAM,IAAI,EACV,IAAI,CAAC,MAAM,KAAK,GAAG,EACnB,KAAK,IAAI;AACZ;AAEA,MAAM,uBAAuB;AAAA,EAC5B,OAAO;AAAA,EACP,gBAAgB;AAAA,EAChB,QAAQ;AAAA,EACR,iBAAiB;AAAA,EACjB,KAAK;AAAA,EACL,cAAc;AACf;AAyDA,MAAM,sBAAsB;AAI5B,MAAM,oBAAoB,IAAI,KAAK,UAAU,QAAW,EAAE,aAAa,WAAW,CAAC;AAEnF,MAAM,uBAAuB,OAAO,OAAO;AAAA,EAC1C,iBAAiB;AAAA,EACjB,cAAc;AAAA,EACd,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,aAAa;AAAA,EACb,aAAa;AACd,CAAC;AAGM,MAAM,oBAAoB,cAAc;AAAA,EACtC;AAAA,EACA,WAAuB,CAAC;AAAA,EAEhC,YAAY,QAAgB;AAC3B,UAAM,MAAM;AACZ,SAAK,MAAM,KAAK,oBAAoB;AACpC,SAAK,OAAO,aAAa,EAAE,YAAY,KAAK,GAAG;AAC/C,SAAK,SAAS,MAAM;AACnB,WAAK,IAAI,OAAO;AAChB,iBAAW,EAAE,GAAG,KAAK,KAAK,UAAU;AACnC,WAAG,OAAO;AAAA,MACX;AACA,WAAK,SAAS,SAAS;AAAA,IACxB,CAAC;AAAA,EACF;AAAA,EAEQ,sBAAsC;AAC7C,UAAM,MAAM,KAAK,OAAO,qBAAqB,EAAE,cAAc,KAAK;AAClE,QAAI,UAAU,IAAI,SAAS;AAC3B,QAAI,UAAU,IAAI,iBAAiB;AACnC,QAAI,aAAa,OAAO,MAAM;AAC9B,QAAI,WAAW;AACf,eAAW,OAAO,cAAc,oBAAoB,GAAG;AACtD,UAAI,MAAM,YAAY,KAAK,qBAAqB,GAAG,CAAC;AAAA,IACrD;AAEA,WAAO;AAAA,EACR;AAAA,EAEQ,mBAAmB,IAAiB,kBAA4B;AACvE,eAAW,OAAO,kBAAkB;AACnC,UAAI,OAAO,sBAAsB;AAChC,WAAG,MAAM,YAAY,KAAK,qBAAqB,GAAwC,CAAC;AAAA,MACzF,OAAO;AACN,WAAG,MAAM,eAAe,GAAG;AAAA,MAC5B;AAAA,IACD;AAAA,EACD;AAAA,EAEQ,iBAAiB,IAAiB,QAAmD;AAI5F,UAAM,UAA0B,CAAC;AAEjC,eAAW,CAAC,KAAK,SAAS,KAAK,OAAO,QAAQ,MAAM,GAAG;AACtD,YAAM,WAAW,GAAG,MAAM,iBAAiB,GAAG;AAE9C,UAAI,OAAO,cAAc,UAAU;AAClC,YAAI,aAAa,UAAW;AAC5B,gBAAQ,KAAK,CAAC,KAAK,YAAY,IAAI,CAAC;AACpC,WAAG,MAAM,YAAY,KAAK,SAAS;AAAA,MACpC,OAAO;AACN,YAAI,CAAC,SAAU;AACf,gBAAQ,KAAK,CAAC,KAAK,QAAQ,CAAC;AAC5B,WAAG,MAAM,eAAe,GAAG;AAAA,MAC5B;AAAA,IACD;AAEA,WAAO,MAAM;AACZ,iBAAW,CAAC,KAAK,KAAK,KAAK,SAAS;AACnC,YAAI,UAAU,QAAQ,UAAU,GAAI,IAAG,MAAM,eAAe,GAAG;AAAA,YAC1D,IAAG,MAAM,YAAY,KAAK,KAAK;AAAA,MACrC;AAAA,IACD;AAAA,EACD;AAAA,EAEQ,iBAAiB,MAA6D;AACrF,WAAO;AAAA,MACN,eAAe,KAAK;AAAA,MACpB,cAAc,KAAK;AAAA,MACnB,eAAe,KAAK;AAAA,MACpB,aAAa,KAAK,WAAW;AAAA,MAC7B,eAAe,GAAG,oBAAoB,KAAK,UAAU,KAAK,UAAU,CAAC;AAAA;AAAA;AAAA,MAGrE,sCAAsC,GAAG,KAAK,UAAU;AAAA,MACxD,SAAS,KAAK;AAAA,MACd,aAAa,KAAK,WAAW,KAAK,WAAW,OAAO;AAAA,MACpD,aAAa,KAAK,WAAW,KAAK,WAAW,OAAO;AAAA,MACpD,iBAAiB,KAAK,8BAA8B,WAAW;AAAA,MAC/D,GAAG,KAAK;AAAA,IACT;AAAA,EACD;AAAA,EAEQ,eAAe,MAAc;AACpC,QAAI,KAAK,SAAS,UAAU,KAAM;AAElC,UAAM,WAAW,KAAK,OAAO,qBAAqB,EAAE,uBAAuB;AAC3E,WAAO,KAAK,SAAS,SAAS,MAAM;AACnC,YAAM,KAAK,KAAK,oBAAoB;AACpC,WAAK,SAAS,KAAK,EAAE,IAAI,MAAM,IAAI,kBAAkB,CAAC,EAAE,CAAC;AACzD,eAAS,YAAY,EAAE;AAAA,IACxB;AACA,SAAK,OAAO,aAAa,EAAE,YAAY,QAAQ;AAAA,EAChD;AAAA,EAEQ,YAAY,OAAyB;AAC5C,SAAK,eAAe,QAAQ,CAAC;AAC7B,WAAO,KAAK,SAAS,KAAK;AAAA,EAC3B;AAAA,EAEA,iBAAiB,UAA2D;AAC3E,QAAI,SAAS,WAAW,EAAG,QAAO,CAAC;AAEnC,WAAO,KAAK,SAAS,SAAS,SAAS,QAAQ;AAC9C,YAAM,EAAE,GAAG,IAAI,KAAK,SAAS,IAAI;AACjC,SAAG,OAAO;AAAA,IACX;AAEA,aAAS,IAAI,GAAG,IAAI,SAAS,QAAQ,KAAK;AACzC,YAAM,EAAE,MAAM,KAAK,IAAI,SAAS,CAAC;AACjC,YAAM,WAAW,KAAK,YAAY,CAAC;AAEnC,YAAM,EAAE,GAAG,IAAI;AACf,WAAK,mBAAmB,IAAI,SAAS,gBAAgB;AACrD,YAAM,SAAS,KAAK,iBAAiB,IAAI;AACzC,WAAK,iBAAiB,IAAI,MAAM;AAChC,eAAS,mBAAmB,OAAO,KAAK,MAAM;AAE9C,UAAI,SAAS,SAAS,MAAM;AAC3B,WAAG,YAAY;AACf,iBAAS,OAAO;AAAA,MACjB;AAAA,IACD;AAEA,UAAM,UAAgC,CAAC;AACvC,aAAS,IAAI,GAAG,IAAI,SAAS,QAAQ,KAAK;AACzC,YAAM,KAAK,KAAK,YAAY,CAAC,EAAE;AAC/B,YAAM,cAAc,SAAS,CAAC,EAAE,KAAK,qBAAqB,GAAG,cAAc;AAC3E,YAAM,OAAO,GAAG,sBAAsB;AACtC,cAAQ,KAAK;AAAA,QACZ,GAAG;AAAA,QACH,GAAG;AAAA,QACH,GAAG,KAAK;AAAA,QACR,GAAG,KAAK;AAAA,QACR;AAAA,MACD,CAAC;AAAA,IACF;AAEA,WAAO;AAAA,EACR;AAAA,EAEA,YAAY,eAAuB,MAA6C;AAC/E,UAAM,MAAM,KAAK,OAAO,qBAAqB,EAAE,cAAc,KAAK;AAClE,QAAI,cAAc,oBAAoB,aAAa;AACnD,WAAO,KAAK,YAAY,IAAI,WAAW,IAAI;AAAA,EAC5C;AAAA,EAEA,YAAY,MAAc,MAA6C;AACtE,UAAM,EAAE,IAAI,IAAI;AAEhB,UAAM,gBAAgB,KAAK,iBAAiB,KAAK,KAAK,iBAAiB,IAAI,CAAC;AAE5E,QAAI;AACH,UAAI,YAAY;AAEhB,YAAM,cAAc,KAAK,qBAAqB,IAAI,cAAc;AAChE,YAAM,OAAO,IAAI,sBAAsB;AAEvC,aAAO;AAAA,QACN,GAAG;AAAA,QACH,GAAG;AAAA,QACH,GAAG,KAAK;AAAA,QACR,GAAG,KAAK;AAAA,QACR;AAAA,MACD;AAAA,IACD,UAAE;AACD,oBAAc;AAAA,IACf;AAAA,EACD;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,4BACC,SACA,EAAE,4BAA4B,MAAM,IAA6C,CAAC,GACb;AACrE,UAAM,QAAQ,CAAC;AAGf,UAAM,YAAY,QAAQ,sBAAsB;AAChD,UAAM,UAAU,CAAC,UAAU;AAC3B,UAAM,UAAU,CAAC,UAAU;AAG3B,UAAM,QAAQ,IAAI,MAAM;AACxB,UAAM,WAAW,QAAQ,WAAW,CAAC;AACrC,QAAI,MAAM;AAEV,QAAI,cAAc;AAClB,QAAI,4BAA4B;AAChC,QAAI,cAAc;AAClB,QAAI,yBAAyB;AAC7B,QAAI,cAAc;AAClB,eAAW,aAAa,QAAQ,YAAY;AAC3C,UAAI,UAAU,aAAa,KAAK,UAAW;AAE3C,iBAAW,EAAE,QAAQ,KAAK,kBAAkB,QAAQ,UAAU,eAAe,EAAE,GAAG;AAEjF,cAAM,SAAS,UAAU,GAAG;AAC5B,cAAM,OAAO,UAAU,MAAM,QAAQ,MAAM;AAI3C,cAAM,QAAQ,MAAM,eAAe;AACnC,cAAM,OAAO,MAAM,MAAM,SAAS,CAAC;AAMnC,YAAI,CAAC,MAAM;AACV,iBAAO,QAAQ;AACf;AAAA,QACD;AAGA,cAAM,MAAM,KAAK,MAAM;AACvB,cAAM,OAAO,KAAK,OAAO;AACzB,cAAM,QAAQ,KAAK,QAAQ;AAC3B,cAAM,QAAQ,OAAO;AAErB,cAAM,mBAAmB,oBAAoB,KAAK,OAAO;AACzD;AAAA;AAAA,UAEC,qBAAqB;AAAA,UAErB,QAAQ;AAAA,UAER,CAAC;AAAA,UACA;AAGD,cAAI,aAAa;AAEhB,gBAAI,6BAA6B,QAAQ,aAAa;AACrD,4BAAc;AACd;AAAA,YACD;AAEA,kBAAM,KAAK,WAAW;AAAA,UACvB;AAGA,wBAAc;AAAA,YACb,KAAK,EAAE,GAAG,MAAM,GAAG,KAAK,GAAG,KAAK,OAAO,GAAG,KAAK,OAAO;AAAA,YACtD,MAAM;AAAA,UACP;AACA,mCAAyB;AAAA,QAC1B,OAAO;AAEN,cAAI,OAAO;AACV,wBAAY,IAAI,IAAI;AAAA,UACrB;AAGA,sBAAY,IAAI,IAAI,QAAQ,YAAY,IAAI,IAAI,KAAK,QAAQ,QAAQ,YAAY,IAAI;AACrF,sBAAY,QAAQ;AAAA,QACrB;AAEA,YAAI,YAAY,MAAM;AACrB,mCAAyB;AAAA,QAC1B;AAEA,oCAA4B;AAC5B,sBAAc;AACd,eAAO,QAAQ;AAAA,MAChB;AAAA,IACD;AAGA,QAAI,aAAa;AAChB,YAAM,KAAK,WAAW;AAAA,IACvB;AAEA,WAAO,EAAE,OAAO,YAAY;AAAA,EAC7B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,iBACC,eACA,MACoC;AACpC,QAAI,kBAAkB,GAAI,QAAO,CAAC;AAElC,UAAM,EAAE,IAAI,IAAI;AAEhB,UAAM,4BACL,KAAK,aAAa,uBAAuB,KAAK,aAAa;AAC5D,UAAM,eAAe,KAAK,KAAK,KAAK,QAAQ,KAAK,UAAU,CAAC;AAC5D,UAAM,YAAY;AAAA,MACjB,eAAe,KAAK;AAAA,MACpB,cAAc,KAAK;AAAA,MACnB,eAAe,KAAK;AAAA,MACpB,aAAa,KAAK,WAAW;AAAA,MAC7B,eAAe,GAAG,oBAAoB,KAAK,UAAU,KAAK,UAAU,CAAC;AAAA,MACrE,OAAO,GAAG,YAAY;AAAA,MACtB,QAAQ;AAAA,MACR,cAAc,qBAAqB,KAAK,SAAS;AAAA,MACjD,iBAAiB,4BAA4B,aAAa;AAAA,MAC1D,cAAc,4BAA4B,cAAc;AAAA,MACxD,GAAG,KAAK;AAAA,IACT;AACA,UAAM,gBAAgB,KAAK,iBAAiB,KAAK,SAAS;AAE1D,QAAI;AACH,YAAM,iBAAiB,oBAAoB,aAAa;AAGxD,UAAI,cAAc;AAGlB,YAAM,EAAE,OAAO,YAAY,IAAI,KAAK,4BAA4B,KAAK;AAAA,QACpE;AAAA,MACD,CAAC;AAED,UAAI,KAAK,aAAa,uBAAuB,aAAa;AAEzD,YAAI,cAAc;AAClB,cAAM,gBAAgB,KAAK,KAAK,KAAK,4BAA4B,GAAG,EAAE,MAAM,CAAC,EAAE,IAAI,CAAC;AAGpF,YAAI,MAAM,YAAY,SAAS,GAAG,eAAe,aAAa,IAAI;AAClE,YAAI,cAAc;AAClB,cAAM,iBAAiB,KAAK,4BAA4B,KAAK;AAAA,UAC5D,2BAA2B;AAAA,QAC5B,CAAC,EAAE;AAMH,cAAM,WAAW,eAAe,eAAe,SAAS,CAAC;AACzD,uBAAe,KAAK;AAAA,UACnB,MAAM;AAAA,UACN,KAAK;AAAA,YACJ,GAAG,KAAK,IAAI,SAAS,IAAI,IAAI,SAAS,IAAI,GAAG,KAAK,QAAQ,KAAK,UAAU,aAAa;AAAA,YACtF,GAAG,SAAS,IAAI;AAAA,YAChB,GAAG;AAAA,YACH,GAAG,SAAS,IAAI;AAAA,UACjB;AAAA,QACD,CAAC;AAED,eAAO;AAAA,MACR;AAEA,aAAO;AAAA,IACR,UAAE;AACD,oBAAc;AAAA,IACf;AAAA,EACD;AACD;",
4
+ "sourcesContent": ["import { BoxModel, TLDefaultHorizontalAlignStyle } from '@tldraw/tlschema'\nimport { iterateGraphemes, objectMapKeys } from '@tldraw/utils'\nimport type { Editor } from '../../Editor'\nimport { EditorManager } from '../EditorManager'\n\n/**\n * The whole-pixel line-height for a given font size and tldraw's unitless line-height\n * multiplier. tldraw's theme stores line-height as a multiplier (e.g. 1.35); resolving it\n * to a whole pixel keeps line spacing identical across rendering engines, which otherwise\n * disagree on fractional line boxes (WebKit snaps them to whole pixels, Blink keeps the\n * fraction) and let multi-line text drift apart. Apply it everywhere line-height is used \u2014\n * measurement, on-canvas render, and export \u2014 so geometry and rendering agree.\n * See https://github.com/tldraw/tldraw/issues/8970.\n *\n * @public\n */\nexport function resolveLineHeightPx(fontSize: number, lineHeight: number): number {\n\treturn Math.round(fontSize * lineHeight)\n}\n\nconst fixNewLines = /\\r?\\n|\\r/g\n\nfunction normalizeTextForDom(text: string) {\n\treturn text\n\t\t.replace(fixNewLines, '\\n')\n\t\t.split('\\n')\n\t\t.map((x) => x || ' ')\n\t\t.join('\\n')\n}\n\nconst textAlignmentsForLtr = {\n\tstart: 'left',\n\t'start-legacy': 'left',\n\tmiddle: 'center',\n\t'middle-legacy': 'center',\n\tend: 'right',\n\t'end-legacy': 'right',\n}\n\ninterface PoolItem {\n\tel: HTMLDivElement\n\thtml: string\n\tappliedStyleKeys: string[]\n}\n\n/** @public */\nexport interface BatchMeasurementRequest {\n\thtml: string\n\topts: TLMeasureTextOpts\n}\n\n/** @public */\nexport type TLMeasuredTextSize = BoxModel & {\n\tscrollWidth: number\n}\n\n/** @public */\nexport interface TLMeasureTextOpts {\n\tfontStyle: string\n\tfontWeight: string\n\tfontFamily: string\n\tfontSize: number\n\t/** This must be a number, e.g. 1.35, not a pixel value. */\n\tlineHeight: number\n\t/**\n\t * When maxWidth is a number, the text will be wrapped to that maxWidth. When maxWidth\n\t * is null, the text will be measured without wrapping, but explicit line breaks and\n\t * space are preserved.\n\t */\n\tmaxWidth: null | number\n\tminWidth?: null | number\n\t// todo: make this a number so that it is consistent with other TLMeasureTextSpanOpts\n\tpadding: string\n\totherStyles?: Record<string, string>\n\tdisableOverflowWrapBreaking?: boolean\n\tmeasureScrollWidth?: boolean\n}\n\n/** @public */\nexport interface TLMeasureTextSpanOpts {\n\toverflow: 'wrap' | 'truncate-ellipsis' | 'truncate-clip'\n\twidth: number\n\theight: number\n\tpadding: number\n\tfontSize: number\n\tfontWeight: string\n\tfontFamily: string\n\tfontStyle: string\n\tlineHeight: number\n\ttextAlign: TLDefaultHorizontalAlignStyle\n\totherStyles?: Record<string, string>\n\tmeasureScrollWidth?: boolean\n}\n\nconst spaceCharacterRegex = /\\s/\n\nconst initialDefaultStyles = Object.freeze({\n\t'overflow-wrap': 'break-word',\n\t'word-break': 'auto',\n\twidth: null,\n\theight: null,\n\t'max-width': null,\n\t'min-width': null,\n})\n\n/** @public */\nexport class TextManager extends EditorManager {\n\tprivate elm: HTMLDivElement\n\tprivate poolElms: PoolItem[] = []\n\n\tconstructor(editor: Editor) {\n\t\tsuper(editor)\n\t\tthis.elm = this.createMeasurementEl()\n\t\tthis.editor.getContainer().appendChild(this.elm)\n\t\tthis.register(() => {\n\t\t\tthis.elm.remove()\n\t\t\tfor (const { el } of this.poolElms) {\n\t\t\t\tel.remove()\n\t\t\t}\n\t\t\tthis.poolElms.length = 0\n\t\t})\n\t}\n\n\tprivate createMeasurementEl(): HTMLDivElement {\n\t\tconst elm = this.editor.getContainerDocument().createElement('div')\n\t\telm.classList.add('tl-text')\n\t\telm.classList.add('tl-text-measure')\n\t\telm.setAttribute('dir', 'auto')\n\t\telm.tabIndex = -1\n\t\tfor (const key of objectMapKeys(initialDefaultStyles)) {\n\t\t\telm.style.setProperty(key, initialDefaultStyles[key])\n\t\t}\n\n\t\treturn elm\n\t}\n\n\tprivate resetElementStyles(el: HTMLElement, appliedStyleKeys: string[]) {\n\t\tfor (const key of appliedStyleKeys) {\n\t\t\tif (key in initialDefaultStyles) {\n\t\t\t\tel.style.setProperty(key, initialDefaultStyles[key as keyof typeof initialDefaultStyles])\n\t\t\t} else {\n\t\t\t\tel.style.removeProperty(key)\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate setElementStyles(el: HTMLElement, styles: Record<string, string | undefined | null>) {\n\t\ttype StyleValue = string | null\n\t\ttype RestoreEntry = [prop: string, value: StyleValue]\n\n\t\tconst restore: RestoreEntry[] = []\n\n\t\tfor (const [key, nextValue] of Object.entries(styles)) {\n\t\t\tconst oldValue = el.style.getPropertyValue(key)\n\n\t\t\tif (typeof nextValue === 'string') {\n\t\t\t\tif (oldValue === nextValue) continue\n\t\t\t\trestore.push([key, oldValue || null])\n\t\t\t\tel.style.setProperty(key, nextValue)\n\t\t\t} else {\n\t\t\t\tif (!oldValue) continue\n\t\t\t\trestore.push([key, oldValue])\n\t\t\t\tel.style.removeProperty(key)\n\t\t\t}\n\t\t}\n\n\t\treturn () => {\n\t\t\tfor (const [key, value] of restore) {\n\t\t\t\tif (value === null || value === '') el.style.removeProperty(key)\n\t\t\t\telse el.style.setProperty(key, value)\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate getMeasureStyles(opts: TLMeasureTextOpts): Record<string, string | undefined> {\n\t\treturn {\n\t\t\t'font-family': opts.fontFamily,\n\t\t\t'font-style': opts.fontStyle,\n\t\t\t'font-weight': opts.fontWeight,\n\t\t\t'font-size': opts.fontSize + 'px',\n\t\t\t'line-height': `${resolveLineHeightPx(opts.fontSize, opts.lineHeight)}px`,\n\t\t\t// Unitless multiplier consumed by the .tl-rich-text h1\u2013h6 rule (see editor.css),\n\t\t\t// so heading measurement matches on-canvas rendering.\n\t\t\t'--tl-rich-text-heading-line-height': `${opts.lineHeight}`,\n\t\t\tpadding: opts.padding,\n\t\t\t'max-width': opts.maxWidth ? opts.maxWidth + 'px' : undefined,\n\t\t\t'min-width': opts.minWidth ? opts.minWidth + 'px' : undefined,\n\t\t\t'overflow-wrap': opts.disableOverflowWrapBreaking ? 'normal' : 'break-word',\n\t\t\t...opts.otherStyles,\n\t\t}\n\t}\n\n\tprivate ensurePoolSize(size: number) {\n\t\tif (this.poolElms.length >= size) return\n\n\t\tconst fragment = this.editor.getContainerDocument().createDocumentFragment()\n\t\twhile (this.poolElms.length < size) {\n\t\t\tconst el = this.createMeasurementEl()\n\t\t\tthis.poolElms.push({ el, html: '', appliedStyleKeys: [] })\n\t\t\tfragment.appendChild(el)\n\t\t}\n\t\tthis.editor.getContainer().appendChild(fragment)\n\t}\n\n\tprivate getPoolItem(index: number): PoolItem {\n\t\tthis.ensurePoolSize(index + 1)\n\t\treturn this.poolElms[index]\n\t}\n\n\tmeasureHtmlBatch(requests: BatchMeasurementRequest[]): TLMeasuredTextSize[] {\n\t\tif (requests.length === 0) return []\n\n\t\twhile (this.poolElms.length > requests.length) {\n\t\t\tconst { el } = this.poolElms.pop()!\n\t\t\tel.remove()\n\t\t}\n\n\t\tfor (let i = 0; i < requests.length; i++) {\n\t\t\tconst { html, opts } = requests[i]\n\t\t\tconst poolItem = this.getPoolItem(i)\n\n\t\t\tconst { el } = poolItem\n\t\t\tthis.resetElementStyles(el, poolItem.appliedStyleKeys)\n\t\t\tconst styles = this.getMeasureStyles(opts)\n\t\t\tthis.setElementStyles(el, styles)\n\t\t\tpoolItem.appliedStyleKeys = Object.keys(styles)\n\t\t\t// Skip innerHTML parsing if the content hasn't changed\n\t\t\tif (poolItem.html !== html) {\n\t\t\t\tel.innerHTML = html\n\t\t\t\tpoolItem.html = html\n\t\t\t}\n\t\t}\n\n\t\tconst results: TLMeasuredTextSize[] = []\n\t\tfor (let i = 0; i < requests.length; i++) {\n\t\t\tconst el = this.getPoolItem(i).el\n\t\t\tconst scrollWidth = requests[i].opts.measureScrollWidth ? el.scrollWidth : 0\n\t\t\tconst rect = el.getBoundingClientRect()\n\t\t\tresults.push({\n\t\t\t\tx: 0,\n\t\t\t\ty: 0,\n\t\t\t\tw: rect.width,\n\t\t\t\th: rect.height,\n\t\t\t\tscrollWidth,\n\t\t\t})\n\t\t}\n\n\t\treturn results\n\t}\n\n\tmeasureText(textToMeasure: string, opts: TLMeasureTextOpts): TLMeasuredTextSize {\n\t\tconst div = this.editor.getContainerDocument().createElement('div')\n\t\tdiv.textContent = normalizeTextForDom(textToMeasure)\n\t\treturn this.measureHtml(div.innerHTML, opts)\n\t}\n\n\tmeasureHtml(html: string, opts: TLMeasureTextOpts): TLMeasuredTextSize {\n\t\tconst { elm } = this\n\n\t\tconst restoreStyles = this.setElementStyles(elm, this.getMeasureStyles(opts))\n\n\t\ttry {\n\t\t\telm.innerHTML = html\n\n\t\t\tconst scrollWidth = opts.measureScrollWidth ? elm.scrollWidth : 0\n\t\t\tconst rect = elm.getBoundingClientRect()\n\n\t\t\treturn {\n\t\t\t\tx: 0,\n\t\t\t\ty: 0,\n\t\t\t\tw: rect.width,\n\t\t\t\th: rect.height,\n\t\t\t\tscrollWidth,\n\t\t\t}\n\t\t} finally {\n\t\t\trestoreStyles()\n\t\t}\n\t}\n\n\t/**\n\t * Given an html element, measure the position of each span of unbroken\n\t * word/white-space characters within any text nodes it contains.\n\t */\n\tmeasureElementTextNodeSpans(\n\t\telement: HTMLElement,\n\t\t{ shouldTruncateToFirstLine = false }: { shouldTruncateToFirstLine?: boolean } = {}\n\t): { spans: { box: BoxModel; text: string }[]; didTruncate: boolean } {\n\t\tconst spans = []\n\n\t\t// Measurements of individual spans are relative to the containing element\n\t\tconst elmBounds = element.getBoundingClientRect()\n\t\tconst offsetX = -elmBounds.left\n\t\tconst offsetY = -elmBounds.top\n\n\t\t// we measure by creating a range that spans each character in the elements text node\n\t\tconst range = new Range()\n\t\tconst textNode = element.childNodes[0]\n\t\tlet idx = 0\n\n\t\tlet currentSpan = null\n\t\tlet prevCharWasSpaceCharacter = null\n\t\tlet prevCharTop = 0\n\t\tlet prevCharLeftForRTLTest = 0\n\t\tlet didTruncate = false\n\t\tfor (const childNode of element.childNodes) {\n\t\t\tif (childNode.nodeType !== Node.TEXT_NODE) continue\n\n\t\t\tfor (const segment of iterateGraphemes(childNode.textContent ?? '')) {\n\t\t\t\t// place the range around the grapheme we're interested in\n\t\t\t\trange.setStart(textNode, idx)\n\t\t\t\trange.setEnd(textNode, idx + segment.length)\n\t\t\t\t// measure the range. some browsers return multiple rects for the\n\t\t\t\t// first char in a new line - one for the line break, and one for\n\t\t\t\t// the character itself. we're only interested in the character.\n\t\t\t\tconst rects = range.getClientRects()\n\t\t\t\tconst rect = rects[rects.length - 1]\n\n\t\t\t\t// some graphemes produce no layout rectangle (e.g. zero-width\n\t\t\t\t// spaces or newlines). skip them rather than crashing, but keep\n\t\t\t\t// advancing the index so later graphemes stay aligned.\n\t\t\t\t// See https://github.com/tldraw/tldraw/issues/9112.\n\t\t\t\tif (!rect) {\n\t\t\t\t\tidx += segment.length\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\t// calculate the position of the character relative to the element\n\t\t\t\tconst top = rect.top + offsetY\n\t\t\t\tconst left = rect.left + offsetX\n\t\t\t\tconst right = rect.right + offsetX\n\t\t\t\tconst isRTL = left < prevCharLeftForRTLTest\n\n\t\t\t\tconst isSpaceCharacter = spaceCharacterRegex.test(segment)\n\t\t\t\tif (\n\t\t\t\t\t// If we're at a word boundary...\n\t\t\t\t\tisSpaceCharacter !== prevCharWasSpaceCharacter ||\n\t\t\t\t\t// ...or we're on a different line...\n\t\t\t\t\ttop !== prevCharTop ||\n\t\t\t\t\t// ...or we're at the start of the text and haven't created a span yet...\n\t\t\t\t\t!currentSpan\n\t\t\t\t) {\n\t\t\t\t\t// ...then we're at a span boundary!\n\n\t\t\t\t\tif (currentSpan) {\n\t\t\t\t\t\t// if we're truncating to a single line & we just finished the first line, stop there\n\t\t\t\t\t\tif (shouldTruncateToFirstLine && top !== prevCharTop) {\n\t\t\t\t\t\t\tdidTruncate = true\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// otherwise add the span to the list ready to start a new one\n\t\t\t\t\t\tspans.push(currentSpan)\n\t\t\t\t\t}\n\n\t\t\t\t\t// start a new span\n\t\t\t\t\tcurrentSpan = {\n\t\t\t\t\t\tbox: { x: left, y: top, w: rect.width, h: rect.height },\n\t\t\t\t\t\ttext: segment,\n\t\t\t\t\t}\n\t\t\t\t\tprevCharLeftForRTLTest = left\n\t\t\t\t} else {\n\t\t\t\t\t// Looks like we're in RTL mode, so we need to adjust the left position.\n\t\t\t\t\tif (isRTL) {\n\t\t\t\t\t\tcurrentSpan.box.x = left\n\t\t\t\t\t}\n\n\t\t\t\t\t// otherwise we just need to extend the current span with the next character\n\t\t\t\t\tcurrentSpan.box.w = isRTL ? currentSpan.box.w + rect.width : right - currentSpan.box.x\n\t\t\t\t\tcurrentSpan.text += segment\n\t\t\t\t}\n\n\t\t\t\tif (segment === '\\n') {\n\t\t\t\t\tprevCharLeftForRTLTest = 0\n\t\t\t\t}\n\n\t\t\t\tprevCharWasSpaceCharacter = isSpaceCharacter\n\t\t\t\tprevCharTop = top\n\t\t\t\tidx += segment.length\n\t\t\t}\n\t\t}\n\n\t\t// Add the last span\n\t\tif (currentSpan) {\n\t\t\tspans.push(currentSpan)\n\t\t}\n\n\t\treturn { spans, didTruncate }\n\t}\n\n\t/**\n\t * Measure text into individual spans. Spans are created by rendering the\n\t * text, then dividing it up according to line breaks and word boundaries.\n\t *\n\t * It works by having the browser render the text, then measuring the\n\t * position of each character. You can use this to replicate the text-layout\n\t * algorithm of the current browser in e.g. an SVG export.\n\t */\n\tmeasureTextSpans(\n\t\ttextToMeasure: string,\n\t\topts: TLMeasureTextSpanOpts\n\t): { text: string; box: BoxModel }[] {\n\t\tif (textToMeasure === '') return []\n\n\t\tconst { elm } = this\n\n\t\tconst shouldTruncateToFirstLine =\n\t\t\topts.overflow === 'truncate-ellipsis' || opts.overflow === 'truncate-clip'\n\t\tconst elementWidth = Math.ceil(opts.width - opts.padding * 2)\n\t\tconst newStyles = {\n\t\t\t'font-family': opts.fontFamily,\n\t\t\t'font-style': opts.fontStyle,\n\t\t\t'font-weight': opts.fontWeight,\n\t\t\t'font-size': opts.fontSize + 'px',\n\t\t\t'line-height': `${resolveLineHeightPx(opts.fontSize, opts.lineHeight)}px`,\n\t\t\twidth: `${elementWidth}px`,\n\t\t\theight: 'min-content',\n\t\t\t'text-align': textAlignmentsForLtr[opts.textAlign],\n\t\t\t'overflow-wrap': shouldTruncateToFirstLine ? 'anywhere' : 'break-word',\n\t\t\t'word-break': shouldTruncateToFirstLine ? 'break-all' : 'normal',\n\t\t\t...opts.otherStyles,\n\t\t}\n\t\tconst restoreStyles = this.setElementStyles(elm, newStyles)\n\n\t\ttry {\n\t\t\tconst normalizedText = normalizeTextForDom(textToMeasure)\n\n\t\t\t// Render the text into the measurement element:\n\t\t\telm.textContent = normalizedText\n\n\t\t\t// actually measure the text:\n\t\t\tconst { spans, didTruncate } = this.measureElementTextNodeSpans(elm, {\n\t\t\t\tshouldTruncateToFirstLine,\n\t\t\t})\n\n\t\t\tif (opts.overflow === 'truncate-ellipsis' && didTruncate) {\n\t\t\t\t// we need to measure the ellipsis to know how much space it takes up\n\t\t\t\telm.textContent = '\u2026'\n\t\t\t\tconst ellipsisSpan = this.measureElementTextNodeSpans(elm).spans[0]\n\t\t\t\tconst ellipsisWidth = ellipsisSpan ? Math.ceil(ellipsisSpan.box.w) : 0\n\n\t\t\t\t// then, we need to subtract that space from the width we have and measure again:\n\t\t\t\telm.style.setProperty('width', `${elementWidth - ellipsisWidth}px`)\n\t\t\t\telm.textContent = normalizedText\n\t\t\t\tconst truncatedSpans = this.measureElementTextNodeSpans(elm, {\n\t\t\t\t\tshouldTruncateToFirstLine: true,\n\t\t\t\t}).spans\n\n\t\t\t\t// Finally, we add in our ellipsis at the end of the last span. We\n\t\t\t\t// have to do this after measuring, not before, because adding the\n\t\t\t\t// ellipsis changes how whitespace might be getting collapsed by the\n\t\t\t\t// browser.\n\t\t\t\tconst lastSpan = truncatedSpans[truncatedSpans.length - 1]\n\t\t\t\t// If nothing measurable remained, return the (possibly empty)\n\t\t\t\t// spans as-is rather than dereferencing a missing last span.\n\t\t\t\tif (!lastSpan) {\n\t\t\t\t\treturn truncatedSpans\n\t\t\t\t}\n\t\t\t\ttruncatedSpans.push({\n\t\t\t\t\ttext: '\u2026',\n\t\t\t\t\tbox: {\n\t\t\t\t\t\tx: Math.min(lastSpan.box.x + lastSpan.box.w, opts.width - opts.padding - ellipsisWidth),\n\t\t\t\t\t\ty: lastSpan.box.y,\n\t\t\t\t\t\tw: ellipsisWidth,\n\t\t\t\t\t\th: lastSpan.box.h,\n\t\t\t\t\t},\n\t\t\t\t})\n\n\t\t\t\treturn truncatedSpans\n\t\t\t}\n\n\t\t\treturn spans\n\t\t} finally {\n\t\t\trestoreStyles()\n\t\t}\n\t}\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -39,9 +39,12 @@ class TickManager extends EditorManager {
39
39
  const now = Date.now();
40
40
  const elapsed = now - this.now;
41
41
  this.now = now;
42
- this.editor.emit("frame", elapsed);
43
- this.editor.emit("tick", elapsed);
44
- this.cancelRaf = throttleToNextFrame(this.tick);
42
+ try {
43
+ this.editor.emit("frame", elapsed);
44
+ this.editor.emit("tick", elapsed);
45
+ } finally {
46
+ this.cancelRaf = throttleToNextFrame(this.tick);
47
+ }
45
48
  }
46
49
  dispose() {
47
50
  this.isPaused = true;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/lib/editor/managers/TickManager/TickManager.ts"],
4
- "sourcesContent": ["import { throttleToNextFrame as _throttleToNextFrame, bind } from '@tldraw/utils'\nimport type { Editor } from '../../Editor'\nimport { EditorManager } from '../EditorManager'\n\nconst throttleToNextFrame =\n\ttypeof process !== 'undefined' && process.env.NODE_ENV === 'test'\n\t\t? // At test time we should use actual raf and not throttle, because throttle was set up to evaluate immediately during tests, which causes stack overflow\n\t\t\t// for the tick manager since it sets up a raf loop.\n\t\t\tfunction mockThrottle(cb: any) {\n\t\t\t\t// eslint-disable-next-line no-restricted-globals\n\t\t\t\tconst frame = requestAnimationFrame(cb)\n\t\t\t\treturn () => cancelAnimationFrame(frame)\n\t\t\t}\n\t\t: _throttleToNextFrame\n\n/** @internal */\nexport class TickManager extends EditorManager {\n\tconstructor(editor: Editor) {\n\t\tsuper(editor)\n\t\tthis.start()\n\t}\n\n\tcancelRaf?: null | (() => void)\n\tisPaused = true\n\tnow = 0\n\n\tstart() {\n\t\tthis.isPaused = false\n\t\tthis.cancelRaf?.()\n\t\tthis.cancelRaf = throttleToNextFrame(this.tick)\n\t\tthis.now = Date.now()\n\t}\n\n\t@bind\n\ttick() {\n\t\tif (this.isPaused) {\n\t\t\treturn\n\t\t}\n\n\t\tconst now = Date.now()\n\t\tconst elapsed = now - this.now\n\t\tthis.now = now\n\n\t\tthis.editor.emit('frame', elapsed)\n\t\tthis.editor.emit('tick', elapsed)\n\t\tthis.cancelRaf = throttleToNextFrame(this.tick)\n\t}\n\n\tdispose() {\n\t\tthis.isPaused = true\n\t\tthis.cancelRaf?.()\n\t\tsuper.dispose()\n\t}\n}\n"],
5
- "mappings": ";;;;;;;;;;AAAA,SAAS,uBAAuB,sBAAsB,YAAY;AAElE,SAAS,qBAAqB;AAE9B,MAAM,sBACL,OAAO,YAAY,eAAe,QAAQ,IAAI,aAAa;AAAA;AAAA;AAAA,EAGzD,SAAS,aAAa,IAAS;AAE9B,UAAM,QAAQ,sBAAsB,EAAE;AACtC,WAAO,MAAM,qBAAqB,KAAK;AAAA,EACxC;AAAA,IACC;AAGG,MAAM,oBAAoB,cAAc;AAAA,EAC9C,YAAY,QAAgB;AAC3B,UAAM,MAAM;AACZ,SAAK,MAAM;AAAA,EACZ;AAAA,EAEA;AAAA,EACA,WAAW;AAAA,EACX,MAAM;AAAA,EAEN,QAAQ;AACP,SAAK,WAAW;AAChB,SAAK,YAAY;AACjB,SAAK,YAAY,oBAAoB,KAAK,IAAI;AAC9C,SAAK,MAAM,KAAK,IAAI;AAAA,EACrB;AAAA,EAGA,OAAO;AACN,QAAI,KAAK,UAAU;AAClB;AAAA,IACD;AAEA,UAAM,MAAM,KAAK,IAAI;AACrB,UAAM,UAAU,MAAM,KAAK;AAC3B,SAAK,MAAM;AAEX,SAAK,OAAO,KAAK,SAAS,OAAO;AACjC,SAAK,OAAO,KAAK,QAAQ,OAAO;AAChC,SAAK,YAAY,oBAAoB,KAAK,IAAI;AAAA,EAC/C;AAAA,EAEA,UAAU;AACT,SAAK,WAAW;AAChB,SAAK,YAAY;AACjB,UAAM,QAAQ;AAAA,EACf;AACD;AAnBC;AAAA,EADC;AAAA,GAjBW,YAkBZ;",
4
+ "sourcesContent": ["import { throttleToNextFrame as _throttleToNextFrame, bind } from '@tldraw/utils'\nimport type { Editor } from '../../Editor'\nimport { EditorManager } from '../EditorManager'\n\nconst throttleToNextFrame =\n\ttypeof process !== 'undefined' && process.env.NODE_ENV === 'test'\n\t\t? // At test time we should use actual raf and not throttle, because throttle was set up to evaluate immediately during tests, which causes stack overflow\n\t\t\t// for the tick manager since it sets up a raf loop.\n\t\t\tfunction mockThrottle(cb: any) {\n\t\t\t\t// eslint-disable-next-line no-restricted-globals\n\t\t\t\tconst frame = requestAnimationFrame(cb)\n\t\t\t\treturn () => cancelAnimationFrame(frame)\n\t\t\t}\n\t\t: _throttleToNextFrame\n\n/** @internal */\nexport class TickManager extends EditorManager {\n\tconstructor(editor: Editor) {\n\t\tsuper(editor)\n\t\tthis.start()\n\t}\n\n\tcancelRaf?: null | (() => void)\n\tisPaused = true\n\tnow = 0\n\n\tstart() {\n\t\tthis.isPaused = false\n\t\tthis.cancelRaf?.()\n\t\tthis.cancelRaf = throttleToNextFrame(this.tick)\n\t\tthis.now = Date.now()\n\t}\n\n\t@bind\n\ttick() {\n\t\tif (this.isPaused) {\n\t\t\treturn\n\t\t}\n\n\t\tconst now = Date.now()\n\t\tconst elapsed = now - this.now\n\t\tthis.now = now\n\n\t\t// Schedule the next frame even if a listener throws. Otherwise a single throwing listener\n\t\t// ends the loop for good, which silently stops every frame-driven behavior \u2014 camera\n\t\t// animations, edge scrolling, pointer velocity, following \u2014 instead of failing once.\n\t\ttry {\n\t\t\tthis.editor.emit('frame', elapsed)\n\t\t\tthis.editor.emit('tick', elapsed)\n\t\t} finally {\n\t\t\tthis.cancelRaf = throttleToNextFrame(this.tick)\n\t\t}\n\t}\n\n\tdispose() {\n\t\tthis.isPaused = true\n\t\tthis.cancelRaf?.()\n\t\tsuper.dispose()\n\t}\n}\n"],
5
+ "mappings": ";;;;;;;;;;AAAA,SAAS,uBAAuB,sBAAsB,YAAY;AAElE,SAAS,qBAAqB;AAE9B,MAAM,sBACL,OAAO,YAAY,eAAe,QAAQ,IAAI,aAAa;AAAA;AAAA;AAAA,EAGzD,SAAS,aAAa,IAAS;AAE9B,UAAM,QAAQ,sBAAsB,EAAE;AACtC,WAAO,MAAM,qBAAqB,KAAK;AAAA,EACxC;AAAA,IACC;AAGG,MAAM,oBAAoB,cAAc;AAAA,EAC9C,YAAY,QAAgB;AAC3B,UAAM,MAAM;AACZ,SAAK,MAAM;AAAA,EACZ;AAAA,EAEA;AAAA,EACA,WAAW;AAAA,EACX,MAAM;AAAA,EAEN,QAAQ;AACP,SAAK,WAAW;AAChB,SAAK,YAAY;AACjB,SAAK,YAAY,oBAAoB,KAAK,IAAI;AAC9C,SAAK,MAAM,KAAK,IAAI;AAAA,EACrB;AAAA,EAGA,OAAO;AACN,QAAI,KAAK,UAAU;AAClB;AAAA,IACD;AAEA,UAAM,MAAM,KAAK,IAAI;AACrB,UAAM,UAAU,MAAM,KAAK;AAC3B,SAAK,MAAM;AAKX,QAAI;AACH,WAAK,OAAO,KAAK,SAAS,OAAO;AACjC,WAAK,OAAO,KAAK,QAAQ,OAAO;AAAA,IACjC,UAAE;AACD,WAAK,YAAY,oBAAoB,KAAK,IAAI;AAAA,IAC/C;AAAA,EACD;AAAA,EAEA,UAAU;AACT,SAAK,WAAW;AAChB,SAAK,YAAY;AACjB,UAAM,QAAQ;AAAA,EACf;AACD;AAzBC;AAAA,EADC;AAAA,GAjBW,YAkBZ;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,37 @@
1
+ import { atom } from "@tldraw/state";
2
+ const mounted = atom("mounted editors", []);
3
+ const tleditors = {
4
+ /**
5
+ * A reactive list of currently mounted editor instances.
6
+ *
7
+ * An editor is added when it emits the `mount` event and removed when it emits `unmount`
8
+ * (including when the editor is disposed while still mounted).
9
+ *
10
+ * @public
11
+ */
12
+ mounted,
13
+ /**
14
+ * Get the currently mounted editor instances.
15
+ *
16
+ * @public
17
+ */
18
+ getMounted() {
19
+ return mounted.get();
20
+ }
21
+ };
22
+ function registerMountedEditor(editor) {
23
+ const current = mounted.get();
24
+ if (current.includes(editor)) return;
25
+ mounted.set([...current, editor]);
26
+ }
27
+ function unregisterMountedEditor(editor) {
28
+ const current = mounted.get();
29
+ if (!current.includes(editor)) return;
30
+ mounted.set(current.filter((e) => e !== editor));
31
+ }
32
+ export {
33
+ registerMountedEditor,
34
+ tleditors,
35
+ unregisterMountedEditor
36
+ };
37
+ //# sourceMappingURL=editors.mjs.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/lib/globals/editors.ts"],
4
+ "sourcesContent": ["import { Signal, atom } from '@tldraw/state'\nimport type { Editor } from '../editor/Editor'\n\nconst mounted = atom<readonly Editor[]>('mounted editors', [])\n\n/**\n * A global registry of currently mounted editor instances. Use it to discover live editors from\n * outside the React tree, for example from sidebar chrome, keyboard shortcuts, or multi-editor\n * layouts.\n *\n * @example\n * ```ts\n * tleditors.getMounted() // readonly Editor[]\n *\n * const editors = useValue('mounted editors', () => tleditors.getMounted(), [])\n * ```\n *\n * @public\n */\nexport const tleditors = {\n\t/**\n\t * A reactive list of currently mounted editor instances.\n\t *\n\t * An editor is added when it emits the `mount` event and removed when it emits `unmount`\n\t * (including when the editor is disposed while still mounted).\n\t *\n\t * @public\n\t */\n\tmounted: mounted as Signal<readonly Editor[]>,\n\n\t/**\n\t * Get the currently mounted editor instances.\n\t *\n\t * @public\n\t */\n\tgetMounted(): readonly Editor[] {\n\t\treturn mounted.get()\n\t},\n}\n\n/** @internal */\nexport function registerMountedEditor(editor: Editor) {\n\tconst current = mounted.get()\n\tif (current.includes(editor)) return\n\tmounted.set([...current, editor])\n}\n\n/** @internal */\nexport function unregisterMountedEditor(editor: Editor) {\n\tconst current = mounted.get()\n\tif (!current.includes(editor)) return\n\tmounted.set(current.filter((e) => e !== editor))\n}\n"],
5
+ "mappings": "AAAA,SAAiB,YAAY;AAG7B,MAAM,UAAU,KAAwB,mBAAmB,CAAC,CAAC;AAgBtD,MAAM,YAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASxB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,aAAgC;AAC/B,WAAO,QAAQ,IAAI;AAAA,EACpB;AACD;AAGO,SAAS,sBAAsB,QAAgB;AACrD,QAAM,UAAU,QAAQ,IAAI;AAC5B,MAAI,QAAQ,SAAS,MAAM,EAAG;AAC9B,UAAQ,IAAI,CAAC,GAAG,SAAS,MAAM,CAAC;AACjC;AAGO,SAAS,wBAAwB,QAAgB;AACvD,QAAM,UAAU,QAAQ,IAAI;AAC5B,MAAI,CAAC,QAAQ,SAAS,MAAM,EAAG;AAC/B,UAAQ,IAAI,QAAQ,OAAO,CAAC,MAAM,MAAM,MAAM,CAAC;AAChD;",
6
+ "names": []
7
+ }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/hooks/useViewportHeight.ts"],
4
- "sourcesContent": ["import { useLayoutEffect, useState } from 'react'\nimport { useMaybeEditor } from './useEditor'\n\n/*!\n * BSD License: https://github.com/outline/rich-markdown-editor/blob/main/LICENSE\n * Copyright (c) 2020 General Outline, Inc (https://www.getoutline.com/) and individual contributors.\n *\n * Returns the height of the viewport.\n * This is mainly to account for virtual keyboards on mobile devices.\n *\n * N.B. On iOS, you have to take into account the offsetTop as well so that you get an accurate position\n * while using the virtual keyboard.\n */\n/** @public */\nexport function useViewportHeight(): number {\n\tconst editor = useMaybeEditor()\n\tconst win = editor?.getContainerWindow() ?? window\n\tconst vv = win.visualViewport\n\tconst [height, setHeight] = useState<number>(() =>\n\t\tvv ? vv.height + vv.offsetTop : win.innerHeight\n\t)\n\n\tuseLayoutEffect(() => {\n\t\tconst win = editor?.getContainerWindow() ?? window\n\t\tconst handleResize = () => {\n\t\t\tconst vv = win.visualViewport\n\t\t\tsetHeight(() => (vv ? vv.height + vv.offsetTop : win.innerHeight))\n\t\t}\n\n\t\twin.visualViewport?.addEventListener('resize', handleResize)\n\t\twin.visualViewport?.addEventListener('scroll', handleResize)\n\n\t\treturn () => {\n\t\t\twin.visualViewport?.removeEventListener('resize', handleResize)\n\t\t\twin.visualViewport?.removeEventListener('scroll', handleResize)\n\t\t}\n\t}, [editor])\n\treturn height\n}\n"],
5
- "mappings": "AAAA,SAAS,iBAAiB,gBAAgB;AAC1C,SAAS,sBAAsB;AAE/B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAWO,SAAS,oBAA4B;AAC3C,QAAM,SAAS,eAAe;AAC9B,QAAM,MAAM,QAAQ,mBAAmB,KAAK;AAC5C,QAAM,KAAK,IAAI;AACf,QAAM,CAAC,QAAQ,SAAS,IAAI;AAAA,IAAiB,MAC5C,KAAK,GAAG,SAAS,GAAG,YAAY,IAAI;AAAA,EACrC;AAEA,kBAAgB,MAAM;AACrB,UAAMA,OAAM,QAAQ,mBAAmB,KAAK;AAC5C,UAAM,eAAe,MAAM;AAC1B,YAAMC,MAAKD,KAAI;AACf,gBAAU,MAAOC,MAAKA,IAAG,SAASA,IAAG,YAAYD,KAAI,WAAY;AAAA,IAClE;AAEA,IAAAA,KAAI,gBAAgB,iBAAiB,UAAU,YAAY;AAC3D,IAAAA,KAAI,gBAAgB,iBAAiB,UAAU,YAAY;AAE3D,WAAO,MAAM;AACZ,MAAAA,KAAI,gBAAgB,oBAAoB,UAAU,YAAY;AAC9D,MAAAA,KAAI,gBAAgB,oBAAoB,UAAU,YAAY;AAAA,IAC/D;AAAA,EACD,GAAG,CAAC,MAAM,CAAC;AACX,SAAO;AACR;",
4
+ "sourcesContent": ["import { useLayoutEffect, useState } from 'react'\nimport { useMaybeEditor } from './useEditor'\n\n/*!\n * BSD License: https://github.com/outline/rich-markdown-editor/blob/main/LICENSE\n * Copyright (c) 2020 General Outline, Inc (https://www.getoutline.com/) and individual contributors.\n *\n * Returns the height of the viewport.\n * This is mainly to account for virtual keyboards on mobile devices.\n *\n * N.B. On iOS, you have to take into account the offsetTop as well so that you get an accurate position\n * while using the virtual keyboard.\n */\n/**\n * The height of the visible viewport, including the visual viewport's own offset.\n *\n * @deprecated Read `window.visualViewport` directly: this flattens the visible rectangle to a\n * single scalar, and keeping UI clear of the software keyboard usually needs the other edges too.\n * Unused since v3.14; will be removed in a future release.\n *\n * @public\n */\nexport function useViewportHeight(): number {\n\tconst editor = useMaybeEditor()\n\tconst win = editor?.getContainerWindow() ?? window\n\tconst vv = win.visualViewport\n\tconst [height, setHeight] = useState<number>(() =>\n\t\tvv ? vv.height + vv.offsetTop : win.innerHeight\n\t)\n\n\tuseLayoutEffect(() => {\n\t\tconst win = editor?.getContainerWindow() ?? window\n\t\tconst handleResize = () => {\n\t\t\tconst vv = win.visualViewport\n\t\t\tsetHeight(() => (vv ? vv.height + vv.offsetTop : win.innerHeight))\n\t\t}\n\n\t\twin.visualViewport?.addEventListener('resize', handleResize)\n\t\twin.visualViewport?.addEventListener('scroll', handleResize)\n\n\t\treturn () => {\n\t\t\twin.visualViewport?.removeEventListener('resize', handleResize)\n\t\t\twin.visualViewport?.removeEventListener('scroll', handleResize)\n\t\t}\n\t}, [editor])\n\treturn height\n}\n"],
5
+ "mappings": "AAAA,SAAS,iBAAiB,gBAAgB;AAC1C,SAAS,sBAAsB;AAE/B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAmBO,SAAS,oBAA4B;AAC3C,QAAM,SAAS,eAAe;AAC9B,QAAM,MAAM,QAAQ,mBAAmB,KAAK;AAC5C,QAAM,KAAK,IAAI;AACf,QAAM,CAAC,QAAQ,SAAS,IAAI;AAAA,IAAiB,MAC5C,KAAK,GAAG,SAAS,GAAG,YAAY,IAAI;AAAA,EACrC;AAEA,kBAAgB,MAAM;AACrB,UAAMA,OAAM,QAAQ,mBAAmB,KAAK;AAC5C,UAAM,eAAe,MAAM;AAC1B,YAAMC,MAAKD,KAAI;AACf,gBAAU,MAAOC,MAAKA,IAAG,SAASA,IAAG,YAAYD,KAAI,WAAY;AAAA,IAClE;AAEA,IAAAA,KAAI,gBAAgB,iBAAiB,UAAU,YAAY;AAC3D,IAAAA,KAAI,gBAAgB,iBAAiB,UAAU,YAAY;AAE3D,WAAO,MAAM;AACZ,MAAAA,KAAI,gBAAgB,oBAAoB,UAAU,YAAY;AAC9D,MAAAA,KAAI,gBAAgB,oBAAoB,UAAU,YAAY;AAAA,IAC/D;AAAA,EACD,GAAG,CAAC,MAAM,CAAC;AACX,SAAO;AACR;",
6
6
  "names": ["win", "vv"]
7
7
  }
@@ -1,4 +1,3 @@
1
- import { EMPTY_ARRAY } from "@tldraw/state";
2
1
  import { assert, invLerp, lerp } from "@tldraw/utils";
3
2
  import { Box } from "../Box.mjs";
4
3
  import { Vec } from "../Vec.mjs";
@@ -24,7 +23,15 @@ class Group2d extends Geometry2d {
24
23
  }
25
24
  getVertices(filters) {
26
25
  if (this.isExcludedByFilter(filters)) return [];
27
- return this.children.filter((c) => !c.isExcludedByFilter(filters)).flatMap((c) => c.getVertices(filters));
26
+ const vertices = [];
27
+ for (const child of this.children) {
28
+ if (child.isExcludedByFilter(filters)) continue;
29
+ const childVertices = child.getVertices(filters);
30
+ for (let i = 0, n = childVertices.length; i < n; i++) {
31
+ vertices.push(childVertices[i]);
32
+ }
33
+ }
34
+ return vertices;
28
35
  }
29
36
  nearestPoint(point, filters) {
30
37
  let dist = Infinity;
@@ -59,38 +66,73 @@ class Group2d extends Geometry2d {
59
66
  return smallestDistance;
60
67
  }
61
68
  hitTestPoint(point, margin, hitInside, filters = Geometry2dFilters.EXCLUDE_LABELS) {
62
- return !!this.children.filter((c) => !c.isExcludedByFilter(filters)).find((c) => c.hitTestPoint(point, margin, hitInside));
69
+ for (const child of this.children) {
70
+ if (child.isExcludedByFilter(filters)) continue;
71
+ if (child.hitTestPoint(point, margin, hitInside)) return true;
72
+ }
73
+ return false;
63
74
  }
64
75
  hitTestLineSegment(A, B, zoom, filters = Geometry2dFilters.EXCLUDE_LABELS) {
65
- return !!this.children.filter((c) => !c.isExcludedByFilter(filters)).find((c) => c.hitTestLineSegment(A, B, zoom));
76
+ for (const child of this.children) {
77
+ if (child.isExcludedByFilter(filters)) continue;
78
+ if (child.hitTestLineSegment(A, B, zoom)) return true;
79
+ }
80
+ return false;
66
81
  }
67
82
  intersectLineSegment(A, B, filters) {
68
- return this.children.flatMap((child) => {
69
- if (child.isExcludedByFilter(filters)) return EMPTY_ARRAY;
70
- return child.intersectLineSegment(A, B, filters);
71
- });
83
+ const result = [];
84
+ for (const child of this.children) {
85
+ if (child.isExcludedByFilter(filters)) continue;
86
+ const hits = child.intersectLineSegment(A, B, filters);
87
+ for (let i = 0, n = hits.length; i < n; i++) {
88
+ result.push(hits[i]);
89
+ }
90
+ }
91
+ return result;
72
92
  }
73
93
  intersectCircle(center, radius, filters) {
74
- return this.children.flatMap((child) => {
75
- if (child.isExcludedByFilter(filters)) return EMPTY_ARRAY;
76
- return child.intersectCircle(center, radius, filters);
77
- });
94
+ const result = [];
95
+ for (const child of this.children) {
96
+ if (child.isExcludedByFilter(filters)) continue;
97
+ const hits = child.intersectCircle(center, radius, filters);
98
+ for (let i = 0, n = hits.length; i < n; i++) {
99
+ result.push(hits[i]);
100
+ }
101
+ }
102
+ return result;
78
103
  }
79
104
  getBoundsVertices() {
80
105
  if (this.excludeFromShapeBounds) return [];
81
- return this.children.flatMap((child) => child.getBoundsVertices());
106
+ const vertices = [];
107
+ for (const child of this.children) {
108
+ const childVertices = child.getBoundsVertices();
109
+ for (let i = 0, n = childVertices.length; i < n; i++) {
110
+ vertices.push(childVertices[i]);
111
+ }
112
+ }
113
+ return vertices;
82
114
  }
83
115
  intersectPolygon(polygon, filters) {
84
- return this.children.flatMap((child) => {
85
- if (child.isExcludedByFilter(filters)) return EMPTY_ARRAY;
86
- return child.intersectPolygon(polygon, filters);
87
- });
116
+ const result = [];
117
+ for (const child of this.children) {
118
+ if (child.isExcludedByFilter(filters)) continue;
119
+ const hits = child.intersectPolygon(polygon, filters);
120
+ for (let i = 0, n = hits.length; i < n; i++) {
121
+ result.push(hits[i]);
122
+ }
123
+ }
124
+ return result;
88
125
  }
89
126
  intersectPolyline(polyline, filters) {
90
- return this.children.flatMap((child) => {
91
- if (child.isExcludedByFilter(filters)) return EMPTY_ARRAY;
92
- return child.intersectPolyline(polyline, filters);
93
- });
127
+ const result = [];
128
+ for (const child of this.children) {
129
+ if (child.isExcludedByFilter(filters)) continue;
130
+ const hits = child.intersectPolyline(polyline, filters);
131
+ for (let i = 0, n = hits.length; i < n; i++) {
132
+ result.push(hits[i]);
133
+ }
134
+ }
135
+ return result;
94
136
  }
95
137
  interpolateAlongEdge(t, filters) {
96
138
  const totalLength = this.getLength(filters);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/lib/primitives/geometry/Group2d.ts"],
4
- "sourcesContent": ["import { EMPTY_ARRAY } from '@tldraw/state'\nimport { assert, invLerp, lerp } from '@tldraw/utils'\nimport { Box } from '../Box'\nimport { Mat } from '../Mat'\nimport { Vec, VecLike } from '../Vec'\nimport { Geometry2d, Geometry2dFilters, Geometry2dOptions } from './Geometry2d'\n\n/** @public */\nexport class Group2d extends Geometry2d {\n\tchildren: Geometry2d[] = []\n\tignoredChildren: Geometry2d[] = []\n\n\tconstructor(\n\t\tconfig: Omit<Geometry2dOptions, 'isClosed' | 'isFilled'> & {\n\t\t\tchildren: Geometry2d[]\n\t\t}\n\t) {\n\t\tsuper({ ...config, isClosed: true, isFilled: false })\n\n\t\tconst addChildren = (children: Geometry2d[]) => {\n\t\t\tfor (const child of children) {\n\t\t\t\tif (child instanceof Group2d) {\n\t\t\t\t\taddChildren(child.children)\n\t\t\t\t} else if (child.ignore) {\n\t\t\t\t\tthis.ignoredChildren.push(child)\n\t\t\t\t} else {\n\t\t\t\t\tthis.children.push(child)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\taddChildren(config.children)\n\n\t\tif (this.children.length === 0) throw Error('Group2d must have at least one child')\n\t}\n\n\toverride getVertices(filters: Geometry2dFilters): Vec[] {\n\t\tif (this.isExcludedByFilter(filters)) return []\n\t\treturn this.children\n\t\t\t.filter((c) => !c.isExcludedByFilter(filters))\n\t\t\t.flatMap((c) => c.getVertices(filters))\n\t}\n\n\toverride nearestPoint(point: VecLike, filters?: Geometry2dFilters): Vec {\n\t\tlet dist = Infinity\n\t\tlet nearest: Vec | undefined\n\n\t\tconst { children } = this\n\n\t\tif (children.length === 0) {\n\t\t\tthrow Error('no children')\n\t\t}\n\n\t\tlet p: Vec\n\t\tlet d: number\n\t\tfor (const child of children) {\n\t\t\tif (child.isExcludedByFilter(filters)) continue\n\t\t\tp = child.nearestPoint(point, filters)\n\t\t\td = Vec.Dist2(p, point)\n\t\t\tif (d < dist) {\n\t\t\t\tdist = d\n\t\t\t\tnearest = p\n\t\t\t}\n\t\t}\n\t\tif (!nearest) throw Error('nearest point not found')\n\t\treturn nearest\n\t}\n\n\toverride distanceToPoint(point: VecLike, hitInside = false, filters?: Geometry2dFilters) {\n\t\tlet smallestDistance = Infinity\n\t\tfor (const child of this.children) {\n\t\t\tif (child.isExcludedByFilter(filters)) continue\n\t\t\tconst distance = child.distanceToPoint(point, hitInside, filters)\n\t\t\tif (distance < smallestDistance) {\n\t\t\t\tsmallestDistance = distance\n\t\t\t}\n\t\t}\n\t\treturn smallestDistance\n\t}\n\n\toverride hitTestPoint(\n\t\tpoint: VecLike,\n\t\tmargin: number,\n\t\thitInside: boolean,\n\t\tfilters = Geometry2dFilters.EXCLUDE_LABELS\n\t): boolean {\n\t\treturn !!this.children\n\t\t\t.filter((c) => !c.isExcludedByFilter(filters))\n\t\t\t.find((c) => c.hitTestPoint(point, margin, hitInside))\n\t}\n\n\toverride hitTestLineSegment(\n\t\tA: VecLike,\n\t\tB: VecLike,\n\t\tzoom: number,\n\t\tfilters = Geometry2dFilters.EXCLUDE_LABELS\n\t): boolean {\n\t\treturn !!this.children\n\t\t\t.filter((c) => !c.isExcludedByFilter(filters))\n\t\t\t.find((c) => c.hitTestLineSegment(A, B, zoom))\n\t}\n\n\toverride intersectLineSegment(A: VecLike, B: VecLike, filters?: Geometry2dFilters) {\n\t\treturn this.children.flatMap((child) => {\n\t\t\tif (child.isExcludedByFilter(filters)) return EMPTY_ARRAY\n\t\t\treturn child.intersectLineSegment(A, B, filters)\n\t\t})\n\t}\n\n\toverride intersectCircle(center: VecLike, radius: number, filters?: Geometry2dFilters) {\n\t\treturn this.children.flatMap((child) => {\n\t\t\tif (child.isExcludedByFilter(filters)) return EMPTY_ARRAY\n\t\t\treturn child.intersectCircle(center, radius, filters)\n\t\t})\n\t}\n\n\toverride getBoundsVertices(): Vec[] {\n\t\tif (this.excludeFromShapeBounds) return []\n\t\treturn this.children.flatMap((child) => child.getBoundsVertices())\n\t}\n\n\toverride intersectPolygon(polygon: VecLike[], filters?: Geometry2dFilters) {\n\t\treturn this.children.flatMap((child) => {\n\t\t\tif (child.isExcludedByFilter(filters)) return EMPTY_ARRAY\n\t\t\treturn child.intersectPolygon(polygon, filters)\n\t\t})\n\t}\n\n\toverride intersectPolyline(polyline: VecLike[], filters?: Geometry2dFilters) {\n\t\treturn this.children.flatMap((child) => {\n\t\t\tif (child.isExcludedByFilter(filters)) return EMPTY_ARRAY\n\t\t\treturn child.intersectPolyline(polyline, filters)\n\t\t})\n\t}\n\n\toverride interpolateAlongEdge(t: number, filters?: Geometry2dFilters): Vec {\n\t\tconst totalLength = this.getLength(filters)\n\n\t\tconst distanceToTravel = t * totalLength\n\t\tlet distanceTraveled = 0\n\t\tfor (const child of this.children) {\n\t\t\tif (child.isExcludedByFilter(filters)) continue\n\t\t\tconst childLength = child.length\n\t\t\tconst newDistanceTraveled = distanceTraveled + childLength\n\t\t\tif (newDistanceTraveled >= distanceToTravel) {\n\t\t\t\treturn child.interpolateAlongEdge(\n\t\t\t\t\tinvLerp(distanceTraveled, newDistanceTraveled, distanceToTravel),\n\t\t\t\t\tfilters\n\t\t\t\t)\n\t\t\t}\n\t\t\tdistanceTraveled = newDistanceTraveled\n\t\t}\n\n\t\treturn this.children[this.children.length - 1].interpolateAlongEdge(1, filters)\n\t}\n\n\toverride uninterpolateAlongEdge(point: VecLike, filters?: Geometry2dFilters): number {\n\t\tconst totalLength = this.getLength(filters)\n\n\t\tlet closestChild = null\n\t\tlet closestDistance = Infinity\n\t\tlet distanceTraveled = 0\n\n\t\tfor (const child of this.children) {\n\t\t\tif (child.isExcludedByFilter(filters)) continue\n\t\t\tconst childLength = child.getLength(filters)\n\t\t\tconst newDistanceTraveled = distanceTraveled + childLength\n\n\t\t\tconst distance = child.distanceToPoint(point, false, filters)\n\t\t\tif (distance < closestDistance) {\n\t\t\t\tclosestDistance = distance\n\t\t\t\tclosestChild = {\n\t\t\t\t\tstartLength: distanceTraveled,\n\t\t\t\t\tendLength: newDistanceTraveled,\n\t\t\t\t\tchild,\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tdistanceTraveled = newDistanceTraveled\n\t\t}\n\n\t\tassert(closestChild)\n\n\t\tconst normalizedDistanceInChild = closestChild.child.uninterpolateAlongEdge(point, filters)\n\t\tconst childTLength = lerp(\n\t\t\tclosestChild.startLength,\n\t\t\tclosestChild.endLength,\n\t\t\tnormalizedDistanceInChild\n\t\t)\n\t\treturn childTLength / totalLength\n\t}\n\n\toverride transform(transform: Mat): Geometry2d {\n\t\treturn new Group2d({\n\t\t\tchildren: this.children.map((c) => c.transform(transform)),\n\t\t\tisLabel: this.isLabel,\n\t\t\tdebugColor: this.debugColor,\n\t\t\tignore: this.ignore,\n\t\t})\n\t}\n\n\tgetArea() {\n\t\t// todo: this is a temporary solution, assuming that the first child defines the group size; we would want to flatten the group and then find the area of the hull polygon\n\t\treturn this.children[0].area\n\t}\n\n\ttoSimpleSvgPath() {\n\t\tlet path = ''\n\t\tfor (const child of this.children) {\n\t\t\tpath += child.toSimpleSvgPath()\n\t\t}\n\n\t\tconst corners = Box.FromPoints(this.boundsVertices).corners\n\t\t// draw just a few pixels around each corner, e.g. an L shape for the bottom left\n\n\t\tfor (let i = 0, n = corners.length; i < n; i++) {\n\t\t\tconst corner = corners[i]\n\t\t\tconst prevCorner = corners[(i - 1 + n) % n]\n\t\t\tconst prevDist = corner.dist(prevCorner)\n\t\t\tconst nextCorner = corners[(i + 1) % n]\n\t\t\tconst nextDist = corner.dist(nextCorner)\n\n\t\t\tconst A = corner.clone().lrp(prevCorner, 4 / prevDist)\n\t\t\tconst B = corner\n\t\t\tconst C = corner.clone().lrp(nextCorner, 4 / nextDist)\n\n\t\t\tpath += `M${A.x},${A.y} L${B.x},${B.y} L${C.x},${C.y} `\n\t\t}\n\t\treturn path\n\t}\n\n\tgetLength(filters?: Geometry2dFilters): number {\n\t\tlet length = 0\n\t\tfor (const child of this.children) {\n\t\t\tif (child.isExcludedByFilter(filters)) continue\n\t\t\tlength += child.length\n\t\t}\n\t\treturn length\n\t}\n\n\tgetSvgPathData(): string {\n\t\treturn this.children.map((c, i) => (c.isLabel ? '' : c.getSvgPathData(i === 0))).join(' ')\n\t}\n\n\toverlapsPolygon(polygon: VecLike[]): boolean {\n\t\treturn this.children.some((child) => child.overlapsPolygon(polygon))\n\t}\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { assert, invLerp, lerp } from '@tldraw/utils'\nimport { Box } from '../Box'\nimport { Mat } from '../Mat'\nimport { Vec, VecLike } from '../Vec'\nimport { Geometry2d, Geometry2dFilters, Geometry2dOptions } from './Geometry2d'\n\n/** @public */\nexport class Group2d extends Geometry2d {\n\tchildren: Geometry2d[] = []\n\tignoredChildren: Geometry2d[] = []\n\n\tconstructor(\n\t\tconfig: Omit<Geometry2dOptions, 'isClosed' | 'isFilled'> & {\n\t\t\tchildren: Geometry2d[]\n\t\t}\n\t) {\n\t\tsuper({ ...config, isClosed: true, isFilled: false })\n\n\t\tconst addChildren = (children: Geometry2d[]) => {\n\t\t\tfor (const child of children) {\n\t\t\t\tif (child instanceof Group2d) {\n\t\t\t\t\taddChildren(child.children)\n\t\t\t\t} else if (child.ignore) {\n\t\t\t\t\tthis.ignoredChildren.push(child)\n\t\t\t\t} else {\n\t\t\t\t\tthis.children.push(child)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\taddChildren(config.children)\n\n\t\tif (this.children.length === 0) throw Error('Group2d must have at least one child')\n\t}\n\n\toverride getVertices(filters: Geometry2dFilters): Vec[] {\n\t\tif (this.isExcludedByFilter(filters)) return []\n\t\tconst vertices: Vec[] = []\n\t\tfor (const child of this.children) {\n\t\t\tif (child.isExcludedByFilter(filters)) continue\n\t\t\tconst childVertices = child.getVertices(filters)\n\t\t\tfor (let i = 0, n = childVertices.length; i < n; i++) {\n\t\t\t\tvertices.push(childVertices[i])\n\t\t\t}\n\t\t}\n\t\treturn vertices\n\t}\n\n\toverride nearestPoint(point: VecLike, filters?: Geometry2dFilters): Vec {\n\t\tlet dist = Infinity\n\t\tlet nearest: Vec | undefined\n\n\t\tconst { children } = this\n\n\t\tif (children.length === 0) {\n\t\t\tthrow Error('no children')\n\t\t}\n\n\t\tlet p: Vec\n\t\tlet d: number\n\t\tfor (const child of children) {\n\t\t\tif (child.isExcludedByFilter(filters)) continue\n\t\t\tp = child.nearestPoint(point, filters)\n\t\t\td = Vec.Dist2(p, point)\n\t\t\tif (d < dist) {\n\t\t\t\tdist = d\n\t\t\t\tnearest = p\n\t\t\t}\n\t\t}\n\t\tif (!nearest) throw Error('nearest point not found')\n\t\treturn nearest\n\t}\n\n\toverride distanceToPoint(point: VecLike, hitInside = false, filters?: Geometry2dFilters) {\n\t\tlet smallestDistance = Infinity\n\t\tfor (const child of this.children) {\n\t\t\tif (child.isExcludedByFilter(filters)) continue\n\t\t\tconst distance = child.distanceToPoint(point, hitInside, filters)\n\t\t\tif (distance < smallestDistance) {\n\t\t\t\tsmallestDistance = distance\n\t\t\t}\n\t\t}\n\t\treturn smallestDistance\n\t}\n\n\toverride hitTestPoint(\n\t\tpoint: VecLike,\n\t\tmargin: number,\n\t\thitInside: boolean,\n\t\tfilters = Geometry2dFilters.EXCLUDE_LABELS\n\t): boolean {\n\t\tfor (const child of this.children) {\n\t\t\tif (child.isExcludedByFilter(filters)) continue\n\t\t\tif (child.hitTestPoint(point, margin, hitInside)) return true\n\t\t}\n\t\treturn false\n\t}\n\n\toverride hitTestLineSegment(\n\t\tA: VecLike,\n\t\tB: VecLike,\n\t\tzoom: number,\n\t\tfilters = Geometry2dFilters.EXCLUDE_LABELS\n\t): boolean {\n\t\tfor (const child of this.children) {\n\t\t\tif (child.isExcludedByFilter(filters)) continue\n\t\t\tif (child.hitTestLineSegment(A, B, zoom)) return true\n\t\t}\n\t\treturn false\n\t}\n\n\toverride intersectLineSegment(A: VecLike, B: VecLike, filters?: Geometry2dFilters) {\n\t\tconst result: VecLike[] = []\n\t\tfor (const child of this.children) {\n\t\t\tif (child.isExcludedByFilter(filters)) continue\n\t\t\tconst hits = child.intersectLineSegment(A, B, filters)\n\t\t\tfor (let i = 0, n = hits.length; i < n; i++) {\n\t\t\t\tresult.push(hits[i])\n\t\t\t}\n\t\t}\n\t\treturn result\n\t}\n\n\toverride intersectCircle(center: VecLike, radius: number, filters?: Geometry2dFilters) {\n\t\tconst result: VecLike[] = []\n\t\tfor (const child of this.children) {\n\t\t\tif (child.isExcludedByFilter(filters)) continue\n\t\t\tconst hits = child.intersectCircle(center, radius, filters)\n\t\t\tfor (let i = 0, n = hits.length; i < n; i++) {\n\t\t\t\tresult.push(hits[i])\n\t\t\t}\n\t\t}\n\t\treturn result\n\t}\n\n\toverride getBoundsVertices(): Vec[] {\n\t\tif (this.excludeFromShapeBounds) return []\n\t\tconst vertices: Vec[] = []\n\t\tfor (const child of this.children) {\n\t\t\tconst childVertices = child.getBoundsVertices()\n\t\t\tfor (let i = 0, n = childVertices.length; i < n; i++) {\n\t\t\t\tvertices.push(childVertices[i])\n\t\t\t}\n\t\t}\n\t\treturn vertices\n\t}\n\n\toverride intersectPolygon(polygon: VecLike[], filters?: Geometry2dFilters) {\n\t\tconst result: VecLike[] = []\n\t\tfor (const child of this.children) {\n\t\t\tif (child.isExcludedByFilter(filters)) continue\n\t\t\tconst hits = child.intersectPolygon(polygon, filters)\n\t\t\tfor (let i = 0, n = hits.length; i < n; i++) {\n\t\t\t\tresult.push(hits[i])\n\t\t\t}\n\t\t}\n\t\treturn result\n\t}\n\n\toverride intersectPolyline(polyline: VecLike[], filters?: Geometry2dFilters) {\n\t\tconst result: VecLike[] = []\n\t\tfor (const child of this.children) {\n\t\t\tif (child.isExcludedByFilter(filters)) continue\n\t\t\tconst hits = child.intersectPolyline(polyline, filters)\n\t\t\tfor (let i = 0, n = hits.length; i < n; i++) {\n\t\t\t\tresult.push(hits[i])\n\t\t\t}\n\t\t}\n\t\treturn result\n\t}\n\n\toverride interpolateAlongEdge(t: number, filters?: Geometry2dFilters): Vec {\n\t\tconst totalLength = this.getLength(filters)\n\n\t\tconst distanceToTravel = t * totalLength\n\t\tlet distanceTraveled = 0\n\t\tfor (const child of this.children) {\n\t\t\tif (child.isExcludedByFilter(filters)) continue\n\t\t\tconst childLength = child.length\n\t\t\tconst newDistanceTraveled = distanceTraveled + childLength\n\t\t\tif (newDistanceTraveled >= distanceToTravel) {\n\t\t\t\treturn child.interpolateAlongEdge(\n\t\t\t\t\tinvLerp(distanceTraveled, newDistanceTraveled, distanceToTravel),\n\t\t\t\t\tfilters\n\t\t\t\t)\n\t\t\t}\n\t\t\tdistanceTraveled = newDistanceTraveled\n\t\t}\n\n\t\treturn this.children[this.children.length - 1].interpolateAlongEdge(1, filters)\n\t}\n\n\toverride uninterpolateAlongEdge(point: VecLike, filters?: Geometry2dFilters): number {\n\t\tconst totalLength = this.getLength(filters)\n\n\t\tlet closestChild = null\n\t\tlet closestDistance = Infinity\n\t\tlet distanceTraveled = 0\n\n\t\tfor (const child of this.children) {\n\t\t\tif (child.isExcludedByFilter(filters)) continue\n\t\t\tconst childLength = child.getLength(filters)\n\t\t\tconst newDistanceTraveled = distanceTraveled + childLength\n\n\t\t\tconst distance = child.distanceToPoint(point, false, filters)\n\t\t\tif (distance < closestDistance) {\n\t\t\t\tclosestDistance = distance\n\t\t\t\tclosestChild = {\n\t\t\t\t\tstartLength: distanceTraveled,\n\t\t\t\t\tendLength: newDistanceTraveled,\n\t\t\t\t\tchild,\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tdistanceTraveled = newDistanceTraveled\n\t\t}\n\n\t\tassert(closestChild)\n\n\t\tconst normalizedDistanceInChild = closestChild.child.uninterpolateAlongEdge(point, filters)\n\t\tconst childTLength = lerp(\n\t\t\tclosestChild.startLength,\n\t\t\tclosestChild.endLength,\n\t\t\tnormalizedDistanceInChild\n\t\t)\n\t\treturn childTLength / totalLength\n\t}\n\n\toverride transform(transform: Mat): Geometry2d {\n\t\treturn new Group2d({\n\t\t\tchildren: this.children.map((c) => c.transform(transform)),\n\t\t\tisLabel: this.isLabel,\n\t\t\tdebugColor: this.debugColor,\n\t\t\tignore: this.ignore,\n\t\t})\n\t}\n\n\tgetArea() {\n\t\t// todo: this is a temporary solution, assuming that the first child defines the group size; we would want to flatten the group and then find the area of the hull polygon\n\t\treturn this.children[0].area\n\t}\n\n\ttoSimpleSvgPath() {\n\t\tlet path = ''\n\t\tfor (const child of this.children) {\n\t\t\tpath += child.toSimpleSvgPath()\n\t\t}\n\n\t\tconst corners = Box.FromPoints(this.boundsVertices).corners\n\t\t// draw just a few pixels around each corner, e.g. an L shape for the bottom left\n\n\t\tfor (let i = 0, n = corners.length; i < n; i++) {\n\t\t\tconst corner = corners[i]\n\t\t\tconst prevCorner = corners[(i - 1 + n) % n]\n\t\t\tconst prevDist = corner.dist(prevCorner)\n\t\t\tconst nextCorner = corners[(i + 1) % n]\n\t\t\tconst nextDist = corner.dist(nextCorner)\n\n\t\t\tconst A = corner.clone().lrp(prevCorner, 4 / prevDist)\n\t\t\tconst B = corner\n\t\t\tconst C = corner.clone().lrp(nextCorner, 4 / nextDist)\n\n\t\t\tpath += `M${A.x},${A.y} L${B.x},${B.y} L${C.x},${C.y} `\n\t\t}\n\t\treturn path\n\t}\n\n\tgetLength(filters?: Geometry2dFilters): number {\n\t\tlet length = 0\n\t\tfor (const child of this.children) {\n\t\t\tif (child.isExcludedByFilter(filters)) continue\n\t\t\tlength += child.length\n\t\t}\n\t\treturn length\n\t}\n\n\tgetSvgPathData(): string {\n\t\treturn this.children.map((c, i) => (c.isLabel ? '' : c.getSvgPathData(i === 0))).join(' ')\n\t}\n\n\toverlapsPolygon(polygon: VecLike[]): boolean {\n\t\treturn this.children.some((child) => child.overlapsPolygon(polygon))\n\t}\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -2,6 +2,8 @@ import { tlenv } from "../globals/environment.mjs";
2
2
  const MAX_ZOOM_STEP = 10;
3
3
  function normalizeWheel(event) {
4
4
  let { deltaY, deltaX } = event;
5
+ if (!Number.isFinite(deltaX)) deltaX = 0;
6
+ if (!Number.isFinite(deltaY)) deltaY = 0;
5
7
  let deltaZ = 0;
6
8
  if (event.ctrlKey || event.altKey || event.metaKey) {
7
9
  deltaZ = (Math.abs(deltaY) > MAX_ZOOM_STEP ? MAX_ZOOM_STEP * Math.sign(deltaY) : deltaY) / 100;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/utils/normalizeWheel.ts"],
4
- "sourcesContent": ["import { tlenv } from '../globals/environment'\n\nconst MAX_ZOOM_STEP = 10\n\n/**\n * Normalizes a wheel event, so that the delta values are consistent across different browsers or devices. Adapted from https://stackoverflow.com/a/13650579.\n * @param event - The wheel event to normalize.\n * @returns The normalized wheel event.\n * @internal */\nexport function normalizeWheel(event: WheelEvent | React.WheelEvent<HTMLElement>) {\n\tlet { deltaY, deltaX } = event\n\tlet deltaZ = 0\n\n\t// wheeling\n\tif (event.ctrlKey || event.altKey || event.metaKey) {\n\t\tdeltaZ = (Math.abs(deltaY) > MAX_ZOOM_STEP ? MAX_ZOOM_STEP * Math.sign(deltaY) : deltaY) / 100\n\t} else {\n\t\tif (event.shiftKey && !tlenv.isDarwin && !tlenv.isIos) {\n\t\t\tdeltaX = deltaY\n\t\t\tdeltaY = 0\n\t\t}\n\t}\n\n\treturn { x: -deltaX, y: -deltaY, z: -deltaZ }\n}\n"],
5
- "mappings": "AAAA,SAAS,aAAa;AAEtB,MAAM,gBAAgB;AAOf,SAAS,eAAe,OAAmD;AACjF,MAAI,EAAE,QAAQ,OAAO,IAAI;AACzB,MAAI,SAAS;AAGb,MAAI,MAAM,WAAW,MAAM,UAAU,MAAM,SAAS;AACnD,cAAU,KAAK,IAAI,MAAM,IAAI,gBAAgB,gBAAgB,KAAK,KAAK,MAAM,IAAI,UAAU;AAAA,EAC5F,OAAO;AACN,QAAI,MAAM,YAAY,CAAC,MAAM,YAAY,CAAC,MAAM,OAAO;AACtD,eAAS;AACT,eAAS;AAAA,IACV;AAAA,EACD;AAEA,SAAO,EAAE,GAAG,CAAC,QAAQ,GAAG,CAAC,QAAQ,GAAG,CAAC,OAAO;AAC7C;",
4
+ "sourcesContent": ["import { tlenv } from '../globals/environment'\n\nconst MAX_ZOOM_STEP = 10\n\n/**\n * Normalizes a wheel event, so that the delta values are consistent across different browsers or devices. Adapted from https://stackoverflow.com/a/13650579.\n * @param event - The wheel event to normalize.\n * @returns The normalized wheel event.\n * @internal */\nexport function normalizeWheel(event: WheelEvent | React.WheelEvent<HTMLElement>) {\n\tlet { deltaY, deltaX } = event\n\n\t// Coerce non-finite deltas to no movement so they can't propagate into the camera and\n\t// crash validation. Also covers deltaZ below, which is derived from deltaY.\n\tif (!Number.isFinite(deltaX)) deltaX = 0\n\tif (!Number.isFinite(deltaY)) deltaY = 0\n\n\tlet deltaZ = 0\n\n\t// wheeling\n\tif (event.ctrlKey || event.altKey || event.metaKey) {\n\t\tdeltaZ = (Math.abs(deltaY) > MAX_ZOOM_STEP ? MAX_ZOOM_STEP * Math.sign(deltaY) : deltaY) / 100\n\t} else {\n\t\tif (event.shiftKey && !tlenv.isDarwin && !tlenv.isIos) {\n\t\t\tdeltaX = deltaY\n\t\t\tdeltaY = 0\n\t\t}\n\t}\n\n\treturn { x: -deltaX, y: -deltaY, z: -deltaZ }\n}\n"],
5
+ "mappings": "AAAA,SAAS,aAAa;AAEtB,MAAM,gBAAgB;AAOf,SAAS,eAAe,OAAmD;AACjF,MAAI,EAAE,QAAQ,OAAO,IAAI;AAIzB,MAAI,CAAC,OAAO,SAAS,MAAM,EAAG,UAAS;AACvC,MAAI,CAAC,OAAO,SAAS,MAAM,EAAG,UAAS;AAEvC,MAAI,SAAS;AAGb,MAAI,MAAM,WAAW,MAAM,UAAU,MAAM,SAAS;AACnD,cAAU,KAAK,IAAI,MAAM,IAAI,gBAAgB,gBAAgB,KAAK,KAAK,MAAM,IAAI,UAAU;AAAA,EAC5F,OAAO;AACN,QAAI,MAAM,YAAY,CAAC,MAAM,YAAY,CAAC,MAAM,OAAO;AACtD,eAAS;AACT,eAAS;AAAA,IACV;AAAA,EACD;AAEA,SAAO,EAAE,GAAG,CAAC,QAAQ,GAAG,CAAC,QAAQ,GAAG,CAAC,OAAO;AAC7C;",
6
6
  "names": []
7
7
  }
@@ -1,8 +1,8 @@
1
- const version = "5.3.2";
1
+ const version = "5.4.0-canary.0063438d1c75";
2
2
  const publishDates = {
3
3
  major: "2026-05-06T16:28:18.473Z",
4
- minor: "2026-08-05T11:39:54.978Z",
5
- patch: "2026-08-18T09:59:40.172Z"
4
+ minor: "2026-08-19T17:17:06.569Z",
5
+ patch: "2026-08-19T17:17:06.569Z"
6
6
  };
7
7
  export {
8
8
  publishDates,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/version.ts"],
4
- "sourcesContent": ["// This file is automatically generated by internal/scripts/refresh-assets.ts.\n// Do not edit manually. Or do, I'm a comment, not a cop.\n\nexport const version = '5.3.2'\nexport const publishDates = {\n\tmajor: '2026-05-06T16:28:18.473Z',\n\tminor: '2026-08-05T11:39:54.978Z',\n\tpatch: '2026-08-18T09:59:40.172Z',\n}\n"],
4
+ "sourcesContent": ["// This file is automatically generated by internal/scripts/refresh-assets.ts.\n// Do not edit manually. Or do, I'm a comment, not a cop.\n\nexport const version = '5.4.0-canary.0063438d1c75'\nexport const publishDates = {\n\tmajor: '2026-05-06T16:28:18.473Z',\n\tminor: '2026-08-19T17:17:06.569Z',\n\tpatch: '2026-08-19T17:17:06.569Z',\n}\n"],
5
5
  "mappings": "AAGO,MAAM,UAAU;AAChB,MAAM,eAAe;AAAA,EAC3B,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AACR;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tldraw/editor",
3
3
  "description": "tldraw infinite canvas SDK (editor).",
4
- "version": "5.3.2",
4
+ "version": "5.4.0-canary.0063438d1c75",
5
5
  "author": {
6
6
  "name": "tldraw Inc.",
7
7
  "email": "hello@tldraw.com"
@@ -55,12 +55,12 @@
55
55
  "@tiptap/core": "^3.12.1",
56
56
  "@tiptap/pm": "^3.12.1",
57
57
  "@tiptap/react": "^3.12.1",
58
- "@tldraw/state": "5.3.2",
59
- "@tldraw/state-react": "5.3.2",
60
- "@tldraw/store": "5.3.2",
61
- "@tldraw/tlschema": "5.3.2",
62
- "@tldraw/utils": "5.3.2",
63
- "@tldraw/validate": "5.3.2",
58
+ "@tldraw/state": "5.4.0-canary.0063438d1c75",
59
+ "@tldraw/state-react": "5.4.0-canary.0063438d1c75",
60
+ "@tldraw/store": "5.4.0-canary.0063438d1c75",
61
+ "@tldraw/tlschema": "5.4.0-canary.0063438d1c75",
62
+ "@tldraw/utils": "5.4.0-canary.0063438d1c75",
63
+ "@tldraw/validate": "5.4.0-canary.0063438d1c75",
64
64
  "classnames": "^2.5.1",
65
65
  "eventemitter3": "^4.0.7",
66
66
  "idb": "^7.1.1",
package/src/index.ts CHANGED
@@ -290,6 +290,7 @@ export {
290
290
  export { getOwnerDocument, getOwnerWindow } from './lib/exports/domUtils'
291
291
  export { getSvgAsImage } from './lib/exports/getSvgAsImage'
292
292
  export { tlenv, tlenvReactive } from './lib/globals/environment'
293
+ export { tleditors } from './lib/globals/editors'
293
294
  export { tlmenus } from './lib/globals/menus'
294
295
  export { tltime } from './lib/globals/time'
295
296
  export {
@@ -329,6 +330,7 @@ export {
329
330
  export { useTransform } from './lib/hooks/useTransform'
330
331
  export { isCursorInViewport } from './lib/utils/collaborators'
331
332
  export { useTLSchemaFromUtils, useTLStore } from './lib/hooks/useTLStore'
333
+ // oxlint-disable-next-line typescript/no-deprecated
332
334
  export { useViewportHeight } from './lib/hooks/useViewportHeight'
333
335
  export {
334
336
  LicenseManager,