@tldraw/editor 3.15.0 → 3.16.0-canary.03deb7f8fe34

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 (105) hide show
  1. package/dist-cjs/index.d.ts +181 -9
  2. package/dist-cjs/index.js +5 -1
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/TldrawEditor.js +3 -1
  5. package/dist-cjs/lib/TldrawEditor.js.map +2 -2
  6. package/dist-cjs/lib/components/MenuClickCapture.js +0 -5
  7. package/dist-cjs/lib/components/MenuClickCapture.js.map +2 -2
  8. package/dist-cjs/lib/components/Shape.js +4 -26
  9. package/dist-cjs/lib/components/Shape.js.map +2 -2
  10. package/dist-cjs/lib/components/default-components/DefaultCollaboratorHint.js +1 -1
  11. package/dist-cjs/lib/components/default-components/DefaultCollaboratorHint.js.map +1 -1
  12. package/dist-cjs/lib/components/default-components/DefaultScribble.js +1 -1
  13. package/dist-cjs/lib/components/default-components/DefaultScribble.js.map +2 -2
  14. package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js +9 -1
  15. package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js.map +2 -2
  16. package/dist-cjs/lib/components/default-components/DefaultShapeWrapper.js +53 -0
  17. package/dist-cjs/lib/components/default-components/DefaultShapeWrapper.js.map +7 -0
  18. package/dist-cjs/lib/config/TLUserPreferences.js +8 -2
  19. package/dist-cjs/lib/config/TLUserPreferences.js.map +2 -2
  20. package/dist-cjs/lib/editor/Editor.js +100 -58
  21. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  22. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js +8 -3
  23. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +2 -2
  24. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
  25. package/dist-cjs/lib/editor/types/misc-types.js.map +1 -1
  26. package/dist-cjs/lib/exports/getSvgJsx.js +1 -2
  27. package/dist-cjs/lib/exports/getSvgJsx.js.map +2 -2
  28. package/dist-cjs/lib/hooks/useCanvasEvents.js +22 -20
  29. package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
  30. package/dist-cjs/lib/hooks/useEditorComponents.js +2 -0
  31. package/dist-cjs/lib/hooks/useEditorComponents.js.map +2 -2
  32. package/dist-cjs/lib/hooks/useStateAttribute.js +35 -0
  33. package/dist-cjs/lib/hooks/useStateAttribute.js.map +7 -0
  34. package/dist-cjs/lib/license/Watermark.js +6 -6
  35. package/dist-cjs/lib/license/Watermark.js.map +1 -1
  36. package/dist-cjs/lib/options.js +1 -0
  37. package/dist-cjs/lib/options.js.map +2 -2
  38. package/dist-cjs/lib/utils/EditorAtom.js +45 -0
  39. package/dist-cjs/lib/utils/EditorAtom.js.map +7 -0
  40. package/dist-cjs/version.js +3 -3
  41. package/dist-cjs/version.js.map +1 -1
  42. package/dist-esm/index.d.mts +181 -9
  43. package/dist-esm/index.mjs +7 -1
  44. package/dist-esm/index.mjs.map +2 -2
  45. package/dist-esm/lib/TldrawEditor.mjs +3 -1
  46. package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
  47. package/dist-esm/lib/components/MenuClickCapture.mjs +0 -5
  48. package/dist-esm/lib/components/MenuClickCapture.mjs.map +2 -2
  49. package/dist-esm/lib/components/Shape.mjs +4 -26
  50. package/dist-esm/lib/components/Shape.mjs.map +2 -2
  51. package/dist-esm/lib/components/default-components/DefaultCollaboratorHint.mjs +1 -1
  52. package/dist-esm/lib/components/default-components/DefaultCollaboratorHint.mjs.map +1 -1
  53. package/dist-esm/lib/components/default-components/DefaultScribble.mjs +1 -1
  54. package/dist-esm/lib/components/default-components/DefaultScribble.mjs.map +2 -2
  55. package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs +9 -1
  56. package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs.map +2 -2
  57. package/dist-esm/lib/components/default-components/DefaultShapeWrapper.mjs +23 -0
  58. package/dist-esm/lib/components/default-components/DefaultShapeWrapper.mjs.map +7 -0
  59. package/dist-esm/lib/config/TLUserPreferences.mjs +8 -2
  60. package/dist-esm/lib/config/TLUserPreferences.mjs.map +2 -2
  61. package/dist-esm/lib/editor/Editor.mjs +100 -58
  62. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  63. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs +8 -3
  64. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
  65. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
  66. package/dist-esm/lib/exports/getSvgJsx.mjs +2 -2
  67. package/dist-esm/lib/exports/getSvgJsx.mjs.map +2 -2
  68. package/dist-esm/lib/hooks/useCanvasEvents.mjs +23 -21
  69. package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
  70. package/dist-esm/lib/hooks/useEditorComponents.mjs +4 -0
  71. package/dist-esm/lib/hooks/useEditorComponents.mjs.map +2 -2
  72. package/dist-esm/lib/hooks/useStateAttribute.mjs +15 -0
  73. package/dist-esm/lib/hooks/useStateAttribute.mjs.map +7 -0
  74. package/dist-esm/lib/license/Watermark.mjs +6 -6
  75. package/dist-esm/lib/license/Watermark.mjs.map +1 -1
  76. package/dist-esm/lib/options.mjs +1 -0
  77. package/dist-esm/lib/options.mjs.map +2 -2
  78. package/dist-esm/lib/utils/EditorAtom.mjs +25 -0
  79. package/dist-esm/lib/utils/EditorAtom.mjs.map +7 -0
  80. package/dist-esm/version.mjs +3 -3
  81. package/dist-esm/version.mjs.map +1 -1
  82. package/editor.css +297 -311
  83. package/package.json +7 -7
  84. package/src/index.ts +7 -0
  85. package/src/lib/TldrawEditor.tsx +7 -5
  86. package/src/lib/components/MenuClickCapture.tsx +0 -8
  87. package/src/lib/components/Shape.tsx +6 -21
  88. package/src/lib/components/default-components/DefaultCollaboratorHint.tsx +1 -1
  89. package/src/lib/components/default-components/DefaultScribble.tsx +1 -1
  90. package/src/lib/components/default-components/DefaultShapeIndicator.tsx +5 -1
  91. package/src/lib/components/default-components/DefaultShapeWrapper.tsx +35 -0
  92. package/src/lib/config/TLUserPreferences.ts +7 -0
  93. package/src/lib/editor/Editor.ts +130 -81
  94. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +13 -0
  95. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +5 -0
  96. package/src/lib/editor/shapes/ShapeUtil.ts +57 -0
  97. package/src/lib/editor/types/misc-types.ts +73 -1
  98. package/src/lib/exports/getSvgJsx.tsx +2 -2
  99. package/src/lib/hooks/useCanvasEvents.ts +36 -32
  100. package/src/lib/hooks/useEditorComponents.tsx +7 -1
  101. package/src/lib/hooks/useStateAttribute.ts +15 -0
  102. package/src/lib/license/Watermark.tsx +6 -6
  103. package/src/lib/options.ts +2 -0
  104. package/src/lib/utils/EditorAtom.ts +37 -0
  105. package/src/version.ts +3 -3
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": "3.15.0",
4
+ "version": "3.16.0-canary.03deb7f8fe34",
5
5
  "author": {
6
6
  "name": "tldraw Inc.",
7
7
  "email": "hello@tldraw.com"
@@ -49,12 +49,12 @@
49
49
  "@tiptap/core": "^2.9.1",
50
50
  "@tiptap/pm": "^2.9.1",
51
51
  "@tiptap/react": "^2.9.1",
52
- "@tldraw/state": "3.15.0",
53
- "@tldraw/state-react": "3.15.0",
54
- "@tldraw/store": "3.15.0",
55
- "@tldraw/tlschema": "3.15.0",
56
- "@tldraw/utils": "3.15.0",
57
- "@tldraw/validate": "3.15.0",
52
+ "@tldraw/state": "3.16.0-canary.03deb7f8fe34",
53
+ "@tldraw/state-react": "3.16.0-canary.03deb7f8fe34",
54
+ "@tldraw/store": "3.16.0-canary.03deb7f8fe34",
55
+ "@tldraw/tlschema": "3.16.0-canary.03deb7f8fe34",
56
+ "@tldraw/utils": "3.16.0-canary.03deb7f8fe34",
57
+ "@tldraw/validate": "3.16.0-canary.03deb7f8fe34",
58
58
  "@types/core-js": "^2.5.8",
59
59
  "@use-gesture/react": "^10.3.1",
60
60
  "classnames": "^2.5.1",
package/src/index.ts CHANGED
@@ -67,6 +67,10 @@ export {
67
67
  DefaultShapeIndicators,
68
68
  type TLShapeIndicatorsProps,
69
69
  } from './lib/components/default-components/DefaultShapeIndicators'
70
+ export {
71
+ DefaultShapeWrapper,
72
+ type TLShapeWrapperProps,
73
+ } from './lib/components/default-components/DefaultShapeWrapper'
70
74
  export {
71
75
  DefaultSnapIndicator,
72
76
  type TLSnapIndicatorProps,
@@ -261,9 +265,11 @@ export {
261
265
  type TLCameraMoveOptions,
262
266
  type TLCameraOptions,
263
267
  type TLExportType,
268
+ type TLGetShapeAtPointOptions,
264
269
  type TLImageExportOptions,
265
270
  type TLSvgExportOptions,
266
271
  type TLSvgOptions,
272
+ type TLUpdatePointerOptions,
267
273
  } from './lib/editor/types/misc-types'
268
274
  export {
269
275
  type TLAdjacentDirection,
@@ -445,6 +451,7 @@ export {
445
451
  setPointerCapture,
446
452
  stopEventPropagation,
447
453
  } from './lib/utils/dom'
454
+ export { EditorAtom } from './lib/utils/EditorAtom'
448
455
  export { getIncrementedName } from './lib/utils/getIncrementedName'
449
456
  export { getPointerInfo } from './lib/utils/getPointerInfo'
450
457
  export { getSvgPathFromPoints } from './lib/utils/getSvgPathFromPoints'
@@ -1,9 +1,9 @@
1
1
  import { MigrationSequence, Store } from '@tldraw/store'
2
2
  import { TLShape, TLStore, TLStoreSnapshot } from '@tldraw/tlschema'
3
- import { Required, annotateError } from '@tldraw/utils'
3
+ import { annotateError, Required } from '@tldraw/utils'
4
4
  import React, {
5
- ReactNode,
6
5
  memo,
6
+ ReactNode,
7
7
  useCallback,
8
8
  useEffect,
9
9
  useLayoutEffect,
@@ -15,13 +15,13 @@ import React, {
15
15
 
16
16
  import classNames from 'classnames'
17
17
  import { version } from '../version'
18
- import { OptionalErrorBoundary } from './components/ErrorBoundary'
19
18
  import { DefaultErrorFallback } from './components/default-components/DefaultErrorFallback'
20
- import { TLEditorSnapshot } from './config/TLEditorSnapshot'
19
+ import { OptionalErrorBoundary } from './components/ErrorBoundary'
21
20
  import { TLStoreBaseOptions } from './config/createTLStore'
22
- import { TLUser, createTLUser } from './config/createTLUser'
21
+ import { createTLUser, TLUser } from './config/createTLUser'
23
22
  import { TLAnyBindingUtilConstructor } from './config/defaultBindings'
24
23
  import { TLAnyShapeUtilConstructor } from './config/defaultShapes'
24
+ import { TLEditorSnapshot } from './config/TLEditorSnapshot'
25
25
  import { Editor } from './editor/Editor'
26
26
  import { TLStateNodeConstructor } from './editor/tools/StateNode'
27
27
  import { TLCameraOptions } from './editor/types/misc-types'
@@ -39,6 +39,7 @@ import { useForceUpdate } from './hooks/useForceUpdate'
39
39
  import { useShallowObjectIdentity } from './hooks/useIdentity'
40
40
  import { useLocalStore } from './hooks/useLocalStore'
41
41
  import { useRefState } from './hooks/useRefState'
42
+ import { useStateAttribute } from './hooks/useStateAttribute'
42
43
  import { useZoomCss } from './hooks/useZoomCss'
43
44
  import { LicenseProvider } from './license/LicenseProvider'
44
45
  import { Watermark } from './license/Watermark'
@@ -646,6 +647,7 @@ function Layout({ children, onMount }: { children: ReactNode; onMount?: TLOnMoun
646
647
  useCursor()
647
648
  useDarkMode()
648
649
  useForceUpdate()
650
+ useStateAttribute()
649
651
  useOnMount((editor) => {
650
652
  const teardownStore = editor.store.props.onMount(editor)
651
653
  const teardownCallback = onMount?.(editor)
@@ -50,12 +50,6 @@ export function MenuClickCapture() {
50
50
  // Do nothing unless we're pointing
51
51
  if (!rPointerState.current.isDown) return
52
52
 
53
- // If we're already dragging, pass on the event as it is
54
- if (rPointerState.current.isDragging) {
55
- canvasEvents.onPointerMove?.(e)
56
- return
57
- }
58
-
59
53
  if (
60
54
  // We're pointing, but are we dragging?
61
55
  Vec.Dist2(rPointerState.current.start, new Vec(e.clientX, e.clientY)) >
@@ -75,8 +69,6 @@ export function MenuClickCapture() {
75
69
  clientY: y,
76
70
  button: 0,
77
71
  })
78
- // call the pointer move with the current pointer position
79
- canvasEvents.onPointerMove?.(e)
80
72
  }
81
73
  },
82
74
  [canvasEvents, editor]
@@ -40,7 +40,7 @@ export const Shape = memo(function Shape({
40
40
  }) {
41
41
  const editor = useEditor()
42
42
 
43
- const { ShapeErrorFallback } = useEditorComponents()
43
+ const { ShapeErrorFallback, ShapeWrapper } = useEditorComponents()
44
44
 
45
45
  const containerRef = useRef<HTMLDivElement>(null)
46
46
  const bgContainerRef = useRef<HTMLDivElement>(null)
@@ -145,37 +145,22 @@ export const Shape = memo(function Shape({
145
145
  [editor]
146
146
  )
147
147
 
148
- if (!shape) return null
149
-
150
- const isFilledShape = 'fill' in shape.props && shape.props.fill !== 'none'
148
+ if (!shape || !ShapeWrapper) return null
151
149
 
152
150
  return (
153
151
  <>
154
152
  {util.backgroundComponent && (
155
- <div
156
- ref={bgContainerRef}
157
- className="tl-shape tl-shape-background"
158
- data-shape-type={shape.type}
159
- data-shape-id={shape.id}
160
- draggable={false}
161
- >
153
+ <ShapeWrapper ref={bgContainerRef} shape={shape} isBackground={true}>
162
154
  <OptionalErrorBoundary fallback={ShapeErrorFallback} onError={annotateError}>
163
155
  <InnerShapeBackground shape={shape} util={util} />
164
156
  </OptionalErrorBoundary>
165
- </div>
157
+ </ShapeWrapper>
166
158
  )}
167
- <div
168
- ref={containerRef}
169
- className="tl-shape"
170
- data-shape-type={shape.type}
171
- data-shape-is-filled={isFilledShape}
172
- data-shape-id={shape.id}
173
- draggable={false}
174
- >
159
+ <ShapeWrapper ref={containerRef} shape={shape} isBackground={false}>
175
160
  <OptionalErrorBoundary fallback={ShapeErrorFallback as any} onError={annotateError}>
176
161
  <InnerShape shape={shape} util={util} />
177
162
  </OptionalErrorBoundary>
178
- </div>
163
+ </ShapeWrapper>
179
164
  </>
180
165
  )
181
166
  })
@@ -44,7 +44,7 @@ export function DefaultCollaboratorHint({
44
44
  href={`#${cursorHintId}`}
45
45
  color={color}
46
46
  strokeWidth={3}
47
- stroke="var(--color-background)"
47
+ stroke="var(--tl-color-background)"
48
48
  />
49
49
  <use href={`#${cursorHintId}`} color={color} opacity={opacity} />
50
50
  </svg>
@@ -21,7 +21,7 @@ export function DefaultScribble({ scribble, zoom, color, opacity, className }: T
21
21
  <path
22
22
  className="tl-scribble"
23
23
  d={getSvgPathFromPoints(scribble.points, false)}
24
- stroke={color ?? `var(--color-${scribble.color})`}
24
+ stroke={color ?? `var(--tl-color-${scribble.color})`}
25
25
  fill="none"
26
26
  strokeWidth={8 / zoom}
27
27
  opacity={opacity ?? scribble.opacity}
@@ -87,7 +87,11 @@ export const DefaultShapeIndicator = memo(function DefaultShapeIndicator({
87
87
 
88
88
  return (
89
89
  <svg ref={rIndicator} className={classNames('tl-overlays__item', className)} aria-hidden="true">
90
- <g className="tl-shape-indicator" stroke={color ?? 'var(--color-selected)'} opacity={opacity}>
90
+ <g
91
+ className="tl-shape-indicator"
92
+ stroke={color ?? 'var(--tl-color-selected)'}
93
+ opacity={opacity}
94
+ >
91
95
  <InnerIndicator editor={editor} id={shapeId} />
92
96
  </g>
93
97
  </svg>
@@ -0,0 +1,35 @@
1
+ import { TLShape } from '@tldraw/tlschema'
2
+ import classNames from 'classnames'
3
+ import { forwardRef, ReactNode } from 'react'
4
+
5
+ /** @public */
6
+ export interface TLShapeWrapperProps extends React.HTMLAttributes<HTMLDivElement> {
7
+ /** The shape being rendered. */
8
+ shape: TLShape
9
+ /** Whether this is the shapes regular, or background component. */
10
+ isBackground: boolean
11
+ /** The shape's rendered component. */
12
+ children: ReactNode
13
+ }
14
+
15
+ /** @public @react */
16
+ export const DefaultShapeWrapper = forwardRef(function DefaultShapeWrapper(
17
+ { children, shape, isBackground, ...props }: TLShapeWrapperProps,
18
+ ref: React.Ref<HTMLDivElement>
19
+ ) {
20
+ const isFilledShape = 'fill' in shape.props && shape.props.fill !== 'none'
21
+
22
+ return (
23
+ <div
24
+ ref={ref}
25
+ data-shape-type={shape.type}
26
+ data-shape-is-filled={isBackground ? undefined : isFilledShape}
27
+ data-shape-id={shape.id}
28
+ draggable={false}
29
+ {...props}
30
+ className={classNames('tl-shape', isBackground && 'tl-shape-background', props.className)}
31
+ >
32
+ {children}
33
+ </div>
34
+ )
35
+ })
@@ -24,6 +24,7 @@ export interface TLUserPreferences {
24
24
  isWrapMode?: boolean | null
25
25
  isDynamicSizeMode?: boolean | null
26
26
  isPasteAtCursorMode?: boolean | null
27
+ showUiLabels?: boolean | null
27
28
  }
28
29
 
29
30
  interface UserDataSnapshot {
@@ -52,6 +53,7 @@ export const userTypeValidator: T.Validator<TLUserPreferences> = T.object<TLUser
52
53
  isWrapMode: T.boolean.nullable().optional(),
53
54
  isDynamicSizeMode: T.boolean.nullable().optional(),
54
55
  isPasteAtCursorMode: T.boolean.nullable().optional(),
56
+ showUiLabels: T.boolean.nullable().optional(),
55
57
  })
56
58
 
57
59
  const Versions = {
@@ -64,6 +66,7 @@ const Versions = {
64
66
  AllowSystemColorScheme: 7,
65
67
  AddPasteAtCursor: 8,
66
68
  AddKeyboardShortcuts: 9,
69
+ AddShowUiLabels: 10,
67
70
  } as const
68
71
 
69
72
  const CURRENT_VERSION = Math.max(...Object.values(Versions))
@@ -102,6 +105,9 @@ function migrateSnapshot(data: { version: number; user: any }) {
102
105
  if (data.version < Versions.AddKeyboardShortcuts) {
103
106
  data.user.areKeyboardShortcutsEnabled = true
104
107
  }
108
+ if (data.version < Versions.AddShowUiLabels) {
109
+ data.user.showUiLabels = false
110
+ }
105
111
 
106
112
  // finally
107
113
  data.version = CURRENT_VERSION
@@ -150,6 +156,7 @@ export const defaultUserPreferences = Object.freeze({
150
156
  isWrapMode: false,
151
157
  isDynamicSizeMode: false,
152
158
  isPasteAtCursorMode: false,
159
+ showUiLabels: false,
153
160
  colorScheme: 'light',
154
161
  }) satisfies Readonly<Omit<TLUserPreferences, 'id'>>
155
162
 
@@ -176,8 +176,10 @@ import {
176
176
  RequiredKeys,
177
177
  TLCameraMoveOptions,
178
178
  TLCameraOptions,
179
+ TLGetShapeAtPointOptions,
179
180
  TLImageExportOptions,
180
181
  TLSvgExportOptions,
182
+ TLUpdatePointerOptions,
181
183
  } from './types/misc-types'
182
184
  import { TLAdjacentDirection, TLResizeHandle } from './types/selection-types'
183
185
 
@@ -3072,7 +3074,6 @@ export class Editor extends EventEmitter<TLEventMap> {
3072
3074
  // Dispatch a new pointer move because the pointer's page will have changed
3073
3075
  // (its screen position will compute to a new page position given the new camera position)
3074
3076
  const { currentScreenPoint, currentPagePoint } = this.inputs
3075
- const { screenBounds } = this.store.unsafeGetWithoutCapture(TLINSTANCE_ID)!
3076
3077
 
3077
3078
  // compare the next page point (derived from the current camera) to the current page point
3078
3079
  if (
@@ -3080,27 +3081,10 @@ export class Editor extends EventEmitter<TLEventMap> {
3080
3081
  currentScreenPoint.y / z - y !== currentPagePoint.y
3081
3082
  ) {
3082
3083
  // If it's changed, dispatch a pointer event
3083
- const event: TLPointerEventInfo = {
3084
- type: 'pointer',
3085
- target: 'canvas',
3086
- name: 'pointer_move',
3087
- // weird but true: we need to put the screen point back into client space
3088
- point: Vec.AddXY(currentScreenPoint, screenBounds.x, screenBounds.y),
3084
+ this.updatePointer({
3085
+ immediate: opts?.immediate,
3089
3086
  pointerId: INTERNAL_POINTER_IDS.CAMERA_MOVE,
3090
- ctrlKey: this.inputs.ctrlKey,
3091
- altKey: this.inputs.altKey,
3092
- shiftKey: this.inputs.shiftKey,
3093
- metaKey: this.inputs.metaKey,
3094
- accelKey: isAccelKey(this.inputs),
3095
- button: 0,
3096
- isPen: this.getInstanceState().isPenMode ?? false,
3097
- }
3098
-
3099
- if (opts?.immediate) {
3100
- this._flushEventForTick(event)
3101
- } else {
3102
- this.dispatch(event)
3103
- }
3087
+ })
3104
3088
  }
3105
3089
 
3106
3090
  this._tickCameraState()
@@ -4421,21 +4405,28 @@ export class Editor extends EventEmitter<TLEventMap> {
4421
4405
  */
4422
4406
  deletePage(page: TLPageId | TLPage): this {
4423
4407
  const id = typeof page === 'string' ? page : page.id
4424
- this.run(() => {
4425
- if (this.getIsReadonly()) return
4426
- const pages = this.getPages()
4427
- if (pages.length === 1) return
4408
+ this.run(
4409
+ () => {
4410
+ if (this.getIsReadonly()) return
4411
+ const pages = this.getPages()
4412
+ if (pages.length === 1) return
4428
4413
 
4429
- const deletedPage = this.getPage(id)
4430
- if (!deletedPage) return
4414
+ const deletedPage = this.getPage(id)
4415
+ if (!deletedPage) return
4431
4416
 
4432
- if (id === this.getCurrentPageId()) {
4433
- const index = pages.findIndex((page) => page.id === id)
4434
- const next = pages[index - 1] ?? pages[index + 1]
4435
- this.setCurrentPage(next.id)
4436
- }
4437
- this.store.remove([deletedPage.id])
4438
- })
4417
+ if (id === this.getCurrentPageId()) {
4418
+ const index = pages.findIndex((page) => page.id === id)
4419
+ const next = pages[index - 1] ?? pages[index + 1]
4420
+ this.setCurrentPage(next.id)
4421
+ }
4422
+
4423
+ const shapes = this.getSortedChildIdsForParent(deletedPage.id)
4424
+ this.deleteShapes(shapes)
4425
+
4426
+ this.store.remove([deletedPage.id])
4427
+ },
4428
+ { ignoreShapeLock: true }
4429
+ )
4439
4430
  return this
4440
4431
  }
4441
4432
 
@@ -5164,20 +5155,7 @@ export class Editor extends EventEmitter<TLEventMap> {
5164
5155
  *
5165
5156
  * @returns The shape at the given point, or undefined if there is no shape at the point.
5166
5157
  */
5167
- getShapeAtPoint(
5168
- point: VecLike,
5169
- opts = {} as {
5170
- renderingOnly?: boolean
5171
- margin?: number
5172
- hitInside?: boolean
5173
- hitLocked?: boolean
5174
- // TODO: we probably need to rename this, we don't quite _always_
5175
- // respect this esp. in the part below that does "Check labels first"
5176
- hitLabels?: boolean
5177
- hitFrameInside?: boolean
5178
- filter?(shape: TLShape): boolean
5179
- }
5180
- ): TLShape | undefined {
5158
+ getShapeAtPoint(point: VecLike, opts: TLGetShapeAtPointOptions = {}): TLShape | undefined {
5181
5159
  const zoomLevel = this.getZoomLevel()
5182
5160
  const viewportPageBounds = this.getViewportPageBounds()
5183
5161
  const {
@@ -5189,6 +5167,8 @@ export class Editor extends EventEmitter<TLEventMap> {
5189
5167
  hitFrameInside = false,
5190
5168
  } = opts
5191
5169
 
5170
+ const [innerMargin, outerMargin] = Array.isArray(margin) ? margin : [margin, margin]
5171
+
5192
5172
  let inHollowSmallestArea = Infinity
5193
5173
  let inHollowSmallestAreaHit: TLShape | null = null
5194
5174
 
@@ -5208,7 +5188,7 @@ export class Editor extends EventEmitter<TLEventMap> {
5208
5188
  return false
5209
5189
  const pageMask = this.getShapeMask(shape)
5210
5190
  if (pageMask && !pointInPolygon(point, pageMask)) return false
5211
- if (filter) return filter(shape)
5191
+ if (filter && !filter(shape)) return false
5212
5192
  return true
5213
5193
  })
5214
5194
 
@@ -5222,8 +5202,8 @@ export class Editor extends EventEmitter<TLEventMap> {
5222
5202
  // Check labels first
5223
5203
  if (
5224
5204
  this.isShapeOfType<TLFrameShape>(shape, 'frame') ||
5225
- (this.isShapeOfType<TLArrowShape>(shape, 'arrow') && shape.props.text.trim()) ||
5226
5205
  ((this.isShapeOfType<TLNoteShape>(shape, 'note') ||
5206
+ this.isShapeOfType<TLArrowShape>(shape, 'arrow') ||
5227
5207
  (this.isShapeOfType<TLGeoShape>(shape, 'geo') && shape.props.fill === 'none')) &&
5228
5208
  this.getShapeUtil(shape).getText(shape)?.trim())
5229
5209
  ) {
@@ -5234,13 +5214,18 @@ export class Editor extends EventEmitter<TLEventMap> {
5234
5214
  }
5235
5215
  }
5236
5216
 
5237
- if (this.isShapeOfType(shape, 'frame')) {
5217
+ if (this.isShapeOfType<TLFrameShape>(shape, 'frame')) {
5238
5218
  // On the rare case that we've hit a frame (not its label), test again hitInside to be forced true;
5239
5219
  // this prevents clicks from passing through the body of a frame to shapes behind it.
5240
5220
 
5241
5221
  // If the hit is within the frame's outer margin, then select the frame
5242
- const distance = geometry.distanceToPoint(pointInShapeSpace, hitInside)
5243
- if (Math.abs(distance) <= margin) {
5222
+ const distance = geometry.distanceToPoint(pointInShapeSpace, hitFrameInside)
5223
+ if (
5224
+ hitFrameInside
5225
+ ? (distance > 0 && distance <= outerMargin) ||
5226
+ (distance <= 0 && distance > -innerMargin)
5227
+ : distance > 0 && distance <= outerMargin
5228
+ ) {
5244
5229
  return inMarginClosestToEdgeHit || shape
5245
5230
  }
5246
5231
 
@@ -5279,11 +5264,11 @@ export class Editor extends EventEmitter<TLEventMap> {
5279
5264
  // If the margin is zero and the geometry has a very small width or height,
5280
5265
  // then check the actual distance. This is to prevent a bug where straight
5281
5266
  // lines would never pass the broad phase (point-in-bounds) check.
5282
- if (margin === 0 && (geometry.bounds.w < 1 || geometry.bounds.h < 1)) {
5267
+ if (outerMargin === 0 && (geometry.bounds.w < 1 || geometry.bounds.h < 1)) {
5283
5268
  distance = geometry.distanceToPoint(pointInShapeSpace, hitInside)
5284
5269
  } else {
5285
5270
  // Broad phase
5286
- if (geometry.bounds.containsPoint(pointInShapeSpace, margin)) {
5271
+ if (geometry.bounds.containsPoint(pointInShapeSpace, outerMargin)) {
5287
5272
  // Narrow phase (actual distance)
5288
5273
  distance = geometry.distanceToPoint(pointInShapeSpace, hitInside)
5289
5274
  } else {
@@ -5298,7 +5283,8 @@ export class Editor extends EventEmitter<TLEventMap> {
5298
5283
  // the shape or negative if inside of the shape. If the distance
5299
5284
  // is greater than the margin, then it's a miss. Otherwise...
5300
5285
 
5301
- if (distance <= margin) {
5286
+ // Are we close to the shape's edge?
5287
+ if (distance <= outerMargin || (hitInside && distance <= 0 && distance > -innerMargin)) {
5302
5288
  if (geometry.isFilled || (isGroup && geometry.children[0].isFilled)) {
5303
5289
  // If the shape is filled, then it's a hit. Remember, we're
5304
5290
  // starting from the TOP-MOST shape in z-index order, so any
@@ -5308,11 +5294,21 @@ export class Editor extends EventEmitter<TLEventMap> {
5308
5294
  // If the shape is bigger than the viewport, then skip it.
5309
5295
  if (this.getShapePageBounds(shape)!.contains(viewportPageBounds)) continue
5310
5296
 
5311
- // For hollow shapes...
5312
- if (Math.abs(distance) < margin) {
5313
- // We want to preference shapes where we're inside of the
5314
- // shape margin; and we would want to hit the shape with the
5315
- // edge closest to the point.
5297
+ // If we're close to the edge of the shape, and if it's the closest edge among
5298
+ // all the edges that we've gotten close to so far, then we will want to hit the
5299
+ // shape unless we hit something else or closer in later iterations.
5300
+ if (
5301
+ hitInside
5302
+ ? // On hitInside, the distance will be negative for hits inside
5303
+ // If the distance is positive, check against the outer margin
5304
+ (distance > 0 && distance <= outerMargin) ||
5305
+ // If the distance is negative, check against the inner margin
5306
+ (distance <= 0 && distance > -innerMargin)
5307
+ : // If hitInside is false, then sadly _we do not know_ whether the
5308
+ // point is inside or outside of the shape, so we check against
5309
+ // the max of the two margins
5310
+ Math.abs(distance) <= Math.max(innerMargin, outerMargin)
5311
+ ) {
5316
5312
  if (Math.abs(distance) < inMarginClosestToEdgeDistance) {
5317
5313
  inMarginClosestToEdgeDistance = Math.abs(distance)
5318
5314
  inMarginClosestToEdgeHit = shape
@@ -5334,6 +5330,7 @@ export class Editor extends EventEmitter<TLEventMap> {
5334
5330
  } else {
5335
5331
  // For open shapes (e.g. lines or draw shapes) always use the margin.
5336
5332
  // If the distance is less than the margin, return the shape as the hit.
5333
+ // Use the editor's configurable hit test margin.
5337
5334
  if (distance < this.options.hitTestMargin / zoomLevel) {
5338
5335
  return shape
5339
5336
  }
@@ -7390,7 +7387,6 @@ export class Editor extends EventEmitter<TLEventMap> {
7390
7387
  if (
7391
7388
  !this.getShapeUtil(shape).canBeLaidOut?.(shape, {
7392
7389
  type: 'stretch',
7393
- shapes: shapesToStretchFirstPass,
7394
7390
  })
7395
7391
  ) {
7396
7392
  continue
@@ -7861,25 +7857,32 @@ export class Editor extends EventEmitter<TLEventMap> {
7861
7857
  ) {
7862
7858
  let parentId: TLParentId = this.getFocusedGroupId()
7863
7859
 
7864
- for (let i = currentPageShapesSorted.length - 1; i >= 0; i--) {
7865
- const parent = currentPageShapesSorted[i]
7866
- const util = this.getShapeUtil(parent)
7867
- if (
7868
- util.canReceiveNewChildrenOfType(parent, partial.type) &&
7869
- !this.isShapeHidden(parent) &&
7870
- this.isPointInShape(
7871
- parent,
7872
- // If no parent is provided, then we can treat the
7873
- // shape's provided x/y as being in the page's space.
7874
- { x: partial.x ?? 0, y: partial.y ?? 0 },
7875
- {
7876
- margin: 0,
7877
- hitInside: true,
7878
- }
7879
- )
7880
- ) {
7881
- parentId = parent.id
7882
- break
7860
+ const isPositioned = partial.x !== undefined && partial.y !== undefined
7861
+
7862
+ // If the shape has been explicitly positioned, we'll try to find a parent at
7863
+ // that position. If not, we'll assume the user isn't deliberately placing the
7864
+ // shape and the positioning will be handled later by another system.
7865
+ if (isPositioned) {
7866
+ for (let i = currentPageShapesSorted.length - 1; i >= 0; i--) {
7867
+ const parent = currentPageShapesSorted[i]
7868
+ const util = this.getShapeUtil(parent)
7869
+ if (
7870
+ util.canReceiveNewChildrenOfType(parent, partial.type) &&
7871
+ !this.isShapeHidden(parent) &&
7872
+ this.isPointInShape(
7873
+ parent,
7874
+ // If no parent is provided, then we can treat the
7875
+ // shape's provided x/y as being in the page's space.
7876
+ { x: partial.x ?? 0, y: partial.y ?? 0 },
7877
+ {
7878
+ margin: 0,
7879
+ hitInside: true,
7880
+ }
7881
+ )
7882
+ ) {
7883
+ parentId = parent.id
7884
+ break
7885
+ }
7883
7886
  }
7884
7887
  }
7885
7888
 
@@ -9673,6 +9676,52 @@ export class Editor extends EventEmitter<TLEventMap> {
9673
9676
  return this
9674
9677
  }
9675
9678
 
9679
+ /**
9680
+ * Dispatch a pointer move event in the current position of the pointer. This is useful when
9681
+ * external circumstances have changed (e.g. the camera moved or a shape was moved) and you want
9682
+ * the current interaction to respond to that change.
9683
+ *
9684
+ * @example
9685
+ * ```ts
9686
+ * editor.updatePointer()
9687
+ * ```
9688
+ *
9689
+ * @param options - The options for updating the pointer.
9690
+ * @returns The editor instance.
9691
+ * @public
9692
+ */
9693
+ updatePointer(options?: TLUpdatePointerOptions): this {
9694
+ const event: TLPointerEventInfo = {
9695
+ type: 'pointer',
9696
+ target: 'canvas',
9697
+ name: 'pointer_move',
9698
+ point:
9699
+ options?.point ??
9700
+ // weird but true: what `inputs` calls screen-space is actually viewport space. so
9701
+ // we need to convert back into true screen space first. we should fix this...
9702
+ Vec.Add(
9703
+ this.inputs.currentScreenPoint,
9704
+ this.store.unsafeGetWithoutCapture(TLINSTANCE_ID)!.screenBounds
9705
+ ),
9706
+ pointerId: options?.pointerId ?? 0,
9707
+ button: options?.button ?? 0,
9708
+ isPen: options?.isPen ?? this.inputs.isPen,
9709
+ shiftKey: options?.shiftKey ?? this.inputs.shiftKey,
9710
+ altKey: options?.altKey ?? this.inputs.altKey,
9711
+ ctrlKey: options?.ctrlKey ?? this.inputs.ctrlKey,
9712
+ metaKey: options?.metaKey ?? this.inputs.metaKey,
9713
+ accelKey: options?.accelKey ?? isAccelKey(this.inputs),
9714
+ }
9715
+
9716
+ if (options?.immediate) {
9717
+ this._flushEventForTick(event)
9718
+ } else {
9719
+ this.dispatch(event)
9720
+ }
9721
+
9722
+ return this
9723
+ }
9724
+
9676
9725
  /**
9677
9726
  * Puts the editor into focused mode.
9678
9727
  *
@@ -25,6 +25,7 @@ describe('UserPreferencesManager', () => {
25
25
  locale: 'en',
26
26
  animationSpeed: 1,
27
27
  areKeyboardShortcutsEnabled: true,
28
+ showUiLabels: false,
28
29
  edgeScrollSpeed: 1,
29
30
  colorScheme: 'light',
30
31
  isSnapMode: false,
@@ -231,6 +232,7 @@ describe('UserPreferencesManager', () => {
231
232
  color: mockUserPreferences.color,
232
233
  animationSpeed: mockUserPreferences.animationSpeed,
233
234
  areKeyboardShortcutsEnabled: mockUserPreferences.areKeyboardShortcutsEnabled,
235
+ showUiLabels: mockUserPreferences.showUiLabels,
234
236
  isSnapMode: mockUserPreferences.isSnapMode,
235
237
  colorScheme: mockUserPreferences.colorScheme,
236
238
  isDarkMode: false, // light mode
@@ -379,6 +381,17 @@ describe('UserPreferencesManager', () => {
379
381
  })
380
382
  })
381
383
 
384
+ describe('getShowUiLabels', () => {
385
+ it('should return user show ui labels setting', () => {
386
+ expect(userPreferencesManager.getShowUiLabels()).toBe(mockUserPreferences.showUiLabels)
387
+ })
388
+
389
+ it('should return default show ui labels when null', () => {
390
+ userPreferencesAtom.set({ ...mockUserPreferences, showUiLabels: null })
391
+ expect(userPreferencesManager.getShowUiLabels()).toBe(defaultUserPreferences.showUiLabels)
392
+ })
393
+ })
394
+
382
395
  describe('getEdgeScrollSpeed', () => {
383
396
  it('should return user edge scroll speed', () => {
384
397
  expect(userPreferencesManager.getEdgeScrollSpeed()).toBe(
@@ -49,6 +49,7 @@ export class UserPreferencesManager {
49
49
  isDarkMode: this.getIsDarkMode(),
50
50
  isWrapMode: this.getIsWrapMode(),
51
51
  isDynamicResizeMode: this.getIsDynamicResizeMode(),
52
+ showUiLabels: this.getShowUiLabels(),
52
53
  }
53
54
  }
54
55
 
@@ -119,4 +120,8 @@ export class UserPreferencesManager {
119
120
  defaultUserPreferences.isPasteAtCursorMode
120
121
  )
121
122
  }
123
+
124
+ @computed getShowUiLabels() {
125
+ return this.user.userPreferences.get().showUiLabels ?? defaultUserPreferences.showUiLabels
126
+ }
122
127
  }