@tldraw/editor 3.12.0-canary.fc675f45caa0 → 3.12.0-canary.fff04afa92e2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/dist-cjs/index.d.ts +153 -18
  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/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/exports/StyleEmbedder.js +19 -5
  22. package/dist-cjs/lib/exports/StyleEmbedder.js.map +2 -2
  23. package/dist-cjs/lib/exports/cssRules.js +127 -0
  24. package/dist-cjs/lib/exports/cssRules.js.map +7 -0
  25. package/dist-cjs/lib/exports/parseCss.js +0 -69
  26. package/dist-cjs/lib/exports/parseCss.js.map +2 -2
  27. package/dist-cjs/lib/hooks/useCanvasEvents.js +8 -13
  28. package/dist-cjs/lib/hooks/useCanvasEvents.js.map +3 -3
  29. package/dist-cjs/lib/hooks/useDocumentEvents.js +16 -0
  30. package/dist-cjs/lib/hooks/useDocumentEvents.js.map +2 -2
  31. package/dist-cjs/lib/license/Watermark.js +10 -20
  32. package/dist-cjs/lib/license/Watermark.js.map +2 -2
  33. package/dist-cjs/lib/primitives/geometry/Geometry2d.js +133 -16
  34. package/dist-cjs/lib/primitives/geometry/Geometry2d.js.map +3 -3
  35. package/dist-cjs/lib/primitives/geometry/Group2d.js +54 -11
  36. package/dist-cjs/lib/primitives/geometry/Group2d.js.map +2 -2
  37. package/dist-cjs/lib/primitives/intersect.js +20 -0
  38. package/dist-cjs/lib/primitives/intersect.js.map +2 -2
  39. package/dist-cjs/lib/utils/reorderShapes.js +2 -8
  40. package/dist-cjs/lib/utils/reorderShapes.js.map +2 -2
  41. package/dist-cjs/version.js +3 -3
  42. package/dist-cjs/version.js.map +1 -1
  43. package/dist-esm/index.d.mts +153 -18
  44. package/dist-esm/index.mjs +8 -2
  45. package/dist-esm/index.mjs.map +2 -2
  46. package/dist-esm/lib/TldrawEditor.mjs +5 -0
  47. package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
  48. package/dist-esm/lib/components/GeometryDebuggingView.mjs +3 -3
  49. package/dist-esm/lib/components/GeometryDebuggingView.mjs.map +2 -2
  50. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +10 -1
  51. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
  52. package/dist-esm/lib/editor/Editor.mjs +209 -18
  53. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  54. package/dist-esm/lib/editor/managers/FocusManager.mjs +1 -1
  55. package/dist-esm/lib/editor/managers/FocusManager.mjs.map +2 -2
  56. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +12 -0
  57. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
  58. package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs +4 -13
  59. package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs.map +2 -2
  60. package/dist-esm/lib/editor/tools/StateNode.mjs +1 -4
  61. package/dist-esm/lib/editor/tools/StateNode.mjs.map +2 -2
  62. package/dist-esm/lib/exports/StyleEmbedder.mjs +21 -12
  63. package/dist-esm/lib/exports/StyleEmbedder.mjs.map +2 -2
  64. package/dist-esm/lib/exports/cssRules.mjs +107 -0
  65. package/dist-esm/lib/exports/cssRules.mjs.map +7 -0
  66. package/dist-esm/lib/exports/parseCss.mjs +0 -69
  67. package/dist-esm/lib/exports/parseCss.mjs.map +2 -2
  68. package/dist-esm/lib/hooks/useCanvasEvents.mjs +8 -13
  69. package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +3 -3
  70. package/dist-esm/lib/hooks/useDocumentEvents.mjs +16 -0
  71. package/dist-esm/lib/hooks/useDocumentEvents.mjs.map +2 -2
  72. package/dist-esm/lib/license/Watermark.mjs +10 -20
  73. package/dist-esm/lib/license/Watermark.mjs.map +2 -2
  74. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs +137 -14
  75. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs.map +2 -2
  76. package/dist-esm/lib/primitives/geometry/Group2d.mjs +55 -12
  77. package/dist-esm/lib/primitives/geometry/Group2d.mjs.map +2 -2
  78. package/dist-esm/lib/primitives/intersect.mjs +20 -0
  79. package/dist-esm/lib/primitives/intersect.mjs.map +2 -2
  80. package/dist-esm/lib/utils/reorderShapes.mjs +2 -8
  81. package/dist-esm/lib/utils/reorderShapes.mjs.map +2 -2
  82. package/dist-esm/version.mjs +3 -3
  83. package/dist-esm/version.mjs.map +1 -1
  84. package/editor.css +34 -19
  85. package/package.json +7 -7
  86. package/src/index.ts +11 -2
  87. package/src/lib/TldrawEditor.tsx +28 -1
  88. package/src/lib/components/GeometryDebuggingView.tsx +3 -3
  89. package/src/lib/components/default-components/DefaultCanvas.tsx +6 -1
  90. package/src/lib/editor/Editor.ts +315 -24
  91. package/src/lib/editor/managers/FocusManager.ts +1 -1
  92. package/src/lib/editor/shapes/ShapeUtil.ts +14 -0
  93. package/src/lib/editor/shapes/group/GroupShapeUtil.tsx +7 -15
  94. package/src/lib/editor/tools/StateNode.ts +1 -6
  95. package/src/lib/editor/types/selection-types.ts +3 -0
  96. package/src/lib/exports/StyleEmbedder.ts +25 -15
  97. package/src/lib/exports/cssRules.ts +126 -0
  98. package/src/lib/exports/parseCss.ts +0 -79
  99. package/src/lib/hooks/useCanvasEvents.ts +8 -15
  100. package/src/lib/hooks/useDocumentEvents.ts +18 -0
  101. package/src/lib/license/Watermark.tsx +18 -29
  102. package/src/lib/primitives/geometry/Geometry2d.ts +196 -16
  103. package/src/lib/primitives/geometry/Group2d.ts +76 -13
  104. package/src/lib/primitives/intersect.ts +41 -0
  105. package/src/lib/utils/reorderShapes.ts +2 -9
  106. package/src/version.ts +3 -3
