@tldraw/editor 3.14.0-canary.f6a0206007b3 → 3.14.0-canary.fd17def565a1

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 (128) hide show
  1. package/dist-cjs/index.d.ts +11 -21
  2. package/dist-cjs/index.js +8 -8
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/editor/Editor.js +42 -71
  5. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  6. package/dist-cjs/lib/editor/derivations/bindingsIndex.js +22 -22
  7. package/dist-cjs/lib/editor/derivations/bindingsIndex.js.map +2 -2
  8. package/dist-cjs/lib/editor/derivations/parentsToChildren.js +16 -16
  9. package/dist-cjs/lib/editor/derivations/parentsToChildren.js.map +2 -2
  10. package/dist-cjs/lib/editor/managers/{ClickManager.js → ClickManager/ClickManager.js} +1 -1
  11. package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js.map +7 -0
  12. package/dist-cjs/lib/editor/managers/{EdgeScrollManager.js → EdgeScrollManager/EdgeScrollManager.js} +2 -2
  13. package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js.map +7 -0
  14. package/dist-cjs/lib/editor/managers/FocusManager/FocusManager.js.map +7 -0
  15. package/dist-cjs/lib/editor/managers/{FontManager.js → FontManager/FontManager.js} +5 -1
  16. package/dist-cjs/lib/editor/managers/FontManager/FontManager.js.map +7 -0
  17. package/dist-cjs/lib/editor/managers/{HistoryManager.js → HistoryManager/HistoryManager.js} +64 -6
  18. package/dist-cjs/lib/editor/managers/HistoryManager/HistoryManager.js.map +7 -0
  19. package/dist-cjs/lib/editor/managers/{ScribbleManager.js → ScribbleManager/ScribbleManager.js} +1 -1
  20. package/dist-cjs/lib/editor/managers/ScribbleManager/ScribbleManager.js.map +7 -0
  21. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +7 -0
  22. package/dist-cjs/lib/editor/managers/{TickManager.js → TickManager/TickManager.js} +1 -1
  23. package/dist-cjs/lib/editor/managers/TickManager/TickManager.js.map +7 -0
  24. package/dist-cjs/lib/editor/managers/{UserPreferencesManager.js → UserPreferencesManager/UserPreferencesManager.js} +1 -1
  25. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +7 -0
  26. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +1 -1
  27. package/dist-cjs/lib/exports/getSvgJsx.js.map +1 -1
  28. package/dist-cjs/lib/primitives/Box.js +33 -33
  29. package/dist-cjs/lib/primitives/Box.js.map +2 -2
  30. package/dist-cjs/lib/utils/reorderShapes.js +11 -10
  31. package/dist-cjs/lib/utils/reorderShapes.js.map +2 -2
  32. package/dist-cjs/lib/utils/richText.js.map +1 -1
  33. package/dist-cjs/version.js +3 -3
  34. package/dist-cjs/version.js.map +1 -1
  35. package/dist-esm/index.d.mts +11 -21
  36. package/dist-esm/index.mjs +12 -8
  37. package/dist-esm/index.mjs.map +2 -2
  38. package/dist-esm/lib/editor/Editor.mjs +42 -71
  39. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  40. package/dist-esm/lib/editor/derivations/bindingsIndex.mjs +22 -22
  41. package/dist-esm/lib/editor/derivations/bindingsIndex.mjs.map +2 -2
  42. package/dist-esm/lib/editor/derivations/parentsToChildren.mjs +16 -16
  43. package/dist-esm/lib/editor/derivations/parentsToChildren.mjs.map +2 -2
  44. package/dist-esm/lib/editor/managers/{ClickManager.mjs → ClickManager/ClickManager.mjs} +1 -1
  45. package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs.map +7 -0
  46. package/dist-esm/lib/editor/managers/{EdgeScrollManager.mjs → EdgeScrollManager/EdgeScrollManager.mjs} +2 -2
  47. package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs.map +7 -0
  48. package/dist-esm/lib/editor/managers/FocusManager/FocusManager.mjs.map +7 -0
  49. package/dist-esm/lib/editor/managers/{FontManager.mjs → FontManager/FontManager.mjs} +5 -1
  50. package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs.map +7 -0
  51. package/dist-esm/lib/editor/managers/{HistoryManager.mjs → HistoryManager/HistoryManager.mjs} +60 -2
  52. package/dist-esm/lib/editor/managers/HistoryManager/HistoryManager.mjs.map +7 -0
  53. package/dist-esm/lib/editor/managers/{ScribbleManager.mjs → ScribbleManager/ScribbleManager.mjs} +1 -1
  54. package/dist-esm/lib/editor/managers/ScribbleManager/ScribbleManager.mjs.map +7 -0
  55. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +7 -0
  56. package/dist-esm/lib/editor/managers/{TickManager.mjs → TickManager/TickManager.mjs} +1 -1
  57. package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs.map +7 -0
  58. package/dist-esm/lib/editor/managers/{UserPreferencesManager.mjs → UserPreferencesManager/UserPreferencesManager.mjs} +1 -1
  59. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +7 -0
  60. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +1 -1
  61. package/dist-esm/lib/exports/getSvgJsx.mjs.map +1 -1
  62. package/dist-esm/lib/primitives/Box.mjs +33 -33
  63. package/dist-esm/lib/primitives/Box.mjs.map +2 -2
  64. package/dist-esm/lib/utils/reorderShapes.mjs +11 -10
  65. package/dist-esm/lib/utils/reorderShapes.mjs.map +2 -2
  66. package/dist-esm/lib/utils/richText.mjs.map +1 -1
  67. package/dist-esm/version.mjs +3 -3
  68. package/dist-esm/version.mjs.map +1 -1
  69. package/package.json +7 -7
  70. package/src/index.ts +13 -7
  71. package/src/lib/editor/Editor.test.ts +252 -3
  72. package/src/lib/editor/Editor.ts +44 -73
  73. package/src/lib/editor/derivations/bindingsIndex.ts +27 -26
  74. package/src/lib/editor/derivations/parentsToChildren.ts +28 -25
  75. package/src/lib/editor/managers/ClickManager/ClickManager.test.ts +442 -0
  76. package/src/lib/editor/managers/{ClickManager.ts → ClickManager/ClickManager.ts} +3 -3
  77. package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.test.ts +374 -0
  78. package/src/lib/editor/managers/{EdgeScrollManager.ts → EdgeScrollManager/EdgeScrollManager.ts} +3 -3
  79. package/src/lib/editor/managers/FocusManager/FocusManager.test.ts +455 -0
  80. package/src/lib/editor/managers/{FocusManager.ts → FocusManager/FocusManager.ts} +1 -1
  81. package/src/lib/editor/managers/FontManager/FontManager.test.ts +263 -0
  82. package/src/lib/editor/managers/{FontManager.ts → FontManager/FontManager.ts} +6 -2
  83. package/src/lib/editor/managers/{HistoryManager.test.ts → HistoryManager/HistoryManager.test.ts} +388 -1
  84. package/src/lib/editor/managers/{HistoryManager.ts → HistoryManager/HistoryManager.ts} +73 -2
  85. package/src/lib/editor/managers/ScribbleManager/ScribbleManager.test.ts +624 -0
  86. package/src/lib/editor/managers/{ScribbleManager.ts → ScribbleManager/ScribbleManager.ts} +2 -2
  87. package/src/lib/editor/managers/SnapManager/SnapManager.test.ts +485 -0
  88. package/src/lib/editor/managers/TextManager/TextManager.test.ts +411 -0
  89. package/src/lib/editor/managers/{TextManager.ts → TextManager/TextManager.ts} +1 -1
  90. package/src/lib/editor/managers/TickManager/TickManager.test.ts +314 -0
  91. package/src/lib/editor/managers/{TickManager.ts → TickManager/TickManager.ts} +2 -2
  92. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +591 -0
  93. package/src/lib/editor/managers/{UserPreferencesManager.ts → UserPreferencesManager/UserPreferencesManager.ts} +2 -2
  94. package/src/lib/editor/shapes/ShapeUtil.ts +1 -1
  95. package/src/lib/exports/getSvgJsx.tsx +1 -1
  96. package/src/lib/primitives/Box.test.ts +588 -7
  97. package/src/lib/primitives/Box.ts +33 -33
  98. package/src/lib/utils/reorderShapes.ts +10 -13
  99. package/src/lib/utils/richText.ts +1 -1
  100. package/src/version.ts +3 -3
  101. package/dist-cjs/lib/editor/managers/ClickManager.js.map +0 -7
  102. package/dist-cjs/lib/editor/managers/EdgeScrollManager.js.map +0 -7
  103. package/dist-cjs/lib/editor/managers/FocusManager.js.map +0 -7
  104. package/dist-cjs/lib/editor/managers/FontManager.js.map +0 -7
  105. package/dist-cjs/lib/editor/managers/HistoryManager.js.map +0 -7
  106. package/dist-cjs/lib/editor/managers/ScribbleManager.js.map +0 -7
  107. package/dist-cjs/lib/editor/managers/Stack.js +0 -82
  108. package/dist-cjs/lib/editor/managers/Stack.js.map +0 -7
  109. package/dist-cjs/lib/editor/managers/TextManager.js.map +0 -7
  110. package/dist-cjs/lib/editor/managers/TickManager.js.map +0 -7
  111. package/dist-cjs/lib/editor/managers/UserPreferencesManager.js.map +0 -7
  112. package/dist-esm/lib/editor/managers/ClickManager.mjs.map +0 -7
  113. package/dist-esm/lib/editor/managers/EdgeScrollManager.mjs.map +0 -7
  114. package/dist-esm/lib/editor/managers/FocusManager.mjs.map +0 -7
  115. package/dist-esm/lib/editor/managers/FontManager.mjs.map +0 -7
  116. package/dist-esm/lib/editor/managers/HistoryManager.mjs.map +0 -7
  117. package/dist-esm/lib/editor/managers/ScribbleManager.mjs.map +0 -7
  118. package/dist-esm/lib/editor/managers/Stack.mjs +0 -62
  119. package/dist-esm/lib/editor/managers/Stack.mjs.map +0 -7
  120. package/dist-esm/lib/editor/managers/TextManager.mjs.map +0 -7
  121. package/dist-esm/lib/editor/managers/TickManager.mjs.map +0 -7
  122. package/dist-esm/lib/editor/managers/UserPreferencesManager.mjs.map +0 -7
  123. package/src/lib/editor/managers/ScribbleManager.test.ts +0 -32
  124. package/src/lib/editor/managers/Stack.ts +0 -71
  125. /package/dist-cjs/lib/editor/managers/{FocusManager.js → FocusManager/FocusManager.js} +0 -0
  126. /package/dist-cjs/lib/editor/managers/{TextManager.js → TextManager/TextManager.js} +0 -0
  127. /package/dist-esm/lib/editor/managers/{FocusManager.mjs → FocusManager/FocusManager.mjs} +0 -0
  128. /package/dist-esm/lib/editor/managers/{TextManager.mjs → TextManager/TextManager.mjs} +0 -0
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/utils/richText.ts"],
4
- "sourcesContent": ["import { getSchema, JSONContent, Editor as TTEditor } from '@tiptap/core'\nimport { Node } from '@tiptap/pm/model'\nimport { EditorProviderProps } from '@tiptap/react'\nimport { TLRichText } from '@tldraw/tlschema'\nimport { assert } from '@tldraw/utils'\nimport { Editor } from '../editor/Editor'\nimport { TLFontFace } from '../editor/managers/FontManager'\n\n/**\n * This is the TipTap editor! Docs are {@link https://tiptap.dev/docs}.\n *\n * @public\n */\nexport type TiptapEditor = TTEditor\n\n/**\n * A TipTap node. See {@link https://tiptap.dev/docs}.\n * @public\n */\nexport type TiptapNode = Node\n\n/** @public */\nexport interface TLTextOptions {\n\ttipTapConfig?: EditorProviderProps\n\taddFontsFromNode?: RichTextFontVisitor\n}\n\n/** @public */\nexport interface RichTextFontVisitorState {\n\treadonly family: string\n\treadonly weight: string\n\treadonly style: string\n}\n\n/** @public */\nexport type RichTextFontVisitor = (\n\tnode: TiptapNode,\n\tstate: RichTextFontVisitorState,\n\taddFont: (font: TLFontFace) => void\n) => RichTextFontVisitorState\n\n/** @public */\nexport function getFontsFromRichText(\n\teditor: Editor,\n\trichText: TLRichText,\n\tinitialState: RichTextFontVisitorState\n) {\n\tconst { tipTapConfig, addFontsFromNode } = editor.getTextOptions()\n\tassert(tipTapConfig, 'textOptions.tipTapConfig must be set to use rich text')\n\tassert(addFontsFromNode, 'textOptions.addFontsFromNode must be set to use rich text')\n\n\tconst schema = getSchema(tipTapConfig.extensions ?? [])\n\tconst rootNode = Node.fromJSON(schema, richText as JSONContent)\n\n\tconst fonts = new Set<TLFontFace>()\n\n\tfunction addFont(font: TLFontFace) {\n\t\tfonts.add(font)\n\t}\n\n\tfunction visit(node: TiptapNode, state: RichTextFontVisitorState) {\n\t\tstate = addFontsFromNode!(node, state, addFont)\n\n\t\tfor (const child of node.children) {\n\t\t\tvisit(child, state)\n\t\t}\n\t}\n\n\tvisit(rootNode, initialState)\n\n\treturn Array.from(fonts)\n}\n"],
4
+ "sourcesContent": ["import { getSchema, JSONContent, Editor as TTEditor } from '@tiptap/core'\nimport { Node } from '@tiptap/pm/model'\nimport { EditorProviderProps } from '@tiptap/react'\nimport { TLRichText } from '@tldraw/tlschema'\nimport { assert } from '@tldraw/utils'\nimport { Editor } from '../editor/Editor'\nimport { TLFontFace } from '../editor/managers/FontManager/FontManager'\n\n/**\n * This is the TipTap editor! Docs are {@link https://tiptap.dev/docs}.\n *\n * @public\n */\nexport type TiptapEditor = TTEditor\n\n/**\n * A TipTap node. See {@link https://tiptap.dev/docs}.\n * @public\n */\nexport type TiptapNode = Node\n\n/** @public */\nexport interface TLTextOptions {\n\ttipTapConfig?: EditorProviderProps\n\taddFontsFromNode?: RichTextFontVisitor\n}\n\n/** @public */\nexport interface RichTextFontVisitorState {\n\treadonly family: string\n\treadonly weight: string\n\treadonly style: string\n}\n\n/** @public */\nexport type RichTextFontVisitor = (\n\tnode: TiptapNode,\n\tstate: RichTextFontVisitorState,\n\taddFont: (font: TLFontFace) => void\n) => RichTextFontVisitorState\n\n/** @public */\nexport function getFontsFromRichText(\n\teditor: Editor,\n\trichText: TLRichText,\n\tinitialState: RichTextFontVisitorState\n) {\n\tconst { tipTapConfig, addFontsFromNode } = editor.getTextOptions()\n\tassert(tipTapConfig, 'textOptions.tipTapConfig must be set to use rich text')\n\tassert(addFontsFromNode, 'textOptions.addFontsFromNode must be set to use rich text')\n\n\tconst schema = getSchema(tipTapConfig.extensions ?? [])\n\tconst rootNode = Node.fromJSON(schema, richText as JSONContent)\n\n\tconst fonts = new Set<TLFontFace>()\n\n\tfunction addFont(font: TLFontFace) {\n\t\tfonts.add(font)\n\t}\n\n\tfunction visit(node: TiptapNode, state: RichTextFontVisitorState) {\n\t\tstate = addFontsFromNode!(node, state, addFont)\n\n\t\tfor (const child of node.children) {\n\t\t\tvisit(child, state)\n\t\t}\n\t}\n\n\tvisit(rootNode, initialState)\n\n\treturn Array.from(fonts)\n}\n"],
5
5
  "mappings": "AAAA,SAAS,iBAAkD;AAC3D,SAAS,YAAY;AAGrB,SAAS,cAAc;AAsChB,SAAS,qBACf,QACA,UACA,cACC;AACD,QAAM,EAAE,cAAc,iBAAiB,IAAI,OAAO,eAAe;AACjE,SAAO,cAAc,uDAAuD;AAC5E,SAAO,kBAAkB,2DAA2D;AAEpF,QAAM,SAAS,UAAU,aAAa,cAAc,CAAC,CAAC;AACtD,QAAM,WAAW,KAAK,SAAS,QAAQ,QAAuB;AAE9D,QAAM,QAAQ,oBAAI,IAAgB;AAElC,WAAS,QAAQ,MAAkB;AAClC,UAAM,IAAI,IAAI;AAAA,EACf;AAEA,WAAS,MAAM,MAAkB,OAAiC;AACjE,YAAQ,iBAAkB,MAAM,OAAO,OAAO;AAE9C,eAAW,SAAS,KAAK,UAAU;AAClC,YAAM,OAAO,KAAK;AAAA,IACnB;AAAA,EACD;AAEA,QAAM,UAAU,YAAY;AAE5B,SAAO,MAAM,KAAK,KAAK;AACxB;",
