@tldraw/editor 3.16.0-canary.ca347c5375a5 → 3.16.0-canary.cb97f41de62b

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 (106) hide show
  1. package/dist-cjs/index.d.ts +84 -9
  2. package/dist-cjs/index.js +3 -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/default-components/DefaultCollaboratorHint.js +1 -1
  9. package/dist-cjs/lib/components/default-components/DefaultCollaboratorHint.js.map +1 -1
  10. package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js +1 -1
  11. package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js.map +2 -2
  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/config/TLUserPreferences.js +9 -3
  17. package/dist-cjs/lib/config/TLUserPreferences.js.map +2 -2
  18. package/dist-cjs/lib/editor/Editor.js +46 -24
  19. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  20. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js +9 -4
  21. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +2 -2
  22. package/dist-cjs/lib/editor/types/misc-types.js.map +1 -1
  23. package/dist-cjs/lib/exports/getSvgJsx.js +1 -2
  24. package/dist-cjs/lib/exports/getSvgJsx.js.map +2 -2
  25. package/dist-cjs/lib/hooks/useCanvasEvents.js +24 -20
  26. package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
  27. package/dist-cjs/lib/hooks/useStateAttribute.js +35 -0
  28. package/dist-cjs/lib/hooks/useStateAttribute.js.map +7 -0
  29. package/dist-cjs/lib/license/Watermark.js +6 -6
  30. package/dist-cjs/lib/license/Watermark.js.map +1 -1
  31. package/dist-cjs/lib/options.js +7 -0
  32. package/dist-cjs/lib/options.js.map +2 -2
  33. package/dist-cjs/lib/utils/EditorAtom.js +45 -0
  34. package/dist-cjs/lib/utils/EditorAtom.js.map +7 -0
  35. package/dist-cjs/version.js +3 -3
  36. package/dist-cjs/version.js.map +1 -1
  37. package/dist-esm/index.d.mts +84 -9
  38. package/dist-esm/index.mjs +3 -1
  39. package/dist-esm/index.mjs.map +2 -2
  40. package/dist-esm/lib/TldrawEditor.mjs +3 -1
  41. package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
  42. package/dist-esm/lib/components/MenuClickCapture.mjs +0 -5
  43. package/dist-esm/lib/components/MenuClickCapture.mjs.map +2 -2
  44. package/dist-esm/lib/components/default-components/DefaultCollaboratorHint.mjs +1 -1
  45. package/dist-esm/lib/components/default-components/DefaultCollaboratorHint.mjs.map +1 -1
  46. package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs +1 -1
  47. package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs.map +2 -2
  48. package/dist-esm/lib/components/default-components/DefaultScribble.mjs +1 -1
  49. package/dist-esm/lib/components/default-components/DefaultScribble.mjs.map +2 -2
  50. package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs +9 -1
  51. package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs.map +2 -2
  52. package/dist-esm/lib/config/TLUserPreferences.mjs +9 -3
  53. package/dist-esm/lib/config/TLUserPreferences.mjs.map +2 -2
  54. package/dist-esm/lib/editor/Editor.mjs +46 -24
  55. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  56. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs +9 -4
  57. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
  58. package/dist-esm/lib/exports/getSvgJsx.mjs +2 -2
  59. package/dist-esm/lib/exports/getSvgJsx.mjs.map +2 -2
  60. package/dist-esm/lib/hooks/useCanvasEvents.mjs +25 -21
  61. package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
  62. package/dist-esm/lib/hooks/useStateAttribute.mjs +15 -0
  63. package/dist-esm/lib/hooks/useStateAttribute.mjs.map +7 -0
  64. package/dist-esm/lib/license/Watermark.mjs +6 -6
  65. package/dist-esm/lib/license/Watermark.mjs.map +1 -1
  66. package/dist-esm/lib/options.mjs +7 -0
  67. package/dist-esm/lib/options.mjs.map +2 -2
  68. package/dist-esm/lib/utils/EditorAtom.mjs +25 -0
  69. package/dist-esm/lib/utils/EditorAtom.mjs.map +7 -0
  70. package/dist-esm/version.mjs +3 -3
  71. package/dist-esm/version.mjs.map +1 -1
  72. package/editor.css +293 -288
  73. package/package.json +14 -37
  74. package/src/index.ts +2 -0
  75. package/src/lib/TldrawEditor.tsx +7 -5
  76. package/src/lib/components/MenuClickCapture.tsx +0 -8
  77. package/src/lib/components/default-components/DefaultCollaboratorHint.tsx +1 -1
  78. package/src/lib/components/default-components/DefaultErrorFallback.tsx +1 -1
  79. package/src/lib/components/default-components/DefaultScribble.tsx +1 -1
  80. package/src/lib/components/default-components/DefaultShapeIndicator.tsx +5 -1
  81. package/src/lib/config/TLUserPreferences.ts +8 -1
  82. package/src/lib/editor/Editor.test.ts +12 -11
  83. package/src/lib/editor/Editor.ts +70 -47
  84. package/src/lib/editor/managers/ClickManager/ClickManager.test.ts +15 -14
  85. package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.test.ts +16 -15
  86. package/src/lib/editor/managers/FocusManager/FocusManager.test.ts +49 -48
  87. package/src/lib/editor/managers/FontManager/FontManager.test.ts +24 -23
  88. package/src/lib/editor/managers/HistoryManager/HistoryManager.test.ts +7 -6
  89. package/src/lib/editor/managers/ScribbleManager/ScribbleManager.test.ts +12 -11
  90. package/src/lib/editor/managers/SnapManager/SnapManager.test.ts +57 -50
  91. package/src/lib/editor/managers/TextManager/TextManager.test.ts +51 -26
  92. package/src/lib/editor/managers/TickManager/TickManager.test.ts +14 -13
  93. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +34 -26
  94. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +6 -1
  95. package/src/lib/editor/types/misc-types.ts +54 -1
  96. package/src/lib/exports/getSvgJsx.tsx +2 -2
  97. package/src/lib/hooks/useCanvasEvents.ts +39 -32
  98. package/src/lib/hooks/useStateAttribute.ts +15 -0
  99. package/src/lib/license/LicenseManager.test.ts +3 -1
  100. package/src/lib/license/Watermark.test.tsx +2 -1
  101. package/src/lib/license/Watermark.tsx +6 -6
  102. package/src/lib/options.ts +8 -0
  103. package/src/lib/utils/EditorAtom.ts +37 -0
  104. package/src/lib/utils/sync/LocalIndexedDb.test.ts +2 -1
  105. package/src/lib/utils/sync/TLLocalSyncClient.test.ts +15 -15
  106. 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.16.0-canary.ca347c5375a5",