@@ -0,0 +1,126 @@
1
+ export type Styles = { [K in string]?: string }
2
+ export type ReadonlyStyles = { readonly [K in string]?: string }
3
+
4
+ type CanSkipRule = (
5
+ value: string,
6
+ property: string,
7
+ options: {
8
+ getStyle(property: string): string
9
+ parentStyles: ReadonlyStyles
10
+ defaultStyles: ReadonlyStyles
11
+ currentColor: string
12
+ }
13
+ ) => boolean
14
+
15
+ const isCoveredByCurrentColor: CanSkipRule = (value, property, { currentColor }) => {
16
+ return value === 'currentColor' || value === currentColor
17
+ }
18
+
19
+ const isInherited: CanSkipRule = (value, property, { parentStyles }) => {
20
+ return parentStyles[property] === value
21
+ }
22
+
23
+ // see comment below about why we exclude border styles
24
+ const isExcludedBorder =
25
+ (borderDirection: string): CanSkipRule =>
26
+ (value, property, { getStyle }) => {
27
+ const borderWidth = getStyle(`border-${borderDirection}-width`)
28
+ const borderStyle = getStyle(`border-${borderDirection}-style`)
29
+
30
+ if (borderWidth === '0px') return true
31
+ if (borderStyle === 'none') return true
32
+ return false
33
+ }
34
+
35
+ export const cssRules = {
36
+ // currentColor properties:
37
+ 'border-block-end-color': isCoveredByCurrentColor,
38
+ 'border-block-start-color': isCoveredByCurrentColor,
39
+ 'border-bottom-color': isCoveredByCurrentColor,
40
+ 'border-inline-end-color': isCoveredByCurrentColor,
41
+ 'border-inline-start-color': isCoveredByCurrentColor,
42
+ 'border-left-color': isCoveredByCurrentColor,
43
+ 'border-right-color': isCoveredByCurrentColor,
44
+ 'border-top-color': isCoveredByCurrentColor,
45
+ 'caret-color': isCoveredByCurrentColor,
46
+ 'column-rule-color': isCoveredByCurrentColor,
47
+ 'outline-color': isCoveredByCurrentColor,
48
+ 'text-decoration': (value, property, { currentColor }) => {
49
+ return value === 'none solid currentColor' || value === 'none solid ' + currentColor
50
+ },
51
+ 'text-decoration-color': isCoveredByCurrentColor,
52
+ 'text-emphasis-color': isCoveredByCurrentColor,
53
+
54
+ // inherited properties:
55
+ 'border-collapse': isInherited,
56
+ 'border-spacing': isInherited,
57
+ 'caption-side': isInherited,
58
+ // N.B. We shouldn't inherit 'color' because there's some UA styling, e.g. `mark` elements
59
+ // 'color': isInherited,
60
+ cursor: isInherited,
61
+ direction: isInherited,
62
+ 'empty-cells': isInherited,
63
+ 'font-family': isInherited,
64
+ 'font-size': isInherited,
65
+ 'font-style': isInherited,
66
+ 'font-variant': isInherited,
67
+ 'font-weight': isInherited,
68
+ 'font-size-adjust': isInherited,
69
+ 'font-stretch': isInherited,
70
+ font: isInherited,
71
+ 'letter-spacing': isInherited,
72
+ 'line-height': isInherited,
73
+ 'list-style-image': isInherited,
74
+ 'list-style-position': isInherited,
75
+ 'list-style-type': isInherited,
76
+ 'list-style': isInherited,
77
+ orphans: isInherited,
78
+ 'overflow-wrap': isInherited,
79
+ quotes: isInherited,
80
+ 'stroke-linecap': isInherited,
81
+ 'stroke-linejoin': isInherited,
82
+ 'tab-size': isInherited,
83
+ 'text-align': isInherited,
84
+ 'text-align-last': isInherited,
85
+ 'text-indent': isInherited,
86
+ 'text-justify': isInherited,
87
+ 'text-shadow': isInherited,
88
+ 'text-transform': isInherited,
89
+ visibility: isInherited,
90
+ 'white-space': isInherited,
91
+ 'white-space-collapse': isInherited,
92
+ widows: isInherited,
93
+ 'word-break': isInherited,
94
+ 'word-spacing': isInherited,
95
+ 'word-wrap': isInherited,
96
+
97
+ // special border cases - we have a weird case (tailwind seems to trigger this) where all
98
+ // border-styles sometimes get set to 'solid', but the border-width is 0 so they don't render.
99
+ // but in SVGs, **sometimes**, the border-width defaults (i think from a UA style-sheet? but
100
+ // honestly can't tell) to 1.5px so the border displays. we work around this by only including
101
+ // border styles at all if both the border-width and border-style are set to something that
102
+ // would show a border.
103
+ 'border-top': isExcludedBorder('top'),
104
+ 'border-right': isExcludedBorder('right'),
105
+ 'border-bottom': isExcludedBorder('bottom'),
106
+ 'border-left': isExcludedBorder('left'),
107
+ 'border-block-end': isExcludedBorder('block-end'),
108
+ 'border-block-start': isExcludedBorder('block-start'),
109
+ 'border-inline-end': isExcludedBorder('inline-end'),
110
+ 'border-inline-start': isExcludedBorder('inline-start'),
111
+ 'border-top-style': isExcludedBorder('top'),
112
+ 'border-right-style': isExcludedBorder('right'),
113
+ 'border-bottom-style': isExcludedBorder('bottom'),
114
+ 'border-left-style': isExcludedBorder('left'),
115
+ 'border-block-end-style': isExcludedBorder('block-end'),
116
+ 'border-block-start-style': isExcludedBorder('block-start'),
117
+ 'border-inline-end-style': isExcludedBorder('inline-end'),
118
+ 'border-inline-start-style': isExcludedBorder('inline-start'),
119
+ 'border-top-width': isExcludedBorder('top'),
120
+ 'border-right-width': isExcludedBorder('right'),
121
+ 'border-bottom-width': isExcludedBorder('bottom'),
122
+ 'border-left-width': isExcludedBorder('left'),
123
+ 'border-block-end-width': isExcludedBorder('block-end'),
124
+ 'border-block-start-width': isExcludedBorder('block-start'),
125
+ 'border-inline-end-width': isExcludedBorder('inline-end'),
126
+ } satisfies Record<string, CanSkipRule>
@@ -110,82 +110,3 @@ export function parseCssValueUrls(value: string) {
110
110
  url: m[1] || m[2] || m[3],
111
111
  }))