6
6
  "names": []
7
7
  }
@@ -1,8 +1,8 @@
1
- const version = "3.14.0-canary.f6a0206007b3";
1
+ const version = "3.14.0-canary.fd17def565a1";
2
2
  const publishDates = {
3
3
  major: "2024-09-13T14:36:29.063Z",
4
- minor: "2025-06-03T09:18:36.909Z",
5
- patch: "2025-06-03T09:18:36.909Z"
4
+ minor: "2025-06-10T15:10:28.174Z",
5
+ patch: "2025-06-10T15:10:28.174Z"
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 = '3.14.0-canary.f6a0206007b3'\nexport const publishDates = {\n\tmajor: '2024-09-13T14:36:29.063Z',\n\tminor: '2025-06-03T09:18:36.909Z',\n\tpatch: '2025-06-03T09:18:36.909Z',\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 = '3.14.0-canary.fd17def565a1'\nexport const publishDates = {\n\tmajor: '2024-09-13T14:36:29.063Z',\n\tminor: '2025-06-10T15:10:28.174Z',\n\tpatch: '2025-06-10T15:10:28.174Z',\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": "A tiny little drawing app (editor).",
4
- "version": "3.14.0-canary.f6a0206007b3",
4
+ "version": "3.14.0-canary.fd17def565a1",
5
5
  "author": {
6
6
  "name": "tldraw Inc.",
7
7
  "email": "hello@tldraw.com"
@@ -48,12 +48,12 @@
48
48
  "@tiptap/core": "^2.9.1",
49
49
  "@tiptap/pm": "^2.9.1",
50
50
  "@tiptap/react": "^2.9.1",
51
- "@tldraw/state": "3.14.0-canary.f6a0206007b3",
52
- "@tldraw/state-react": "3.14.0-canary.f6a0206007b3",
53
- "@tldraw/store": "3.14.0-canary.f6a0206007b3",
54
- "@tldraw/tlschema": "3.14.0-canary.f6a0206007b3",
55
- "@tldraw/utils": "3.14.0-canary.f6a0206007b3",
56
- "@tldraw/validate": "3.14.0-canary.f6a0206007b3",
51
+ "@tldraw/state": "3.14.0-canary.fd17def565a1",
52
+ "@tldraw/state-react": "3.14.0-canary.fd17def565a1",
53
+ "@tldraw/store": "3.14.0-canary.fd17def565a1",
54
+ "@tldraw/tlschema": "3.14.0-canary.fd17def565a1",
55
+ "@tldraw/utils": "3.14.0-canary.fd17def565a1",
56
+ "@tldraw/validate": "3.14.0-canary.fd17def565a1",
57
57
  "@types/core-js": "^2.5.8",
58
58
  "@use-gesture/react": "^10.3.1",
59
59
  "classnames": "^2.5.1",
package/src/index.ts CHANGED
@@ -148,15 +148,18 @@ export {
148
148
  type BindingOnShapeIsolateOptions,
149
149
  type TLBindingUtilConstructor,
150
150
  } from './lib/editor/bindings/BindingUtil'
151
- export { ClickManager, type TLClickState } from './lib/editor/managers/ClickManager'
152
- export { EdgeScrollManager } from './lib/editor/managers/EdgeScrollManager'
151
+ export { ClickManager, type TLClickState } from './lib/editor/managers/ClickManager/ClickManager'
152
+ export { EdgeScrollManager } from './lib/editor/managers/EdgeScrollManager/EdgeScrollManager'
153
153
  export {
154
154
  FontManager,
155
155
  type TLFontFace,
156
156
  type TLFontFaceSource,
157
- } from './lib/editor/managers/FontManager'
158
- export { HistoryManager } from './lib/editor/managers/HistoryManager'
159
- export { ScribbleManager, type ScribbleItem } from './lib/editor/managers/ScribbleManager'
157
+ } from './lib/editor/managers/FontManager/FontManager'
158
+ export { HistoryManager } from './lib/editor/managers/HistoryManager/HistoryManager'
159
+ export {
160
+ ScribbleManager,
161
+ type ScribbleItem,
162
+ } from './lib/editor/managers/ScribbleManager/ScribbleManager'
160
163
  export {
161
164
  BoundsSnaps,
162
165
  type BoundsSnapGeometry,
@@ -170,8 +173,11 @@ export {
170
173
  type SnapData,
171
174
  type SnapIndicator,
172
175
  } from './lib/editor/managers/SnapManager/SnapManager'
173
- export { TextManager, type TLMeasureTextSpanOpts } from './lib/editor/managers/TextManager'
174
- export { UserPreferencesManager } from './lib/editor/managers/UserPreferencesManager'
176
+ export {
177
+ TextManager,
178
+ type TLMeasureTextSpanOpts,
179
+ } from './lib/editor/managers/TextManager/TextManager'
180
+ export { UserPreferencesManager } from './lib/editor/managers/UserPreferencesManager/UserPreferencesManager'
175
181
  export { BaseBoxShapeUtil, type TLBaseBoxShape } from './lib/editor/shapes/BaseBoxShapeUtil'
176
182
  export {
177
183
  ShapeUtil,
@@ -17,6 +17,7 @@ type ICustomShape = TLBaseShape<
17
17
  w: number
18
18
  h: number
19
19
  text: string | undefined
20
+ isFilled: boolean
20
21
  }
21
22
  >
22
23
 
@@ -26,19 +27,21 @@ class CustomShape extends ShapeUtil<ICustomShape> {
26
27
  w: T.number,
27
28
  h: T.number,
28
29
  text: T.string.optional(),
30
+ isFilled: T.boolean,
29
31
  }
30
32
  getDefaultProps(): ICustomShape['props'] {
31
33
  return {
32
34
  w: 200,
33
35
  h: 200,
34
36
  text: '',
37
+ isFilled: false,
35
38
  }
36
39
  }
37
40
  getGeometry(shape: ICustomShape): Geometry2d {
38
41
  return new Rectangle2d({
39
42
  width: shape.props.w,
40
43
  height: shape.props.h,
41
- isFilled: true,
44
+ isFilled: shape.props.isFilled,
42
45
  })
43
46
  }
44
47
  indicator() {}
@@ -81,11 +84,11 @@ describe('updateShape', () => {
81
84
  props: { w: 100, h: 100, text: 'Hello' },
82
85
  })
83
86
  const shape = editor.getShape(id) as ICustomShape
84
- expect(shape.props).toEqual({ w: 100, h: 100, text: 'Hello' })
87
+ expect(shape.props).toEqual({ w: 100, h: 100, text: 'Hello', isFilled: false })
85
88
 
86
89
  editor.updateShape({ ...shape, props: { ...shape.props, text: undefined } })
87
90
  const updatedShape = editor.getShape(id) as ICustomShape
88
- expect(updatedShape.props).toEqual({ w: 100, h: 100, text: undefined })
91
+ expect(updatedShape.props).toEqual({ w: 100, h: 100, text: undefined, isFilled: false })
89
92
  })
90
93
  })
