@tldraw/editor 4.6.0-next.fe1474dc57d8 → 5.0.0

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 (207) hide show
  1. package/dist-cjs/index.d.ts +412 -179
  2. package/dist-cjs/index.js +12 -23
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/TldrawEditor.js +3 -0
  5. package/dist-cjs/lib/TldrawEditor.js.map +2 -2
  6. package/dist-cjs/lib/components/default-components/CanvasOverlays.js +180 -0
  7. package/dist-cjs/lib/components/default-components/CanvasOverlays.js.map +7 -0
  8. package/dist-cjs/lib/components/default-components/DefaultCanvas.js +44 -249
  9. package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +3 -3
  10. package/dist-cjs/lib/editor/Editor.js +78 -28
  11. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  12. package/dist-cjs/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.js +98 -0
  13. package/dist-cjs/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.js.map +7 -0
  14. package/dist-cjs/lib/editor/managers/ThemeManager/defaultThemes.js +14 -0
  15. package/dist-cjs/lib/editor/managers/ThemeManager/defaultThemes.js.map +2 -2
  16. package/dist-cjs/lib/editor/overlays/OverlayManager.js +154 -0
  17. package/dist-cjs/lib/editor/overlays/OverlayManager.js.map +7 -0
  18. package/dist-cjs/lib/editor/overlays/OverlayUtil.js +92 -0
  19. package/dist-cjs/lib/editor/overlays/OverlayUtil.js.map +7 -0
  20. package/dist-cjs/lib/editor/overlays/ShapeIndicatorOverlayUtil.js +161 -0
  21. package/dist-cjs/lib/editor/overlays/ShapeIndicatorOverlayUtil.js.map +7 -0
  22. package/dist-cjs/lib/editor/overlays/getOverlayDisplayValues.js +39 -0
  23. package/dist-cjs/lib/editor/overlays/getOverlayDisplayValues.js.map +7 -0
  24. package/dist-cjs/lib/editor/shapes/BaseFrameLikeShapeUtil.js +3 -0
  25. package/dist-cjs/lib/editor/shapes/BaseFrameLikeShapeUtil.js.map +2 -2
  26. package/dist-cjs/lib/editor/shapes/ShapeUtil.js +25 -23
  27. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
  28. package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js +32 -2
  29. package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js.map +2 -2
  30. package/dist-cjs/lib/editor/types/event-types.js.map +2 -2
  31. package/dist-cjs/lib/exports/fetchCache.js +1 -1
  32. package/dist-cjs/lib/exports/fetchCache.js.map +2 -2
  33. package/dist-cjs/lib/hooks/EditorComponentsContext.js.map +2 -2
  34. package/dist-cjs/lib/hooks/useCanvasEvents.js +3 -3
  35. package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
  36. package/dist-cjs/lib/hooks/useEditorComponents.js +0 -28
  37. package/dist-cjs/lib/hooks/useEditorComponents.js.map +2 -2
  38. package/dist-cjs/lib/hooks/usePeerIds.js +1 -36
  39. package/dist-cjs/lib/hooks/usePeerIds.js.map +2 -2
  40. package/dist-cjs/lib/hooks/useShapeCulling.js +2 -1
  41. package/dist-cjs/lib/hooks/useShapeCulling.js.map +2 -2
  42. package/dist-cjs/lib/options.js +0 -1
  43. package/dist-cjs/lib/options.js.map +2 -2
  44. package/dist-cjs/lib/utils/reparenting.js +20 -7
  45. package/dist-cjs/lib/utils/reparenting.js.map +2 -2
  46. package/dist-cjs/lib/utils/sync/TLLocalSyncClient.js +3 -0
  47. package/dist-cjs/lib/utils/sync/TLLocalSyncClient.js.map +2 -2
  48. package/dist-cjs/version.js +4 -4
  49. package/dist-cjs/version.js.map +1 -1
  50. package/dist-esm/index.d.mts +412 -179
  51. package/dist-esm/index.mjs +19 -41
  52. package/dist-esm/index.mjs.map +2 -2
  53. package/dist-esm/lib/TldrawEditor.mjs +3 -0
  54. package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
  55. package/dist-esm/lib/components/default-components/CanvasOverlays.mjs +160 -0
  56. package/dist-esm/lib/components/default-components/CanvasOverlays.mjs.map +7 -0
  57. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +45 -250
  58. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +3 -3
  59. package/dist-esm/lib/editor/Editor.mjs +78 -29
  60. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  61. package/dist-esm/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.mjs +83 -0
  62. package/dist-esm/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.mjs.map +7 -0
  63. package/dist-esm/lib/editor/managers/ThemeManager/defaultThemes.mjs +14 -0
  64. package/dist-esm/lib/editor/managers/ThemeManager/defaultThemes.mjs.map +2 -2
  65. package/dist-esm/lib/editor/overlays/OverlayManager.mjs +136 -0
  66. package/dist-esm/lib/editor/overlays/OverlayManager.mjs.map +7 -0
  67. package/dist-esm/lib/editor/overlays/OverlayUtil.mjs +72 -0
  68. package/dist-esm/lib/editor/overlays/OverlayUtil.mjs.map +7 -0
  69. package/dist-esm/lib/editor/overlays/ShapeIndicatorOverlayUtil.mjs +141 -0
  70. package/dist-esm/lib/editor/overlays/ShapeIndicatorOverlayUtil.mjs.map +7 -0
  71. package/dist-esm/lib/editor/overlays/getOverlayDisplayValues.mjs +19 -0
  72. package/dist-esm/lib/editor/overlays/getOverlayDisplayValues.mjs.map +7 -0
  73. package/dist-esm/lib/editor/shapes/BaseFrameLikeShapeUtil.mjs +3 -0
  74. package/dist-esm/lib/editor/shapes/BaseFrameLikeShapeUtil.mjs.map +2 -2
  75. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +25 -23
  76. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
  77. package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs +32 -2
  78. package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs.map +2 -2
  79. package/dist-esm/lib/editor/types/event-types.mjs.map +2 -2
  80. package/dist-esm/lib/exports/fetchCache.mjs +2 -2
  81. package/dist-esm/lib/exports/fetchCache.mjs.map +2 -2
  82. package/dist-esm/lib/hooks/EditorComponentsContext.mjs.map +2 -2
  83. package/dist-esm/lib/hooks/useCanvasEvents.mjs +3 -3
  84. package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
  85. package/dist-esm/lib/hooks/useEditorComponents.mjs +0 -28
  86. package/dist-esm/lib/hooks/useEditorComponents.mjs.map +2 -2
  87. package/dist-esm/lib/hooks/usePeerIds.mjs +2 -40
  88. package/dist-esm/lib/hooks/usePeerIds.mjs.map +2 -2
  89. package/dist-esm/lib/hooks/useShapeCulling.mjs +2 -1
  90. package/dist-esm/lib/hooks/useShapeCulling.mjs.map +2 -2
  91. package/dist-esm/lib/options.mjs +0 -1
  92. package/dist-esm/lib/options.mjs.map +2 -2
  93. package/dist-esm/lib/utils/reparenting.mjs +20 -7
  94. package/dist-esm/lib/utils/reparenting.mjs.map +2 -2
  95. package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs +3 -0
  96. package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs.map +2 -2
  97. package/dist-esm/version.mjs +4 -4
  98. package/dist-esm/version.mjs.map +1 -1
  99. package/editor.css +4 -239
  100. package/package.json +7 -7
  101. package/src/index.ts +17 -39
  102. package/src/lib/TldrawEditor.tsx +9 -0
  103. package/src/lib/components/default-components/CanvasOverlays.tsx +208 -0
  104. package/src/lib/components/default-components/DefaultCanvas.tsx +49 -324
  105. package/src/lib/editor/Editor.test.ts +3 -1
  106. package/src/lib/editor/Editor.ts +80 -24
  107. package/src/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.ts +98 -0
  108. package/src/lib/editor/managers/ThemeManager/defaultThemes.ts +14 -0
  109. package/src/lib/editor/overlays/OverlayManager.ts +183 -0
  110. package/src/lib/editor/overlays/OverlayUtil.ts +143 -0
  111. package/src/lib/editor/overlays/ShapeIndicatorOverlayUtil.ts +216 -0
  112. package/src/lib/editor/overlays/getOverlayDisplayValues.ts +51 -0
  113. package/src/lib/editor/shapes/BaseFrameLikeShapeUtil.tsx +9 -2
  114. package/src/lib/editor/shapes/ShapeUtil.ts +34 -26
  115. package/src/lib/editor/shapes/group/GroupShapeUtil.tsx +40 -3
  116. package/src/lib/editor/types/event-types.ts +2 -0
  117. package/src/lib/exports/fetchCache.ts +2 -4
  118. package/src/lib/exports/getSvgJsx.test.ts +3 -1
  119. package/src/lib/hooks/EditorComponentsContext.tsx +0 -27
  120. package/src/lib/hooks/useCanvasEvents.ts +13 -8
  121. package/src/lib/hooks/useEditorComponents.tsx +0 -28
  122. package/src/lib/hooks/usePeerIds.ts +6 -55
  123. package/src/lib/hooks/useShapeCulling.tsx +3 -1
  124. package/src/lib/options.ts +0 -7
  125. package/src/lib/utils/reparenting.ts +22 -9
  126. package/src/lib/utils/sync/TLLocalSyncClient.ts +3 -0
  127. package/src/version.ts +4 -4
  128. package/dist-cjs/lib/components/GeometryDebuggingView.js +0 -115
  129. package/dist-cjs/lib/components/GeometryDebuggingView.js.map +0 -7
  130. package/dist-cjs/lib/components/LiveCollaborators.js +0 -152
  131. package/dist-cjs/lib/components/LiveCollaborators.js.map +0 -7
  132. package/dist-cjs/lib/components/default-components/CanvasShapeIndicators.js +0 -234
  133. package/dist-cjs/lib/components/default-components/CanvasShapeIndicators.js.map +0 -7
  134. package/dist-cjs/lib/components/default-components/DefaultBrush.js +0 -38
  135. package/dist-cjs/lib/components/default-components/DefaultBrush.js.map +0 -7
  136. package/dist-cjs/lib/components/default-components/DefaultCollaboratorHint.js +0 -71
  137. package/dist-cjs/lib/components/default-components/DefaultCollaboratorHint.js.map +0 -7
  138. package/dist-cjs/lib/components/default-components/DefaultCursor.js +0 -59
  139. package/dist-cjs/lib/components/default-components/DefaultCursor.js.map +0 -7
  140. package/dist-cjs/lib/components/default-components/DefaultHandle.js +0 -56
  141. package/dist-cjs/lib/components/default-components/DefaultHandle.js.map +0 -7
  142. package/dist-cjs/lib/components/default-components/DefaultHandles.js +0 -28
  143. package/dist-cjs/lib/components/default-components/DefaultHandles.js.map +0 -7
  144. package/dist-cjs/lib/components/default-components/DefaultScribble.js +0 -51
  145. package/dist-cjs/lib/components/default-components/DefaultScribble.js.map +0 -7
  146. package/dist-cjs/lib/components/default-components/DefaultSelectionForeground.js +0 -69
  147. package/dist-cjs/lib/components/default-components/DefaultSelectionForeground.js.map +0 -7
  148. package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js +0 -107
  149. package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js.map +0 -7
  150. package/dist-cjs/lib/components/default-components/DefaultShapeIndicatorErrorFallback.js +0 -28
  151. package/dist-cjs/lib/components/default-components/DefaultShapeIndicatorErrorFallback.js.map +0 -7
  152. package/dist-cjs/lib/components/default-components/DefaultShapeIndicators.js +0 -102
  153. package/dist-cjs/lib/components/default-components/DefaultShapeIndicators.js.map +0 -7
  154. package/dist-cjs/lib/components/default-components/DefaultSnapIndictor.js +0 -170
  155. package/dist-cjs/lib/components/default-components/DefaultSnapIndictor.js.map +0 -7
  156. package/dist-cjs/lib/hooks/useHandleEvents.js +0 -100
  157. package/dist-cjs/lib/hooks/useHandleEvents.js.map +0 -7
  158. package/dist-cjs/lib/hooks/useSelectionEvents.js +0 -98
  159. package/dist-cjs/lib/hooks/useSelectionEvents.js.map +0 -7
  160. package/dist-esm/lib/components/GeometryDebuggingView.mjs +0 -95
  161. package/dist-esm/lib/components/GeometryDebuggingView.mjs.map +0 -7
  162. package/dist-esm/lib/components/LiveCollaborators.mjs +0 -135
  163. package/dist-esm/lib/components/LiveCollaborators.mjs.map +0 -7
  164. package/dist-esm/lib/components/default-components/CanvasShapeIndicators.mjs +0 -214
  165. package/dist-esm/lib/components/default-components/CanvasShapeIndicators.mjs.map +0 -7
  166. package/dist-esm/lib/components/default-components/DefaultBrush.mjs +0 -18
  167. package/dist-esm/lib/components/default-components/DefaultBrush.mjs.map +0 -7
  168. package/dist-esm/lib/components/default-components/DefaultCollaboratorHint.mjs +0 -41
  169. package/dist-esm/lib/components/default-components/DefaultCollaboratorHint.mjs.map +0 -7
  170. package/dist-esm/lib/components/default-components/DefaultCursor.mjs +0 -29
  171. package/dist-esm/lib/components/default-components/DefaultCursor.mjs.map +0 -7
  172. package/dist-esm/lib/components/default-components/DefaultHandle.mjs +0 -26
  173. package/dist-esm/lib/components/default-components/DefaultHandle.mjs.map +0 -7
  174. package/dist-esm/lib/components/default-components/DefaultHandles.mjs +0 -8
  175. package/dist-esm/lib/components/default-components/DefaultHandles.mjs.map +0 -7
  176. package/dist-esm/lib/components/default-components/DefaultScribble.mjs +0 -21
  177. package/dist-esm/lib/components/default-components/DefaultScribble.mjs.map +0 -7
  178. package/dist-esm/lib/components/default-components/DefaultSelectionForeground.mjs +0 -39
  179. package/dist-esm/lib/components/default-components/DefaultSelectionForeground.mjs.map +0 -7
  180. package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs +0 -77
  181. package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs.map +0 -7
  182. package/dist-esm/lib/components/default-components/DefaultShapeIndicatorErrorFallback.mjs +0 -8
  183. package/dist-esm/lib/components/default-components/DefaultShapeIndicatorErrorFallback.mjs.map +0 -7
  184. package/dist-esm/lib/components/default-components/DefaultShapeIndicators.mjs +0 -82
  185. package/dist-esm/lib/components/default-components/DefaultShapeIndicators.mjs.map +0 -7
  186. package/dist-esm/lib/components/default-components/DefaultSnapIndictor.mjs +0 -142
  187. package/dist-esm/lib/components/default-components/DefaultSnapIndictor.mjs.map +0 -7
  188. package/dist-esm/lib/hooks/useHandleEvents.mjs +0 -70
  189. package/dist-esm/lib/hooks/useHandleEvents.mjs.map +0 -7
  190. package/dist-esm/lib/hooks/useSelectionEvents.mjs +0 -78
  191. package/dist-esm/lib/hooks/useSelectionEvents.mjs.map +0 -7
  192. package/src/lib/components/GeometryDebuggingView.tsx +0 -108
  193. package/src/lib/components/LiveCollaborators.tsx +0 -180
  194. package/src/lib/components/default-components/CanvasShapeIndicators.tsx +0 -300
  195. package/src/lib/components/default-components/DefaultBrush.tsx +0 -35
  196. package/src/lib/components/default-components/DefaultCollaboratorHint.tsx +0 -52
  197. package/src/lib/components/default-components/DefaultCursor.tsx +0 -59
  198. package/src/lib/components/default-components/DefaultHandle.tsx +0 -42
  199. package/src/lib/components/default-components/DefaultHandles.tsx +0 -15
  200. package/src/lib/components/default-components/DefaultScribble.tsx +0 -31
  201. package/src/lib/components/default-components/DefaultSelectionForeground.tsx +0 -50
  202. package/src/lib/components/default-components/DefaultShapeIndicator.tsx +0 -104
  203. package/src/lib/components/default-components/DefaultShapeIndicatorErrorFallback.tsx +0 -9
  204. package/src/lib/components/default-components/DefaultShapeIndicators.tsx +0 -118
  205. package/src/lib/components/default-components/DefaultSnapIndictor.tsx +0 -174
  206. package/src/lib/hooks/useHandleEvents.ts +0 -88
  207. package/src/lib/hooks/useSelectionEvents.ts +0 -97