112
112
  }
113
-
114
- const currentColorProperties = new Set([
115
- 'border-block-end-color',
116
- 'border-block-start-color',
117
- 'border-bottom-color',
118
- 'border-inline-end-color',
119
- 'border-inline-start-color',
120
- 'border-left-color',
121
- 'border-right-color',
122
- 'border-top-color',
123
- 'caret-color',
124
- 'column-rule-color',
125
- 'outline-color',
126
- 'text-decoration',
127
- 'text-decoration-color',
128
- 'text-emphasis-color',
129
- ])
130
-
131
- export function isPropertyCoveredByCurrentColor(
132
- currentColor: string,
133
- property: string,
134
- value: string
135
- ) {
136
- if (currentColorProperties.has(property)) {
137
- return (
138
- value === 'currentColor' ||
139
- value === currentColor ||
140
- (property === 'text-decoration' && value === `none solid ${currentColor}`)
141
- )
142
- }
143
- }
144
-
145
- const inheritedProperties = new Set([
146
- 'border-collapse',
147
- 'border-spacing',
148
- 'caption-side',
149
- // N.B. We shouldn't inherit 'color' because there's some UA styling, e.g. `mark` elements
150
- // 'color',
151
- 'cursor',
152
- 'direction',
153
- 'empty-cells',
154
- 'font-family',
155
- 'font-size',
156
- 'font-style',
157
- 'font-variant',
158
- 'font-weight',
159
- 'font-size-adjust',
160
- 'font-stretch',
161
- 'font',
162
- 'letter-spacing',
163
- 'line-height',
164
- 'list-style-image',
165
- 'list-style-position',
166
- 'list-style-type',
167
- 'list-style',
168
- 'orphans',
169
- 'overflow-wrap',
170
- 'quotes',
171
- 'stroke-linecap',
172
- 'stroke-linejoin',
173
- 'tab-size',
174
- 'text-align',
175
- 'text-align-last',
176
- 'text-indent',
177
- 'text-justify',
178
- 'text-shadow',
179
- 'text-transform',
180
- 'visibility',
181
- 'white-space',
182
- 'white-space-collapse',
183
- 'widows',
184
- 'word-break',
185
- 'word-spacing',
186
- 'word-wrap',
187
- ])
188
-
189
- export function isPropertyInherited(property: string) {
190
- return inheritedProperties.has(property)
191
- }
@@ -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
  }