91
94
 
@@ -176,3 +179,249 @@ describe('zoomToBounds', () => {
176
179
  expect(editor.setCamera).toHaveBeenCalled()
177
180
  })
178
181
  })
182
+
183
+ describe('getShapesAtPoint', () => {
184
+ const ids = {
185
+ shape1: createShapeId('shape1'),
186
+ shape2: createShapeId('shape2'),
187
+ shape3: createShapeId('shape3'),
188
+ shape4: createShapeId('shape4'),
189
+ shape5: createShapeId('shape5'),
190
+ overlap1: createShapeId('overlap1'),
191
+ overlap2: createShapeId('overlap2'),
192
+ filledShape: createShapeId('filledShape'),
193
+ hollowShape: createShapeId('hollowShape'),
194
+ hiddenShape: createShapeId('hiddenShape'),
195
+ }
196
+
197
+ beforeEach(() => {
198
+ // Create test shapes with different z-index positions
199
+ // Shape 1: Bottom layer, large square
200
+ editor.createShape({
201
+ id: ids.shape1,
202
+ type: 'my-custom-shape',
203
+ x: 0,
204
+ y: 0,
205
+ props: { w: 200, h: 200, text: 'Bottom' },
206
+ })
207
+
208
+ // Shape 2: Middle layer, overlapping square
209
+ editor.createShape({
210
+ id: ids.shape2,
211
+ type: 'my-custom-shape',
212
+ x: 100,
213
+ y: 0,
214
+ props: { w: 200, h: 200, text: 'Middle' },
215
+ })
216
+
217
+ // Shape 3: Top layer, small square
218
+ editor.createShape({
219
+ id: ids.shape3,
220
+ type: 'my-custom-shape',
221
+ x: 50,
222
+ y: 50,
223
+ props: { w: 100, h: 100, text: 'Top' },
224
+ })
225
+
226
+ // Shape 4: Separate area, no overlap
227
+ editor.createShape({
228
+ id: ids.shape4,
229
+ type: 'my-custom-shape',
230
+ x: 50,
231
+ y: 100,
232
+ props: { w: 100, h: 100, text: 'Separate' },
233
+ })
234
+ })
235
+
236
+ it('returns shapes at a point in reverse z-index order', () => {
237
+ // Point at (50, 50) should hit shape3's edge (since it's at 50,50 with size 100x100)
238
+ // This point is exactly at the top-left corner of shape3
239
+ const shapes = editor.getShapesAtPoint({ x: 50, y: 50 })
240
+ const shapeIds = shapes.map((s) => s.id)
241
+
242
+ expect(shapeIds).toEqual([ids.shape3])
243
+ expect(shapes).toHaveLength(1)
244
+ })
245
+
246
+ it('returns empty array when no shapes at point', () => {
247
+ const shapes = editor.getShapesAtPoint({ x: 1000, y: 1000 })
248
+ expect(shapes).toEqual([])
249
+ })
250
+
251
+ it('returns single shape when point hits only one shape', () => {
252
+ // Point at right edge of shape2 where it doesn't overlap with other shapes
253
+ // Shape2 is at (100,0) with size 200x200, so right edge is at x=300
254
+ const shapes = editor.getShapesAtPoint({ x: 300, y: 100 })
255
+ expect(shapes).toHaveLength(1)
256
+ expect(shapes[0].id).toBe(ids.shape2)
257
+ })
258
+
259
+ it('returns shapes on edge when point is exactly on boundary', () => {
260
+ // Point at exact edge of shape1
261
+ const shapes = editor.getShapesAtPoint({ x: 0, y: 0 })
262
+ expect(shapes).toHaveLength(1)
263
+ expect(shapes[0].id).toBe(ids.shape1)
264
+ })
265
+
266
+ it('respects hitInside option when false (default)', () => {
267
+ // Point inside shape1 (at 0,0 with size 200x200) but with hitInside false should not hit
268
+ const shapes = editor.getShapesAtPoint({ x: 25, y: 25 }, { hitInside: false })
269
+ expect(shapes).toEqual([])
270
+ })
271
+
272
+ it('respects hitInside option when true', () => {
273
+ // Point inside shape1 (at 0,0 with size 200x200) with hitInside true should hit
274
+ const shapes = editor.getShapesAtPoint({ x: 25, y: 25 }, { hitInside: true })
275
+ expect(shapes).toHaveLength(1)
276
+ expect(shapes[0].id).toBe(ids.shape1)
277
+ })
278
+
279
+ it('respects margin option', () => {
280
+ // Point slightly outside shape1 at bottom edge but within margin should hit only shape1
281
+ // Shape1 is at (0,0) with size 200x200, shape2 goes to (300,200) so avoid overlap at (200,200)
282
+ const shapes = editor.getShapesAtPoint({ x: 205, y: 100 }, { margin: 10 })
283
+ expect(shapes).toHaveLength(1)
284
+ expect(shapes[0].id).toBe(ids.shape1)
285
+ })
286
+
287
+ it('filters out hidden shapes', () => {
288
+ // Create a spy to mock isShapeHidden
289
+ const isShapeHiddenSpy = jest.spyOn(editor, 'isShapeHidden')
290
+ isShapeHiddenSpy.mockImplementation((shape) => {
291
+ return typeof shape === 'string' ? shape === ids.shape3 : shape.id === ids.shape3
292
+ })
293
+
294
+ const shapes = editor.getShapesAtPoint({ x: 50, y: 50 })
295
+ const shapeIds = shapes.map((s) => s.id)
296
+
297
+ // Should not include shape3 since it's hidden, and no other shapes are at this point
298
+ expect(shapeIds).toEqual([])
299
+ expect(shapes).toHaveLength(0)
300
+
301
+ isShapeHiddenSpy.mockRestore()
302
+ })
303
+
304
+ it('handles point exactly at shape corner', () => {
305
+ // Point at bottom-left corner of shape1 where it doesn't overlap with other shapes
306
+ const shapes = editor.getShapesAtPoint({ x: 0, y: 200 })
307
+ expect(shapes).toHaveLength(1)
308
+ expect(shapes[0].id).toBe(ids.shape1)
309
+ })
310
+
311
+ it('handles overlapping shapes with different hit areas', () => {
312
+ // Point that hits both shape1 and shape2 edges (they overlap at x=100,y=0)
313
+ const shapes = editor.getShapesAtPoint({ x: 100, y: 0 })
314
+ const shapeIds = shapes.map((s) => s.id)
315
+
316
+ // Both shapes should be detected at this overlapping point (reversed order - top-most first)
317
+ expect(shapeIds).toEqual([ids.shape2, ids.shape1])
318
+ expect(shapes).toHaveLength(2)
319
+ })
320
+
321
+ it('maintains reverse shape order and responds to z-index changes', () => {
322
+ // Create filled shape that overlaps with shape2
323
+ editor.createShape({
324
+ id: ids.shape5,
325
+ type: 'my-custom-shape',
326
+ x: 110,
327
+ y: 110,
328
+ props: { w: 200, h: 200, isFilled: true, text: 'Shape5' },
329
+ })
330
+
331
+ // Test with hitInside to detect multiple shapes
332
+ // Point (120,120) will hit shape1, shape2, shape3, shape4, and shape5 with hitInside: true
333
+ const shapes = editor.getShapesAtPoint({ x: 120, y: 120 }, { hitInside: true })
334
+ const shapeIds = shapes.map((s) => s.id)
335
+
336
+ // All shapes that contain this point should be returned in reverse z-index order (top-most first)
337
+ expect(shapeIds).toEqual([ids.shape5, ids.shape4, ids.shape3, ids.shape2, ids.shape1])
338
+
339
+ // After bringing shape2 to front, order should change (shape2 becomes top-most)
340
+ editor.bringToFront([ids.shape2])
341
+ const shapes2 = editor.getShapesAtPoint({ x: 120, y: 120 }, { hitInside: true })
342
+ const shapeIds2 = shapes2.map((s) => s.id)
343
+ expect(shapeIds2).toEqual([ids.shape2, ids.shape5, ids.shape4, ids.shape3, ids.shape1])
344
+ })
345
+
346
+ it('combines hitInside and margin options', () => {
347
+ // Point inside shape1 (at 0,0 with size 200x200) with hitInside and margin
348
+ const shapes = editor.getShapesAtPoint({ x: 25, y: 25 }, { hitInside: true, margin: 5 })
349
+ expect(shapes).toHaveLength(1)
350
+ expect(shapes[0].id).toBe(ids.shape1)
351
+ })
352
+
353
+ it('returns empty array when all shapes are hidden', () => {
354
+ // Mock all shapes as hidden
355
+ const isShapeHiddenSpy = jest.spyOn(editor, 'isShapeHidden')
356
+ isShapeHiddenSpy.mockReturnValue(true)
357
+
358
+ const shapes = editor.getShapesAtPoint({ x: 50, y: 50 })
359
+ expect(shapes).toEqual([])
360
+
361
+ isShapeHiddenSpy.mockRestore()
362
+ })
363
+
364
+ it('returns multiple shapes at same point in reverse z-index order', () => {
365
+ // Create two shapes at exactly the same position (away from existing shapes)
366
+ editor.createShape({
367
+ id: ids.overlap1,
368
+ type: 'my-custom-shape',
369
+ x: 600,
370
+ y: 600,
371
+ props: { w: 100, h: 100, text: 'First' },
372
+ })
373
+
374
+ editor.createShape({
375
+ id: ids.overlap2,
376
+ type: 'my-custom-shape',
377
+ x: 600,
378
+ y: 600,
379
+ props: { w: 100, h: 100, text: 'Second' },
380
+ })
381
+
382
+ // Test at corner where both shapes' edges meet
383
+ const shapes = editor.getShapesAtPoint({ x: 600, y: 600 })
384
+ const shapeIds = shapes.map((s) => s.id)
385
+
386
+ // Should return both shapes in reverse z-index order (top-most first)
387
+ expect(shapeIds).toEqual([ids.overlap2, ids.overlap1])
388
+ expect(shapes).toHaveLength(2)
389
+ })
390
+
391
+ it('respects isFilled property for hit detection', () => {
392
+ // Create a filled shape
393
+ editor.createShape({
394
+ id: ids.filledShape,
395
+ type: 'my-custom-shape',
396
+ x: 300,
397
+ y: 300,
398
+ props: { w: 100, h: 100, isFilled: true, text: 'Filled' },
399
+ })
400
+
401
+ // Create a hollow shape at the same position
402
+ editor.createShape({
403
+ id: ids.hollowShape,
404
+ type: 'my-custom-shape',
405
+ x: 400,
406
+ y: 300,
407
+ props: { w: 100, h: 100, isFilled: false, text: 'Hollow' },
408
+ })
409
+
410
+ // Test point inside filled shape - should hit without hitInside option
411
+ const filledShapes = editor.getShapesAtPoint({ x: 350, y: 350 })
412
+ expect(filledShapes).toHaveLength(1)
413
+ expect(filledShapes[0].id).toBe(ids.filledShape)
414
+
415
+ // Test point inside hollow shape - should not hit without hitInside option
416
+ const hollowShapes = editor.getShapesAtPoint({ x: 450, y: 350 })
417
+ expect(hollowShapes).toHaveLength(0)
418
+
419
+ // Test point inside hollow shape with hitInside - should hit
420
+ const hollowShapesWithHitInside = editor.getShapesAtPoint(
421
+ { x: 450, y: 350 },
422
+ { hitInside: true }
423
+ )
424
+ expect(hollowShapesWithHitInside).toHaveLength(1)
425
+ expect(hollowShapesWithHitInside[0].id).toBe(ids.hollowShape)
426
+ })
427
+ })
@@ -148,16 +148,16 @@ import { bindingsIndex } from './derivations/bindingsIndex'
148
148
  import { notVisibleShapes } from './derivations/notVisibleShapes'
