@tldraw/editor 4.3.0-next.7f179bd04d6c → 4.3.0-next.842fb21476f2

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 (165) hide show
  1. package/dist-cjs/index.d.ts +441 -120
  2. package/dist-cjs/index.js +6 -1
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/components/ErrorBoundary.js.map +1 -1
  5. package/dist-cjs/lib/components/GeometryDebuggingView.js +1 -17
  6. package/dist-cjs/lib/components/GeometryDebuggingView.js.map +2 -2
  7. package/dist-cjs/lib/components/default-components/DefaultCanvas.js +3 -3
  8. package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
  9. package/dist-cjs/lib/constants.js +1 -3
  10. package/dist-cjs/lib/constants.js.map +2 -2
  11. package/dist-cjs/lib/editor/Editor.js +288 -275
  12. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  13. package/dist-cjs/lib/editor/derivations/notVisibleShapes.js +18 -17
  14. package/dist-cjs/lib/editor/derivations/notVisibleShapes.js.map +3 -3
  15. package/dist-cjs/lib/editor/derivations/parentsToChildren.js +12 -3
  16. package/dist-cjs/lib/editor/derivations/parentsToChildren.js.map +2 -2
  17. package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js +1 -1
  18. package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js.map +2 -2
  19. package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js +5 -6
  20. package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js.map +2 -2
  21. package/dist-cjs/lib/editor/managers/InputsManager/InputsManager.js +591 -0
  22. package/dist-cjs/lib/editor/managers/InputsManager/InputsManager.js.map +7 -0
  23. package/dist-cjs/lib/editor/managers/SnapManager/SnapManager.js +1 -1
  24. package/dist-cjs/lib/editor/managers/SnapManager/SnapManager.js.map +2 -2
  25. package/dist-cjs/lib/editor/managers/TickManager/TickManager.js +1 -22
  26. package/dist-cjs/lib/editor/managers/TickManager/TickManager.js.map +2 -2
  27. package/dist-cjs/lib/editor/shapes/ShapeUtil.js +31 -23
  28. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
  29. package/dist-cjs/lib/editor/shapes/group/DashedOutlineBox.js +1 -1
  30. package/dist-cjs/lib/editor/shapes/group/DashedOutlineBox.js.map +2 -2
  31. package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js +3 -3
  32. package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js.map +2 -2
  33. package/dist-cjs/lib/exports/parseCss.js +1 -1
  34. package/dist-cjs/lib/exports/parseCss.js.map +2 -2
  35. package/dist-cjs/lib/globals/environment.js +45 -9
  36. package/dist-cjs/lib/globals/environment.js.map +2 -2
  37. package/dist-cjs/lib/hooks/useCoarsePointer.js +14 -29
  38. package/dist-cjs/lib/hooks/useCoarsePointer.js.map +2 -2
  39. package/dist-cjs/lib/hooks/useEvent.js +1 -1
  40. package/dist-cjs/lib/hooks/useEvent.js.map +2 -2
  41. package/dist-cjs/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.js.map +2 -2
  42. package/dist-cjs/lib/hooks/useGestureEvents.js +1 -1
  43. package/dist-cjs/lib/hooks/useGestureEvents.js.map +2 -2
  44. package/dist-cjs/lib/hooks/usePassThroughMouseOverEvents.js.map +2 -2
  45. package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js.map +2 -2
  46. package/dist-cjs/lib/hooks/useScreenBounds.js.map +2 -2
  47. package/dist-cjs/lib/hooks/useStateAttribute.js +4 -1
  48. package/dist-cjs/lib/hooks/useStateAttribute.js.map +2 -2
  49. package/dist-cjs/lib/hooks/useTransform.js.map +1 -1
  50. package/dist-cjs/lib/hooks/useZoomCss.js +4 -8
  51. package/dist-cjs/lib/hooks/useZoomCss.js.map +2 -2
  52. package/dist-cjs/lib/options.js +6 -1
  53. package/dist-cjs/lib/options.js.map +2 -2
  54. package/dist-cjs/lib/primitives/Box.js +3 -0
  55. package/dist-cjs/lib/primitives/Box.js.map +2 -2
  56. package/dist-cjs/lib/primitives/geometry/Geometry2d.js +1 -0
  57. package/dist-cjs/lib/primitives/geometry/Geometry2d.js.map +2 -2
  58. package/dist-cjs/lib/utils/rotation.js +1 -1
  59. package/dist-cjs/lib/utils/rotation.js.map +2 -2
  60. package/dist-cjs/version.js +3 -3
  61. package/dist-cjs/version.js.map +1 -1
  62. package/dist-esm/index.d.mts +441 -120
  63. package/dist-esm/index.mjs +7 -2
  64. package/dist-esm/index.mjs.map +2 -2
  65. package/dist-esm/lib/components/ErrorBoundary.mjs.map +1 -1
  66. package/dist-esm/lib/components/GeometryDebuggingView.mjs +1 -17
  67. package/dist-esm/lib/components/GeometryDebuggingView.mjs.map +2 -2
  68. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +3 -3
  69. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
  70. package/dist-esm/lib/constants.mjs +1 -3
  71. package/dist-esm/lib/constants.mjs.map +2 -2
  72. package/dist-esm/lib/editor/Editor.mjs +289 -278
  73. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  74. package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs +18 -17
  75. package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs.map +3 -3
  76. package/dist-esm/lib/editor/derivations/parentsToChildren.mjs +13 -4
  77. package/dist-esm/lib/editor/derivations/parentsToChildren.mjs.map +2 -2
  78. package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs +1 -1
  79. package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs.map +2 -2
  80. package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs +5 -6
  81. package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs.map +2 -2
  82. package/dist-esm/lib/editor/managers/InputsManager/InputsManager.mjs +573 -0
  83. package/dist-esm/lib/editor/managers/InputsManager/InputsManager.mjs.map +7 -0
  84. package/dist-esm/lib/editor/managers/SnapManager/SnapManager.mjs +1 -1
  85. package/dist-esm/lib/editor/managers/SnapManager/SnapManager.mjs.map +2 -2
  86. package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs +1 -22
  87. package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs.map +2 -2
  88. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +31 -23
  89. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
  90. package/dist-esm/lib/editor/shapes/group/DashedOutlineBox.mjs +1 -1
  91. package/dist-esm/lib/editor/shapes/group/DashedOutlineBox.mjs.map +2 -2
  92. package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs +3 -3
  93. package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs.map +2 -2
  94. package/dist-esm/lib/exports/parseCss.mjs +1 -1
  95. package/dist-esm/lib/exports/parseCss.mjs.map +2 -2
  96. package/dist-esm/lib/globals/environment.mjs +45 -9
  97. package/dist-esm/lib/globals/environment.mjs.map +2 -2
  98. package/dist-esm/lib/hooks/useCoarsePointer.mjs +15 -30
  99. package/dist-esm/lib/hooks/useCoarsePointer.mjs.map +2 -2
  100. package/dist-esm/lib/hooks/useEvent.mjs +1 -1
  101. package/dist-esm/lib/hooks/useEvent.mjs.map +2 -2
  102. package/dist-esm/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.mjs.map +2 -2
  103. package/dist-esm/lib/hooks/useGestureEvents.mjs +1 -1
  104. package/dist-esm/lib/hooks/useGestureEvents.mjs.map +2 -2
  105. package/dist-esm/lib/hooks/usePassThroughMouseOverEvents.mjs.map +2 -2
  106. package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs.map +2 -2
  107. package/dist-esm/lib/hooks/useScreenBounds.mjs.map +2 -2
  108. package/dist-esm/lib/hooks/useStateAttribute.mjs +4 -1
  109. package/dist-esm/lib/hooks/useStateAttribute.mjs.map +2 -2
  110. package/dist-esm/lib/hooks/useTransform.mjs.map +1 -1
  111. package/dist-esm/lib/hooks/useZoomCss.mjs +4 -8
  112. package/dist-esm/lib/hooks/useZoomCss.mjs.map +2 -2
  113. package/dist-esm/lib/options.mjs +6 -1
  114. package/dist-esm/lib/options.mjs.map +2 -2
  115. package/dist-esm/lib/primitives/Box.mjs +3 -0
  116. package/dist-esm/lib/primitives/Box.mjs.map +2 -2
  117. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs +1 -0
  118. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs.map +2 -2
  119. package/dist-esm/lib/utils/rotation.mjs +1 -1
  120. package/dist-esm/lib/utils/rotation.mjs.map +2 -2
  121. package/dist-esm/version.mjs +3 -3
  122. package/dist-esm/version.mjs.map +1 -1
  123. package/editor.css +14 -12
  124. package/package.json +18 -16
  125. package/src/index.ts +4 -1
  126. package/src/lib/components/ErrorBoundary.tsx +1 -1
  127. package/src/lib/components/GeometryDebuggingView.tsx +1 -19
  128. package/src/lib/components/default-components/DefaultCanvas.tsx +3 -3
  129. package/src/lib/config/TLUserPreferences.test.ts +40 -0
  130. package/src/lib/constants.ts +0 -2
  131. package/src/lib/editor/Editor.test.ts +140 -0
  132. package/src/lib/editor/Editor.ts +374 -321
  133. package/src/lib/editor/derivations/notVisibleShapes.ts +37 -23
  134. package/src/lib/editor/derivations/parentsToChildren.ts +18 -7
  135. package/src/lib/editor/managers/ClickManager/ClickManager.test.ts +17 -31
  136. package/src/lib/editor/managers/ClickManager/ClickManager.ts +1 -1
  137. package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.test.ts +129 -79
  138. package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.ts +10 -6
  139. package/src/lib/editor/managers/InputsManager/InputsManager.ts +566 -0
  140. package/src/lib/editor/managers/ScribbleManager/ScribbleManager.test.ts +0 -4
  141. package/src/lib/editor/managers/SnapManager/SnapManager.test.ts +12 -0
  142. package/src/lib/editor/managers/SnapManager/SnapManager.ts +1 -1
  143. package/src/lib/editor/managers/TickManager/TickManager.test.ts +40 -107
  144. package/src/lib/editor/managers/TickManager/TickManager.ts +2 -32
  145. package/src/lib/editor/shapes/ShapeUtil.ts +67 -24
  146. package/src/lib/editor/shapes/group/DashedOutlineBox.tsx +1 -1
  147. package/src/lib/editor/tools/BaseBoxShapeTool/children/Pointing.ts +3 -3
  148. package/src/lib/exports/parseCss.test.ts +1 -0
  149. package/src/lib/exports/parseCss.ts +1 -1
  150. package/src/lib/globals/environment.ts +65 -10
  151. package/src/lib/hooks/useCoarsePointer.ts +16 -59
  152. package/src/lib/hooks/useEvent.tsx +1 -1
  153. package/src/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.ts +1 -1
  154. package/src/lib/hooks/useGestureEvents.ts +2 -2
  155. package/src/lib/hooks/usePassThroughMouseOverEvents.ts +1 -1
  156. package/src/lib/hooks/usePassThroughWheelEvents.ts +1 -1
  157. package/src/lib/hooks/useScreenBounds.ts +1 -1
  158. package/src/lib/hooks/useStateAttribute.ts +4 -1
  159. package/src/lib/hooks/useTransform.ts +1 -1
  160. package/src/lib/hooks/useZoomCss.ts +3 -8
  161. package/src/lib/options.ts +32 -0
  162. package/src/lib/primitives/Box.ts +9 -0
  163. package/src/lib/primitives/geometry/Geometry2d.ts +1 -0
  164. package/src/lib/utils/rotation.ts +1 -1
  165. package/src/version.ts +3 -3