@@ -1,12 +1,44 @@
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
+ /** @public */
15
+ export interface Geometry2dFilters {
16
+ readonly includeLabels?: boolean
17
+ readonly includeInternal?: boolean
18
+ }
19
+
20
+ /** @public */
21
+ export const Geometry2dFilters: {
22
+ EXCLUDE_NON_STANDARD: Geometry2dFilters
23
+ INCLUDE_ALL: Geometry2dFilters
24
+ EXCLUDE_LABELS: Geometry2dFilters
25
+ EXCLUDE_INTERNAL: Geometry2dFilters
26
+ } = {
27
+ EXCLUDE_NON_STANDARD: {
28
+ includeLabels: false,
29
+ includeInternal: false,
30
+ },
31
+ INCLUDE_ALL: { includeLabels: true, includeInternal: true },
32
+ EXCLUDE_LABELS: { includeLabels: false, includeInternal: true },
33
+ EXCLUDE_INTERNAL: { includeLabels: true, includeInternal: false },
34
+ }
4
35
 
5
36
  /** @public */
6
37
  export interface Geometry2dOptions {
7
38
  isFilled: boolean
8
39
  isClosed: boolean
9
40
  isLabel?: boolean
41
+ isInternal?: boolean
10
42
  debugColor?: string
11
43
  ignore?: boolean
12
44
  }