@@ -0,0 +1,216 @@
1
+ import { computed } from '@tldraw/state'
2
+ import { createComputedCache } from '@tldraw/store'
3
+ import { TLShape, TLShapeId } from '@tldraw/tlschema'
4
+ import type { Editor } from '../Editor'
5
+ import { OverlayUtil, TLOverlay } from './OverlayUtil'
6
+
7
+ interface RelevantInstanceFlags {
8
+ isChangingStyle: boolean
9
+ isHoveringCanvas: boolean | null
10
+ isCoarsePointer: boolean
11
+ }
12
+
13
+ /** @public */
14
+ export interface TLShapeIndicatorOverlay extends TLOverlay {
15
+ props: {
16
+ idsToDisplay: TLShapeId[]
17
+ hintingShapeIds: TLShapeId[]
18
+ }
19
+ }
20
+
21
+ const indicatorPathCache = createComputedCache(
22
+ 'shapeIndicatorPath',
23
+ (editor: Editor, shape: TLShape) => {
24
+ const util = editor.getShapeUtil(shape)
25
+ return util.getIndicatorPath(shape)
26
+ },
27
+ {
28
+ areRecordsEqual(a, b) {
29
+ return a.props === b.props
30
+ },
31
+ }
32
+ )
33
+
34
+ /**
35
+ * Combine every batchable shape indicator into a single page-space `Path2D` and
36
+ * emit one stroke call. Shapes whose indicator needs an evenodd clip (e.g.
37
+ * arrows with labels or complex arrowheads) can't be batched — they still
38
+ * stroke individually inside a save/restore with `ctx.clip` applied.
39
+ *
40
+ * Shared by {@link ShapeIndicatorOverlayUtil} and any overlay util that paints
41
+ * shape indicators (e.g. collaborator selections).
42
+ *
43
+ * @public
44
+ */
45
+ export function strokeShapeIndicators(
46
+ editor: Editor,
47
+ ctx: CanvasRenderingContext2D,
48
+ shapeIds: TLShapeId[]
49
+ ): void {
50
+ if (shapeIds.length === 0) return
51
+
52
+ const batched = new Path2D()
53
+
54
+ for (const shapeId of shapeIds) {
55
+ const shape = editor.getShape(shapeId)
56
+ if (!shape || shape.isLocked) continue
57
+
58
+ const pageTransform = editor.getShapePageTransform(shape)
59
+ if (!pageTransform) continue
60
+
61
+ const indicatorPath = indicatorPathCache.get(editor, shape.id)
62
+ if (!indicatorPath) continue
63
+
64
+ if (indicatorPath instanceof Path2D) {
65
+ batched.addPath(indicatorPath, pageTransform)
66
+ continue
67
+ }
68
+
69
+ const { path, clipPath, additionalPaths } = indicatorPath
70
+
71
+ if (!clipPath) {
72
+ batched.addPath(path, pageTransform)
73
+ if (additionalPaths) {
74
+ for (const p of additionalPaths) batched.addPath(p, pageTransform)
75
+ }
76
+ continue
77
+ }
78
+
79
+ // Clipped case: fall back to an individual stroke. Rare (arrows with
80
+ // labels / complex arrowheads), so the extra save/restore/stroke
81
+ // pair per such shape isn't worth batching away.
82
+ ctx.save()
83
+ ctx.transform(
84
+ pageTransform.a,
85
+ pageTransform.b,
86
+ pageTransform.c,
87
+ pageTransform.d,
88
+ pageTransform.e,
89
+ pageTransform.f
90
+ )
91
+ ctx.save()
92
+ ctx.clip(clipPath, 'evenodd')
93
+ ctx.stroke(path)
94
+ ctx.restore()
95
+ if (additionalPaths) {
96
+ for (const p of additionalPaths) ctx.stroke(p)
97
+ }
98
+ ctx.restore()
99
+ }
100
+
101
+ ctx.stroke(batched)
102
+ }
103
+
104
+ /**
105
+ * Overlay util for shape indicators — the selection / hover / hint outlines drawn
106
+ * under the selection foreground. Paints local indicators in the theme's
107
+ * selection color.
108
+ *
109
+ * Remote collaborator selection indicators are drawn by a separate overlay util
110
+ * (e.g. `CollaboratorShapeIndicatorOverlayUtil` from `tldraw`) that runs at a
111
+ * lower z-index so peer selections appear under the local indicators.
112
+ *
113
+ * Non-interactive: contributes no hit-test geometry.
114
+ *
115
+ * @public
116
+ */
117
+ export class ShapeIndicatorOverlayUtil extends OverlayUtil<TLShapeIndicatorOverlay> {
118
+ static override type = 'shape_indicator'
119
+ override options = { zIndex: 50, lineWidth: 1.5, hintedLineWidth: 2.5 }
120
+
121
+ // Narrow projection of instance state. Reading the full record would
122
+ // re-fire getOverlays on every cursor move / brush update; gating on these
123
+ // three booleans means we only re-fire when one of them actually flips.
124
+ private _instanceFlags$ = computed<RelevantInstanceFlags>(
125
+ 'shape indicator instance flags',
126
+ () => {
127
+ const i = this.editor.getInstanceState()
128
+ return {
129
+ isChangingStyle: i.isChangingStyle,
130
+ isHoveringCanvas: i.isHoveringCanvas,
131
+ isCoarsePointer: i.isCoarsePointer,
132
+ }
133
+ },
134
+ {
135
+ isEqual: (a, b) =>
136
+ a.isChangingStyle === b.isChangingStyle &&
137
+ a.isHoveringCanvas === b.isHoveringCanvas &&
138
+ a.isCoarsePointer === b.isCoarsePointer,
139
+ }
140
+ )
141
+
142
+ override isActive(): boolean {
143
+ return true
144
+ }
145
+
146
+ override getOverlays(): TLShapeIndicatorOverlay[] {
147
+ const editor = this.editor
148
+ const renderingShapeIds = new Set(editor.getRenderingShapes().map((s) => s.id))
149
+
150
+ // Local selected / hovered indicators.
151
+ const idsToDisplay: TLShapeId[] = []
152
+ const { isChangingStyle, isHoveringCanvas, isCoarsePointer } = this._instanceFlags$.get()
153
+ const isIdleOrEditing = editor.isInAny('select.idle', 'select.editing_shape')
154
+ const isInSelectState = editor.isInAny(
155
+ 'select.brushing',
156
+ 'select.scribble_brushing',
157
+ 'select.pointing_shape',
158
+ 'select.pointing_selection',
159
+ 'select.pointing_handle'
160
+ )
161
+
162
+ if (!isChangingStyle && (isIdleOrEditing || isInSelectState)) {
163
+ for (const id of editor.getSelectedShapeIds()) {
164
+ if (renderingShapeIds.has(id)) idsToDisplay.push(id)
165
+ }
166
+ if (isIdleOrEditing && isHoveringCanvas && !isCoarsePointer) {
167
+ const hovered = editor.getHoveredShapeId()
168
+ if (hovered && renderingShapeIds.has(hovered) && !idsToDisplay.includes(hovered)) {
169
+ idsToDisplay.push(hovered)
170
+ }
171
+ }
172
+ }
173
+
174
+ // Hinted shapes (drawn thicker). Already deduped at write time in
175
+ // `updateHintingShapeIds`, so no need to dedupe again here.
176
+ const hintingShapeIds: TLShapeId[] = []
177
+ for (const id of editor.getHintingShapeIds()) {
178
+ if (renderingShapeIds.has(id)) hintingShapeIds.push(id)
179
+ }
180
+
181
+ if (idsToDisplay.length === 0 && hintingShapeIds.length === 0) {
182
+ return []
183
+ }
184
+
185
+ return [
186
+ {
187
+ id: 'shape_indicator',
188
+ type: 'shape_indicator',
189
+ props: { idsToDisplay, hintingShapeIds },
190
+ },
191
+ ]
192
+ }
193
+
194
+ override render(ctx: CanvasRenderingContext2D, overlays: TLShapeIndicatorOverlay[]): void {
195
+ const overlay = overlays[0]
196
+ if (!overlay) return
197
+
198
+ const editor = this.editor
199
+ const zoom = editor.getZoomLevel()
200
+ const { idsToDisplay, hintingShapeIds } = overlay.props
201
+
202
+ ctx.lineCap = 'round'
203
+ ctx.lineJoin = 'round'
204
+
205
+ // Local selected / hovered indicators — one stroke call for the whole batch.
206
+ ctx.strokeStyle = editor.getCurrentTheme().colors[editor.getColorMode()].selectionStroke
207
+ ctx.lineWidth = this.options.lineWidth / zoom
208
+ strokeShapeIndicators(editor, ctx, idsToDisplay)
209
+
210
+ // Hinted shapes — thicker stroke, one call for the whole batch.
211
+ if (hintingShapeIds.length > 0) {
212
+ ctx.lineWidth = this.options.hintedLineWidth / zoom
213
+ strokeShapeIndicators(editor, ctx, hintingShapeIds)
214
+ }
215
+ }
216
+ }
@@ -0,0 +1,51 @@
1
+ import { TLTheme } from '@tldraw/tlschema'
2
+ import type { Editor } from '../Editor'
3
+ import { TLOverlay } from './OverlayUtil'
4
+
5
+ /** @public */
6
+ export interface OverlayOptionsWithDisplayValues<
7
+ Overlay extends TLOverlay,
8
+ DisplayValues extends object,
9
+ > {
10
+ getDefaultDisplayValues(
11
+ editor: Editor,
12
+ overlay: Overlay,
13
+ theme: TLTheme,
14
+ colorMode: 'light' | 'dark'
15
+ ): DisplayValues
16
+ getCustomDisplayValues(
17
+ editor: Editor,
18
+ overlay: Overlay,
19
+ theme: TLTheme,
20
+ colorMode: 'light' | 'dark'
21
+ ): Partial<DisplayValues>
22
+ }
23
+
24
+ const dvCache = new WeakMap<
25
+ TLOverlay,
26
+ { theme: TLTheme; colorMode: 'light' | 'dark'; values: object }
27
+ >()
28
+
29
+ /**
30
+ * Get the resolved display values for an overlay, merging the base values with any overrides.
31
+ *
32
+ * @public
33
+ */
34
+ export function getOverlayDisplayValues<Overlay extends TLOverlay, DisplayValues extends object>(
35
+ util: { editor: Editor; options: OverlayOptionsWithDisplayValues<Overlay, DisplayValues> },
36
+ overlay: Overlay,
37
+ colorMode?: 'light' | 'dark'
38
+ ): DisplayValues {
39
+ const theme = util.editor.getCurrentTheme()
40
+ const resolvedColorMode = colorMode ?? util.editor.getColorMode()
41
+ const cached = dvCache.get(overlay)
42
+ if (cached && cached.theme === theme && cached.colorMode === resolvedColorMode) {
43
+ return cached.values as DisplayValues
44
+ }
45
+ const values = {
46
+ ...util.options.getDefaultDisplayValues(util.editor, overlay, theme, resolvedColorMode),
47
+ ...util.options.getCustomDisplayValues(util.editor, overlay, theme, resolvedColorMode),
48
+ }
49
+ dvCache.set(overlay, { theme, colorMode: resolvedColorMode, values })
50
+ return values
51
+ }
@@ -15,6 +15,7 @@ import { TLDragShapesInInfo, TLDragShapesOutInfo } from './ShapeUtil'
15
15
  * - `isFrameLike()` returns `true`