4
+ "version": "3.16.0-canary.cb97f41de62b",
5
5
  "author": {
6
6
  "name": "tldraw Inc.",
7
7
  "email": "hello@tldraw.com"
@@ -34,27 +34,28 @@
34
34
  "src"
35
35
  ],
36
36
  "scripts": {
37
- "test-ci": "lazy inherit",
38
- "test": "yarn run -T jest",
37
+ "test-ci": "yarn run -T vitest run --passWithNoTests",
38
+ "test": "yarn run -T vitest --passWithNoTests",
39
39
  "benchmark": "yarn run -T tsx ./internal/scripts/benchmark.ts",
40
- "test-coverage": "lazy inherit",
40
+ "test-coverage": "yarn run -T vitest run --coverage --passWithNoTests",
41
41
  "build": "yarn run -T tsx ../../internal/scripts/build-package.ts",
42
42
  "build-api": "yarn run -T tsx ../../internal/scripts/build-api.ts",
43
43
  "prepack": "yarn run -T tsx ../../internal/scripts/prepack.ts",
44
44
  "postpack": "../../internal/scripts/postpack.sh",
45
45
  "pack-tarball": "yarn pack",
46
- "lint": "yarn run -T tsx ../../internal/scripts/lint.ts"
46
+ "lint": "yarn run -T tsx ../../internal/scripts/lint.ts",
47
+ "context": "yarn run -T tsx ../../internal/scripts/context.ts"
47
48
  },
