@tldraw/editor 3.12.0-canary.e333011facbe → 3.12.0-canary.e45d65cbc328

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 (91) hide show
  1. package/dist-cjs/index.d.ts +113 -15
  2. package/dist-cjs/index.js +3 -1
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/TldrawEditor.js +1 -0
  5. package/dist-cjs/lib/TldrawEditor.js.map +2 -2
  6. package/dist-cjs/lib/components/GeometryDebuggingView.js +2 -2
  7. package/dist-cjs/lib/components/GeometryDebuggingView.js.map +2 -2
  8. package/dist-cjs/lib/components/default-components/DefaultCanvas.js +10 -1
  9. package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
  10. package/dist-cjs/lib/editor/Editor.js +184 -10
  11. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  12. package/dist-cjs/lib/editor/managers/FocusManager.js +1 -1
  13. package/dist-cjs/lib/editor/managers/FocusManager.js.map +2 -2
  14. package/dist-cjs/lib/editor/shapes/ShapeUtil.js +12 -0
  15. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
  16. package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js +4 -13
  17. package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js.map +2 -2
  18. package/dist-cjs/lib/editor/tools/StateNode.js +1 -4
  19. package/dist-cjs/lib/editor/tools/StateNode.js.map +2 -2
  20. package/dist-cjs/lib/editor/types/selection-types.js.map +1 -1
  21. package/dist-cjs/lib/hooks/useCanvasEvents.js +8 -13
  22. package/dist-cjs/lib/hooks/useCanvasEvents.js.map +3 -3
  23. package/dist-cjs/lib/hooks/useDocumentEvents.js +16 -0
  24. package/dist-cjs/lib/hooks/useDocumentEvents.js.map +2 -2
  25. package/dist-cjs/lib/license/Watermark.js +10 -20
  26. package/dist-cjs/lib/license/Watermark.js.map +2 -2
  27. package/dist-cjs/lib/primitives/geometry/Geometry2d.js +133 -16
  28. package/dist-cjs/lib/primitives/geometry/Geometry2d.js.map +3 -3
  29. package/dist-cjs/lib/primitives/geometry/Group2d.js +54 -11
  30. package/dist-cjs/lib/primitives/geometry/Group2d.js.map +2 -2
  31. package/dist-cjs/lib/primitives/intersect.js +20 -0
  32. package/dist-cjs/lib/primitives/intersect.js.map +2 -2
  33. package/dist-cjs/lib/utils/reorderShapes.js +2 -8
  34. package/dist-cjs/lib/utils/reorderShapes.js.map +2 -2
  35. package/dist-cjs/version.js +3 -3
  36. package/dist-cjs/version.js.map +1 -1
  37. package/dist-esm/index.d.mts +113 -15
  38. package/dist-esm/index.mjs +8 -2
  39. package/dist-esm/index.mjs.map +2 -2
  40. package/dist-esm/lib/TldrawEditor.mjs +1 -0
  41. package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
  42. package/dist-esm/lib/components/GeometryDebuggingView.mjs +3 -3
  43. package/dist-esm/lib/components/GeometryDebuggingView.mjs.map +2 -2
  44. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +10 -1
  45. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
  46. package/dist-esm/lib/editor/Editor.mjs +185 -10
  47. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  48. package/dist-esm/lib/editor/managers/FocusManager.mjs +1 -1
  49. package/dist-esm/lib/editor/managers/FocusManager.mjs.map +2 -2
  50. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +12 -0
  51. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
  52. package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs +4 -13
  53. package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs.map +2 -2
  54. package/dist-esm/lib/editor/tools/StateNode.mjs +1 -4
  55. package/dist-esm/lib/editor/tools/StateNode.mjs.map +2 -2
  56. package/dist-esm/lib/hooks/useCanvasEvents.mjs +8 -13
  57. package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +3 -3
  58. package/dist-esm/lib/hooks/useDocumentEvents.mjs +16 -0
  59. package/dist-esm/lib/hooks/useDocumentEvents.mjs.map +2 -2
  60. package/dist-esm/lib/license/Watermark.mjs +10 -20
  61. package/dist-esm/lib/license/Watermark.mjs.map +2 -2
  62. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs +137 -14
  63. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs.map +2 -2
  64. package/dist-esm/lib/primitives/geometry/Group2d.mjs +55 -12
  65. package/dist-esm/lib/primitives/geometry/Group2d.mjs.map +2 -2
  66. package/dist-esm/lib/primitives/intersect.mjs +20 -0
  67. package/dist-esm/lib/primitives/intersect.mjs.map +2 -2
  68. package/dist-esm/lib/utils/reorderShapes.mjs +2 -8
  69. package/dist-esm/lib/utils/reorderShapes.mjs.map +2 -2
  70. package/dist-esm/version.mjs +3 -3
  71. package/dist-esm/version.mjs.map +1 -1
  72. package/editor.css +34 -19
  73. package/package.json +7 -7
  74. package/src/index.ts +11 -2
  75. package/src/lib/TldrawEditor.tsx +1 -0
  76. package/src/lib/components/GeometryDebuggingView.tsx +3 -3
  77. package/src/lib/components/default-components/DefaultCanvas.tsx +6 -1
  78. package/src/lib/editor/Editor.ts +263 -16
  79. package/src/lib/editor/managers/FocusManager.ts +1 -1
  80. package/src/lib/editor/shapes/ShapeUtil.ts +14 -0
  81. package/src/lib/editor/shapes/group/GroupShapeUtil.tsx +7 -15
  82. package/src/lib/editor/tools/StateNode.ts +1 -6
  83. package/src/lib/editor/types/selection-types.ts +3 -0
  84. package/src/lib/hooks/useCanvasEvents.ts +8 -15
  85. package/src/lib/hooks/useDocumentEvents.ts +18 -0
  86. package/src/lib/license/Watermark.tsx +18 -29
  87. package/src/lib/primitives/geometry/Geometry2d.ts +196 -16
  88. package/src/lib/primitives/geometry/Group2d.ts +76 -13
  89. package/src/lib/primitives/intersect.ts +41 -0
  90. package/src/lib/utils/reorderShapes.ts +2 -9
  91. package/src/version.ts +3 -3