@@ -2,29 +2,7 @@ import { computed, isUninitialized } from '@tldraw/state'
2
2
  import { TLShapeId } from '@tldraw/tlschema'
3
3
  import { Editor } from '../Editor'
4
4
 
5
- function fromScratch(editor: Editor): Set<TLShapeId> {
6
- const shapesIds = editor.getCurrentPageShapeIds()
7
- const viewportPageBounds = editor.getViewportPageBounds()
8
- const notVisibleShapes = new Set<TLShapeId>()
9
- shapesIds.forEach((id) => {
10
- const shape = editor.getShape(id)
11
- if (!shape) return
12
-
13
- const canCull = editor.getShapeUtil(shape.type).canCull(shape)
14
- if (!canCull) return
15
-
16
- // If the shape is fully outside of the viewport page bounds, add it to the set.
17
- // We'll ignore masks here, since they're more expensive to compute and the overhead is not worth it.
18
- const pageBounds = editor.getShapePageBounds(id)
19
- if (pageBounds === undefined || !viewportPageBounds.includes(pageBounds)) {
20
- notVisibleShapes.add(id)
21
- }
22
- })
23
- return notVisibleShapes
24
- }
25
-
26
5
  /**
27
- * Incremental derivation of not visible shapes.
28
6
  * Non visible shapes are shapes outside of the viewport page bounds.
29
7
  *
30
8
  * @param editor - Instance of the tldraw Editor.
@@ -32,7 +10,43 @@ function fromScratch(editor: Editor): Set<TLShapeId> {
32
10
  */