48
49
  "dependencies": {
49
50
  "@tiptap/core": "^2.9.1",
50
51
  "@tiptap/pm": "^2.9.1",
51
52
  "@tiptap/react": "^2.9.1",
52
- "@tldraw/state": "3.16.0-canary.ca347c5375a5",
53
- "@tldraw/state-react": "3.16.0-canary.ca347c5375a5",
54
- "@tldraw/store": "3.16.0-canary.ca347c5375a5",
55
- "@tldraw/tlschema": "3.16.0-canary.ca347c5375a5",
56
- "@tldraw/utils": "3.16.0-canary.ca347c5375a5",
57
- "@tldraw/validate": "3.16.0-canary.ca347c5375a5",
53
+ "@tldraw/state": "3.16.0-canary.cb97f41de62b",
54
+ "@tldraw/state-react": "3.16.0-canary.cb97f41de62b",
55
+ "@tldraw/store": "3.16.0-canary.cb97f41de62b",
56
+ "@tldraw/tlschema": "3.16.0-canary.cb97f41de62b",
57
+ "@tldraw/utils": "3.16.0-canary.cb97f41de62b",
58
+ "@tldraw/validate": "3.16.0-canary.cb97f41de62b",
58
59
  "@types/core-js": "^2.5.8",
59
60
  "@use-gesture/react": "^10.3.1",
60
61
  "classnames": "^2.5.1",
@@ -69,41 +70,17 @@
69
70
  },
70
71
  "devDependencies": {
71
72
  "@peculiar/webcrypto": "^1.5.0",
72
- "@testing-library/jest-dom": "^5.17.0",
73
73
  "@testing-library/react": "^15.0.7",
74
74
  "@types/benchmark": "^2.1.5",
75
75
  "@types/react": "^18.3.18",
76
76
  "@types/wicg-file-system-access": "^2020.9.8",
77
77
  "benchmark": "^2.1.4",
78
78
  "fake-indexeddb": "^4.0.2",
79
- "jest-canvas-mock": "^2.5.2",
80
- "jest-environment-jsdom": "^29.7.0",
81
79
  "lazyrepo": "0.0.0-alpha.27",
82
80
  "react": "^18.3.1",
83
81
  "react-dom": "^18.3.1",
84
- "resize-observer-polyfill": "^1.5.1"
85
- },
86
- "jest": {
87
- "preset": "../../internal/config/jest/node/jest-preset.js",
88
- "testEnvironment": "../../../packages/utils/patchedJestJsDom.js",
89
- "fakeTimers": {
90
- "enableGlobally": true
91
- },
92
- "testPathIgnorePatterns": [
93
- "^.+\\.*.css$"
94
- ],
95
- "moduleNameMapper": {
96
- "^~(.*)": "<rootDir>/src/$1",
97
- "\\.(css|less|scss|sass)$": "identity-obj-proxy"
98
- },
99
- "setupFiles": [
100
- "raf/polyfill",
101
- "jest-canvas-mock",
102
- "<rootDir>/setupTests.js"
103
- ],
104
- "setupFilesAfterEnv": [
105
- "../../internal/config/setupJest.ts"
106
- ]
82
+ "resize-observer-polyfill": "^1.5.1",
83
+ "vitest": "^3.2.4"
107
84
  },
108
85
  "module": "dist-esm/index.mjs",
109
86
  "source": "src/index.ts",