@@ -87,6 +87,7 @@ import {
87
87
  last,
88
88
  lerp,
89
89
  maxBy,
90
+ minBy,
90
91
  sortById,
91
92
  sortByIndex,
92
93
  structuredClone,
@@ -176,7 +177,7 @@ import {
176
177
  TLImageExportOptions,
177
178
  TLSvgExportOptions,
178
179
  } from './types/misc-types'
179
- import { TLResizeHandle } from './types/selection-types'
180
+ import { TLAdjacentDirection, TLResizeHandle } from './types/selection-types'
180
181
 
181
182
  /** @public */
182
183
  export type TLResizeShapeOptions = Partial<{
@@ -1813,6 +1814,195 @@ export class Editor extends EventEmitter<TLEventMap> {
1813
1814
  return this
1814
1815
  }
1815
1816
 
1817
+ selectAdjacentShape(direction: TLAdjacentDirection) {
1818
+ const readingOrderShapes = this.getCurrentPageShapesInReadingOrder()
1819
+ const selectedShapeIds = this.getSelectedShapeIds()
1820
+ const currentShapeId: TLShapeId | undefined =
1821
+ selectedShapeIds.length === 1
1822
+ ? selectedShapeIds[0]
1823
+ : readingOrderShapes.find((shape) => selectedShapeIds.includes(shape.id))?.id
1824
+
1825
+ let adjacentShapeId: TLShapeId
1826
+ if (direction === 'next' || direction === 'prev') {
1827
+ const shapeIds = readingOrderShapes.map((shape) => shape.id)
1828
+
1829
+ const currentIndex = currentShapeId ? shapeIds.indexOf(currentShapeId) : -1
1830
+ const adjacentIndex =
1831
+ (currentIndex + (direction === 'next' ? 1 : -1) + shapeIds.length) % shapeIds.length
1832
+ adjacentShapeId = shapeIds[adjacentIndex]
1833
+ } else {
1834
+ if (!currentShapeId) return
1835
+ adjacentShapeId = this.getNearestAdjacentShape(currentShapeId, direction)
1836
+ }
1837
+
1838
+ const shape = this.getShape(adjacentShapeId)
1839
+ if (!shape) return
1840
+
1841
+ this.setSelectedShapes([shape.id])
1842
+ this.zoomToSelectionIfOffscreen(256, {
1843
+ animation: {
1844
+ duration: this.options.animationMediumMs,
1845
+ },
1846
+ inset: 0,
1847
+ })
1848
+ }
1849
+
1850
+ /**
1851
+ * Generates a reading order for shapes based on rows grouping.
1852
+ * Tries to keep a natural reading order (left-to-right, top-to-bottom).
1853
+ *
1854
+ * @public
1855
+ */
1856
+ @computed getCurrentPageShapesInReadingOrder(): TLShape[] {
1857
+ const SHALLOW_ANGLE = 20
1858
+ const ROW_THRESHOLD = 100
1859
+
1860
+ const shapes = this.getCurrentPageShapes()
1861
+ const tabbableShapes = shapes.filter((shape) => this.getShapeUtil(shape).canTabTo(shape))
1862
+
1863
+ if (tabbableShapes.length <= 1) return tabbableShapes
1864
+
1865
+ const shapesWithCenters = tabbableShapes.map((shape) => ({
1866
+ shape,
1867
+ center: this.getShapePageBounds(shape)!.center,
1868
+ }))
1869
+ shapesWithCenters.sort((a, b) => a.center.y - b.center.y)
1870
+
1871
+ const rows: Array<typeof shapesWithCenters> = []
1872
+
1873
+ // First, group shapes into rows based on y-coordinates.
1874
+ for (const shapeWithCenter of shapesWithCenters) {
1875
+ let rowIndex = -1
1876
+ for (let i = rows.length - 1; i >= 0; i--) {
1877
+ const row = rows[i]
1878
+ const lastShapeInRow = row[row.length - 1]
1879
+
1880
+ // If the shape is close enough vertically to the last shape in this row.
1881
+ if (Math.abs(shapeWithCenter.center.y - lastShapeInRow.center.y) < ROW_THRESHOLD) {
1882
+ rowIndex = i
1883
+ break
1884
+ }
1885
+ }
1886
+
1887
+ // If no suitable row found, create a new row.
1888
+ if (rowIndex === -1) {
1889
+ rows.push([shapeWithCenter])
1890
+ } else {
1891
+ rows[rowIndex].push(shapeWithCenter)
1892
+ }
1893
+ }
1894
+
1895
+ // Then, sort each row by x-coordinate (left-to-right).
1896
+ for (const row of rows) {
1897
+ row.sort((a, b) => a.center.x - b.center.x)
1898
+ }
1899
+
1900
+ // Finally, apply angle/distance weight adjustments within rows for closely positioned shapes.
1901
+ for (const row of rows) {
1902
+ if (row.length <= 2) continue
1903
+
1904
+ for (let i = 0; i < row.length - 2; i++) {
1905
+ const currentShape = row[i]
1906
+ const nextShape = row[i + 1]
1907
+ const nextNextShape = row[i + 2]
1908
+
1909
+ // Only consider adjustment if the next two shapes are relatively close to each other.
1910
+ const dist1 = Vec.Dist2(currentShape.center, nextShape.center)
1911
+ const dist2 = Vec.Dist2(currentShape.center, nextNextShape.center)
1912
+
1913
+ // Check if the 2nd shape is actually closer to the current shape.
1914
+ if (dist2 < dist1 * 0.9) {
1915
+ // Check if it's a shallow enough angle.
1916
+ const angle = Math.abs(
1917
+ Vec.Angle(currentShape.center, nextNextShape.center) * (180 / Math.PI)
1918
+ )
1919
+ if (angle <= SHALLOW_ANGLE) {
1920
+ // Swap swap.
1921
+ ;[row[i + 1], row[i + 2]] = [row[i + 2], row[i + 1]]
1922
+ }
1923
+ }
1924
+ }
1925
+ }
1926
+
1927
+ return rows.flat().map((item) => item.shape)
1928
+ }
1929
+
1930
+ /**
1931
+ * Find the nearest adjacent shape in a specific direction.
1932
+ *
1933
+ * @public
1934
+ */
1935
+ getNearestAdjacentShape(
1936
+ currentShapeId: TLShapeId,
1937
+ direction: 'left' | 'right' | 'up' | 'down'
1938
+ ): TLShapeId {
1939
+ const directionToAngle = { right: 0, left: 180, down: 90, up: 270 }
1940
+ const currentShape = this.getShape(currentShapeId)
1941
+ if (!currentShape) return currentShapeId
1942
+
1943
+ const shapes = this.getCurrentPageShapes()
1944
+ const tabbableShapes = shapes.filter(
1945
+ (shape) => this.getShapeUtil(shape).canTabTo(shape) && shape.id !== currentShapeId
1946
+ )
1947
+ if (!tabbableShapes.length) return currentShapeId
1948
+
1949
+ const currentCenter = this.getShapePageBounds(currentShape)!.center
1950
+ const shapesWithCenters = tabbableShapes.map((shape) => ({
1951
+ shape,
1952
+ center: this.getShapePageBounds(shape)!.center,
1953
+ }))
1954
+
1955
+ // Filter shapes that are in the same direction.
1956
+ const shapesInDirection = shapesWithCenters.filter(({ center }) => {
1957
+ const isRight = center.x > currentCenter.x
1958
+ const isDown = center.y > currentCenter.y
1959
+ const xDist = center.x - currentCenter.x
1960
+ const yDist = center.y - currentCenter.y
1961
+ const isInXDirection = Math.abs(yDist) < Math.abs(xDist) * 2
1962
+ const isInYDirection = Math.abs(xDist) < Math.abs(yDist) * 2
1963
+ if (direction === 'left' || direction === 'right') {
1964
+ return isInXDirection && (direction === 'right' ? isRight : !isRight)
1965
+ }
1966
+ if (direction === 'up' || direction === 'down') {
1967
+ return isInYDirection && (direction === 'down' ? isDown : !isDown)
1968
+ }
1969
+ })
1970
+
1971
+ if (shapesInDirection.length === 0) return currentShapeId
1972
+
1973
+ // Ok, now score that subset of shapes.
1974
+ const lowestScoringShape = minBy(shapesInDirection, ({ center }) => {
1975
+ // Distance is the primary weighting factor.
1976
+ const distance = Vec.Dist2(currentCenter, center)
1977
+
1978
+ // Distance along the primary axis.
1979
+ const dirProp = ['left', 'right'].includes(direction) ? 'x' : 'y'
1980
+ const directionalDistance = Math.abs(center[dirProp] - currentCenter[dirProp])
1981
+
1982
+ // Distance off the perpendicular to the primary axis.
1983
+ const offProp = ['left', 'right'].includes(direction) ? 'y' : 'x'
1984
+ const offAxisDeviation = Math.abs(center[offProp] - currentCenter[offProp])
1985
+
1986
+ // Angle in degrees
1987
+ const angle = Math.abs(Vec.Angle(currentCenter, center) * (180 / Math.PI))
1988
+ const angleDeviation = Math.abs(angle - directionToAngle[direction])
1989
+
1990
+ // Calculate final score (lower is better).
1991
+ // Weight factors to prioritize:
1992
+ // 1. Shapes directly in line with the current shape
1993
+ // 2. Shapes closer to the current shape
1994
+ // 3. Shapes with less angular deviation from the primary direction
1995
+ return (
1996
+ distance * 1.0 + // Base distance
1997
+ offAxisDeviation * 2.0 + // Heavy penalty for off-axis deviation
1998
+ (distance - directionalDistance) * 1.5 + // Penalty for diagonal distance
1999
+ angleDeviation * 0.5
2000
+ ) // Slight penalty for angular deviation
2001
+ })
2002
+
2003
+ return lowestScoringShape!.shape.id
2004
+ }
2005
+
1816
2006
  /**
1817
2007
  * Clear the selection.
1818
2008
  *
@@ -3054,6 +3244,34 @@ export class Editor extends EventEmitter<TLEventMap> {
3054
3244
  return this
3055
3245
  }
3056
3246
 
3247
+ /**
3248
+ * Zoom the camera to the current selection if offscreen.
3249
+ *
3250
+ * @public
3251
+ */
3252
+ zoomToSelectionIfOffscreen(
3253
+ padding = 16,
3254
+ opts?: { targetZoom?: number; inset?: number } & TLCameraMoveOptions
3255
+ ) {
3256
+ const selectionPageBounds = this.getSelectionPageBounds()
3257
+ const viewportPageBounds = this.getViewportPageBounds()
3258
+ if (selectionPageBounds && !viewportPageBounds.contains(selectionPageBounds)) {
3259
+ const eb = selectionPageBounds
3260
+ .clone()
3261
+ // Expand the bounds by the padding
3262
+ .expandBy(padding / this.getZoomLevel())
3263
+ // then expand the bounds to include the viewport bounds
3264
+ .expand(viewportPageBounds)
3265
+
3266
+ // then use the difference between the centers to calculate the offset
3267
+ const nextBounds = viewportPageBounds.clone().translate({
3268
+ x: (eb.center.x - viewportPageBounds.center.x) * 2,
3269
+ y: (eb.center.y - viewportPageBounds.center.y) * 2,
3270
+ })
3271
+ this.zoomToBounds(nextBounds, opts)
3272
+ }
3273
+ }
3274
+
3057
3275
  /**
3058
3276
  * Zoom the camera to fit a bounding box (in the current page space).
3059
3277
  *
@@ -4330,7 +4548,7 @@ export class Editor extends EventEmitter<TLEventMap> {
4330
4548
  private _shapeGeometryCaches: Record<string, ComputedCache<Geometry2d, TLShape>> = {}
4331
4549
 
4332
4550
  /**
4333
- * Get the geometry of a shape.
4551
+ * Get the geometry of a shape in shape-space.
4334
4552
  *
4335
4553
  * @example
4336
4554
  * ```ts
@@ -4361,6 +4579,44 @@ export class Editor extends EventEmitter<TLEventMap> {
4361
4579
  )! as T
4362
4580
  }
4363
4581
 
4582
+ private _shapePageGeometryCaches: Record<string, ComputedCache<Geometry2d, TLShape>> = {}
4583
+
4584
+ /**
4585
+ * Get the geometry of a shape in page-space.
4586
+ *
4587
+ * @example
4588
+ * ```ts
4589
+ * editor.getShapePageGeometry(myShape)
4590
+ * editor.getShapePageGeometry(myShapeId)
4591
+ * editor.getShapePageGeometry(myShapeId, { context: "arrow" })
4592
+ * ```
4593
+ *
4594
+ * @param shape - The shape (or shape id) to get the geometry for.
4595
+ * @param opts - Additional options about the request for geometry. Passed to {@link ShapeUtil.getGeometry}.
4596
+ *
4597
+ * @public
4598
+ */
4599
+ getShapePageGeometry<T extends Geometry2d>(shape: TLShape | TLShapeId, opts?: TLGeometryOpts): T {
4600
+ const context = opts?.context ?? 'none'
4601
+ if (!this._shapePageGeometryCaches[context]) {
4602
+ this._shapePageGeometryCaches[context] = this.store.createComputedCache(
4603
+ 'bounds',
4604
+ (shape) => {
4605
+ const geometry = this.getShapeGeometry(shape.id, opts)
4606
+ const pageTransform = this.getShapePageTransform(shape.id)
4607
+ return geometry.transform(pageTransform)
4608
+ },
4609
+ {
4610
+ // we only depend directly on the shape id, and changing geometry/transform will update us anyway
4611
+ areRecordsEqual: () => true,
4612
+ }
4613
+ )
4614
+ }
4615
+ return this._shapePageGeometryCaches[context].get(
4616
+ typeof shape === 'string' ? shape : shape.id
4617
+ )! as T
4618
+ }
4619
+
4364
4620
  /** @internal */
4365
4621
  @computed private _getShapeHandlesCache(): ComputedCache<TLHandle[] | undefined, TLShape> {
4366
4622
  return this.store.createComputedCache('handles', (shape) => {
@@ -4467,15 +4723,7 @@ export class Editor extends EventEmitter<TLEventMap> {
4467
4723
  /** @internal */
4468
4724
  @computed private _getShapePageBoundsCache(): ComputedCache<Box, TLShape> {
4469
4725
  return this.store.createComputedCache<Box, TLShape>('pageBoundsCache', (shape) => {
4470
- const pageTransform = this._getShapePageTransformCache().get(shape.id)
4471
-
4472
- if (!pageTransform) return new Box()
4473
-
4474
- const result = Box.FromPoints(
4475
- Mat.applyToPoints(pageTransform, this.getShapeGeometry(shape).vertices)
4476
- )
4477
-
4478
- return result
4726
+ return this.getShapePageGeometry(shape).bounds
4479
4727
  })
4480
4728
  }
4481
4729
 
@@ -4549,11 +4797,10 @@ export class Editor extends EventEmitter<TLEventMap> {
4549
4797
  if (frameAncestors.length === 0) return undefined
4550
4798
 
4551
4799
  const pageMask = frameAncestors
4552
- .map<Vec[] | undefined>((s) =>
4553
- // Apply the frame transform to the frame outline to get the frame outline in the current page space
4554
- this._getShapePageTransformCache()
4555
- .get(s.id)!
4556
- .applyToPoints(this.getShapeGeometry(s).vertices)
4800
+ .map<Vec[] | undefined>(
4801
+ (s) =>
4802
+ // Apply the frame transform to the frame outline to get the frame outline in the current page space
4803
+ this.getShapePageGeometry(s.id).vertices
4557
4804
  )
4558
4805
  .reduce((acc, b) => {
4559
4806
  if (!(b && acc)) return undefined
@@ -58,7 +58,7 @@ export class FocusManager {
58
58
 
59
59
  private handleKeyDown(keyEvent: KeyboardEvent) {
60
60
  const container = this.editor.getContainer()
61
- if (keyEvent.key === 'Tab') {
61
+ if (['Tab', 'ArrowUp', 'ArrowDown'].includes(keyEvent.key)) {
62
62
  container.classList.remove('tl-container__no-focus-ring')
63
63
  }
64
64
  }
@@ -193,6 +193,16 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
193
193
  return true
194
194
  }
195
195
 
196
+ /**
197
+ * Whether the shape can be tabbed to.
198
+ *
199
+ * @param shape - The shape.
200
+ * @public
201
+ */
202
+ canTabTo(_shape: Shape): boolean {
203
+ return true
204
+ }
205
+
196
206
  /**
197
207
  * Whether the shape can be scrolled while editing.
198
208
  *
@@ -438,6 +448,10 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
438
448
  return undefined
439
449
  }
440
450
 
451
+ getAriaDescriptor(_shape: Shape): string | undefined {
452
+ return undefined
453
+ }
454
+
441
455
  // Events
442
456
 
443
457
  /**
@@ -2,8 +2,6 @@ import { TLGroupShape, groupShapeMigrations, groupShapeProps } from '@tldraw/tls
2
2
  import { SVGContainer } from '../../../components/SVGContainer'
3
3
  import { Geometry2d } from '../../../primitives/geometry/Geometry2d'
4
4
  import { Group2d } from '../../../primitives/geometry/Group2d'
5
- import { Polygon2d } from '../../../primitives/geometry/Polygon2d'
6
- import { Polyline2d } from '../../../primitives/geometry/Polyline2d'
7
5
  import { Rectangle2d } from '../../../primitives/geometry/Rectangle2d'
8
6
  import { ShapeUtil } from '../ShapeUtil'
9
7
  import { DashedOutlineBox } from './DashedOutlineBox'
@@ -14,6 +12,10 @@ export class GroupShapeUtil extends ShapeUtil<TLGroupShape> {
14
12
  static override props = groupShapeProps
15
13
  static override migrations = groupShapeMigrations
16
14
 
15
+ override canTabTo() {
16
+ return false
17
+ }
18
+
17
19
  override hideSelectionBoundsFg() {
18
20
  return true
19
21
  }
@@ -35,19 +37,9 @@ export class GroupShapeUtil extends ShapeUtil<TLGroupShape> {
35
37
  return new Group2d({
36
38
  children: children.map((childId) => {
37
39
  const shape = this.editor.getShape(childId)!
38
- const geometry = this.editor.getShapeGeometry(childId)
39
- const points = this.editor.getShapeLocalTransform(shape)!.applyToPoints(geometry.vertices)
40
-
41
- if (geometry.isClosed) {
42
- return new Polygon2d({
43
- points,
44
- isFilled: true,
45
- })
46
- }
47
-
48
- return new Polyline2d({
49
- points,
50
- })
40
+ return this.editor
41
+ .getShapeGeometry(childId)
42
+ .transform(this.editor.getShapeLocalTransform(shape)!)
51
43
  }),
52
44
  })
53
45
  }
@@ -38,7 +38,6 @@ export interface TLStateNodeConstructor {
38
38
  initial?: string
39
39
  children?(): TLStateNodeConstructor[]
40
40
  isLockable: boolean
41
- useCoalescedEvents: boolean
42
41
  }
43
42
 
44
43
  /** @public */
@@ -48,8 +47,7 @@ export abstract class StateNode implements Partial<TLEventHandlers> {
48
47
  public editor: Editor,
49
48
  parent?: StateNode
50
49
  ) {
51
- const { id, children, initial, isLockable, useCoalescedEvents } = this
52
- .constructor as TLStateNodeConstructor
50
+ const { id, children, initial, isLockable } = this.constructor as TLStateNodeConstructor
53
51
 
54
52
  this.id = id
55
53
  this._isActive = atom<boolean>('toolIsActive' + this.id, false)
@@ -85,7 +83,6 @@ export abstract class StateNode implements Partial<TLEventHandlers> {
85
83
  }
86
84
  }
87
85
  this.isLockable = isLockable
88
- this.useCoalescedEvents = useCoalescedEvents
89
86
  this.performanceTracker = new PerformanceTracker()
90
87
  }
91
88
 
@@ -93,7 +90,6 @@ export abstract class StateNode implements Partial<TLEventHandlers> {
93
90
  static initial?: string
94
91
  static children?: () => TLStateNodeConstructor[]
95
92
  static isLockable = true
96
- static useCoalescedEvents = false
97
93
 
98
94
  id: string
99
95
  type: 'branch' | 'leaf' | 'root'
@@ -101,7 +97,6 @@ export abstract class StateNode implements Partial<TLEventHandlers> {
101
97
  initial?: string
102
98
  children?: Record<string, StateNode>
103
99
  isLockable: boolean
104
- useCoalescedEvents: boolean
105
100
  parent: StateNode
106
101
 
107
102
  /**
@@ -5,3 +5,6 @@ export type TLSelectionHandle = SelectionCorner | SelectionEdge | RotateCorner
5
5
 
6
6
  /** @public */
7
7
  export type TLResizeHandle = SelectionCorner | SelectionEdge
8
+
9
+ /** @public */
10
+ export type TLAdjacentDirection = 'next' | 'prev' | 'left' | 'right' | 'up' | 'down'
@@ -1,4 +1,3 @@
1
- import { useValue } from '@tldraw/state-react'
2
1
  import React, { useMemo } from 'react'
3
2
  import { RIGHT_MOUSE_BUTTON } from '../constants'
4
3
  import {
@@ -12,7 +11,6 @@ import { useEditor } from './useEditor'
12
11
 
13
12
  export function useCanvasEvents() {
14
13
  const editor = useEditor()
15
- const currentTool = useValue('current tool', () => editor.getCurrentTool(), [editor])
16
14
 
17
15
  const events = useMemo(
18
16
  function canvasEvents() {
@@ -51,17 +49,12 @@ export function useCanvasEvents() {
51
49
  lastX = e.clientX
52
50
  lastY = e.clientY
53
51
 
54
- // For tools that benefit from a higher fidelity of events,
55
- // we dispatch the coalesced events.
56
- const events = currentTool.useCoalescedEvents ? e.nativeEvent.getCoalescedEvents() : [e]
57
- for (const singleEvent of events) {
58
- editor.dispatch({
59
- type: 'pointer',
60
- target: 'canvas',
61
- name: 'pointer_move',
62
- ...getPointerInfo(singleEvent),
63
- })
64
- }
52
+ editor.dispatch({
53
+ type: 'pointer',
54
+ target: 'canvas',
55
+ name: 'pointer_move',
56
+ ...getPointerInfo(e),
57
+ })
65
58
  }
66
59
 
67
60
  function onPointerUp(e: React.PointerEvent) {
@@ -107,7 +100,7 @@ export function useCanvasEvents() {
107
100
  if (
108
101
  e.target.tagName !== 'A' &&
109
102
  e.target.tagName !== 'TEXTAREA' &&
110
- e.target.isContentEditable &&
103
+ !e.target.isContentEditable &&
111
104
  // When in EditingShape state, we are actually clicking on a 'DIV'
112
105
  // not A/TEXTAREA/contenteditable element yet. So, to preserve cursor position
113
106
  // for edit mode on mobile we need to not preventDefault.
@@ -166,7 +159,7 @@ export function useCanvasEvents() {
166
159
  onClick,
167
160
  }
168
161
  },
169
- [editor, currentTool]
162
+ [editor]
170
163
  )
171
164
 
172
165
  return events
@@ -104,6 +104,7 @@ export function useDocumentEvents() {
104
104
 
105
105
  if ((e as any).isKilled) return
106
106
  ;(e as any).isKilled = true
107
+ const hasSelectedShapes = !!editor.getSelectedShapeIds().length
107
108
 
108
109
  switch (e.key) {
109
110
  case '=':
@@ -124,6 +125,23 @@ export function useDocumentEvents() {
124
125
  if (areShortcutsDisabled(editor)) {
125
126
  return
126
127
  }
128
+ if (hasSelectedShapes) {
129
+ // This is used in tandem with shape navigation.
130
+ preventDefault(e)
131
+ }
132
+ break
133
+ }
134
+ case 'ArrowLeft':
135
+ case 'ArrowRight':
136
+ case 'ArrowUp':
137
+ case 'ArrowDown': {
138
+ if (areShortcutsDisabled(editor)) {
139
+ return
140
+ }
141
+ if (hasSelectedShapes && (e.metaKey || e.ctrlKey)) {
142
+ // This is used in tandem with shape navigation.
143
+ preventDefault(e)
144
+ }
127
145
  break
128
146
  }
129
147
  case ',': {
@@ -1,6 +1,5 @@
1
1
  import { useValue } from '@tldraw/state-react'
2
2
  import { memo, useRef } from 'react'
3
- import { tlenv } from '../globals/environment'
4
3
  import { useCanvasEvents } from '../hooks/useCanvasEvents'
5
4
  import { useEditor } from '../hooks/useEditor'
6
5
  import { usePassThroughWheelEvents } from '../hooks/usePassThroughWheelEvents'
@@ -57,29 +56,17 @@ const WatermarkInner = memo(function WatermarkInner({ src }: { src: string }) {
57
56
  draggable={false}
58
57
  {...events}
59
58
  >
60
- {tlenv.isWebview ? (
61
- <a
62
- draggable={false}
63
- role="button"
64
- onPointerDown={(e) => {
65
- stopEventPropagation(e)
66
- preventDefault(e)
67
- }}
68
- onClick={() => runtime.openWindow(url, '_blank')}
69
- style={{ mask: maskCss, WebkitMask: maskCss }}
70
- />
71
- ) : (
72
- <a
73
- href={url}
74
- target="_blank"
75
- rel="noreferrer"
76
- draggable={false}
77
- onPointerDown={(e) => {
78
- stopEventPropagation(e)
79
- }}
80
- style={{ mask: maskCss, WebkitMask: maskCss }}
81
- />
82
- )}
59
+ <button
60
+ draggable={false}
61
+ role="button"
62
+ onPointerDown={(e) => {
63
+ stopEventPropagation(e)
64
+ preventDefault(e)
65
+ }}
66
+ title="made with tldraw"
67
+ onClick={() => runtime.openWindow(url, '_blank')}
68
+ style={{ mask: maskCss, WebkitMask: maskCss }}
69
+ />
83
70
  </div>
84
71
  )
85
72
  })
@@ -115,7 +102,7 @@ To remove the watermark, please purchase a license at tldraw.dev.
115
102
  box-sizing: content-box;
116
103
  }
117
104
 
118
- .${className} > a {
105
+ .${className} > button {
119
106
  position: absolute;
120
107
  width: 96px;
121
108
  height: 32px;
@@ -123,6 +110,8 @@ To remove the watermark, please purchase a license at tldraw.dev.
123
110
  cursor: inherit;
124
111
  color: var(--color-text);
125
112
  opacity: .38;
113
+ border: 0;
114
+ padding: 0;
126
115
  background-color: currentColor;
127
116
  }
128
117
 
@@ -137,13 +126,13 @@ To remove the watermark, please purchase a license at tldraw.dev.
137
126
  height: 48px;
138
127
  }
139
128
 
140
- .${className}[data-mobile='true'] > a {
129
+ .${className}[data-mobile='true'] > button {
141
130
  width: 8px;
142
131
  height: 32px;
143
132
  }
144
133
 
145
134
  @media (hover: hover) {
146
- .${className} > a {
135
+ .${className} > button {
147
136
  pointer-events: none;
148
137
  }
149
138
 
@@ -153,12 +142,12 @@ To remove the watermark, please purchase a license at tldraw.dev.
153
142
  transition-delay: 0.32s;
154
143
  }
155
144
 
156
- .${className}:hover > a {
145
+ .${className}:hover > button {
157
146
  animation: delayed_link 0.2s forwards ease-in-out;
158
147
  animation-delay: 0.32s;
159
148
  }
160
149
 
161
- .${className} > a:focus-visible {
150
+ .${className} > button:focus-visible {
162
151
  opacity: 1;
163
152
  }
164
153
  }