@tldraw/editor 3.12.0-internal.624e32507d98 → 3.13.0-canary.1793786aff8a

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 (102) hide show
  1. package/CHANGELOG.md +134 -0
  2. package/dist-cjs/index.d.ts +170 -21
  3. package/dist-cjs/index.js +3 -1
  4. package/dist-cjs/index.js.map +2 -2
  5. package/dist-cjs/lib/TldrawEditor.js +5 -0
  6. package/dist-cjs/lib/TldrawEditor.js.map +2 -2
  7. package/dist-cjs/lib/components/GeometryDebuggingView.js +2 -2
  8. package/dist-cjs/lib/components/GeometryDebuggingView.js.map +2 -2
  9. package/dist-cjs/lib/components/default-components/DefaultCanvas.js +10 -1
  10. package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
  11. package/dist-cjs/lib/editor/Editor.js +208 -18
  12. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  13. package/dist-cjs/lib/editor/managers/FocusManager.js +1 -1
  14. package/dist-cjs/lib/editor/managers/FocusManager.js.map +2 -2
  15. package/dist-cjs/lib/editor/shapes/ShapeUtil.js +12 -0
  16. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
  17. package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js +4 -13
  18. package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js.map +2 -2
  19. package/dist-cjs/lib/editor/types/selection-types.js.map +1 -1
  20. package/dist-cjs/lib/exports/StyleEmbedder.js +31 -60
  21. package/dist-cjs/lib/exports/StyleEmbedder.js.map +2 -2
  22. package/dist-cjs/lib/exports/cssRules.js +3 -9
  23. package/dist-cjs/lib/exports/cssRules.js.map +2 -2
  24. package/dist-cjs/lib/exports/exportToSvg.js +1 -4
  25. package/dist-cjs/lib/exports/exportToSvg.js.map +2 -2
  26. package/dist-cjs/lib/hooks/useCanvasEvents.js +2 -2
  27. package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
  28. package/dist-cjs/lib/hooks/useDocumentEvents.js +16 -0
  29. package/dist-cjs/lib/hooks/useDocumentEvents.js.map +2 -2
  30. package/dist-cjs/lib/license/Watermark.js +10 -20
  31. package/dist-cjs/lib/license/Watermark.js.map +2 -2
  32. package/dist-cjs/lib/primitives/geometry/Geometry2d.js +139 -16
  33. package/dist-cjs/lib/primitives/geometry/Geometry2d.js.map +3 -3
  34. package/dist-cjs/lib/primitives/geometry/Group2d.js +54 -11
  35. package/dist-cjs/lib/primitives/geometry/Group2d.js.map +2 -2
  36. package/dist-cjs/lib/primitives/intersect.js +20 -0
  37. package/dist-cjs/lib/primitives/intersect.js.map +2 -2
  38. package/dist-cjs/lib/utils/reorderShapes.js +2 -8
  39. package/dist-cjs/lib/utils/reorderShapes.js.map +2 -2
  40. package/dist-cjs/version.js +3 -3
  41. package/dist-cjs/version.js.map +1 -1
  42. package/dist-esm/index.d.mts +170 -21
  43. package/dist-esm/index.mjs +8 -2
  44. package/dist-esm/index.mjs.map +2 -2
  45. package/dist-esm/lib/TldrawEditor.mjs +5 -0
  46. package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
  47. package/dist-esm/lib/components/GeometryDebuggingView.mjs +3 -3
  48. package/dist-esm/lib/components/GeometryDebuggingView.mjs.map +2 -2
  49. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +10 -1
  50. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
  51. package/dist-esm/lib/editor/Editor.mjs +209 -18
  52. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  53. package/dist-esm/lib/editor/managers/FocusManager.mjs +1 -1
  54. package/dist-esm/lib/editor/managers/FocusManager.mjs.map +2 -2
  55. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +12 -0
  56. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
  57. package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs +4 -13
  58. package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs.map +2 -2
  59. package/dist-esm/lib/exports/StyleEmbedder.mjs +32 -61
  60. package/dist-esm/lib/exports/StyleEmbedder.mjs.map +2 -2
  61. package/dist-esm/lib/exports/cssRules.mjs +3 -9
  62. package/dist-esm/lib/exports/cssRules.mjs.map +2 -2
  63. package/dist-esm/lib/exports/exportToSvg.mjs +1 -4
  64. package/dist-esm/lib/exports/exportToSvg.mjs.map +2 -2
  65. package/dist-esm/lib/hooks/useCanvasEvents.mjs +2 -2
  66. package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
  67. package/dist-esm/lib/hooks/useDocumentEvents.mjs +16 -0
  68. package/dist-esm/lib/hooks/useDocumentEvents.mjs.map +2 -2
  69. package/dist-esm/lib/license/Watermark.mjs +10 -20
  70. package/dist-esm/lib/license/Watermark.mjs.map +2 -2
  71. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs +143 -14
  72. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs.map +2 -2
  73. package/dist-esm/lib/primitives/geometry/Group2d.mjs +55 -12
  74. package/dist-esm/lib/primitives/geometry/Group2d.mjs.map +2 -2
  75. package/dist-esm/lib/primitives/intersect.mjs +20 -0
  76. package/dist-esm/lib/primitives/intersect.mjs.map +2 -2
  77. package/dist-esm/lib/utils/reorderShapes.mjs +2 -8
  78. package/dist-esm/lib/utils/reorderShapes.mjs.map +2 -2
  79. package/dist-esm/version.mjs +3 -3
  80. package/dist-esm/version.mjs.map +1 -1
  81. package/editor.css +34 -19
  82. package/package.json +7 -7
  83. package/src/index.ts +12 -2
  84. package/src/lib/TldrawEditor.tsx +30 -3
  85. package/src/lib/components/GeometryDebuggingView.tsx +3 -3
  86. package/src/lib/components/default-components/DefaultCanvas.tsx +6 -1
  87. package/src/lib/editor/Editor.ts +315 -24
  88. package/src/lib/editor/managers/FocusManager.ts +1 -1
  89. package/src/lib/editor/shapes/ShapeUtil.ts +14 -0
  90. package/src/lib/editor/shapes/group/GroupShapeUtil.tsx +7 -15
  91. package/src/lib/editor/types/selection-types.ts +3 -0
  92. package/src/lib/exports/StyleEmbedder.ts +34 -81
  93. package/src/lib/exports/cssRules.ts +5 -11
  94. package/src/lib/exports/exportToSvg.tsx +1 -5
  95. package/src/lib/hooks/useCanvasEvents.ts +6 -2
  96. package/src/lib/hooks/useDocumentEvents.ts +18 -0
  97. package/src/lib/license/Watermark.tsx +18 -29
  98. package/src/lib/primitives/geometry/Geometry2d.ts +216 -19
  99. package/src/lib/primitives/geometry/Group2d.ts +76 -13
  100. package/src/lib/primitives/intersect.ts +41 -0
  101. package/src/lib/utils/reorderShapes.ts +2 -9
  102. package/src/version.ts +3 -3