16
16
  * - `providesBackgroundForChildren()` returns `true`
17
17
  * - `canReceiveNewChildrenOfType()` returns `true` unless the container is locked
18
+ * - `canRemoveChildrenOfType()` returns `true` unless the container is locked
18
19
  * - `getClipPath()` returns the shape geometry's vertices
19
20
  * - `onDragShapesIn()` reparents shapes into the frame (with index restoration)
20
21
  * - `onDragShapesOut()` reparents shapes back to the page
@@ -35,8 +36,10 @@ import { TLDragShapesInInfo, TLDragShapesOutInfo } from './ShapeUtil'
35
36
  * return <SVGContainer>...</SVGContainer>
36
37
  * }
37
38
  *
38
- * override indicator(shape: MyContainerShape) {
39
- * return <rect width={shape.props.w} height={shape.props.h} />
39
+ * override getIndicatorPath(shape: MyContainerShape) {
40
+ * const path = new Path2D()
41
+ * path.rect(0, 0, shape.props.w, shape.props.h)
42
+ * return path
40
43
  * }
41
44
  * }
42
45
  * ```
@@ -58,6 +61,10 @@ export abstract class BaseFrameLikeShapeUtil<
58
61
  return !shape.isLocked
59
62
  }
60
63
 
64
+ override canRemoveChildrenOfType(shape: Shape, _type: TLShape['type']): boolean {
65
+ return !shape.isLocked
66
+ }
67
+
61
68
  override getClipPath(shape: Shape): Vec[] | undefined {
62
69
  return this.editor.getShapeGeometry(shape.id).vertices
63
70
  }
@@ -218,42 +218,32 @@ export abstract class ShapeUtil<Shape extends TLShape = TLShape> {
218
218
  abstract component(shape: Shape): any
219
219
 
220
220
  /**
221
- * Get JSX describing the shape's indicator (as an SVG element).
222
- *
223
- * @param shape - The shape.
224
- * @public
225
- */
226
- abstract indicator(shape: Shape): any
227
-
228
- /**
229
- * Whether to use the legacy React-based indicator rendering.
221
+ * Get a Path2D (or a richer object with clip/additional paths) for rendering the
222
+ * shape's indicator on the canvas. Shapes that return `undefined` will not render
223
+ * an indicator.
230
224
  *
231
- * Override this to return `false` if your shape implements {@link ShapeUtil.getIndicatorPath}
232
- * for canvas-based indicator rendering.
225
+ * For complex indicators that need clipping (e.g., arrows with labels), return an
226
+ * object with `path`, `clipPath`, and `additionalPaths` properties.
233
227
  *
234
- * @returns `true` to use SVG indicators (default), `false` to use canvas indicators.
228
+ * @param shape - The shape.
229
+ * @returns A Path2D to stroke, or an object with clipping info, or undefined to skip.
235
230
  * @public
236
231
  */
237
- useLegacyIndicator(): boolean {
238
- return true
239
- }
232
+ abstract getIndicatorPath(shape: Shape): TLIndicatorPath | undefined
240
233
 
241
234
  /**
242
- * Get a Path2D for rendering the shape's indicator on the canvas.
243
- *
244
- * When implemented, this is used instead of {@link ShapeUtil.indicator} for more
245
- * efficient canvas-based indicator rendering. Shapes that return `undefined` will
246
- * fall back to SVG-based rendering via {@link ShapeUtil.indicator}.
235
+ * Get JSX describing the shape's indicator (as an SVG element).
247
236
  *
248
- * For complex indicators that need clipping (e.g., arrows with labels), return an
249
- * object with `path`, `clipPath`, and `additionalPaths` properties.
237
+ * @deprecated SVG indicators are no longer rendered. Override
238
+ * {@link ShapeUtil.getIndicatorPath} instead. This stub is retained so legacy
239
+ * subclasses that still call `super.indicator()` keep type-checking; new shapes
240
+ * should not implement it.
250
241
  *
251
242
  * @param shape - The shape.
252
- * @returns A Path2D to stroke, or an object with clipping info, or undefined to use SVG fallback.
253
243
  * @public
254
244
  */
255
- getIndicatorPath(shape: Shape): TLIndicatorPath | undefined {
256
- return undefined
245
+ indicator(_shape: Shape): any {
246
+ return null
257
247
  }
258
248
 
259
249
  /**
@@ -556,7 +546,9 @@ export abstract class ShapeUtil<Shape extends TLShape = TLShape> {
556
546
  getHandles?(shape: Shape): TLHandle[]
557
547
 
558
548
  /**
559
- * Get whether the shape can receive children of a given type.
549
+ * Get whether the shape can receive children of a given type. Used by the drag and drop system
550
+ * to decide whether {@link ShapeUtil.onDragShapesIn} should fire when a shape of the given type
551
+ * is dragged over this one.
560
552
  *
561
553
  * @param shape - The shape.
562
554
  * @param type - The shape type.
@@ -566,6 +558,22 @@ export abstract class ShapeUtil<Shape extends TLShape = TLShape> {
566
558
  return false
567
559
  }
568
560
 
561
+ /**
562
+ * Get whether children of a given type can be removed from this shape. Used by the drag and
563
+ * drop system to decide whether {@link ShapeUtil.onDragShapesOut} should fire when a child of
564
+ * the given type is dragged out of this shape, and by `kickoutOccludedShapes` to decide
565
+ * whether to auto-reparent a child of the given type when it has moved outside this shape's
566
+ * geometry. Returning `false` therefore "pins" matching children — they stay parented to this
567
+ * shape even when dragged or moved outside it. Defaults to `true`.
568
+ *
569
+ * @param shape - The shape.
570
+ * @param type - The shape type.
571
+ * @public
572
+ */
573
+ canRemoveChildrenOfType(shape: Shape, type: TLShape['type']) {
574
+ return true
575
+ }
576
+
569
577
  /**
570
578
  * Get the shape as an SVG object.
571
579
  *
@@ -4,6 +4,7 @@ import { Geometry2d } from '../../../primitives/geometry/Geometry2d'
4
4
  import { Group2d } from '../../../primitives/geometry/Group2d'
5
5
  import { Rectangle2d } from '../../../primitives/geometry/Rectangle2d'
6
6
  import { ShapeUtil } from '../ShapeUtil'
7
+ import { getPerfectDashProps } from '../shared/getPerfectDashProps'
7
8
  import { DashedOutlineBox } from './DashedOutlineBox'
8
9
 
9
10
  /** @public */
@@ -78,10 +79,46 @@ export class GroupShapeUtil extends ShapeUtil<TLGroupShape> {
78
79
  )
79
80
  }
80
81
 
81
- indicator(shape: TLGroupShape) {
82
- // Not a class component, but eslint can't tell that :(
82
+ override getIndicatorPath(shape: TLGroupShape): Path2D {
83
83
  const bounds = this.editor.getShapeGeometry(shape).bounds
84
- return <DashedOutlineBox className="" bounds={bounds} />
84
+ const zoomLevel = this.editor.getEfficientZoomLevel()
85
+ const path = new Path2D()
86
+
87
+ for (const side of bounds.sides) {
88
+ const [start, end] = side
89
+ const length = start.dist(end)
90
+ if (length <= 0) continue
91
+
92
+ const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(length, 1 / zoomLevel, {
93
+ style: 'dashed',
94
+ lengthRatio: 4,
95
+ })
96
+
97
+ if (strokeDasharray === 'none') {
98
+ path.moveTo(start.x, start.y)
99
+ path.lineTo(end.x, end.y)
100
+ continue
101
+ }
102
+
103
+ const [dashLength, gapLength] = strokeDasharray.split(' ').map(Number)
104
+ const dashOffset = Number(strokeDashoffset)
105
+ const period = dashLength + gapLength
106
+ if (!Number.isFinite(period) || period <= 0) continue
107
+
108
+ const dx = (end.x - start.x) / length
109
+ const dy = (end.y - start.y) / length
110
+
111
+ for (let dashStart = -dashOffset; dashStart < length; dashStart += period) {
112
+ const dashEnd = Math.min(length, dashStart + dashLength)
113
+ const clippedDashStart = Math.max(0, dashStart)
114
+ if (dashEnd <= clippedDashStart) continue
115
+
116
+ path.moveTo(start.x + dx * clippedDashStart, start.y + dy * clippedDashStart)
117
+ path.lineTo(start.x + dx * dashEnd, start.y + dy * dashEnd)
118
+ }
119
+ }
120
+
121
+ return path
85
122
  }
86
123
 
87
124
  override onChildrenChange(group: TLGroupShape) {
@@ -1,5 +1,6 @@
1
1
  import { TLHandle, TLShape, VecModel } from '@tldraw/tlschema'
2
2
  import { VecLike } from '../../primitives/Vec'
3
+ import { TLOverlay } from '../overlays/OverlayUtil'
3
4
  import { TLSelectionHandle } from './selection-types'
4
5
 
5
6
  /** @public */
@@ -11,6 +12,7 @@ export type TLPointerEventTarget =
11
12
  | { target: 'selection'; handle?: TLSelectionHandle; shape?: undefined }
12
13
  | { target: 'shape'; shape: TLShape }
13
14
  | { target: 'handle'; shape: TLShape; handle: TLHandle }
15
+ | { target: 'overlay'; overlay: TLOverlay; shape?: undefined }
14
16
 
15
17
  /** @public */
16
18
  export type TLPointerEventName =
@@ -1,9 +1,7 @@
1
- import { FileHelpers, assert, fetch } from '@tldraw/utils'
1
+ import { FileHelpers, LruCache, assert, fetch } from '@tldraw/utils'
2
2
 
3
- // TODO(alex): currently, this cache will grow unbounded. we should come up with a better strategy
4
- // for clearing items from the cache over time.
5
3
  export function fetchCache<T>(cb: (response: Response) => Promise<T>, init?: RequestInit) {
6
- const cache = new Map<string, Promise<T | null>>()
4
+ const cache = new LruCache<string, Promise<T | null>>(100)
7
5
 
8
6
  return async function fetchCached(url: string): Promise<T | null> {
9
7
  const existing = cache.get(url)
@@ -46,7 +46,9 @@ class TestShape extends ShapeUtil<ITestShape> {
46
46
  return shape.props.isContainer ?? false
47
47
  }
48
48
 
49
- indicator() {}
49
+ getIndicatorPath() {
50
+ return undefined
51
+ }
50
52
  component() {}
51
53
  }
52
54
 
@@ -1,54 +1,27 @@
1
1
  import { ComponentType, RefAttributes, createContext, useContext } from 'react'
2
- import type { TLBrushProps } from '../components/default-components/DefaultBrush'
3
2
  import type { TLCanvasComponentProps } from '../components/default-components/DefaultCanvas'
4
- import type { TLCollaboratorHintProps } from '../components/default-components/DefaultCollaboratorHint'
5
- import type { TLCursorProps } from '../components/default-components/DefaultCursor'
6
3
  import type { TLErrorFallbackComponent } from '../components/default-components/DefaultErrorFallback'
7
4
  import type { TLGridProps } from '../components/default-components/DefaultGrid'
8
- import type { TLHandleProps } from '../components/default-components/DefaultHandle'
9
- import type { TLHandlesProps } from '../components/default-components/DefaultHandles'
10
- import type { TLScribbleProps } from '../components/default-components/DefaultScribble'
11
5
  import type { TLSelectionBackgroundProps } from '../components/default-components/DefaultSelectionBackground'
12
- import type { TLSelectionForegroundProps } from '../components/default-components/DefaultSelectionForeground'
13
6
  import type { TLShapeErrorFallbackComponent } from '../components/default-components/DefaultShapeErrorFallback'
14
- import type { TLShapeIndicatorProps } from '../components/default-components/DefaultShapeIndicator'
15
- import type { TLShapeIndicatorErrorFallbackComponent } from '../components/default-components/DefaultShapeIndicatorErrorFallback'
16
7
  import type { TLShapeWrapperProps } from '../components/default-components/DefaultShapeWrapper'
17
- import type { TLSnapIndicatorProps } from '../components/default-components/DefaultSnapIndictor'
18
8
 
19
9
  /** @public */
20
10
  export interface TLEditorComponents {
21
11
  Background?: ComponentType | null
22
- Brush?: ComponentType<TLBrushProps> | null
23
12
  Canvas?: ComponentType<TLCanvasComponentProps> | null
24
- CollaboratorBrush?: ComponentType<TLBrushProps> | null
25
- CollaboratorCursor?: ComponentType<TLCursorProps> | null
26
- CollaboratorHint?: ComponentType<TLCollaboratorHintProps> | null
27
- CollaboratorScribble?: ComponentType<TLScribbleProps> | null
28
- CollaboratorShapeIndicator?: ComponentType<TLShapeIndicatorProps> | null
29
- Cursor?: ComponentType<TLCursorProps> | null
30
13
  Grid?: ComponentType<TLGridProps> | null
31
- Handle?: ComponentType<TLHandleProps> | null
32
- Handles?: ComponentType<TLHandlesProps> | null
33
14
  InFrontOfTheCanvas?: ComponentType | null
34
15
  LoadingScreen?: ComponentType | null
35
16
  OnTheCanvas?: ComponentType | null
36
- Overlays?: ComponentType | null
37
- Scribble?: ComponentType<TLScribbleProps> | null
38
17
  SelectionBackground?: ComponentType<TLSelectionBackgroundProps> | null
39
- SelectionForeground?: ComponentType<TLSelectionForegroundProps> | null
40
- ShapeIndicator?: ComponentType<TLShapeIndicatorProps> | null
41
- ShapeIndicators?: ComponentType | null
42
18
  ShapeWrapper?: ComponentType<TLShapeWrapperProps & RefAttributes<HTMLDivElement>> | null
43
- SnapIndicator?: ComponentType<TLSnapIndicatorProps> | null
44
19
  Spinner?: ComponentType<React.SVGProps<SVGSVGElement>> | null
45
20
  SvgDefs?: ComponentType | null
46
- ZoomBrush?: ComponentType<TLBrushProps> | null
47
21
 
48
22
  // These will always have defaults
49
23
  ErrorFallback?: TLErrorFallbackComponent
50
24
  ShapeErrorFallback?: TLShapeErrorFallbackComponent
51
- ShapeIndicatorErrorFallback?: TLShapeIndicatorErrorFallbackComponent
52
25
  }
53
26
 
54
27
  export const EditorComponentsContext = createContext<null | Required<TLEditorComponents>>(null)
@@ -169,14 +169,19 @@ export function useCanvasEvents() {
169
169
  // menu opens on press.
170
170
  if (!editor.options.rightClickPanning) return
171
171
  // Synthetic events — our own dispatch from onPointerUp, or tests using
172
- // fireEvent.contextMenu — pass through so Radix can open the menu. The real
173
- // browser contextmenu is always suppressed: right-click behavior has
174
- // already been decided by our pointer handling (either we dispatched a
175
- // synthetic to open the menu at the release position, or we panned and
176
- // don't want a menu at all).
177
- if (e.nativeEvent.isTrusted) {
178
- preventDefault(e)
179
- }
172
+ // fireEvent.contextMenu — pass through so Radix can open the menu.
173
+ if (!e.nativeEvent.isTrusted) return
174
+ // Only suppress the native browser contextmenu when it follows a real
175
+ // right-click (button=2 with no ctrl modifier). For those, our pointer
176
+ // handling has already decided what to do (either we'll dispatch a
177
+ // synthetic contextmenu on pointerup to open the menu at the release
178
+ // position, or we panned and don't want a menu at all).
179
+ //
180
+ // Other contextmenu sources must reach Radix so the menu opens:
181
+ // - ctrl+click on macOS (button=0, or button=2 with ctrlKey=true)
182
+ // - long-press on touch devices (button=0, pointerType=touch)
183
+ if (e.button !== 2 || e.ctrlKey) return
184
+ preventDefault(e)
180
185
  }
181
186
 
182
187
  return {
@@ -1,22 +1,11 @@
1
1
  import { ReactNode, useMemo } from 'react'
2
2
  import { DefaultBackground } from '../components/default-components/DefaultBackground'
3
- import { DefaultBrush } from '../components/default-components/DefaultBrush'
4
3
  import { DefaultCanvas } from '../components/default-components/DefaultCanvas'
5
- import { DefaultCollaboratorHint } from '../components/default-components/DefaultCollaboratorHint'
6
- import { DefaultCursor } from '../components/default-components/DefaultCursor'
7
4
  import { DefaultErrorFallback } from '../components/default-components/DefaultErrorFallback'
8
5
  import { DefaultGrid } from '../components/default-components/DefaultGrid'
9
- import { DefaultHandle } from '../components/default-components/DefaultHandle'
10
- import { DefaultHandles } from '../components/default-components/DefaultHandles'
11
6
  import { DefaultLoadingScreen } from '../components/default-components/DefaultLoadingScreen'
12
- import { DefaultScribble } from '../components/default-components/DefaultScribble'
13
- import { DefaultSelectionForeground } from '../components/default-components/DefaultSelectionForeground'
14
7
  import { DefaultShapeErrorFallback } from '../components/default-components/DefaultShapeErrorFallback'
15
- import { DefaultShapeIndicator } from '../components/default-components/DefaultShapeIndicator'
16
- import { DefaultShapeIndicatorErrorFallback } from '../components/default-components/DefaultShapeIndicatorErrorFallback'
17
- import { DefaultShapeIndicators } from '../components/default-components/DefaultShapeIndicators'
18
8
  import { DefaultShapeWrapper } from '../components/default-components/DefaultShapeWrapper'
19
- import { DefaultSnapIndicator } from '../components/default-components/DefaultSnapIndictor'
20
9
  import { DefaultSpinner } from '../components/default-components/DefaultSpinner'
21
10
  import { DefaultSvgDefs } from '../components/default-components/DefaultSvgDefs'
22
11
  import { EditorComponentsContext } from './EditorComponentsContext'
@@ -39,35 +28,18 @@ export function EditorComponentsProvider({
39
28
  const value = useMemo(
40
29
  (): Required<TLEditorComponents> => ({
41
30
  Background: DefaultBackground,
42
- Brush: DefaultBrush,
43
31
  Canvas: DefaultCanvas,
44
- CollaboratorBrush: DefaultBrush,
45
- CollaboratorCursor: DefaultCursor,
46
- CollaboratorHint: DefaultCollaboratorHint,
47
- CollaboratorScribble: DefaultScribble,
48
- CollaboratorShapeIndicator: DefaultShapeIndicator,
49
- Cursor: DefaultCursor,
50
32
  Grid: DefaultGrid,
51
- Handle: DefaultHandle,
52
- Handles: DefaultHandles,
53
33
  InFrontOfTheCanvas: null,
54
34
  LoadingScreen: DefaultLoadingScreen,
55
35
  OnTheCanvas: null,
56
- Overlays: null,
57
- Scribble: DefaultScribble,
58
36
  SelectionBackground: null,
59
- SelectionForeground: DefaultSelectionForeground,
60
- ShapeIndicator: DefaultShapeIndicator,
61
- ShapeIndicators: DefaultShapeIndicators,
62
37
  ShapeWrapper: DefaultShapeWrapper,
63
- SnapIndicator: DefaultSnapIndicator,
64
38
  Spinner: DefaultSpinner,
65
39
  SvgDefs: DefaultSvgDefs,
66
- ZoomBrush: DefaultBrush,
67
40
 
68
41
  ErrorFallback: DefaultErrorFallback,
69
42
  ShapeErrorFallback: DefaultShapeErrorFallback,
70
- ShapeIndicatorErrorFallback: DefaultShapeIndicatorErrorFallback,
71
43
 
72
44
  ..._overrides,
73
45
  }),