@tldraw/editor 3.12.0-canary.5fcdc0f29d81 → 3.12.0-canary.629125a2e474
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.
- package/dist-cjs/index.d.ts +154 -17
- package/dist-cjs/index.js +3 -1
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/TldrawEditor.js +4 -0
- package/dist-cjs/lib/TldrawEditor.js.map +2 -2
- package/dist-cjs/lib/components/GeometryDebuggingView.js +2 -2
- package/dist-cjs/lib/components/GeometryDebuggingView.js.map +2 -2
- package/dist-cjs/lib/editor/Editor.js +215 -30
- package/dist-cjs/lib/editor/Editor.js.map +2 -2
- package/dist-cjs/lib/editor/shapes/ShapeUtil.js +9 -0
- package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
- package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js +4 -13
- package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js.map +2 -2
- package/dist-cjs/lib/editor/types/selection-types.js.map +1 -1
- package/dist-cjs/lib/exports/StyleEmbedder.js +19 -5
- package/dist-cjs/lib/exports/StyleEmbedder.js.map +2 -2
- package/dist-cjs/lib/exports/cssRules.js +127 -0
- package/dist-cjs/lib/exports/cssRules.js.map +7 -0
- package/dist-cjs/lib/exports/parseCss.js +0 -69
- package/dist-cjs/lib/exports/parseCss.js.map +2 -2
- package/dist-cjs/lib/hooks/useDocumentEvents.js +16 -0
- package/dist-cjs/lib/hooks/useDocumentEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/useGestureEvents.js +12 -6
- package/dist-cjs/lib/hooks/useGestureEvents.js.map +2 -2
- package/dist-cjs/lib/license/Watermark.js +2 -0
- package/dist-cjs/lib/license/Watermark.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Geometry2d.js +133 -16
- package/dist-cjs/lib/primitives/geometry/Geometry2d.js.map +3 -3
- package/dist-cjs/lib/primitives/geometry/Group2d.js +54 -11
- package/dist-cjs/lib/primitives/geometry/Group2d.js.map +2 -2
- package/dist-cjs/lib/primitives/intersect.js +20 -0
- package/dist-cjs/lib/primitives/intersect.js.map +2 -2
- package/dist-cjs/lib/utils/reorderShapes.js +2 -8
- package/dist-cjs/lib/utils/reorderShapes.js.map +2 -2
- package/dist-cjs/version.js +3 -3
- package/dist-cjs/version.js.map +1 -1
- package/dist-esm/index.d.mts +154 -17
- package/dist-esm/index.mjs +8 -2
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/TldrawEditor.mjs +4 -0
- package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
- package/dist-esm/lib/components/GeometryDebuggingView.mjs +3 -3
- package/dist-esm/lib/components/GeometryDebuggingView.mjs.map +2 -2
- package/dist-esm/lib/editor/Editor.mjs +216 -30
- package/dist-esm/lib/editor/Editor.mjs.map +2 -2
- package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +9 -0
- package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
- package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs +4 -13
- package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs.map +2 -2
- package/dist-esm/lib/exports/StyleEmbedder.mjs +21 -12
- package/dist-esm/lib/exports/StyleEmbedder.mjs.map +2 -2
- package/dist-esm/lib/exports/cssRules.mjs +107 -0
- package/dist-esm/lib/exports/cssRules.mjs.map +7 -0
- package/dist-esm/lib/exports/parseCss.mjs +0 -69
- package/dist-esm/lib/exports/parseCss.mjs.map +2 -2
- package/dist-esm/lib/hooks/useDocumentEvents.mjs +16 -0
- package/dist-esm/lib/hooks/useDocumentEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/useGestureEvents.mjs +12 -6
- package/dist-esm/lib/hooks/useGestureEvents.mjs.map +2 -2
- package/dist-esm/lib/license/Watermark.mjs +2 -0
- package/dist-esm/lib/license/Watermark.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Geometry2d.mjs +137 -14
- package/dist-esm/lib/primitives/geometry/Geometry2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Group2d.mjs +55 -12
- package/dist-esm/lib/primitives/geometry/Group2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/intersect.mjs +20 -0
- package/dist-esm/lib/primitives/intersect.mjs.map +2 -2
- package/dist-esm/lib/utils/reorderShapes.mjs +2 -8
- package/dist-esm/lib/utils/reorderShapes.mjs.map +2 -2
- package/dist-esm/version.mjs +3 -3
- package/dist-esm/version.mjs.map +1 -1
- package/package.json +7 -7
- package/src/index.ts +11 -2
- package/src/lib/TldrawEditor.tsx +29 -3
- package/src/lib/components/GeometryDebuggingView.tsx +3 -3
- package/src/lib/editor/Editor.ts +322 -36
- package/src/lib/editor/shapes/ShapeUtil.ts +10 -0
- package/src/lib/editor/shapes/group/GroupShapeUtil.tsx +7 -15
- package/src/lib/editor/types/selection-types.ts +3 -0
- package/src/lib/exports/StyleEmbedder.ts +25 -15
- package/src/lib/exports/cssRules.ts +126 -0
- package/src/lib/exports/parseCss.ts +0 -79
- package/src/lib/hooks/useDocumentEvents.ts +18 -0
- package/src/lib/hooks/useGestureEvents.ts +12 -6
- package/src/lib/license/Watermark.tsx +2 -0
- package/src/lib/primitives/geometry/Geometry2d.ts +196 -16
- package/src/lib/primitives/geometry/Group2d.ts +76 -13
- package/src/lib/primitives/intersect.ts +41 -0
- package/src/lib/utils/reorderShapes.ts +2 -9
- 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
|
-
}
|
|
@@ -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 ',': {
|
|
@@ -135,7 +135,6 @@ export function useGestureEvents(ref: React.RefObject<HTMLDivElement>) {
|
|
|
135
135
|
|
|
136
136
|
let initDistanceBetweenFingers = 1 // the distance between the two fingers when the pinch starts
|
|
137
137
|
let initZoom = 1 // the browser's zoom level when the pinch starts
|
|
138
|
-
let currZoom = 1 // the current zoom level according to the pinch gesture recognizer
|
|
139
138
|
let currDistanceBetweenFingers = 0
|
|
140
139
|
const initPointBetweenFingers = new Vec()
|
|
141
140
|
const prevPointBetweenFingers = new Vec()
|
|
@@ -239,7 +238,7 @@ export function useGestureEvents(ref: React.RefObject<HTMLDivElement>) {
|
|
|
239
238
|
|
|
240
239
|
switch (pinchState) {
|
|
241
240
|
case 'zooming': {
|
|
242
|
-
currZoom = offset[0]
|
|
241
|
+
const currZoom = offset[0] ** editor.getCameraOptions().zoomSpeed
|
|
243
242
|
|
|
244
243
|
editor.dispatch({
|
|
245
244
|
type: 'pinch',
|
|
@@ -278,7 +277,7 @@ export function useGestureEvents(ref: React.RefObject<HTMLDivElement>) {
|
|
|
278
277
|
if (event instanceof WheelEvent) return
|
|
279
278
|
if (!(event.target === elm || elm?.contains(event.target as Node))) return
|
|
280
279
|
|
|
281
|
-
const scale = offset[0]
|
|
280
|
+
const scale = offset[0] ** editor.getCameraOptions().zoomSpeed
|
|
282
281
|
|
|
283
282
|
pinchState = 'not sure'
|
|
284
283
|
|
|
@@ -309,14 +308,21 @@ export function useGestureEvents(ref: React.RefObject<HTMLDivElement>) {
|
|
|
309
308
|
target: ref,
|
|
310
309
|
eventOptions: { passive: false },
|
|
311
310
|
pinch: {
|
|
312
|
-
from: () =>
|
|
311
|
+
from: () => {
|
|
312
|
+
const { zoomSpeed } = editor.getCameraOptions()
|
|
313
|
+
const level = editor.getZoomLevel() ** (1 / zoomSpeed)
|
|
314
|
+
return [level, 0]
|
|
315
|
+
}, // Return the camera z to use when pinch starts
|
|
313
316
|
scaleBounds: () => {
|
|
314
317
|
const baseZoom = editor.getBaseZoom()
|
|
315
|
-
const zoomSteps = editor.getCameraOptions()
|
|
318
|
+
const { zoomSteps, zoomSpeed } = editor.getCameraOptions()
|
|
316
319
|
const zoomMin = zoomSteps[0] * baseZoom
|
|
317
320
|
const zoomMax = zoomSteps[zoomSteps.length - 1] * baseZoom
|
|
318
321
|
|
|
319
|
-
return {
|
|
322
|
+
return {
|
|
323
|
+
max: zoomMax ** (1 / zoomSpeed),
|
|
324
|
+
min: zoomMin ** (1 / zoomSpeed),
|
|
325
|
+
}
|
|
320
326
|
},
|
|
321
327
|
},
|
|
322
328
|
})
|
|
@@ -65,6 +65,7 @@ const WatermarkInner = memo(function WatermarkInner({ src }: { src: string }) {
|
|
|
65
65
|
stopEventPropagation(e)
|
|
66
66
|
preventDefault(e)
|
|
67
67
|
}}
|
|
68
|
+
title="made with tldraw"
|
|
68
69
|
onClick={() => runtime.openWindow(url, '_blank')}
|
|
69
70
|
style={{ mask: maskCss, WebkitMask: maskCss }}
|
|
70
71
|
/>
|
|
@@ -77,6 +78,7 @@ const WatermarkInner = memo(function WatermarkInner({ src }: { src: string }) {
|
|
|
77
78
|
onPointerDown={(e) => {
|
|
78
79
|
stopEventPropagation(e)
|
|
79
80
|
}}
|
|
81
|
+
title="made with tldraw"
|
|
80
82
|
style={{ mask: maskCss, WebkitMask: maskCss }}
|
|
81
83
|
/>
|
|
82
84
|
)}
|
|
@@ -1,12 +1,44 @@
|
|
|
1
|
+
import { assert } from '@tldraw/utils'
|
|
1
2
|
import { Box } from '../Box'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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
|
-
|
|
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
|
|
71
|
+
abstract getVertices(filters: Geometry2dFilters): Vec[]
|
|
33
72
|
|
|
34
|
-
|
|
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
|
+
}
|