@@ -5,20 +5,13 @@ type CanSkipRule = (
5
5
  value: string,
6
6
  property: string,
7
7
  options: {
8
- styles: StylePropertyMapReadOnly | CSSStyleDeclaration
8
+ getStyle(property: string): string
9
9
  parentStyles: ReadonlyStyles
10
10
  defaultStyles: ReadonlyStyles
11
11
  currentColor: string
12
12
  }
13
13
  ) => boolean
14
14
 
15
- const getStyle = (styles: StylePropertyMapReadOnly | CSSStyleDeclaration, property: string) => {
16
- if (styles instanceof CSSStyleDeclaration) {
17
- return styles.getPropertyValue(property)
18
- }
19
- return styles.get(property)?.toString()
20
- }
21
-
22
15
  const isCoveredByCurrentColor: CanSkipRule = (value, property, { currentColor }) => {
23
16
  return value === 'currentColor' || value === currentColor
24
17
  }
@@ -27,11 +20,12 @@ const isInherited: CanSkipRule = (value, property, { parentStyles }) => {
27
20
  return parentStyles[property] === value
28
21
  }
29
22
 
23
+ // see comment below about why we exclude border styles
30
24
  const isExcludedBorder =
31
25
  (borderDirection: string): CanSkipRule =>
32
- (value, property, { styles }) => {
33
- const borderWidth = getStyle(styles, `border-${borderDirection}-width`)
34
- const borderStyle = getStyle(styles, `border-${borderDirection}-style`)
26
+ (value, property, { getStyle }) => {
27
+ const borderWidth = getStyle(`border-${borderDirection}-width`)
28
+ const borderStyle = getStyle(`border-${borderDirection}-style`)
35
29
 
36
30
  if (borderWidth === '0px') return true
37
31
  if (borderStyle === 'none') return true
@@ -101,15 +101,11 @@ async function applyChangesToForeignObjects(svg: SVGSVGElement) {
101
101
  // urls, and things like videos will be converted to images.
102
102
  await Promise.all(foreignObjectChildren.map((el) => embedMedia(el as HTMLElement)))
103
103
 
104
- await styleEmbedder.collectDefaultStyles([
105
- ...svg.querySelectorAll('foreignObject.tl-export-embed-styles *'),
106
- ])
107
-
108
104
  // read the computed styles of every element (+ it's children & pseudo-elements) in the
109
105
  // document. we do this in a single pass before we start embedding any CSS stuff to avoid
110
106
  // constantly forcing the browser to recompute styles & layout.
111
107
  for (const el of foreignObjectChildren) {
112
- await styleEmbedder.readRootElementStyles(el as HTMLElement)
108
+ styleEmbedder.readRootElementStyles(el as HTMLElement)
113
109
  }
114
110
 
115
111
  // fetch any resources that we need to embed in the CSS, like background images.
@@ -53,7 +53,11 @@ export function useCanvasEvents() {
53
53
 
54
54
  // For tools that benefit from a higher fidelity of events,
55
55
  // we dispatch the coalesced events.
56
- const events = currentTool.useCoalescedEvents ? e.nativeEvent.getCoalescedEvents() : [e]
56
+ // N.B. Sometimes getCoalescedEvents isn't present on iOS, ugh.
57
+ const events =
58
+ currentTool.useCoalescedEvents && e.nativeEvent.getCoalescedEvents
59
+ ? e.nativeEvent.getCoalescedEvents()
60
+ : [e]
57
61
  for (const singleEvent of events) {
58
62
  editor.dispatch({
59
63
  type: 'pointer',
@@ -107,7 +111,7 @@ export function useCanvasEvents() {
107
111
  if (
108
112
  e.target.tagName !== 'A' &&
109
113
  e.target.tagName !== 'TEXTAREA' &&
110
- e.target.isContentEditable &&
114
+ !e.target.isContentEditable &&
111
115
  // When in EditingShape state, we are actually clicking on a 'DIV'
112
116
  // not A/TEXTAREA/contenteditable element yet. So, to preserve cursor position
113
117
  // for edit mode on mobile we need to not preventDefault.
@@ -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
  }
@@ -1,21 +1,65 @@
1
+ import { assert } from '@tldraw/utils'
1
2
  import { Box } from '../Box'
2
- import { Vec } from '../Vec'
3
- import { pointInPolygon } from '../utils'
3
+ import { Mat, MatModel } from '../Mat'
4
+ import { Vec, VecLike } from '../Vec'
5
+ import {
6
+ intersectCirclePolygon,
7
+ intersectCirclePolyline,
8
+ intersectLineSegmentPolygon,
9
+ intersectLineSegmentPolyline,
10
+ intersectPolys,
11
+ } from '../intersect'
12
+ import { approximately, pointInPolygon } from '../utils'
13
+
14
+ /**
15
+ * Filter geometry within a group.
16
+ *
17
+ * Filters are ignored when called directly on primitive geometries, but can be used to narrow down
18
+ * the results of an operation on `Group2d` geometries.
19
+ *
20
+ * @public
21
+ */
22
+ export interface Geometry2dFilters {
23
+ readonly includeLabels?: boolean
24
+ readonly includeInternal?: boolean
25
+ }
4
26
 
5
27
  /** @public */
6
- export interface Geometry2dOptions {
7
- isFilled: boolean
8
- isClosed: boolean
28
+ export const Geometry2dFilters: {
29
+ EXCLUDE_NON_STANDARD: Geometry2dFilters
30
+ INCLUDE_ALL: Geometry2dFilters
31
+ EXCLUDE_LABELS: Geometry2dFilters
32
+ EXCLUDE_INTERNAL: Geometry2dFilters
33
+ } = {
34
+ EXCLUDE_NON_STANDARD: {
35
+ includeLabels: false,
36
+ includeInternal: false,
37
+ },
38
+ INCLUDE_ALL: { includeLabels: true, includeInternal: true },
39
+ EXCLUDE_LABELS: { includeLabels: false, includeInternal: true },
40
+ EXCLUDE_INTERNAL: { includeLabels: true, includeInternal: false },
41
+ }
42
+
43
+ /** @public */
44
+ export interface TransformedGeometry2dOptions {
9
45
  isLabel?: boolean
46
+ isInternal?: boolean
10
47
  debugColor?: string
11
48
  ignore?: boolean
12
49
  }
13
50
 
51
+ /** @public */
52
+ export interface Geometry2dOptions extends TransformedGeometry2dOptions {
53
+ isFilled: boolean
54
+ isClosed: boolean
55
+ }
56
+
14
57
  /** @public */
15
58
  export abstract class Geometry2d {
16
59
  isFilled = false
17
60
  isClosed = true
18
61
  isLabel = false
62
+ isInternal = false
19
63
  debugColor?: string
20
64
  ignore?: boolean
21
65
 
@@ -23,20 +67,23 @@ export abstract class Geometry2d {
23
67
  this.isFilled = opts.isFilled
24
68
  this.isClosed = opts.isClosed
25
69
  this.isLabel = opts.isLabel ?? false
70
+ this.isInternal = opts.isInternal ?? false
26
71
  this.debugColor = opts.debugColor
27
72
  this.ignore = opts.ignore
28
73
  }
29
74
 
30
- abstract getVertices(): Vec[]
75
+ isExcludedByFilter(filters?: Geometry2dFilters) {
76
+ if (!filters) return false
77
+ if (this.isLabel && !filters.includeLabels) return true
78
+ if (this.isInternal && !filters.includeInternal) return true
79
+ return false
80
+ }
31
81
 
32
- abstract nearestPoint(point: Vec): Vec
82
+ abstract getVertices(filters: Geometry2dFilters): Vec[]
33
83
 
34
- // hitTestPoint(point: Vec, margin = 0, hitInside = false) {
35
- // // We've removed the broad phase here; that should be done outside of the call
36
- // return this.distanceToPoint(point, hitInside) <= margin
37
- // }
84
+ abstract nearestPoint(point: Vec, _filters?: Geometry2dFilters): Vec
38
85
 
39
- hitTestPoint(point: Vec, margin = 0, hitInside = false) {
86
+ hitTestPoint(point: Vec, margin = 0, hitInside = false, _filters?: Geometry2dFilters) {
40
87
  // First check whether the point is inside
41
88
  if (this.isClosed && (this.isFilled || hitInside) && pointInPolygon(point, this.vertices)) {
42
89
  return true
@@ -45,17 +92,17 @@ export abstract class Geometry2d {
45
92
  return Vec.Dist2(point, this.nearestPoint(point)) <= margin * margin
46
93
  }
47
94
 
48
- distanceToPoint(point: Vec, hitInside = false) {
95
+ distanceToPoint(point: Vec, hitInside = false, filters?: Geometry2dFilters) {
49
96
  return (
50
- point.dist(this.nearestPoint(point)) *
97
+ point.dist(this.nearestPoint(point, filters)) *
51
98
  (this.isClosed && (this.isFilled || hitInside) && pointInPolygon(point, this.vertices)
52
99
  ? -1
53
100
  : 1)
54
101
  )
55
102
  }
56
103
 
57
- distanceToLineSegment(A: Vec, B: Vec) {
58
- if (A.equals(B)) return this.distanceToPoint(A)
104
+ distanceToLineSegment(A: Vec, B: Vec, filters?: Geometry2dFilters) {
105
+ if (A.equals(B)) return this.distanceToPoint(A, false, filters)
59
106
  const { vertices } = this
60
107
  let nearest: Vec | undefined
61
108
  let dist = Infinity
@@ -73,10 +120,35 @@ export abstract class Geometry2d {
73
120
  return this.isClosed && this.isFilled && pointInPolygon(nearest, this.vertices) ? -dist : dist
74
121
  }
75
122
 
76
- hitTestLineSegment(A: Vec, B: Vec, distance = 0): boolean {
77
- return this.distanceToLineSegment(A, B) <= distance
123
+ hitTestLineSegment(A: Vec, B: Vec, distance = 0, filters?: Geometry2dFilters): boolean {
124
+ return this.distanceToLineSegment(A, B, filters) <= distance
125
+ }
126
+
127
+ intersectLineSegment(A: VecLike, B: VecLike, _filters?: Geometry2dFilters): VecLike[] {
128
+ const intersections = this.isClosed
129
+ ? intersectLineSegmentPolygon(A, B, this.vertices)
130
+ : intersectLineSegmentPolyline(A, B, this.vertices)
131
+
132
+ return intersections ?? []
78
133
  }
79
134
 
135
+ intersectCircle(center: VecLike, radius: number, _filters?: Geometry2dFilters): VecLike[] {
136
+ const intersections = this.isClosed
137
+ ? intersectCirclePolygon(center, radius, this.vertices)
138
+ : intersectCirclePolyline(center, radius, this.vertices)
139
+
140
+ return intersections ?? []
141
+ }
142
+
143
+ intersectPolygon(polygon: VecLike[], _filters?: Geometry2dFilters): VecLike[] {
144
+ return intersectPolys(polygon, this.vertices, true, this.isClosed)
145
+ }
146
+
147
+ intersectPolyline(polyline: VecLike[], _filters?: Geometry2dFilters): VecLike[] {
148
+ return intersectPolys(polyline, this.vertices, false, this.isClosed)
149
+ }
150
+
151
+ /** @deprecated Iterate the vertices instead. */
80
152
  nearestPointOnLineSegment(A: Vec, B: Vec): Vec {
81
153
  const { vertices } = this
82
154
  let nearest: Vec | undefined
@@ -105,12 +177,16 @@ export abstract class Geometry2d {
105
177
  )
106
178
  }
107
179
 
180
+ transform(transform: MatModel, opts?: TransformedGeometry2dOptions): Geometry2d {
181
+ return new TransformedGeometry2d(this, transform, opts)
182
+ }
183
+
108
184
  private _vertices: Vec[] | undefined
109
185
 
110
186
  // eslint-disable-next-line no-restricted-syntax
111
187
  get vertices(): Vec[] {
112
188
  if (!this._vertices) {
113
- this._vertices = this.getVertices()
189
+ this._vertices = this.getVertices(Geometry2dFilters.EXCLUDE_LABELS)
114
190
  }
115
191
 
116
192
  return this._vertices
@@ -204,3 +280,124 @@ export abstract class Geometry2d {
204
280
 
205
281
  abstract getSvgPathData(first: boolean): string
206
282
  }
283
+
284
+ // =================================================================================================
285
+ // Because Geometry2d.transform depends on TransformedGeometry2d, we need to define it here instead
286
+ // of in its own files. This prevents a circular import error.
287
+ // =================================================================================================
288
+
289
+ /** @public */
290
+ export class TransformedGeometry2d extends Geometry2d {
291
+ private readonly inverse: MatModel
292
+ private readonly decomposed
293
+
294
+ constructor(
295
+ private readonly geometry: Geometry2d,
296
+ private readonly matrix: MatModel,
297
+ opts?: TransformedGeometry2dOptions
298
+ ) {
299
+ super(geometry)
300
+ this.inverse = Mat.Inverse(matrix)
301
+ this.decomposed = Mat.Decompose(matrix)
302
+
303
+ if (opts) {
304
+ if (opts.isLabel != null) this.isLabel = opts.isLabel
305
+ if (opts.isInternal != null) this.isInternal = opts.isInternal
306
+ if (opts.debugColor != null) this.debugColor = opts.debugColor
307
+ if (opts.ignore != null) this.ignore = opts.ignore
308
+ }
309
+
310
+ assert(
311
+ approximately(this.decomposed.scaleX, this.decomposed.scaleY),
312
+ 'non-uniform scaling is not yet supported'
313
+ )
314
+ }
315
+
316
+ getVertices(filters: Geometry2dFilters): Vec[] {
317
+ return this.geometry.getVertices(filters).map((v) => Mat.applyToPoint(this.matrix, v))
318
+ }
319
+
320
+ nearestPoint(point: Vec, filters?: Geometry2dFilters): Vec {
321
+ return Mat.applyToPoint(
322
+ this.matrix,
323
+ this.geometry.nearestPoint(Mat.applyToPoint(this.inverse, point), filters)
324
+ )
325
+ }
326
+
327
+ override hitTestPoint(
328
+ point: Vec,
329
+ margin = 0,
330
+ hitInside?: boolean,
331
+ filters?: Geometry2dFilters
332
+ ): boolean {
333
+ return this.geometry.hitTestPoint(
334
+ Mat.applyToPoint(this.inverse, point),
335
+ margin / this.decomposed.scaleX,
336
+ hitInside,
337
+ filters
338
+ )
339
+ }
340
+
341
+ override distanceToPoint(point: Vec, hitInside = false, filters?: Geometry2dFilters) {
342
+ return (
343
+ this.geometry.distanceToPoint(Mat.applyToPoint(this.inverse, point), hitInside, filters) *
344
+ this.decomposed.scaleX
345
+ )
346
+ }
347
+
348
+ override distanceToLineSegment(A: Vec, B: Vec, filters?: Geometry2dFilters) {
349
+ return (
350
+ this.geometry.distanceToLineSegment(
351
+ Mat.applyToPoint(this.inverse, A),
352
+ Mat.applyToPoint(this.inverse, B),
353
+ filters
354
+ ) * this.decomposed.scaleX
355
+ )
356
+ }
357
+
358
+ override hitTestLineSegment(A: Vec, B: Vec, distance = 0, filters?: Geometry2dFilters): boolean {
359
+ return this.geometry.hitTestLineSegment(
360
+ Mat.applyToPoint(this.inverse, A),
361
+ Mat.applyToPoint(this.inverse, B),
362
+ distance / this.decomposed.scaleX,
363
+ filters
364
+ )
365
+ }
366
+
367
+ override intersectLineSegment(A: VecLike, B: VecLike, filters?: Geometry2dFilters) {
368
+ return this.geometry.intersectLineSegment(
369
+ Mat.applyToPoint(this.inverse, A),
370
+ Mat.applyToPoint(this.inverse, B),
371
+ filters
372
+ )
373
+ }
374
+
375
+ override intersectCircle(center: VecLike, radius: number, filters?: Geometry2dFilters) {
376
+ return this.geometry.intersectCircle(
377
+ Mat.applyToPoint(this.inverse, center),
378
+ radius / this.decomposed.scaleX,
379
+ filters
380
+ )
381
+ }
382
+
383
+ override intersectPolygon(polygon: VecLike[], filters?: Geometry2dFilters): VecLike[] {
384
+ return this.geometry.intersectPolygon(Mat.applyToPoints(this.inverse, polygon), filters)
385
+ }
386
+
387
+ override intersectPolyline(polyline: VecLike[], filters?: Geometry2dFilters): VecLike[] {
388
+ return this.geometry.intersectPolyline(Mat.applyToPoints(this.inverse, polyline), filters)
389
+ }
390
+
391
+ override transform(transform: MatModel, opts?: TransformedGeometry2dOptions): Geometry2d {
392
+ return new TransformedGeometry2d(this.geometry, Mat.Multiply(transform, this.matrix), {
393
+ isLabel: opts?.isLabel ?? this.isLabel,
394
+ isInternal: opts?.isInternal ?? this.isInternal,
395
+ debugColor: opts?.debugColor ?? this.debugColor,
396
+ ignore: opts?.ignore ?? this.ignore,
397
+ })
398
+ }
399
+
400
+ getSvgPathData(): string {
401
+ throw new Error('Cannot get SVG path data for transformed geometry.')
402
+ }
403
+ }
@@ -1,6 +1,8 @@
1
+ import { EMPTY_ARRAY } from '@tldraw/state'
1
2
  import { Box } from '../Box'
2
- import { Vec } from '../Vec'
3
- import { Geometry2d, Geometry2dOptions } from './Geometry2d'
3
+ import { Mat } from '../Mat'
4
+ import { Vec, VecLike } from '../Vec'
5
+ import { Geometry2d, Geometry2dFilters, Geometry2dOptions } from './Geometry2d'
4
6
 
5
7
  /** @public */
6
8
  export class Group2d extends Geometry2d {
@@ -25,11 +27,14 @@ export class Group2d extends Geometry2d {
25
27
  if (this.children.length === 0) throw Error('Group2d must have at least one child')
26
28
  }
27
29
 
28
- override getVertices(): Vec[] {
29
- return this.children.filter((c) => !c.isLabel).flatMap((c) => c.vertices)
30
+ override getVertices(filters: Geometry2dFilters): Vec[] {
31
+ if (this.isExcludedByFilter(filters)) return []
32
+ return this.children
33
+ .filter((c) => !c.isExcludedByFilter(filters))
34
+ .flatMap((c) => c.getVertices(filters))
30
35
  }
31
36
 
32
- override nearestPoint(point: Vec): Vec {
37
+ override nearestPoint(point: Vec, filters?: Geometry2dFilters): Vec {
33
38
  let dist = Infinity
34
39
  let nearest: Vec | undefined
35
40
 
@@ -42,7 +47,8 @@ export class Group2d extends Geometry2d {
42
47
  let p: Vec
43
48
  let d: number
44
49
  for (const child of children) {
45
- p = child.nearestPoint(point)
50
+ if (child.isExcludedByFilter(filters)) continue
51
+ p = child.nearestPoint(point, filters)
46
52
  d = Vec.Dist2(p, point)
47
53
  if (d < dist) {
48
54
  dist = d
@@ -53,18 +59,75 @@ export class Group2d extends Geometry2d {
53
59
  return nearest
54
60
  }
55
61
 
56
- override distanceToPoint(point: Vec, hitInside = false) {
57
- return Math.min(...this.children.map((c, i) => c.distanceToPoint(point, hitInside || i > 0)))
62
+ override distanceToPoint(point: Vec, hitInside = false, filters?: Geometry2dFilters) {
63
+ let smallestDistance = Infinity
64
+ for (const child of this.children) {
65
+ if (child.isExcludedByFilter(filters)) continue
66
+ const distance = child.distanceToPoint(point, hitInside, filters)
67
+ if (distance < smallestDistance) {
68
+ smallestDistance = distance
69
+ }
70
+ }
71
+ return smallestDistance
58
72
  }
59
73
 
60
- override hitTestPoint(point: Vec, margin: number, hitInside: boolean): boolean {
74
+ override hitTestPoint(
75
+ point: Vec,
76
+ margin: number,
77
+ hitInside: boolean,
78
+ filters = Geometry2dFilters.EXCLUDE_LABELS
79
+ ): boolean {
61
80
  return !!this.children
62
- .filter((c) => !c.isLabel)
81
+ .filter((c) => !c.isExcludedByFilter(filters))
63
82
  .find((c) => c.hitTestPoint(point, margin, hitInside))
64
83
  }
65
84
 
66
- override hitTestLineSegment(A: Vec, B: Vec, zoom: number): boolean {
67
- return !!this.children.filter((c) => !c.isLabel).find((c) => c.hitTestLineSegment(A, B, zoom))
85
+ override hitTestLineSegment(
86
+ A: Vec,
87
+ B: Vec,
88
+ zoom: number,
89
+ filters = Geometry2dFilters.EXCLUDE_LABELS
90
+ ): boolean {
91
+ return !!this.children
92
+ .filter((c) => !c.isExcludedByFilter(filters))
93
+ .find((c) => c.hitTestLineSegment(A, B, zoom))
94
+ }
95
+
96
+ override intersectLineSegment(A: VecLike, B: VecLike, filters?: Geometry2dFilters) {
97
+ return this.children.flatMap((child) => {
98
+ if (child.isExcludedByFilter(filters)) return EMPTY_ARRAY
99
+ return child.intersectLineSegment(A, B, filters)
100
+ })
101
+ }
102
+
103
+ override intersectCircle(center: VecLike, radius: number, filters?: Geometry2dFilters) {
104
+ return this.children.flatMap((child) => {
105
+ if (child.isExcludedByFilter(filters)) return EMPTY_ARRAY
106
+ return child.intersectCircle(center, radius, filters)
107
+ })
108
+ }
109
+
110
+ override intersectPolygon(polygon: VecLike[], filters?: Geometry2dFilters) {
111
+ return this.children.flatMap((child) => {
112
+ if (child.isExcludedByFilter(filters)) return EMPTY_ARRAY
113
+ return child.intersectPolygon(polygon, filters)
114
+ })
115
+ }
116
+
117
+ override intersectPolyline(polyline: VecLike[], filters?: Geometry2dFilters) {
118
+ return this.children.flatMap((child) => {
119
+ if (child.isExcludedByFilter(filters)) return EMPTY_ARRAY
120
+ return child.intersectPolyline(polyline, filters)
121
+ })
122
+ }
123
+
124
+ override transform(transform: Mat): Geometry2d {
125
+ return new Group2d({
126
+ children: this.children.map((c) => c.transform(transform)),
127
+ isLabel: this.isLabel,
128
+ debugColor: this.debugColor,
129
+ ignore: this.ignore,
130
+ })
68
131
  }
69
132
 
70
133
  getArea() {
@@ -102,6 +165,6 @@ export class Group2d extends Geometry2d {
102
165
  }
103
166
 
104
167
  getSvgPathData(): string {
105
- return this.children.map((c) => (c.isLabel ? '' : c.getSvgPathData(true))).join(' ')
168
+ return this.children.map((c, i) => (c.isLabel ? '' : c.getSvgPathData(i === 0))).join(' ')
106
169
  }
107
170
  }
@@ -293,6 +293,47 @@ export function intersectPolygonPolygon(
293
293
  return orderClockwise([...result.values()])
294
294
  }
295
295
 
296
+ /**
297
+ * Find all the points where `polyA` and `polyB` intersect and returns them in an undefined order.
298
+ * To find the polygon that's the intersection of polyA and polyB, use `intersectPolygonPolygon`
299
+ * instead, which orders the points and includes internal points.
300
+ *
301
+ * @param polyA - The first polygon.
302
+ * @param polyB - The second polygon.
303
+ * @param isAClosed - Whether `polyA` is a closed polygon or a polyline.
304
+ * @param isBClosed - Whether `polyB` is a closed polygon or a polyline.
305
+ * @public
306
+ */
307
+ export function intersectPolys(
308
+ polyA: VecLike[],
309
+ polyB: VecLike[],
310
+ isAClosed: boolean,
311
+ isBClosed: boolean
312
+ ): VecLike[] {
313
+ const result: Map<string, VecLike> = new Map()
314
+
315
+ // Add all intersection points to result
316
+ for (let i = 0, n = isAClosed ? polyA.length : polyA.length - 1; i < n; i++) {
317
+ const currentA = polyA[i]
318
+ const nextA = polyA[(i + 1) % polyA.length]
319
+
320
+ for (let j = 0, m = isBClosed ? polyB.length : polyB.length - 1; j < m; j++) {
321
+ const currentB = polyB[j]
322
+ const nextB = polyB[(j + 1) % polyB.length]
323
+ const intersection = intersectLineSegmentLineSegment(currentA, nextA, currentB, nextB)
324
+
325
+ if (intersection !== null) {
326
+ const id = getPointId(intersection)
327
+ if (!result.has(id)) {
328
+ result.set(id, intersection)
329
+ }
330
+ }
331
+ }
332
+ }
333
+
334
+ return [...result.values()]
335
+ }
336
+
296
337
  function getPointId(point: VecLike) {
297
338
  return `${point.x},${point.y}`
298
339
  }