149
149
  import { parentsToChildren } from './derivations/parentsToChildren'
150
150
  import { deriveShapeIdsInCurrentPage } from './derivations/shapeIdsInCurrentPage'
151
- import { ClickManager } from './managers/ClickManager'
152
- import { EdgeScrollManager } from './managers/EdgeScrollManager'
153
- import { FocusManager } from './managers/FocusManager'
154
- import { FontManager } from './managers/FontManager'
155
- import { HistoryManager } from './managers/HistoryManager'
156
- import { ScribbleManager } from './managers/ScribbleManager'
151
+ import { ClickManager } from './managers/ClickManager/ClickManager'
152
+ import { EdgeScrollManager } from './managers/EdgeScrollManager/EdgeScrollManager'
153
+ import { FocusManager } from './managers/FocusManager/FocusManager'
154
+ import { FontManager } from './managers/FontManager/FontManager'
155
+ import { HistoryManager } from './managers/HistoryManager/HistoryManager'
156
+ import { ScribbleManager } from './managers/ScribbleManager/ScribbleManager'
157
157
  import { SnapManager } from './managers/SnapManager/SnapManager'
158
- import { TextManager } from './managers/TextManager'
159
- import { TickManager } from './managers/TickManager'
160
- import { UserPreferencesManager } from './managers/UserPreferencesManager'
158
+ import { TextManager } from './managers/TextManager/TextManager'
159
+ import { TickManager } from './managers/TickManager/TickManager'
160
+ import { UserPreferencesManager } from './managers/UserPreferencesManager/UserPreferencesManager'
161
161
  import { ShapeUtil, TLGeometryOpts, TLResizeMode } from './shapes/ShapeUtil'
