@tldraw/editor 3.12.0-canary.81c3581f0bf0 → 3.12.0-canary.8beaa3d8bd3b

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 (90) hide show
  1. package/dist-cjs/index.d.ts +153 -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 +5 -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 +208 -18
  11. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  12. package/dist-cjs/lib/editor/shapes/ShapeUtil.js +12 -0
  13. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
  14. package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js +4 -13
  15. package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js.map +2 -2
  16. package/dist-cjs/lib/editor/types/selection-types.js.map +1 -1
  17. package/dist-cjs/lib/exports/StyleEmbedder.js +19 -5
  18. package/dist-cjs/lib/exports/StyleEmbedder.js.map +2 -2
  19. package/dist-cjs/lib/exports/cssRules.js +127 -0
  20. package/dist-cjs/lib/exports/cssRules.js.map +7 -0
  21. package/dist-cjs/lib/exports/parseCss.js +0 -69
  22. package/dist-cjs/lib/exports/parseCss.js.map +2 -2
  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 +9 -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 +153 -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 +5 -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 +209 -18
  47. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  48. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +12 -0
  49. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
  50. package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs +4 -13
  51. package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs.map +2 -2
  52. package/dist-esm/lib/exports/StyleEmbedder.mjs +21 -12
  53. package/dist-esm/lib/exports/StyleEmbedder.mjs.map +2 -2
  54. package/dist-esm/lib/exports/cssRules.mjs +107 -0
  55. package/dist-esm/lib/exports/cssRules.mjs.map +7 -0
  56. package/dist-esm/lib/exports/parseCss.mjs +0 -69
  57. package/dist-esm/lib/exports/parseCss.mjs.map +2 -2
  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 +9 -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/package.json +7 -7
  73. package/src/index.ts +11 -2
  74. package/src/lib/TldrawEditor.tsx +28 -1
  75. package/src/lib/components/GeometryDebuggingView.tsx +3 -3
  76. package/src/lib/components/default-components/DefaultCanvas.tsx +6 -1
  77. package/src/lib/editor/Editor.ts +315 -24
  78. package/src/lib/editor/shapes/ShapeUtil.ts +14 -0
  79. package/src/lib/editor/shapes/group/GroupShapeUtil.tsx +7 -15
  80. package/src/lib/editor/types/selection-types.ts +3 -0
  81. package/src/lib/exports/StyleEmbedder.ts +25 -15
  82. package/src/lib/exports/cssRules.ts +126 -0
  83. package/src/lib/exports/parseCss.ts +0 -79
  84. package/src/lib/hooks/useDocumentEvents.ts +18 -0
  85. package/src/lib/license/Watermark.tsx +17 -29
  86. package/src/lib/primitives/geometry/Geometry2d.ts +196 -16
  87. package/src/lib/primitives/geometry/Group2d.ts +76 -13
  88. package/src/lib/primitives/intersect.ts +41 -0
  89. package/src/lib/utils/reorderShapes.ts +2 -9
  90. 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<{
@@ -241,10 +242,33 @@ export interface TLEditorOptions {
241
242
  fontAssetUrls?: { [key: string]: string | undefined }
242
243
  /**
243
244
  * A predicate that should return true if the given shape should be hidden.
245
+ *
246
+ * @deprecated Use {@link Editor#getShapeVisibility} instead.
247
+ *
244
248
  * @param shape - The shape to check.
245
249
  * @param editor - The editor instance.
246
250
  */
247
251
  isShapeHidden?(shape: TLShape, editor: Editor): boolean
252
+
253
+ /**
254
+ * Provides a way to hide shapes.
255
+ *
256
+ * @example
257
+ * ```ts
258
+ * getShapeVisibility={(shape, editor) => shape.meta.hidden ? 'hidden' : 'inherit'}
259
+ * ```
260
+ *
261
+ * - `'inherit' | undefined` - (default) The shape will be visible unless its parent is hidden.
262
+ * - `'hidden'` - The shape will be hidden.
263
+ * - `'visible'` - The shape will be visible.
264
+ *
265
+ * @param shape - The shape to check.
266
+ * @param editor - The editor instance.
267
+ */
268
+ getShapeVisibility?(
269
+ shape: TLShape,
270
+ editor: Editor
271
+ ): 'visible' | 'hidden' | 'inherit' | null | undefined
248
272
  }
249
273
 
250
274
  /**
@@ -281,12 +305,21 @@ export class Editor extends EventEmitter<TLEventMap> {
281
305
  autoFocus,
282
306
  inferDarkMode,
283
307
  options,
308
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
284
309
  isShapeHidden,
310
+ getShapeVisibility,
285
311
  fontAssetUrls,
286
312
  }: TLEditorOptions) {
287
313
  super()
314
+ assert(
315
+ !(isShapeHidden && getShapeVisibility),
316
+ 'Cannot use both isShapeHidden and getShapeVisibility'
317
+ )
288
318
 
289
- this._isShapeHiddenPredicate = isShapeHidden
319
+ this._getShapeVisibility = isShapeHidden
320
+ ? // eslint-disable-next-line @typescript-eslint/no-deprecated
321
+ (shape: TLShape, editor: Editor) => (isShapeHidden(shape, editor) ? 'hidden' : 'inherit')
322
+ : getShapeVisibility
290
323
 
291
324
  this.options = { ...defaultTldrawOptions, ...options }
292
325
 
@@ -773,18 +806,22 @@ export class Editor extends EventEmitter<TLEventMap> {
773
806
  }
774
807
  }
775
808
 
776
- private readonly _isShapeHiddenPredicate?: (shape: TLShape, editor: Editor) => boolean
809
+ private readonly _getShapeVisibility?: TLEditorOptions['getShapeVisibility']
777
810
  @computed
778
811
  private getIsShapeHiddenCache() {
779
- if (!this._isShapeHiddenPredicate) return null
812
+ if (!this._getShapeVisibility) return null
780
813
  return this.store.createComputedCache<boolean, TLShape>('isShapeHidden', (shape: TLShape) => {
781
- const hiddenParent = this.findShapeAncestor(shape, (p) => this.isShapeHidden(p))
782
- if (hiddenParent) return true
783
- return this._isShapeHiddenPredicate!(shape, this) ?? false
814
+ const visibility = this._getShapeVisibility!(shape, this)
815
+ const isParentHidden = PageRecordType.isId(shape.parentId)
816
+ ? false
817
+ : this.isShapeHidden(shape.parentId)
818
+
819
+ if (isParentHidden) return visibility !== 'visible'
820
+ return visibility === 'hidden'
784
821
  })
785
822
  }
786
823
  isShapeHidden(shapeOrId: TLShape | TLShapeId): boolean {
787
- if (!this._isShapeHiddenPredicate) return false
824
+ if (!this._getShapeVisibility) return false
788
825
  return !!this.getIsShapeHiddenCache!()!.get(
789
826
  typeof shapeOrId === 'string' ? shapeOrId : shapeOrId.id
790
827
  )
@@ -1777,6 +1814,195 @@ export class Editor extends EventEmitter<TLEventMap> {
1777
1814
  return this
1778
1815
  }
1779
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
+
1780
2006
  /**
1781
2007
  * Clear the selection.
1782
2008
  *
@@ -3018,6 +3244,34 @@ export class Editor extends EventEmitter<TLEventMap> {
3018
3244
  return this
3019
3245
  }
3020
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
+
3021
3275
  /**
3022
3276
  * Zoom the camera to fit a bounding box (in the current page space).
3023
3277
  *
@@ -3711,7 +3965,15 @@ export class Editor extends EventEmitter<TLEventMap> {
3711
3965
  const addShapeById = (id: TLShapeId, opacity: number, isAncestorErasing: boolean) => {
3712
3966
  const shape = this.getShape(id)
3713
3967
  if (!shape) return
3714
- if (this.isShapeHidden(shape)) return
3968
+
3969
+ if (this.isShapeHidden(shape)) {
3970
+ // process children just in case they are overriding the hidden state
3971
+ const isErasing = isAncestorErasing || erasingShapeIds.includes(id)
3972
+ for (const childId of this.getSortedChildIdsForParent(id)) {
3973
+ addShapeById(childId, opacity, isErasing)
3974
+ }
3975
+ return
3976
+ }
3715
3977
 
3716
3978
  opacity *= shape.opacity
3717
3979
  let isShapeErasing = false
@@ -4286,7 +4548,7 @@ export class Editor extends EventEmitter<TLEventMap> {
4286
4548
  private _shapeGeometryCaches: Record<string, ComputedCache<Geometry2d, TLShape>> = {}
4287
4549
 
4288
4550
  /**
4289
- * Get the geometry of a shape.
4551
+ * Get the geometry of a shape in shape-space.
4290
4552
  *
4291
4553
  * @example
4292
4554
  * ```ts
@@ -4317,6 +4579,44 @@ export class Editor extends EventEmitter<TLEventMap> {
4317
4579
  )! as T
4318
4580
  }
4319
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
+
4320
4620
  /** @internal */
4321
4621
  @computed private _getShapeHandlesCache(): ComputedCache<TLHandle[] | undefined, TLShape> {
4322
4622
  return this.store.createComputedCache('handles', (shape) => {
@@ -4423,15 +4723,7 @@ export class Editor extends EventEmitter<TLEventMap> {
4423
4723
  /** @internal */
4424
4724
  @computed private _getShapePageBoundsCache(): ComputedCache<Box, TLShape> {
4425
4725
  return this.store.createComputedCache<Box, TLShape>('pageBoundsCache', (shape) => {
4426
- const pageTransform = this._getShapePageTransformCache().get(shape.id)
4427
-
4428
- if (!pageTransform) return new Box()
4429
-
4430
- const result = Box.FromPoints(
4431
- Mat.applyToPoints(pageTransform, this.getShapeGeometry(shape).vertices)
4432
- )
4433
-
4434
- return result
4726
+ return this.getShapePageGeometry(shape).bounds
4435
4727
  })
4436
4728
  }
4437
4729
 
@@ -4505,11 +4797,10 @@ export class Editor extends EventEmitter<TLEventMap> {
4505
4797
  if (frameAncestors.length === 0) return undefined
4506
4798
 
4507
4799
  const pageMask = frameAncestors
4508
- .map<Vec[] | undefined>((s) =>
4509
- // Apply the frame transform to the frame outline to get the frame outline in the current page space
4510
- this._getShapePageTransformCache()
4511
- .get(s.id)!
4512
- .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
4513
4804
  )
4514
4805
  .reduce((acc, b) => {
4515
4806
  if (!(b && acc)) return undefined
@@ -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
  }
@@ -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,5 +1,6 @@
1
- import { assertExists, objectMapValues, uniqueId } from '@tldraw/utils'
1
+ import { assertExists, getOwnProperty, objectMapValues, uniqueId } from '@tldraw/utils'
2
2
  import { FontEmbedder } from './FontEmbedder'
3
+ import { ReadonlyStyles, Styles, cssRules } from './cssRules'
3
4
  import {
4
5
  elementStyle,
5
6
  getComputedStyle,
@@ -7,15 +8,8 @@ import {
7
8
  getRenderedChildren,
8
9
  } from './domUtils'
9
10
  import { resourceToDataUrl } from './fetchCache'
10
- import {
11
- isPropertyCoveredByCurrentColor,
12
- isPropertyInherited,
13
- parseCssValueUrls,
14
- shouldIncludeCssProperty,
15
- } from './parseCss'
16
-
17
- type Styles = { [K in string]?: string }
18
- type ReadonlyStyles = { readonly [K in string]?: string }
11
+ import { parseCssValueUrls, shouldIncludeCssProperty } from './parseCss'
12
+
19
13
  const NO_STYLES = {} as const
20
14
 
21
15
  interface ElementStyleInfo {
@@ -239,15 +233,22 @@ function styleFromComputedStyleMap(
239
233
  { defaultStyles, parentStyles }: ReadStyleOpts
240
234
  ) {
241
235
  const styles: Record<string, string> = {}
236
+ const currentColor = style.get('color')?.toString() || ''
237
+ const ruleOptions = {
238
+ currentColor,
239
+ parentStyles,
240
+ defaultStyles,
241
+ getStyle: (property: string) => style.get(property)?.toString() ?? '',
242
+ }
242
243
  for (const property of style.keys()) {
243
244
  if (!shouldIncludeCssProperty(property)) continue
244
245
 
245
246
  const value = style.get(property)!.toString()
246
247
 
247
248
  if (defaultStyles[property] === value) continue
248
- if (parentStyles[property] === value && isPropertyInherited(property)) continue
249
- if (isPropertyCoveredByCurrentColor(style.get('color')?.toString() || '', property, value))
250
- continue
249
+
250
+ const rule = getOwnProperty(cssRules, property)
251
+ if (rule && rule(value, property, ruleOptions)) continue
251
252
 
252
253
  styles[property] = value
253
254
  }
@@ -260,14 +261,23 @@ function styleFromComputedStyle(
260
261
  { defaultStyles, parentStyles }: ReadStyleOpts
261
262
  ) {
262
263
  const styles: Record<string, string> = {}
264
+ const currentColor = style.color
265
+ const ruleOptions = {
266
+ currentColor,
267
+ parentStyles,
268
+ defaultStyles,
269
+ getStyle: (property: string) => style.getPropertyValue(property),
270
+ }
271
+
263
272
  for (const property in style) {
264
273
  if (!shouldIncludeCssProperty(property)) continue
265
274
 
266
275
  const value = style.getPropertyValue(property)
267
276
 
268
277
  if (defaultStyles[property] === value) continue
269
- if (parentStyles[property] === value && isPropertyInherited(property)) continue
270
- if (isPropertyCoveredByCurrentColor(style.color, property, value)) continue
278
+
279
+ const rule = getOwnProperty(cssRules, property)
280
+ if (rule && rule(value, property, ruleOptions)) continue
271
281
 
272
282
  styles[property] = value
273
283
  }