package/src/index.ts CHANGED
@@ -265,6 +265,7 @@ export {
265
265
  type TLCameraMoveOptions,
266
266
  type TLCameraOptions,
267
267
  type TLExportType,
268
+ type TLGetShapeAtPointOptions,
268
269
  type TLImageExportOptions,
269
270
  type TLSvgExportOptions,
270
271
  type TLSvgOptions,
@@ -450,6 +451,7 @@ export {
450
451
  setPointerCapture,
451
452
  stopEventPropagation,
452
453
  } from './lib/utils/dom'
454
+ export { EditorAtom } from './lib/utils/EditorAtom'
453
455
  export { getIncrementedName } from './lib/utils/getIncrementedName'
454
456
  export { getPointerInfo } from './lib/utils/getPointerInfo'
455
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]
@@ -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>
@@ -75,7 +75,7 @@ export const DefaultErrorFallback: TLErrorFallbackComponent = ({ error, editor }
75
75
 
76
76
  // if we can't find a theme class from the app or from a parent, we have
77
77
  // to fall back on using a media query:
78
- if (typeof window !== 'undefined' && 'matchMedia' in window) {
78
+ if (typeof window !== 'undefined' && window.matchMedia) {
79
79
  setIsDarkMode(window.matchMedia('(prefers-color-scheme: dark)').matches)
80
80
  }
81
81
  }, [isDarkModeFromApp])