162
162
  import { RootState } from './tools/RootState'
163
163
  import { StateNode, TLStateNodeConstructor } from './tools/StateNode'
@@ -328,7 +328,7 @@ export class Editor extends EventEmitter<TLEventMap> {
328
328
  this.store = store
329
329
  this.history = new HistoryManager<TLRecord>({
330
330
  store,
331
- annotateError: (error) => {
331
+ annotateError: (error: any) => {
332
332
  this.annotateError(error, { origin: 'history.batch', willCrashApp: true })
333
333
  this.crash(error)
334
334
  },
@@ -3717,10 +3717,7 @@ export class Editor extends EventEmitter<TLEventMap> {
3717
3717
  */
3718
3718
  @computed getViewportScreenCenter() {
3719
3719
  const viewportScreenBounds = this.getViewportScreenBounds()
3720
- return new Vec(
3721
- viewportScreenBounds.midX - viewportScreenBounds.minX,
3722
- viewportScreenBounds.midY - viewportScreenBounds.minY
3723
- )
3720
+ return new Vec(viewportScreenBounds.w / 2, viewportScreenBounds.h / 2)
3724
3721
  }
3725
3722
 
3726
3723
  /**
@@ -4646,44 +4643,6 @@ export class Editor extends EventEmitter<TLEventMap> {
4646
4643
  )! as T
4647
4644
  }
4648
4645
 
4649
- private _shapePageGeometryCaches: Record<string, ComputedCache<Geometry2d, TLShape>> = {}
4650
-
4651
- /**
4652
- * Get the geometry of a shape in page-space.
4653
- *
4654
- * @example
4655
- * ```ts
4656
- * editor.getShapePageGeometry(myShape)
4657
- * editor.getShapePageGeometry(myShapeId)
4658
- * editor.getShapePageGeometry(myShapeId, { context: "arrow" })
4659
- * ```
4660
- *
4661
- * @param shape - The shape (or shape id) to get the geometry for.
4662
- * @param opts - Additional options about the request for geometry. Passed to {@link ShapeUtil.getGeometry}.
4663
- *
4664
- * @public
4665
- */
4666
- getShapePageGeometry<T extends Geometry2d>(shape: TLShape | TLShapeId, opts?: TLGeometryOpts): T {
4667
- const context = opts?.context ?? 'none'
4668
- if (!this._shapePageGeometryCaches[context]) {
4669
- this._shapePageGeometryCaches[context] = this.store.createComputedCache(
4670
- 'bounds',
4671
- (shape) => {
4672
- const geometry = this.getShapeGeometry(shape.id, opts)
4673
- const pageTransform = this.getShapePageTransform(shape.id)
4674
- return geometry.transform(pageTransform)
4675
- },
4676
- {
4677
- // we only depend directly on the shape id, and changing geometry/transform will update us anyway
4678
- areRecordsEqual: () => true,
4679
- }
4680
- )
4681
- }
4682
- return this._shapePageGeometryCaches[context].get(
4683
- typeof shape === 'string' ? shape : shape.id
4684
- )! as T
4685
- }
4686
-
4687
4646
  /** @internal */
4688
4647
  @computed private _getShapeHandlesCache(): ComputedCache<TLHandle[] | undefined, TLShape> {
4689
4648
  return this.store.createComputedCache(
@@ -4796,7 +4755,10 @@ export class Editor extends EventEmitter<TLEventMap> {
4796
4755
  /** @internal */
4797
4756
  @computed private _getShapePageBoundsCache(): ComputedCache<Box, TLShape> {
4798
4757
  return this.store.createComputedCache<Box, TLShape>('pageBoundsCache', (shape) => {
4799
- return this.getShapePageGeometry(shape).bounds
4758
+ const pageTransform = this.getShapePageTransform(shape)
4759
+ if (!pageTransform) return undefined
4760
+ const geometry = this.getShapeGeometry(shape)
4761
+ return Box.FromPoints(pageTransform.applyToPoints(geometry.vertices))
4800
4762
  })
4801
4763
  }
4802
4764
 
@@ -4870,11 +4832,12 @@ export class Editor extends EventEmitter<TLEventMap> {
4870
4832
  if (frameAncestors.length === 0) return undefined
4871
4833
 
4872
4834
  const pageMask = frameAncestors
4873
- .map<Vec[] | undefined>(
4874
- (s) =>
4875
- // Apply the frame transform to the frame outline to get the frame outline in the current page space
4876
- this.getShapePageGeometry(s.id).vertices
4877
- )
4835
+ .map<Vec[] | undefined>((s) => {
4836
+ // Apply the frame transform to the frame outline to get the frame outline in the current page space
4837
+ const geometry = this.getShapeGeometry(s.id)
4838
+ const pageTransform = this.getShapePageTransform(s.id)
4839
+ return pageTransform.applyToPoints(geometry.vertices)
4840
+ })
4878
4841
  .reduce((acc, b) => {
4879
4842
  if (!(b && acc)) return undefined
4880
4843
  const intersection = intersectPolygonPolygon(acc, b)
@@ -5072,28 +5035,33 @@ export class Editor extends EventEmitter<TLEventMap> {
5072
5035
  *
5073
5036
  * @public
5074
5037
  */
5075
- isShapeOrAncestorLocked(shape?: TLShape): boolean
5076
- isShapeOrAncestorLocked(id?: TLShapeId): boolean
5077
- isShapeOrAncestorLocked(arg?: TLShape | TLShapeId): boolean {
5078
- const shape = typeof arg === 'string' ? this.getShape(arg) : arg
5079
- if (shape === undefined) return false
5080
- if (shape.isLocked) return true
5081
- return this.isShapeOrAncestorLocked(this.getShapeParent(shape))
5038
+ isShapeOrAncestorLocked(shape?: TLShape | TLShapeId): boolean {
5039
+ const _shape = shape && this.getShape(shape)
5040
+ if (_shape === undefined) return false
5041
+ if (_shape.isLocked) return true
5042
+ return this.isShapeOrAncestorLocked(this.getShapeParent(_shape))
5082
5043
  }
5083
5044
 
5045
+ /**
5046
+ * Get shapes that are outside of the viewport.
5047
+ *
5048
+ * @public
5049
+ */
5084
5050
  @computed
5085
- private _notVisibleShapes() {
5086
- return notVisibleShapes(this)
5051
+ getNotVisibleShapes() {
5052
+ return this._notVisibleShapes.get()
5087
5053
  }
5088
5054
 
5055
+ private _notVisibleShapes = notVisibleShapes(this)
5056
+
5089
5057
  /**
5090
- * Get culled shapes.
5058
+ * Get culled shapes (those that should not render), taking into account which shapes are selected or editing.
5091
5059
  *
5092
5060
  * @public
5093
5061
  */
5094
5062
  @computed
5095
5063
  getCulledShapes() {
5096
- const notVisibleShapes = this._notVisibleShapes().get()
5064
+ const notVisibleShapes = this.getNotVisibleShapes()
5097
5065
  const selectedShapeIds = this.getSelectedShapeIds()
5098
5066
  const editingId = this.getEditingShapeId()
5099
5067
  const culledShapes = new Set<TLShapeId>(notVisibleShapes)
@@ -5342,21 +5310,23 @@ export class Editor extends EventEmitter<TLEventMap> {
5342
5310
  * @example
5343
5311
  * ```ts
5344
5312
  * editor.getShapesAtPoint({ x: 100, y: 100 })
5345
- * editor.getShapesAtPoint({ x: 100, y: 100 }, { hitInside: true, exact: true })
5313
+ * editor.getShapesAtPoint({ x: 100, y: 100 }, { hitInside: true, margin: 8 })
5346
5314
  * ```
5347
5315
  *
5348
5316
  * @param point - The page point to test.
5349
5317
  * @param opts - The options for the hit point testing.
5350
5318
  *
5319
+ * @returns An array of shapes at the given point, sorted in reverse order of their absolute z-index (top-most shape first).
5320
+ *
5351
5321
  * @public
5352
5322
  */
5353
5323
  getShapesAtPoint(
5354
5324
  point: VecLike,
5355
5325
  opts = {} as { margin?: number; hitInside?: boolean }
5356
5326
  ): TLShape[] {
5357
- return this.getCurrentPageShapes().filter(
5358
- (shape) => !this.isShapeHidden(shape) && this.isPointInShape(shape, point, opts)
5359
- )
5327
+ return this.getCurrentPageShapesSorted()
5328
+ .filter((shape) => !this.isShapeHidden(shape) && this.isPointInShape(shape, point, opts))
5329
+ .reverse()
5360
5330
  }
5361
5331
 
5362
5332
  /**
@@ -9305,6 +9275,7 @@ export class Editor extends EventEmitter<TLEventMap> {
9305
9275
  if (rootShapes.length === 1) {
9306
9276
  const onlyRoot = rootShapes[0] as TLFrameShape
9307
9277
  // If the old bounds are in the viewport...
9278
+ // todo: replace frame references with shapes that can accept children
9308
9279
  if (this.isShapeOfType<TLFrameShape>(onlyRoot, 'frame')) {
9309
9280
  while (
9310
9281
  this.getShapesAtPoint(point).some(