@@ -16,6 +48,7 @@ export abstract class Geometry2d {
16
48
  isFilled = false
17
49
  isClosed = true
18
50
  isLabel = false
51
+ isInternal = false
19
52
  debugColor?: string
20
53
  ignore?: boolean
21
54
 
@@ -23,20 +56,24 @@ export abstract class Geometry2d {
23
56
  this.isFilled = opts.isFilled
24
57
  this.isClosed = opts.isClosed
25
58
  this.isLabel = opts.isLabel ?? false
59
+ this.isInternal = opts.isInternal ?? false
26
60
  this.debugColor = opts.debugColor
27
61
  this.ignore = opts.ignore
28
62
  }
29
63
 
30
- abstract getVertices(): Vec[]
64
+ isExcludedByFilter(filters?: Geometry2dFilters) {
65
+ if (!filters) return false
66
+ if (this.isLabel && !filters.includeLabels) return true
67
+ if (this.isInternal && !filters.includeInternal) return true
68
+ return false
69
+ }
31
70
 
32
- abstract nearestPoint(point: Vec): Vec
71
+ abstract getVertices(filters: Geometry2dFilters): Vec[]
33
72
 
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
- // }
73
+ abstract nearestPoint(point: Vec, filters?: Geometry2dFilters): Vec
38
74
 
39
- hitTestPoint(point: Vec, margin = 0, hitInside = false) {
75
+ hitTestPoint(point: Vec, margin = 0, hitInside = false, filters?: Geometry2dFilters) {
76
+ if (this.isExcludedByFilter(filters)) return false
40
77
  // First check whether the point is inside
41
78
  if (this.isClosed && (this.isFilled || hitInside) && pointInPolygon(point, this.vertices)) {
42
79
  return true
@@ -45,17 +82,17 @@ export abstract class Geometry2d {
45
82
  return Vec.Dist2(point, this.nearestPoint(point)) <= margin * margin
46
83
  }
47
84
 
48
- distanceToPoint(point: Vec, hitInside = false) {
85
+ distanceToPoint(point: Vec, hitInside = false, filters?: Geometry2dFilters) {
49
86
  return (
50
- point.dist(this.nearestPoint(point)) *
87
+ point.dist(this.nearestPoint(point, filters)) *
51
88
  (this.isClosed && (this.isFilled || hitInside) && pointInPolygon(point, this.vertices)
52
89
  ? -1
53
90
  : 1)
54
91
  )
55
92
  }
56
93
 
57
- distanceToLineSegment(A: Vec, B: Vec) {
58
- if (A.equals(B)) return this.distanceToPoint(A)
94
+ distanceToLineSegment(A: Vec, B: Vec, filters?: Geometry2dFilters) {
95
+ if (A.equals(B)) return this.distanceToPoint(A, false, filters)
59
96
  const { vertices } = this
60
97
  let nearest: Vec | undefined
61
98
  let dist = Infinity
@@ -73,10 +110,41 @@ export abstract class Geometry2d {
73
110
  return this.isClosed && this.isFilled && pointInPolygon(nearest, this.vertices) ? -dist : dist
74
111
  }
75
112
 
76
- hitTestLineSegment(A: Vec, B: Vec, distance = 0): boolean {
77
- return this.distanceToLineSegment(A, B) <= distance
113
+ hitTestLineSegment(A: Vec, B: Vec, distance = 0, filters?: Geometry2dFilters): boolean {
114
+ return this.distanceToLineSegment(A, B, filters) <= distance
115
+ }
116
+
117
+ intersectLineSegment(A: VecLike, B: VecLike, filters?: Geometry2dFilters): VecLike[] {
118
+ if (this.isExcludedByFilter(filters)) return []
119
+
120
+ const intersections = this.isClosed
121
+ ? intersectLineSegmentPolygon(A, B, this.vertices)
122
+ : intersectLineSegmentPolyline(A, B, this.vertices)
123
+
124
+ return intersections ?? []
125
+ }
126
+
127
+ intersectCircle(center: VecLike, radius: number, filters?: Geometry2dFilters): VecLike[] {
128
+ if (this.isExcludedByFilter(filters)) return []
129
+ const intersections = this.isClosed
130
+ ? intersectCirclePolygon(center, radius, this.vertices)
131
+ : intersectCirclePolyline(center, radius, this.vertices)
132
+
133
+ return intersections ?? []
134
+ }
135
+
136
+ intersectPolygon(polygon: VecLike[], filters?: Geometry2dFilters): VecLike[] {
137
+ if (this.isExcludedByFilter(filters)) return []
138
+
139
+ return intersectPolys(polygon, this.vertices, true, this.isClosed)
140
+ }
141
+
142
+ intersectPolyline(polyline: VecLike[], filters?: Geometry2dFilters): VecLike[] {
143
+ if (this.isExcludedByFilter(filters)) return []
144
+ return intersectPolys(polyline, this.vertices, false, this.isClosed)
78
145
  }
79
146
 
147
+ /** @deprecated Iterate the vertices instead. */
80
148
  nearestPointOnLineSegment(A: Vec, B: Vec): Vec {
81
149
  const { vertices } = this
82
150
  let nearest: Vec | undefined
@@ -105,12 +173,16 @@ export abstract class Geometry2d {
105
173
  )
106
174
  }
107
175
 
176
+ transform(transform: MatModel): Geometry2d {
177
+ return new TransformedGeometry2d(this, transform)
178
+ }
179
+
108
180
  private _vertices: Vec[] | undefined
109
181
 
110
182
  // eslint-disable-next-line no-restricted-syntax
111
183
  get vertices(): Vec[] {
112
184
  if (!this._vertices) {
113
- this._vertices = this.getVertices()
185
+ this._vertices = this.getVertices(Geometry2dFilters.EXCLUDE_LABELS)
114
186
  }
115
187
 
116
188
  return this._vertices
@@ -204,3 +276,111 @@ export abstract class Geometry2d {
204
276
 
205
277
  abstract getSvgPathData(first: boolean): string
206
278
  }
279
+
280
+ // =================================================================================================
281
+ // Because Geometry2d.transform depends on TransformedGeometry2d, we need to define it here instead
282
+ // of in its own files. This prevents a circular import error.
283
+ // =================================================================================================
284
+
285
+ /** @public */
286
+ export class TransformedGeometry2d extends Geometry2d {
287
+ private readonly inverse: MatModel
288
+ private readonly decomposed
289
+
290
+ constructor(
291
+ private readonly geometry: Geometry2d,
292
+ private readonly matrix: MatModel
293
+ ) {
294
+ super(geometry)
295
+ this.inverse = Mat.Inverse(matrix)
296
+ this.decomposed = Mat.Decompose(matrix)
297
+
298
+ assert(
299
+ approximately(this.decomposed.scaleX, this.decomposed.scaleY),
300
+ 'non-uniform scaling is not yet supported'
301
+ )
302
+ }
303
+
304
+ getVertices(filters: Geometry2dFilters): Vec[] {
305
+ return this.geometry.getVertices(filters).map((v) => Mat.applyToPoint(this.matrix, v))
306
+ }
307
+
308
+ nearestPoint(point: Vec, filters?: Geometry2dFilters): Vec {
309
+ return Mat.applyToPoint(
310
+ this.matrix,
311
+ this.geometry.nearestPoint(Mat.applyToPoint(this.inverse, point), filters)
312
+ )
313
+ }
314
+
315
+ override hitTestPoint(
316
+ point: Vec,
317
+ margin = 0,
318
+ hitInside?: boolean,
319
+ filters?: Geometry2dFilters
320
+ ): boolean {
321
+ return this.geometry.hitTestPoint(
322
+ Mat.applyToPoint(this.inverse, point),
323
+ margin / this.decomposed.scaleX,
324
+ hitInside,
325
+ filters
326
+ )
327
+ }
328
+
329
+ override distanceToPoint(point: Vec, hitInside = false, filters?: Geometry2dFilters) {
330
+ return (
331
+ this.geometry.distanceToPoint(Mat.applyToPoint(this.inverse, point), hitInside, filters) *
332
+ this.decomposed.scaleX
333
+ )
334
+ }
335
+
336
+ override distanceToLineSegment(A: Vec, B: Vec, filters?: Geometry2dFilters) {
337
+ return (
338
+ this.geometry.distanceToLineSegment(
339
+ Mat.applyToPoint(this.inverse, A),
340
+ Mat.applyToPoint(this.inverse, B),
341
+ filters
342
+ ) * this.decomposed.scaleX
343
+ )
344
+ }
345
+
346
+ override hitTestLineSegment(A: Vec, B: Vec, distance = 0, filters?: Geometry2dFilters): boolean {
347
+ return this.geometry.hitTestLineSegment(
348
+ Mat.applyToPoint(this.inverse, A),
349
+ Mat.applyToPoint(this.inverse, B),
350
+ distance / this.decomposed.scaleX,
351
+ filters
352
+ )
353
+ }
354
+
355
+ override intersectLineSegment(A: VecLike, B: VecLike, filters?: Geometry2dFilters) {
356
+ return this.geometry.intersectLineSegment(
357
+ Mat.applyToPoint(this.inverse, A),
358
+ Mat.applyToPoint(this.inverse, B),
359
+ filters
360
+ )
361
+ }
362
+
363
+ override intersectCircle(center: VecLike, radius: number, filters?: Geometry2dFilters) {
364
+ return this.geometry.intersectCircle(
365
+ Mat.applyToPoint(this.inverse, center),
366
+ radius / this.decomposed.scaleX,
367
+ filters
368
+ )
369
+ }
370
+
371
+ override intersectPolygon(polygon: VecLike[], filters?: Geometry2dFilters): VecLike[] {
372
+ return this.geometry.intersectPolygon(Mat.applyToPoints(this.inverse, polygon), filters)
373
+ }
374
+
375
+ override intersectPolyline(polyline: VecLike[], filters?: Geometry2dFilters): VecLike[] {
376
+ return this.geometry.intersectPolyline(Mat.applyToPoints(this.inverse, polyline), filters)
377
+ }
378
+
379
+ override transform(transform: MatModel): Geometry2d {
380
+ return new TransformedGeometry2d(this.geometry, Mat.Multiply(transform, this.matrix))
381
+ }
382
+
383
+ getSvgPathData(): string {
384
+ throw new Error('Cannot get SVG path data for transformed geometry.')
385
+ }
386
+ }