@@ -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>
@@ -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
@@ -129,7 +135,7 @@ function getRandomColor() {
129
135
 
130
136
  /** @internal */
131
137
  export function userPrefersReducedMotion() {
132
- if (typeof window !== 'undefined' && 'matchMedia' in window) {
138
+ if (typeof window !== 'undefined' && window.matchMedia) {
133
139
  return window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches ?? false
134
140
  }
135
141
 
@@ -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
 
@@ -1,3 +1,4 @@
1
+ import { vi } from 'vitest'
1
2
  import {
2
3
  Box,
3
4
  Geometry2d,
@@ -59,8 +60,8 @@ beforeEach(() => {
59
60
  getContainer: () => document.body,
60
61
  })
61
62
  editor.setCameraOptions({ isLocked: true })
62
- editor.setCamera = jest.fn()
63
- editor.user.getAnimationSpeed = jest.fn()
63
+ editor.setCamera = vi.fn()
64
+ editor.user.getAnimationSpeed = vi.fn()
64
65
  })
65
66
 
66
67
  describe('centerOnPoint', () => {
@@ -94,13 +95,13 @@ describe('updateShape', () => {
94
95
 
95
96
  describe('zoomToFit', () => {
96
97
  it('no-op when isLocked is set', () => {
97
- editor.getCurrentPageShapeIds = jest.fn(() => new Set([createShapeId('box1')]))
98
+ editor.getCurrentPageShapeIds = vi.fn(() => new Set([createShapeId('box1')]))
98
99
  editor.zoomToFit()
99
100
  expect(editor.setCamera).not.toHaveBeenCalled()
100
101
  })
101
102
 
102
103
  it('sets camera when isLocked is set and force flag is set', () => {
103
- editor.getCurrentPageShapeIds = jest.fn(() => new Set([createShapeId('box1')]))
104
+ editor.getCurrentPageShapeIds = vi.fn(() => new Set([createShapeId('box1')]))
104
105
  editor.zoomToFit({ force: true })
105
106
  expect(editor.setCamera).toHaveBeenCalled()
106
107
  })
@@ -144,13 +145,13 @@ describe('zoomOut', () => {
144
145
 
145
146
  describe('zoomToSelection', () => {
146
147
  it('no-op when isLocked is set', () => {
147
- editor.getSelectionPageBounds = jest.fn(() => Box.From({ x: 0, y: 0, w: 100, h: 100 }))
148
+ editor.getSelectionPageBounds = vi.fn(() => Box.From({ x: 0, y: 0, w: 100, h: 100 }))
148
149
  editor.zoomToSelection()
149
150
  expect(editor.setCamera).not.toHaveBeenCalled()
150
151
  })
151
152
 
152
153
  it('sets camera when isLocked is set and force flag is set', () => {
153
- editor.getSelectionPageBounds = jest.fn(() => Box.From({ x: 0, y: 0, w: 100, h: 100 }))
154
+ editor.getSelectionPageBounds = vi.fn(() => Box.From({ x: 0, y: 0, w: 100, h: 100 }))
154
155
  editor.zoomToSelection({ force: true })
155
156
  expect(editor.setCamera).toHaveBeenCalled()
156
157
  })
@@ -286,7 +287,7 @@ describe('getShapesAtPoint', () => {
286
287
 
287
288
  it('filters out hidden shapes', () => {
288
289
  // Create a spy to mock isShapeHidden
289
- const isShapeHiddenSpy = jest.spyOn(editor, 'isShapeHidden')
290
+ const isShapeHiddenSpy = vi.spyOn(editor, 'isShapeHidden')
290
291
  isShapeHiddenSpy.mockImplementation((shape) => {
291
292
  return typeof shape === 'string' ? shape === ids.shape3 : shape.id === ids.shape3
292
293
  })
@@ -352,7 +353,7 @@ describe('getShapesAtPoint', () => {
352
353
 
353
354
  it('returns empty array when all shapes are hidden', () => {
354
355
  // Mock all shapes as hidden
355
- const isShapeHiddenSpy = jest.spyOn(editor, 'isShapeHidden')
356
+ const isShapeHiddenSpy = vi.spyOn(editor, 'isShapeHidden')
356
357
  isShapeHiddenSpy.mockReturnValue(true)
357
358
 
358
359
  const shapes = editor.getShapesAtPoint({ x: 50, y: 50 })
@@ -692,7 +693,7 @@ describe('selectAll', () => {
692
693
  const initialSelectedIds = editor.getSelectedShapeIds()
693
694
 
694
695
  // Spy on setSelectedShapes to verify it's not called
695
- const setSelectedShapesSpy = jest.spyOn(editor, 'setSelectedShapes')
696
+ const setSelectedShapesSpy = vi.spyOn(editor, 'setSelectedShapes')
696
697
 
697
698
  // Call selectAll
698
699
  editor.selectAll()
@@ -713,7 +714,7 @@ describe('selectAll', () => {
713
714
  const initialSelectedIds = editor.getSelectedShapeIds()
714
715
 
715
716
  // Spy on setSelectedShapes to verify it's not called
716
- const setSelectedShapesSpy = jest.spyOn(editor, 'setSelectedShapes')
717
+ const setSelectedShapesSpy = vi.spyOn(editor, 'setSelectedShapes')
717
718
 
718
719
  // Call selectAll
719
720
  editor.selectAll()
@@ -818,7 +819,7 @@ describe('selectAll', () => {
818
819
  const initialSelectedIds = Array.from(editor.getSelectedShapeIds())
819
820
 
820
821
  // Spy on setSelectedShapes to verify it's not called
821
- const setSelectedShapesSpy = jest.spyOn(editor, 'setSelectedShapes')
822
+ const setSelectedShapesSpy = vi.spyOn(editor, 'setSelectedShapes')
822
823
 
823
824
  // Call selectAll
824
825
  editor.selectAll()
@@ -176,6 +176,7 @@ import {
176
176
  RequiredKeys,
177
177
  TLCameraMoveOptions,
178
178
  TLCameraOptions,
179
+ TLGetShapeAtPointOptions,
179
180
  TLImageExportOptions,
180
181
  TLSvgExportOptions,
181
182
  TLUpdatePointerOptions,
@@ -5154,20 +5155,7 @@ export class Editor extends EventEmitter<TLEventMap> {
5154
5155
  *
5155
5156
  * @returns The shape at the given point, or undefined if there is no shape at the point.
5156
5157
  */
5157
- getShapeAtPoint(
5158
- point: VecLike,
5159
- opts = {} as {
5160
- renderingOnly?: boolean
5161
- margin?: number
5162
- hitInside?: boolean
5163
- hitLocked?: boolean
5164
- // TODO: we probably need to rename this, we don't quite _always_
5165
- // respect this esp. in the part below that does "Check labels first"
5166
- hitLabels?: boolean
5167
- hitFrameInside?: boolean
5168
- filter?(shape: TLShape): boolean
5169
- }
5170
- ): TLShape | undefined {
5158
+ getShapeAtPoint(point: VecLike, opts: TLGetShapeAtPointOptions = {}): TLShape | undefined {
5171
5159
  const zoomLevel = this.getZoomLevel()
5172
5160
  const viewportPageBounds = this.getViewportPageBounds()
5173
5161
  const {
@@ -5179,6 +5167,8 @@ export class Editor extends EventEmitter<TLEventMap> {
5179
5167
  hitFrameInside = false,
5180
5168
  } = opts
5181
5169
 
5170
+ const [innerMargin, outerMargin] = Array.isArray(margin) ? margin : [margin, margin]
5171
+
5182
5172
  let inHollowSmallestArea = Infinity
5183
5173
  let inHollowSmallestAreaHit: TLShape | null = null
5184
5174
 
@@ -5198,7 +5188,7 @@ export class Editor extends EventEmitter<TLEventMap> {
5198
5188
  return false
5199
5189
  const pageMask = this.getShapeMask(shape)
5200
5190
  if (pageMask && !pointInPolygon(point, pageMask)) return false
5201
- if (filter) return filter(shape)
5191
+ if (filter && !filter(shape)) return false
5202
5192
  return true
5203
5193
  })
5204
5194
 
@@ -5224,13 +5214,18 @@ export class Editor extends EventEmitter<TLEventMap> {
5224
5214
  }
5225
5215
  }
5226
5216
 
5227
- if (this.isShapeOfType(shape, 'frame')) {
5217
+ if (this.isShapeOfType<TLFrameShape>(shape, 'frame')) {
5228
5218
  // On the rare case that we've hit a frame (not its label), test again hitInside to be forced true;
5229
5219
  // this prevents clicks from passing through the body of a frame to shapes behind it.
5230
5220
 
5231
5221
  // If the hit is within the frame's outer margin, then select the frame
5232
- const distance = geometry.distanceToPoint(pointInShapeSpace, hitInside)
5233
- 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
+ ) {
5234
5229
  return inMarginClosestToEdgeHit || shape
5235
5230
  }
5236
5231
 
@@ -5269,11 +5264,11 @@ export class Editor extends EventEmitter<TLEventMap> {
5269
5264
  // If the margin is zero and the geometry has a very small width or height,
5270
5265
  // then check the actual distance. This is to prevent a bug where straight
5271
5266
  // lines would never pass the broad phase (point-in-bounds) check.
5272
- if (margin === 0 && (geometry.bounds.w < 1 || geometry.bounds.h < 1)) {
5267
+ if (outerMargin === 0 && (geometry.bounds.w < 1 || geometry.bounds.h < 1)) {
5273
5268
  distance = geometry.distanceToPoint(pointInShapeSpace, hitInside)
5274
5269
  } else {
5275
5270
  // Broad phase
5276
- if (geometry.bounds.containsPoint(pointInShapeSpace, margin)) {
5271
+ if (geometry.bounds.containsPoint(pointInShapeSpace, outerMargin)) {
5277
5272
  // Narrow phase (actual distance)
5278
5273
  distance = geometry.distanceToPoint(pointInShapeSpace, hitInside)
5279
5274
  } else {
@@ -5288,7 +5283,8 @@ export class Editor extends EventEmitter<TLEventMap> {
5288
5283
  // the shape or negative if inside of the shape. If the distance
5289
5284
  // is greater than the margin, then it's a miss. Otherwise...
5290
5285
 
5291
- if (distance <= margin) {
5286
+ // Are we close to the shape's edge?
5287
+ if (distance <= outerMargin || (hitInside && distance <= 0 && distance > -innerMargin)) {
5292
5288
  if (geometry.isFilled || (isGroup && geometry.children[0].isFilled)) {
5293
5289
  // If the shape is filled, then it's a hit. Remember, we're
5294
5290
  // starting from the TOP-MOST shape in z-index order, so any
@@ -5298,11 +5294,21 @@ export class Editor extends EventEmitter<TLEventMap> {
5298
5294
  // If the shape is bigger than the viewport, then skip it.
5299
5295
  if (this.getShapePageBounds(shape)!.contains(viewportPageBounds)) continue
5300
5296
 
5301
- // For hollow shapes...
5302
- if (Math.abs(distance) < margin) {
5303
- // We want to preference shapes where we're inside of the
5304
- // shape margin; and we would want to hit the shape with the
5305
- // 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
+ ) {
5306
5312
  if (Math.abs(distance) < inMarginClosestToEdgeDistance) {
5307
5313
  inMarginClosestToEdgeDistance = Math.abs(distance)
5308
5314
  inMarginClosestToEdgeHit = shape
@@ -5324,6 +5330,7 @@ export class Editor extends EventEmitter<TLEventMap> {
5324
5330
  } else {
5325
5331
  // For open shapes (e.g. lines or draw shapes) always use the margin.
5326
5332
  // If the distance is less than the margin, return the shape as the hit.
5333
+ // Use the editor's configurable hit test margin.
5327
5334
  if (distance < this.options.hitTestMargin / zoomLevel) {
5328
5335
  return shape
5329
5336
  }
@@ -6326,7 +6333,17 @@ export class Editor extends EventEmitter<TLEventMap> {
6326
6333
 
6327
6334
  this.createShapes(shapesToCreate)
6328
6335
  this.createBindings(bindingsToCreate)
6329
- this.setSelectedShapes(compact(ids.map((id) => shapeIds.get(id))))
6336
+
6337
+ this.setSelectedShapes(
6338
+ compact(
6339
+ ids.map((oldId) => {
6340
+ const newId = shapeIds.get(oldId)
6341
+ if (!newId) return null
6342
+ if (!this.getShape(newId)) return null
6343
+ return newId
6344
+ })
6345
+ )
6346
+ )
6330
6347
 
6331
6348
  if (offset !== undefined) {
6332
6349
  // If we've offset the duplicated shapes, check to see whether their new bounds is entirely
@@ -7380,7 +7397,6 @@ export class Editor extends EventEmitter<TLEventMap> {
7380
7397
  if (
7381
7398
  !this.getShapeUtil(shape).canBeLaidOut?.(shape, {
7382
7399
  type: 'stretch',
7383
- shapes: shapesToStretchFirstPass,
7384
7400
  })
7385
7401
  ) {
7386
7402
  continue
@@ -7851,25 +7867,32 @@ export class Editor extends EventEmitter<TLEventMap> {
7851
7867
  ) {
7852
7868
  let parentId: TLParentId = this.getFocusedGroupId()
7853
7869
 
7854
- for (let i = currentPageShapesSorted.length - 1; i >= 0; i--) {
7855
- const parent = currentPageShapesSorted[i]
7856
- const util = this.getShapeUtil(parent)
7857
- if (
7858
- util.canReceiveNewChildrenOfType(parent, partial.type) &&
7859
- !this.isShapeHidden(parent) &&
7860
- this.isPointInShape(
7861
- parent,
7862
- // If no parent is provided, then we can treat the
7863
- // shape's provided x/y as being in the page's space.
7864
- { x: partial.x ?? 0, y: partial.y ?? 0 },
7865
- {
7866
- margin: 0,
7867
- hitInside: true,
7868
- }
7869
- )
7870
- ) {
7871
- parentId = parent.id
7872
- break
7870
+ const isPositioned = partial.x !== undefined && partial.y !== undefined
7871
+
7872
+ // If the shape has been explicitly positioned, we'll try to find a parent at
7873
+ // that position. If not, we'll assume the user isn't deliberately placing the
7874
+ // shape and the positioning will be handled later by another system.
7875
+ if (isPositioned) {
7876
+ for (let i = currentPageShapesSorted.length - 1; i >= 0; i--) {
7877
+ const parent = currentPageShapesSorted[i]
7878
+ const util = this.getShapeUtil(parent)
7879
+ if (
7880
+ util.canReceiveNewChildrenOfType(parent, partial.type) &&
7881
+ !this.isShapeHidden(parent) &&
7882
+ this.isPointInShape(
7883
+ parent,
7884
+ // If no parent is provided, then we can treat the
7885
+ // shape's provided x/y as being in the page's space.
7886
+ { x: partial.x ?? 0, y: partial.y ?? 0 },
7887
+ {
7888
+ margin: 0,
7889
+ hitInside: true,
7890
+ }
7891
+ )
7892
+ ) {
7893
+ parentId = parent.id
7894
+ break
7895
+ }
7873
7896
  }
7874
7897
  }
7875
7898