33
11
  export function notVisibleShapes(editor: Editor) {
34
12
  return computed<Set<TLShapeId>>('notVisibleShapes', function updateNotVisibleShapes(prevValue) {
35
- const nextValue = fromScratch(editor)
13
+ const shapeIds = editor.getCurrentPageShapeIds()
14
+ const nextValue = new Set<TLShapeId>()
15
+
16
+ // Extract viewport bounds once to avoid repeated property access
17
+ const viewportPageBounds = editor.getViewportPageBounds()
18
+ const viewMinX = viewportPageBounds.minX
19
+ const viewMinY = viewportPageBounds.minY
20
+ const viewMaxX = viewportPageBounds.maxX
21
+ const viewMaxY = viewportPageBounds.maxY
22
+
23
+ for (const id of shapeIds) {
24
+ const pageBounds = editor.getShapePageBounds(id)
25
+
26
+ // Hybrid check: if bounds exist and shape overlaps viewport, it's visible.
27
+ // This inlines Box.Collides to avoid function call overhead and the
28
+ // redundant Contains check that Box.Includes was doing.
29
+ if (
30
+ pageBounds !== undefined &&
31
+ pageBounds.maxX >= viewMinX &&
32
+ pageBounds.minX <= viewMaxX &&
33
+ pageBounds.maxY >= viewMinY &&
34
+ pageBounds.minY <= viewMaxY
35
+ ) {
36
+ continue
37
+ }
38
+
39
+ // Shape is outside viewport or has no bounds - check if it can be culled.
40
+ // We defer getShape and canCull checks until here since most shapes are
41
+ // typically visible and we can skip these calls for them.
42
+ const shape = editor.getShape(id)
43
+ if (!shape) continue
44
+
45
+ const canCull = editor.getShapeUtil(shape.type).canCull(shape)
46
+ if (!canCull) continue
47
+
48
+ nextValue.add(id)
49
+ }
36
50
 
37
51
  if (isUninitialized(prevValue)) {
38
52
  return nextValue
@@ -1,7 +1,7 @@
1
1
  import { Computed, computed, isUninitialized, RESET_VALUE } from '@tldraw/state'
2
2
  import { CollectionDiff, RecordsDiff } from '@tldraw/store'
3
- import { isShape, TLParentId, TLRecord, TLShapeId, TLStore } from '@tldraw/tlschema'
4
- import { compact, sortByIndex } from '@tldraw/utils'
3
+ import { isShape, TLParentId, TLRecord, TLShape, TLShapeId, TLStore } from '@tldraw/tlschema'
4
+ import { sortByIndex } from '@tldraw/utils'
5
5
 
6
6
  type ParentShapeIdsToChildShapeIds = Record<TLParentId, TLShapeId[]>
7
7
 
@@ -98,12 +98,23 @@ export const parentsToChildren = (store: TLStore) => {
98
98
  }
99
99
  }
100
100
 
101
- // Sort the arrays that have been marked for sorting
101
+ // Sort the arrays that have been marked for sorting (in-place to avoid intermediate arrays)
102
102
  for (const arr of toSort) {
103
- // It's possible that some of the shapes may be deleted. But in which case would this be so?
104
- const shapesInArr = compact(arr.map((id) => store.get(id)))
105
- shapesInArr.sort(sortByIndex)
106
- arr.splice(0, arr.length, ...shapesInArr.map((shape) => shape.id))
103
+ // Filter out any deleted shapes in-place
104
+ let writeIdx = 0
105
+ for (let readIdx = 0; readIdx < arr.length; readIdx++) {
106
+ if (store.get(arr[readIdx])) {
107
+ arr[writeIdx++] = arr[readIdx]
108
+ }
109
+ }
110
+ arr.length = writeIdx
111
+
112
+ // Sort in-place by index
113
+ arr.sort((a, b) => {
114
+ const shapeA = store.get(a) as TLShape
115
+ const shapeB = store.get(b) as TLShape
116
+ return sortByIndex(shapeA, shapeB)
117
+ })
107
118
  }
108
119
 
109
120
  return newValue ?? lastValue
@@ -1,4 +1,5 @@
1
1
  import { Mocked, vi } from 'vitest'
2
+ import { Vec } from '../../../primitives/Vec'
2
3
  import { Editor } from '../../Editor'
3
4
  import { TLClickEventInfo, TLPointerEventInfo } from '../../types/event-types'
4
5
  import { ClickManager } from './ClickManager'
@@ -35,6 +36,8 @@ describe('ClickManager', () => {
35
36
  setTimeout: vi.fn((fn, delay) => setTimeout(fn, delay)),
36
37
  }
37
38
 
39
+ const mockCurrentScreenPoint = new Vec(0, 0)
40
+
38
41
  editor = {
39
42
  timers: mockTimers,
40
43
  dispatch: vi.fn(),
@@ -45,7 +48,8 @@ describe('ClickManager', () => {
45
48
  coarseDragDistanceSquared: 36,
46
49
  },
47
50
  inputs: {
48
- currentScreenPoint: { x: 0, y: 0 },
51
+ getCurrentScreenPoint: vi.fn(() => mockCurrentScreenPoint),
52
+ currentScreenPoint: mockCurrentScreenPoint, // deprecated getter for compatibility
49
53
  },
50
54
  getInstanceState: vi.fn(() => ({
51
55
  isCoarsePointer: false,
@@ -321,8 +325,9 @@ describe('ClickManager', () => {
321
325
  const downEvent = createPointerEvent('pointer_down', { x: 0, y: 0 })
322
326
  const moveEvent = createPointerEvent('pointer_move', { x: 10, y: 10 })
323
327
 
324
- editor.inputs.currentScreenPoint.x = 10
325
- editor.inputs.currentScreenPoint.y = 10
328
+ const currentScreenPoint = editor.inputs.getCurrentScreenPoint()
329
+ currentScreenPoint.x = 10
330
+ currentScreenPoint.y = 10
326
331
 
327
332
  clickManager.handlePointerEvent(downEvent)
328
333
  expect(clickManager.clickState).toBe('pendingDouble')
@@ -347,13 +352,15 @@ describe('ClickManager', () => {
347
352
  expect(clickManager.clickState).toBe('pendingDouble')
348
353
 
349
354
  // Should not cancel for coarse pointer with small movement
350
- editor.inputs.currentScreenPoint.x = 1
351
- editor.inputs.currentScreenPoint.y = 1
355
+ const currentScreenPoint1 = editor.inputs.getCurrentScreenPoint()
356
+ currentScreenPoint1.x = 1
357
+ currentScreenPoint1.y = 1
352
358
  clickManager.handlePointerEvent(moveEvent1)
353
359
  expect(clickManager.clickState).toBe('pendingDouble')
354
360
 
355
- editor.inputs.currentScreenPoint.x = 5
356
- editor.inputs.currentScreenPoint.y = 5
361
+ const currentScreenPoint2 = editor.inputs.getCurrentScreenPoint()
362
+ currentScreenPoint2.x = 5
363
+ currentScreenPoint2.y = 5
357
364
  clickManager.handlePointerEvent(moveEvent2)
358
365
 
359
366
  expect(clickManager.clickState).toBe('idle')
@@ -362,8 +369,9 @@ describe('ClickManager', () => {
362
369
  it('should not cancel in idle state', () => {
363
370
  const moveEvent = createPointerEvent('pointer_move', { x: 100, y: 100 })
364
371
 
365
- editor.inputs.currentScreenPoint.x = 100
366
- editor.inputs.currentScreenPoint.y = 100
372
+ const currentScreenPoint = editor.inputs.getCurrentScreenPoint()
373
+ currentScreenPoint.x = 100
374
+ currentScreenPoint.y = 100
367
375
 
368
376
  clickManager.handlePointerEvent(moveEvent)
369
377
 
@@ -401,28 +409,6 @@ describe('ClickManager', () => {
401
409
  })
402
410
 
403
411
  describe('edge cases', () => {
404
- it('should handle null click state gracefully', () => {
405
- // Force null state
406
- ;(clickManager as any)._clickState = null
407
-
408
- const pointerEvent = createPointerEvent('pointer_down', { x: 100, y: 100 })
409
- const result = clickManager.handlePointerEvent(pointerEvent)
410
-
411
- expect(result).toBe(pointerEvent)
412
- })
413
-
414
- it('should handle missing previous screen point', () => {
415
- const firstDown = createPointerEvent('pointer_down', { x: 0, y: 0 })
416
-
417
- // Clear previous point
418
- ;(clickManager as any)._previousScreenPoint = undefined
419
-
420
- const result = clickManager.handlePointerEvent(firstDown)
421
-
422
- expect(result).toBe(firstDown)
423
- expect(clickManager.clickState).toBe('pendingDouble')
424
- })
425
-
426
412
  it('should handle overflow state correctly', () => {
427
413
  const pointerDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
428
414
  const pointerUp = createPointerEvent('pointer_up', { x: 100, y: 100 })
@@ -197,7 +197,7 @@ export class ClickManager {
197
197
  if (
198
198
  this._clickState !== 'idle' &&
199
199
  this._clickScreenPoint &&
200
- Vec.Dist2(this._clickScreenPoint, this.editor.inputs.currentScreenPoint) >
200
+ Vec.Dist2(this._clickScreenPoint, this.editor.inputs.getCurrentScreenPoint()) >
201
201
  (this.editor.getInstanceState().isCoarsePointer
202
202
  ? this.editor.options.coarseDragDistanceSquared
203
203
  : this.editor.options.dragDistanceSquared)
@@ -18,8 +18,56 @@ describe('EdgeScrollManager', () => {
18
18
  }
19
19
  >
20
20
  let edgeScrollManager: EdgeScrollManager
21
+ let mockInputs: {
22
+ _currentScreenPoint: Vec
23
+ currentScreenPoint: Vec
24
+ getCurrentScreenPoint(): Vec
25
+ setCurrentScreenPoint(value: Vec): void
26
+ _isDragging: boolean
27
+ isDragging: boolean
28
+ getIsDragging(): boolean
29
+ setIsDragging(value: boolean): void
30
+ _isPanning: boolean
31
+ isPanning: boolean
32
+ getIsPanning(): boolean
33
+ setIsPanning(value: boolean): void
34
+ }
21
35
 
22
36
  beforeEach(() => {
37
+ // Create a mock inputs object with writable properties and getters
38
+ mockInputs = {
39
+ _currentScreenPoint: new Vec(500, 300),
40
+ get currentScreenPoint() {
41
+ return this._currentScreenPoint
42
+ },
43
+ getCurrentScreenPoint() {
44
+ return this._currentScreenPoint
45
+ },
46
+ setCurrentScreenPoint(value: Vec) {
47
+ this._currentScreenPoint = value
48
+ },
49
+ _isDragging: true,
50
+ get isDragging() {
51
+ return this._isDragging
52
+ },
53
+ getIsDragging() {
54
+ return this._isDragging
55
+ },
56
+ setIsDragging(value: boolean) {
57
+ this._isDragging = value
58
+ },
59
+ _isPanning: false,
60
+ get isPanning() {
61
+ return this._isPanning
62
+ },
63
+ getIsPanning() {
64
+ return this._isPanning
65
+ },
66
+ setIsPanning(value: boolean) {
67
+ this._isPanning = value
68
+ },
69
+ }
70
+
23
71
  editor = {
24
72
  options: {
25
73
  edgeScrollDelay: 200,
@@ -28,22 +76,15 @@ describe('EdgeScrollManager', () => {
28
76
  edgeScrollDistance: 8,
29
77
  coarsePointerWidth: 12,
30
78
  },
31
- inputs: {
32
- currentScreenPoint: new Vec(500, 300),
33
- isDragging: true,
34
- isPanning: false,
35
- },
79
+ inputs: mockInputs as unknown as Editor['inputs'],
36
80
  user: {
37
81
  getEdgeScrollSpeed: vi.fn(() => 1),
38
82
  },
39
83
  getViewportScreenBounds: vi.fn(() => new Box(0, 0, 1000, 600)),
40
- getInstanceState: vi.fn(
41
- () =>
42
- ({
43
- isCoarsePointer: false,
44
- insets: [false, false, false, false], // [top, right, bottom, left]
45
- }) as any
46
- ),
84
+ getInstanceState: vi.fn(() => ({
85
+ isCoarsePointer: false,
86
+ insets: [false, false, false, false], // [top, right, bottom, left]
87
+ })),
47
88
  getCameraOptions: vi.fn(() => ({
48
89
  isLocked: false,
49
90
  panSpeed: 1,
@@ -54,9 +95,17 @@ describe('EdgeScrollManager', () => {
54
95
  getZoomLevel: vi.fn(() => 1),
55
96
  getCamera: vi.fn(() => new Vec(0, 0, 1)),
56
97
  setCamera: vi.fn(),
57
- } as any
98
+ } as unknown as Mocked<
99
+ Editor & {
100
+ user: { getEdgeScrollSpeed: Mock }
101
+ getCamera: Mock
102
+ getCameraOptions: Mock
103
+ getZoomLevel: Mock
104
+ getViewportScreenBounds: Mock
105
+ }
106
+ >
58
107
 
59
- edgeScrollManager = new EdgeScrollManager(editor as any)
108
+ edgeScrollManager = new EdgeScrollManager(editor)
60
109
  })
61
110
 
62
111
  afterEach(() => {
@@ -67,49 +116,45 @@ describe('EdgeScrollManager', () => {
67
116
  it('should initialize with editor reference', () => {
68
117
  expect(edgeScrollManager.editor).toBe(editor)
69
118
  })
70
-
71
- it('should initialize edge scrolling state as false', () => {
72
- // Access private properties for testing
73
- expect((edgeScrollManager as any)._isEdgeScrolling).toBe(false)
74
- expect((edgeScrollManager as any)._edgeScrollDuration).toBe(-1)
75
- })
76
119
  })
77
120
 
78
121
  describe('basic edge scrolling behavior', () => {
79
122
  it('should not trigger edge scrolling when pointer is in center', () => {
80
- editor.inputs.currentScreenPoint = new Vec(500, 300)
123
+ mockInputs.setCurrentScreenPoint(new Vec(500, 300))
81
124
 
82
125
  edgeScrollManager.updateEdgeScrolling(16)
83
126
 
84
- expect((edgeScrollManager as any)._isEdgeScrolling).toBe(false)
85
127
  expect(editor.setCamera).not.toHaveBeenCalled()
86
128
  })
87
129
 
88
130
  it('should start edge scrolling when pointer is near edge', () => {
89
- editor.inputs.currentScreenPoint = new Vec(5, 300)
131
+ mockInputs.setCurrentScreenPoint(new Vec(5, 300))
90
132
 
133
+ // Should not scroll immediately due to delay
91
134
  edgeScrollManager.updateEdgeScrolling(16)
135
+ expect(editor.setCamera).not.toHaveBeenCalled()
92
136
 
93
- expect((edgeScrollManager as any)._isEdgeScrolling).toBe(true)
94
- expect((edgeScrollManager as any)._edgeScrollDuration).toBe(16)
137
+ // Should scroll after delay
138
+ edgeScrollManager.updateEdgeScrolling(200)
139
+ expect(editor.setCamera).toHaveBeenCalled()
95
140
  })
96
141
 
97
142
  it('should stop edge scrolling when pointer moves away from edge', () => {
98
- // Start edge scrolling
99
- editor.inputs.currentScreenPoint = new Vec(5, 300)
100
- edgeScrollManager.updateEdgeScrolling(16)
101
- expect((edgeScrollManager as any)._isEdgeScrolling).toBe(true)
143
+ // Start edge scrolling near edge
144
+ mockInputs.setCurrentScreenPoint(new Vec(5, 300))
145
+ edgeScrollManager.updateEdgeScrolling(300)
146
+ expect(editor.setCamera).toHaveBeenCalled()
102
147
 
103
- // Move pointer to center
104
- editor.inputs.currentScreenPoint = new Vec(500, 300)
148
+ // Move pointer to center - should stop scrolling
149
+ editor.setCamera.mockClear()
150
+ mockInputs.setCurrentScreenPoint(new Vec(500, 300))
105
151
  edgeScrollManager.updateEdgeScrolling(16)
106
152
 
107
- expect((edgeScrollManager as any)._isEdgeScrolling).toBe(false)
108
- expect((edgeScrollManager as any)._edgeScrollDuration).toBe(0)
153
+ expect(editor.setCamera).not.toHaveBeenCalled()
109
154
  })
110
155
 
111
156
  it('should respect edge scroll delay', () => {
112
- editor.inputs.currentScreenPoint = new Vec(5, 300)
157
+ mockInputs.setCurrentScreenPoint(new Vec(5, 300))
113
158
 
114
159
  // First update - should not scroll yet due to delay
115
160
  edgeScrollManager.updateEdgeScrolling(100)
@@ -123,7 +168,7 @@ describe('EdgeScrollManager', () => {
123
168
 
124
169
  describe('edge proximity detection', () => {
125
170
  it('should detect left edge proximity', () => {
126
- editor.inputs.currentScreenPoint = new Vec(5, 300)
171
+ mockInputs.setCurrentScreenPoint(new Vec(5, 300))
127
172
  edgeScrollManager.updateEdgeScrolling(300) // Enough to trigger after delay
128
173
 
129
174
  expect(editor.setCamera).toHaveBeenCalled()
@@ -132,7 +177,7 @@ describe('EdgeScrollManager', () => {
132
177
  })
133
178
 
134
179
  it('should detect right edge proximity', () => {
135
- editor.inputs.currentScreenPoint = new Vec(995, 300)
180
+ mockInputs.setCurrentScreenPoint(new Vec(995, 300))
136
181
  edgeScrollManager.updateEdgeScrolling(300)
137
182
 
138
183
  expect(editor.setCamera).toHaveBeenCalled()
@@ -141,7 +186,7 @@ describe('EdgeScrollManager', () => {
141
186
  })
142
187
 
143
188
  it('should detect top edge proximity', () => {
144
- editor.inputs.currentScreenPoint = new Vec(500, 5)
189
+ mockInputs.setCurrentScreenPoint(new Vec(500, 5))
145
190
  edgeScrollManager.updateEdgeScrolling(300)
146
191
 
147
192
  expect(editor.setCamera).toHaveBeenCalled()
@@ -150,7 +195,7 @@ describe('EdgeScrollManager', () => {
150
195
  })
151
196
 
152
197
  it('should detect bottom edge proximity', () => {
153
- editor.inputs.currentScreenPoint = new Vec(500, 595)
198
+ mockInputs.setCurrentScreenPoint(new Vec(500, 595))
154
199
  edgeScrollManager.updateEdgeScrolling(300)
155
200
 
156
201
  expect(editor.setCamera).toHaveBeenCalled()
@@ -159,7 +204,7 @@ describe('EdgeScrollManager', () => {
159
204
  })
160
205
 
161
206
  it('should handle corner proximity (both x and y)', () => {
162
- editor.inputs.currentScreenPoint = new Vec(5, 5)
207
+ mockInputs.setCurrentScreenPoint(new Vec(5, 5))
163
208
  edgeScrollManager.updateEdgeScrolling(300)
164
209
 
165
210
  expect(editor.setCamera).toHaveBeenCalled()
@@ -172,11 +217,11 @@ describe('EdgeScrollManager', () => {
172
217
  describe('coarse pointer handling', () => {
173
218
  it('should account for coarse pointer width', () => {
174
219
  editor.getInstanceState.mockReturnValue({
220
+ ...editor.getInstanceState(),
175
221
  isCoarsePointer: true,
176
222
  insets: [false, false, false, false],
177
- } as any)
178
-
179
- editor.inputs.currentScreenPoint = new Vec(15, 300)
223
+ })
224
+ mockInputs.setCurrentScreenPoint(new Vec(15, 300))
180
225
  edgeScrollManager.updateEdgeScrolling(300)
181
226
 
182
227
  expect(editor.setCamera).toHaveBeenCalled()
@@ -184,11 +229,11 @@ describe('EdgeScrollManager', () => {
184
229
 
185
230
  it('should not trigger edge scrolling for fine pointer at same position', () => {
186
231
  editor.getInstanceState.mockReturnValue({
232
+ ...editor.getInstanceState(),
187
233
  isCoarsePointer: false,
188
234
  insets: [false, false, false, false],
189
- } as any)
190
-
191
- editor.inputs.currentScreenPoint = new Vec(15, 300)
235
+ })
236
+ mockInputs.setCurrentScreenPoint(new Vec(15, 300))
192
237
  edgeScrollManager.updateEdgeScrolling(300)
193
238
 
194
239
  expect(editor.setCamera).not.toHaveBeenCalled()
@@ -197,8 +242,8 @@ describe('EdgeScrollManager', () => {
197
242
 
198
243
  describe('camera movement conditions', () => {
199
244
  it('should not move camera when not dragging', () => {
200
- editor.inputs.isDragging = false
201
- editor.inputs.currentScreenPoint = new Vec(5, 300)
245
+ editor.inputs.setIsDragging(false)
246
+ mockInputs.setCurrentScreenPoint(new Vec(5, 300))
202
247
 
203
248
  edgeScrollManager.updateEdgeScrolling(300)
204
249
 
@@ -206,8 +251,8 @@ describe('EdgeScrollManager', () => {
206
251
  })
207
252
 
208
253
  it('should not move camera when panning', () => {
209
- editor.inputs.isPanning = true
210
- editor.inputs.currentScreenPoint = new Vec(5, 300)
254
+ editor.inputs.setIsPanning(true)
255
+ mockInputs.setCurrentScreenPoint(new Vec(5, 300))
211
256
 
212
257
  edgeScrollManager.updateEdgeScrolling(300)
213
258
 
@@ -222,7 +267,7 @@ describe('EdgeScrollManager', () => {
222
267
  zoomSteps: [1],
223
268
  wheelBehavior: 'pan' as const,
224
269
  })
225
- editor.inputs.currentScreenPoint = new Vec(5, 300)
270
+ mockInputs.setCurrentScreenPoint(new Vec(5, 300))
226
271
 
227
272
  edgeScrollManager.updateEdgeScrolling(300)
228
273
 
@@ -233,7 +278,7 @@ describe('EdgeScrollManager', () => {
233
278
  describe('camera movement calculation', () => {
234
279
  it('should calculate scroll speed based on user preference', () => {
235
280
  editor.user.getEdgeScrollSpeed.mockReturnValue(2)
236
- editor.inputs.currentScreenPoint = new Vec(5, 300)
281
+ mockInputs.setCurrentScreenPoint(new Vec(5, 300))
237
282
 
238
283
  edgeScrollManager.updateEdgeScrolling(300)
239
284
 
@@ -244,7 +289,7 @@ describe('EdgeScrollManager', () => {
244
289
 
245
290
  it('should apply screen size factor for small screens', () => {
246
291
  editor.getViewportScreenBounds.mockReturnValue(new Box(0, 0, 800, 600))
247
- editor.inputs.currentScreenPoint = new Vec(5, 300)
292
+ mockInputs.setCurrentScreenPoint(new Vec(5, 300))
248
293
 
249
294
  edgeScrollManager.updateEdgeScrolling(300)
250
295
 
@@ -253,7 +298,7 @@ describe('EdgeScrollManager', () => {
253
298
 
254
299
  it('should adjust scroll speed based on zoom level', () => {
255
300
  editor.getZoomLevel.mockReturnValue(2)
256
- editor.inputs.currentScreenPoint = new Vec(5, 300)
301
+ mockInputs.setCurrentScreenPoint(new Vec(5, 300))
257
302
 
258
303
  edgeScrollManager.updateEdgeScrolling(300)
259
304
 
@@ -266,7 +311,7 @@ describe('EdgeScrollManager', () => {
266
311
  it('should add scroll delta to current camera position', () => {
267
312
  const currentCamera = new Vec(100, 200, 1)
268
313
  editor.getCamera.mockReturnValue(currentCamera)
269
- editor.inputs.currentScreenPoint = new Vec(5, 5)
314
+ mockInputs.setCurrentScreenPoint(new Vec(5, 5))
270
315
 
271
316
  edgeScrollManager.updateEdgeScrolling(300)
272
317
 
@@ -280,14 +325,14 @@ describe('EdgeScrollManager', () => {
280
325
 
281
326
  describe('proximity factor calculation', () => {
282
327
  it('should return 0 when not near any edge', () => {
283
- editor.inputs.currentScreenPoint = new Vec(500, 300)
328
+ mockInputs.setCurrentScreenPoint(new Vec(500, 300))
284
329
  edgeScrollManager.updateEdgeScrolling(16)
285
330
 
286
331
  expect(editor.setCamera).not.toHaveBeenCalled()
287
332
  })
288
333
 
289
334
  it('should cap proximity factor at 1', () => {
290
- editor.inputs.currentScreenPoint = new Vec(0, 300)
335
+ mockInputs.setCurrentScreenPoint(new Vec(0, 300))
291
336
  edgeScrollManager.updateEdgeScrolling(300)
292
337
 
293
338
  expect(editor.setCamera).toHaveBeenCalled()
@@ -297,20 +342,20 @@ describe('EdgeScrollManager', () => {
297
342
 
298
343
  describe('edge cases and error handling', () => {
299
344
  it('should handle negative elapsed time', () => {
300
- editor.inputs.currentScreenPoint = new Vec(5, 300)
345
+ mockInputs.setCurrentScreenPoint(new Vec(5, 300))
301
346
 
302
347
  expect(() => edgeScrollManager.updateEdgeScrolling(-16)).not.toThrow()
303
348
  })
304
349
 
305
350
  it('should handle very large elapsed time', () => {
306
- editor.inputs.currentScreenPoint = new Vec(5, 300)
351
+ mockInputs.setCurrentScreenPoint(new Vec(5, 300))
307
352
 
308
353
  expect(() => edgeScrollManager.updateEdgeScrolling(100000)).not.toThrow()
309
354
  })
310
355
 
311
356
  it('should handle zero user edge scroll speed', () => {
312
357
  editor.user.getEdgeScrollSpeed.mockReturnValue(0)
313
- editor.inputs.currentScreenPoint = new Vec(5, 300)
358
+ mockInputs.setCurrentScreenPoint(new Vec(5, 300))
314
359
 
315
360
  edgeScrollManager.updateEdgeScrolling(300)
316
361
 
@@ -322,7 +367,7 @@ describe('EdgeScrollManager', () => {
322
367
  })
323
368
 
324
369
  it('should handle extreme zoom levels', () => {
325
- editor.inputs.currentScreenPoint = new Vec(5, 300)
370
+ mockInputs.setCurrentScreenPoint(new Vec(5, 300))
326
371
 
327
372
  editor.getZoomLevel.mockReturnValue(0.01) // Very zoomed out
328
373
  expect(() => edgeScrollManager.updateEdgeScrolling(300)).not.toThrow()
@@ -335,41 +380,46 @@ describe('EdgeScrollManager', () => {
335
380
  describe('state transitions', () => {
336
381
  it('should properly transition from not scrolling to scrolling', () => {
337
382
  // Start with no edge scrolling
338
- editor.inputs.currentScreenPoint = new Vec(500, 300)
383
+ mockInputs.setCurrentScreenPoint(new Vec(500, 300))
339
384
  edgeScrollManager.updateEdgeScrolling(16)
340
- expect((edgeScrollManager as any)._isEdgeScrolling).toBe(false)
385
+ expect(editor.setCamera).not.toHaveBeenCalled()
341
386
 
342
- // Move to edge
343
- editor.inputs.currentScreenPoint = new Vec(5, 300)
387
+ // Move to edge - should start scrolling after delay
388
+ mockInputs.setCurrentScreenPoint(new Vec(5, 300))
344
389
  edgeScrollManager.updateEdgeScrolling(16)
345
- expect((edgeScrollManager as any)._isEdgeScrolling).toBe(true)
346
- expect((edgeScrollManager as any)._edgeScrollDuration).toBe(16)
390
+ expect(editor.setCamera).not.toHaveBeenCalled() // Not yet, due to delay
391
+
392
+ edgeScrollManager.updateEdgeScrolling(200)
393
+ expect(editor.setCamera).toHaveBeenCalled()
347
394
  })
348
395
 
349
396
  it('should accumulate edge scroll duration over multiple updates', () => {
350
- editor.inputs.currentScreenPoint = new Vec(5, 300)
397
+ mockInputs.setCurrentScreenPoint(new Vec(5, 300))
351
398
 
399
+ // First update - not enough time
352
400
  edgeScrollManager.updateEdgeScrolling(50)
353
- expect((edgeScrollManager as any)._edgeScrollDuration).toBe(50)
401
+ expect(editor.setCamera).not.toHaveBeenCalled()
354
402
 
355
- edgeScrollManager.updateEdgeScrolling(30)
356
- expect((edgeScrollManager as any)._edgeScrollDuration).toBe(80)
403
+ // Second update - still not enough
404
+ edgeScrollManager.updateEdgeScrolling(50)
405
+ expect(editor.setCamera).not.toHaveBeenCalled()
357
406
 
358
- edgeScrollManager.updateEdgeScrolling(25)
359
- expect((edgeScrollManager as any)._edgeScrollDuration).toBe(105)
407
+ // Third update - now should trigger (50 + 50 + 101 = 201ms > 200ms delay)
408
+ edgeScrollManager.updateEdgeScrolling(101)
409
+ expect(editor.setCamera).toHaveBeenCalled()
360
410
  })
361
411
 
362
412
  it('should reset duration when stopping edge scroll', () => {
363
413
  // Start edge scrolling
364
- editor.inputs.currentScreenPoint = new Vec(5, 300)
365
- edgeScrollManager.updateEdgeScrolling(100)
366
- expect((edgeScrollManager as any)._edgeScrollDuration).toBe(100)
414
+ mockInputs.setCurrentScreenPoint(new Vec(5, 300))
415
+ edgeScrollManager.updateEdgeScrolling(300)
416
+ expect(editor.setCamera).toHaveBeenCalled()
367
417
 
368
- // Stop edge scrolling
369
- editor.inputs.currentScreenPoint = new Vec(500, 300)
418
+ // Stop edge scrolling - move away
419
+ editor.setCamera.mockClear()
420
+ mockInputs.setCurrentScreenPoint(new Vec(500, 300))
370
421
  edgeScrollManager.updateEdgeScrolling(16)
371
- expect((edgeScrollManager as any)._isEdgeScrolling).toBe(false)
372
- expect((edgeScrollManager as any)._edgeScrollDuration).toBe(0)
422
+ expect(editor.setCamera).not.toHaveBeenCalled()
373
423
  })
374
424
  })
375
425
  })
@@ -9,6 +9,10 @@ export class EdgeScrollManager {
9
9
  private _isEdgeScrolling = false
10
10
  private _edgeScrollDuration = -1
11
11
 
12
+ getIsEdgeScrolling() {
13
+ return this._isEdgeScrolling
14
+ }
15
+
12
16
  /**
13
17
  * Update the camera position when the mouse is close to the edge of the screen.
14
18
  * Run this on every tick when in a state where edge scrolling is enabled.
@@ -81,11 +85,7 @@ export class EdgeScrollManager {
81
85
 
82
86
  private getEdgeScroll() {
83
87
  const { editor } = this
84
- const {
85
- inputs: {
86
- currentScreenPoint: { x, y },
87
- },
88
- } = editor
88
+ const { x, y } = editor.inputs.getCurrentScreenPoint()
89
89
  const screenBounds = editor.getViewportScreenBounds()
90
90
 
91
91
  const {
@@ -107,7 +107,11 @@ export class EdgeScrollManager {
107
107
  */
108
108
  private moveCameraWhenCloseToEdge(proximityFactor: { x: number; y: number }) {
109
109
  const { editor } = this
110
- if (!editor.inputs.isDragging || editor.inputs.isPanning || editor.getCameraOptions().isLocked)
110
+ if (
111
+ !editor.inputs.getIsDragging() ||
112
+ editor.inputs.getIsPanning() ||
113
+ editor.getCameraOptions().isLocked
114
+ )
111
115
  return
112
116
 
113
117
  if (proximityFactor.x === 0 && proximityFactor.y === 0) return