@tldraw/editor 3.16.0-next.fe14f1b4181f → 4.0.1
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 +91 -111
- package/dist-cjs/index.js +3 -5
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/TldrawEditor.js +1 -7
- package/dist-cjs/lib/TldrawEditor.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultCanvas.js +11 -1
- package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
- package/dist-cjs/lib/config/TLUserPreferences.js +15 -4
- package/dist-cjs/lib/config/TLUserPreferences.js.map +2 -2
- package/dist-cjs/lib/editor/Editor.js +58 -114
- package/dist-cjs/lib/editor/Editor.js.map +2 -2
- package/dist-cjs/lib/editor/derivations/notVisibleShapes.js +4 -0
- package/dist-cjs/lib/editor/derivations/notVisibleShapes.js.map +2 -2
- package/dist-cjs/lib/editor/managers/FocusManager/FocusManager.js +4 -2
- package/dist-cjs/lib/editor/managers/FocusManager/FocusManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js +11 -6
- package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +2 -2
- package/dist-cjs/lib/editor/shapes/ShapeUtil.js +10 -0
- package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
- package/dist-cjs/lib/editor/types/misc-types.js.map +1 -1
- package/dist-cjs/lib/hooks/useCanvasEvents.js +19 -16
- package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/useDocumentEvents.js +5 -5
- package/dist-cjs/lib/hooks/useDocumentEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.js +1 -2
- package/dist-cjs/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/useGestureEvents.js +1 -1
- package/dist-cjs/lib/hooks/useGestureEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/useHandleEvents.js +6 -6
- package/dist-cjs/lib/hooks/useHandleEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/usePassThroughMouseOverEvents.js +4 -1
- package/dist-cjs/lib/hooks/usePassThroughMouseOverEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/useSelectionEvents.js +8 -8
- package/dist-cjs/lib/hooks/useSelectionEvents.js.map +2 -2
- package/dist-cjs/lib/license/LicenseManager.js +147 -59
- package/dist-cjs/lib/license/LicenseManager.js.map +2 -2
- package/dist-cjs/lib/license/LicenseProvider.js +39 -1
- package/dist-cjs/lib/license/LicenseProvider.js.map +2 -2
- package/dist-cjs/lib/license/Watermark.js +144 -75
- package/dist-cjs/lib/license/Watermark.js.map +3 -3
- package/dist-cjs/lib/license/useLicenseManagerState.js.map +2 -2
- package/dist-cjs/lib/primitives/Vec.js +0 -4
- package/dist-cjs/lib/primitives/Vec.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Geometry2d.js +50 -20
- package/dist-cjs/lib/primitives/geometry/Geometry2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Group2d.js +8 -1
- package/dist-cjs/lib/primitives/geometry/Group2d.js.map +2 -2
- package/dist-cjs/lib/utils/dom.js.map +2 -2
- package/dist-cjs/lib/utils/getPointerInfo.js +2 -3
- package/dist-cjs/lib/utils/getPointerInfo.js.map +2 -2
- package/dist-cjs/lib/utils/reparenting.js +7 -36
- package/dist-cjs/lib/utils/reparenting.js.map +3 -3
- package/dist-cjs/version.js +4 -4
- package/dist-cjs/version.js.map +1 -1
- package/dist-esm/index.d.mts +91 -111
- package/dist-esm/index.mjs +3 -5
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/TldrawEditor.mjs +1 -7
- package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +11 -1
- package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
- package/dist-esm/lib/config/TLUserPreferences.mjs +15 -4
- package/dist-esm/lib/config/TLUserPreferences.mjs.map +2 -2
- package/dist-esm/lib/editor/Editor.mjs +58 -114
- package/dist-esm/lib/editor/Editor.mjs.map +2 -2
- package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs +4 -0
- package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/FocusManager/FocusManager.mjs +4 -2
- package/dist-esm/lib/editor/managers/FocusManager/FocusManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs +11 -6
- package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
- package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +10 -0
- package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
- package/dist-esm/lib/hooks/useCanvasEvents.mjs +20 -22
- package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/useDocumentEvents.mjs +6 -6
- package/dist-esm/lib/hooks/useDocumentEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.mjs +1 -2
- package/dist-esm/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/useGestureEvents.mjs +2 -2
- package/dist-esm/lib/hooks/useGestureEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/useHandleEvents.mjs +6 -6
- package/dist-esm/lib/hooks/useHandleEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/usePassThroughMouseOverEvents.mjs +4 -1
- package/dist-esm/lib/hooks/usePassThroughMouseOverEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/useSelectionEvents.mjs +9 -14
- package/dist-esm/lib/hooks/useSelectionEvents.mjs.map +2 -2
- package/dist-esm/lib/license/LicenseManager.mjs +148 -60
- package/dist-esm/lib/license/LicenseManager.mjs.map +2 -2
- package/dist-esm/lib/license/LicenseProvider.mjs +39 -2
- package/dist-esm/lib/license/LicenseProvider.mjs.map +2 -2
- package/dist-esm/lib/license/Watermark.mjs +145 -76
- package/dist-esm/lib/license/Watermark.mjs.map +3 -3
- package/dist-esm/lib/license/useLicenseManagerState.mjs.map +2 -2
- package/dist-esm/lib/primitives/Vec.mjs +0 -4
- package/dist-esm/lib/primitives/Vec.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Geometry2d.mjs +53 -21
- package/dist-esm/lib/primitives/geometry/Geometry2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Group2d.mjs +8 -1
- package/dist-esm/lib/primitives/geometry/Group2d.mjs.map +2 -2
- package/dist-esm/lib/utils/dom.mjs.map +2 -2
- package/dist-esm/lib/utils/getPointerInfo.mjs +2 -3
- package/dist-esm/lib/utils/getPointerInfo.mjs.map +2 -2
- package/dist-esm/lib/utils/reparenting.mjs +8 -41
- package/dist-esm/lib/utils/reparenting.mjs.map +2 -2
- package/dist-esm/version.mjs +4 -4
- package/dist-esm/version.mjs.map +1 -1
- package/editor.css +8 -3
- package/package.json +7 -7
- package/src/index.ts +2 -10
- package/src/lib/TldrawEditor.tsx +1 -15
- package/src/lib/components/default-components/DefaultCanvas.tsx +7 -1
- package/src/lib/config/TLUserPreferences.ts +16 -3
- package/src/lib/editor/Editor.test.ts +90 -0
- package/src/lib/editor/Editor.ts +77 -151
- package/src/lib/editor/derivations/notVisibleShapes.ts +6 -0
- package/src/lib/editor/managers/FocusManager/FocusManager.ts +6 -2
- package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +30 -8
- package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +10 -3
- package/src/lib/editor/shapes/ShapeUtil.ts +32 -0
- package/src/lib/editor/types/misc-types.ts +0 -6
- package/src/lib/hooks/useCanvasEvents.ts +20 -20
- package/src/lib/hooks/useDocumentEvents.ts +6 -6
- package/src/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.ts +1 -1
- package/src/lib/hooks/useGestureEvents.ts +2 -2
- package/src/lib/hooks/useHandleEvents.ts +6 -6
- package/src/lib/hooks/usePassThroughMouseOverEvents.ts +4 -1
- package/src/lib/hooks/useSelectionEvents.ts +9 -14
- package/src/lib/license/LicenseManager.test.ts +780 -377
- package/src/lib/license/LicenseManager.ts +207 -70
- package/src/lib/license/LicenseProvider.tsx +74 -2
- package/src/lib/license/Watermark.tsx +152 -77
- package/src/lib/license/useLicenseManagerState.ts +2 -2
- package/src/lib/primitives/Vec.ts +0 -5
- package/src/lib/primitives/geometry/Geometry2d.test.ts +420 -0
- package/src/lib/primitives/geometry/Geometry2d.ts +78 -21
- package/src/lib/primitives/geometry/Group2d.ts +10 -1
- package/src/lib/test/InFrontOfTheCanvas.test.tsx +187 -0
- package/src/lib/utils/dom.test.ts +103 -0
- package/src/lib/utils/dom.ts +8 -1
- package/src/lib/utils/getPointerInfo.ts +3 -2
- package/src/lib/utils/reparenting.ts +10 -70
- package/src/version.ts +4 -4
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Mat } from '../Mat'
|
|
2
2
|
import { Vec, VecLike } from '../Vec'
|
|
3
3
|
import { Geometry2dFilters } from './Geometry2d'
|
|
4
|
+
import { Group2d } from './Group2d'
|
|
4
5
|
import { Rectangle2d } from './Rectangle2d'
|
|
5
6
|
|
|
6
7
|
describe('TransformedGeometry2d', () => {
|
|
@@ -36,6 +37,425 @@ describe('TransformedGeometry2d', () => {
|
|
|
36
37
|
})
|
|
37
38
|
})
|
|
38
39
|
|
|
40
|
+
describe('excludeFromShapeBounds', () => {
|
|
41
|
+
test('simple geometry with excludeFromShapeBounds flag', () => {
|
|
42
|
+
const rect = new Rectangle2d({
|
|
43
|
+
width: 100,
|
|
44
|
+
height: 50,
|
|
45
|
+
isFilled: true,
|
|
46
|
+
excludeFromShapeBounds: true,
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
// The bounds should still be calculated normally for simple geometry
|
|
50
|
+
const bounds = rect.bounds
|
|
51
|
+
expect(bounds.width).toBe(100)
|
|
52
|
+
expect(bounds.height).toBe(50)
|
|
53
|
+
expect(bounds.x).toBe(0)
|
|
54
|
+
expect(bounds.y).toBe(0)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('group with excluded child geometry', () => {
|
|
58
|
+
const mainRect = new Rectangle2d({
|
|
59
|
+
width: 100,
|
|
60
|
+
height: 50,
|
|
61
|
+
isFilled: true,
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const excludedRect = new Rectangle2d({
|
|
65
|
+
width: 200,
|
|
66
|
+
height: 100,
|
|
67
|
+
isFilled: true,
|
|
68
|
+
excludeFromShapeBounds: true,
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
const group = new Group2d({
|
|
72
|
+
children: [mainRect, excludedRect],
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
// The bounds should only include the non-excluded rectangle
|
|
76
|
+
const bounds = group.bounds
|
|
77
|
+
expect(bounds.width).toBe(100) // Only the main rectangle width
|
|
78
|
+
expect(bounds.height).toBe(50) // Only the main rectangle height
|
|
79
|
+
expect(bounds.x).toBe(0)
|
|
80
|
+
expect(bounds.y).toBe(0)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('group with multiple excluded children', () => {
|
|
84
|
+
const rect1 = new Rectangle2d({
|
|
85
|
+
width: 50,
|
|
86
|
+
height: 50,
|
|
87
|
+
isFilled: true,
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
const rect2 = new Rectangle2d({
|
|
91
|
+
width: 100,
|
|
92
|
+
height: 30,
|
|
93
|
+
isFilled: true,
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
const excludedRect1 = new Rectangle2d({
|
|
97
|
+
width: 200,
|
|
98
|
+
height: 200,
|
|
99
|
+
isFilled: true,
|
|
100
|
+
excludeFromShapeBounds: true,
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
const excludedRect2 = new Rectangle2d({
|
|
104
|
+
width: 300,
|
|
105
|
+
height: 300,
|
|
106
|
+
isFilled: true,
|
|
107
|
+
excludeFromShapeBounds: true,
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
const group = new Group2d({
|
|
111
|
+
children: [rect1, excludedRect1, rect2, excludedRect2],
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
// The bounds should include both non-excluded rectangles
|
|
115
|
+
const bounds = group.bounds
|
|
116
|
+
expect(bounds.width).toBe(100) // Width of rect2 (larger of the two)
|
|
117
|
+
expect(bounds.height).toBe(50) // Height of rect1 (larger of the two)
|
|
118
|
+
expect(bounds.x).toBe(0)
|
|
119
|
+
expect(bounds.y).toBe(0)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test('group with all children excluded', () => {
|
|
123
|
+
const excludedRect1 = new Rectangle2d({
|
|
124
|
+
width: 100,
|
|
125
|
+
height: 50,
|
|
126
|
+
isFilled: true,
|
|
127
|
+
excludeFromShapeBounds: true,
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
const excludedRect2 = new Rectangle2d({
|
|
131
|
+
width: 200,
|
|
132
|
+
height: 100,
|
|
133
|
+
isFilled: true,
|
|
134
|
+
excludeFromShapeBounds: true,
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
const group = new Group2d({
|
|
138
|
+
children: [excludedRect1, excludedRect2],
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
// The bounds should be empty when all children are excluded
|
|
142
|
+
const bounds = group.bounds
|
|
143
|
+
expect(bounds.width).toBe(0)
|
|
144
|
+
expect(bounds.height).toBe(0)
|
|
145
|
+
expect(bounds.x).toBe(0)
|
|
146
|
+
expect(bounds.y).toBe(0)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
test('nested groups with excluded geometry', () => {
|
|
150
|
+
const innerRect = new Rectangle2d({
|
|
151
|
+
width: 50,
|
|
152
|
+
height: 50,
|
|
153
|
+
isFilled: true,
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
const excludedRect = new Rectangle2d({
|
|
157
|
+
width: 200,
|
|
158
|
+
height: 200,
|
|
159
|
+
isFilled: true,
|
|
160
|
+
excludeFromShapeBounds: true,
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
const innerGroup = new Group2d({
|
|
164
|
+
children: [innerRect, excludedRect],
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
const outerRect = new Rectangle2d({
|
|
168
|
+
width: 100,
|
|
169
|
+
height: 30,
|
|
170
|
+
isFilled: true,
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
const outerGroup = new Group2d({
|
|
174
|
+
children: [innerGroup, outerRect],
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
// The bounds should include both the inner group (without excluded rect) and outer rect
|
|
178
|
+
const bounds = outerGroup.bounds
|
|
179
|
+
expect(bounds.width).toBe(100) // Width of outerRect (larger)
|
|
180
|
+
expect(bounds.height).toBe(50) // Height of innerRect (larger)
|
|
181
|
+
expect(bounds.x).toBe(0)
|
|
182
|
+
expect(bounds.y).toBe(0)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
test('bounds calculation with transformed geometry', () => {
|
|
186
|
+
const rect = new Rectangle2d({
|
|
187
|
+
width: 50,
|
|
188
|
+
height: 50,
|
|
189
|
+
isFilled: true,
|
|
190
|
+
}).transform(Mat.Translate(100, 100))
|
|
191
|
+
|
|
192
|
+
const excludedRect = new Rectangle2d({
|
|
193
|
+
width: 200,
|
|
194
|
+
height: 200,
|
|
195
|
+
isFilled: true,
|
|
196
|
+
excludeFromShapeBounds: true,
|
|
197
|
+
}).transform(Mat.Translate(50, 50))
|
|
198
|
+
|
|
199
|
+
const group = new Group2d({
|
|
200
|
+
children: [rect, excludedRect],
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
// The bounds should only include the non-excluded rectangle
|
|
204
|
+
const bounds = group.bounds
|
|
205
|
+
// Verify that the excluded rectangle doesn't affect the bounds
|
|
206
|
+
// The bounds should be smaller than if the excluded rect was included
|
|
207
|
+
expect(bounds.width).toBeLessThan(200) // Should not include the excluded rect's width
|
|
208
|
+
expect(bounds.height).toBeLessThan(200) // Should not include the excluded rect's height
|
|
209
|
+
// The bounds should not be empty
|
|
210
|
+
expect(bounds.width).toBeGreaterThan(0)
|
|
211
|
+
expect(bounds.height).toBeGreaterThan(0)
|
|
212
|
+
})
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
describe('getBoundsVertices', () => {
|
|
216
|
+
test('basic geometry returns vertices when not excluded from bounds', () => {
|
|
217
|
+
const rect = new Rectangle2d({
|
|
218
|
+
width: 100,
|
|
219
|
+
height: 50,
|
|
220
|
+
isFilled: true,
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
const boundsVertices = rect.getBoundsVertices()
|
|
224
|
+
const vertices = rect.getVertices()
|
|
225
|
+
|
|
226
|
+
expect(boundsVertices).toEqual(vertices)
|
|
227
|
+
expect(boundsVertices.length).toBe(4)
|
|
228
|
+
expect(boundsVertices).toMatchObject([
|
|
229
|
+
{ x: 0, y: 0, z: 1 },
|
|
230
|
+
{ x: 100, y: 0, z: 1 },
|
|
231
|
+
{ x: 100, y: 50, z: 1 },
|
|
232
|
+
{ x: 0, y: 50, z: 1 },
|
|
233
|
+
])
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
test('geometry excluded from shape bounds returns empty array', () => {
|
|
237
|
+
const rect = new Rectangle2d({
|
|
238
|
+
width: 100,
|
|
239
|
+
height: 50,
|
|
240
|
+
isFilled: true,
|
|
241
|
+
excludeFromShapeBounds: true,
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
const boundsVertices = rect.getBoundsVertices()
|
|
245
|
+
expect(boundsVertices).toEqual([])
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
test('cached boundsVertices property', () => {
|
|
249
|
+
const rect = new Rectangle2d({
|
|
250
|
+
width: 100,
|
|
251
|
+
height: 50,
|
|
252
|
+
isFilled: true,
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
// Access the cached property multiple times
|
|
256
|
+
const boundsVertices1 = rect.boundsVertices
|
|
257
|
+
const boundsVertices2 = rect.boundsVertices
|
|
258
|
+
|
|
259
|
+
// Should return the same reference (cached)
|
|
260
|
+
expect(boundsVertices1).toBe(boundsVertices2)
|
|
261
|
+
expect(boundsVertices1.length).toBe(4)
|
|
262
|
+
})
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
describe('TransformedGeometry2d getBoundsVertices', () => {
|
|
266
|
+
test('transforms bounds vertices correctly', () => {
|
|
267
|
+
const rect = new Rectangle2d({
|
|
268
|
+
width: 100,
|
|
269
|
+
height: 50,
|
|
270
|
+
isFilled: true,
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
const transformed = rect.transform(Mat.Translate(50, 100).scale(2, 2))
|
|
274
|
+
const boundsVertices = transformed.getBoundsVertices()
|
|
275
|
+
|
|
276
|
+
expect(boundsVertices).toMatchObject([
|
|
277
|
+
{ x: 50, y: 100, z: 1 },
|
|
278
|
+
{ x: 250, y: 100, z: 1 },
|
|
279
|
+
{ x: 250, y: 200, z: 1 },
|
|
280
|
+
{ x: 50, y: 200, z: 1 },
|
|
281
|
+
])
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
test('transforms empty bounds vertices for excluded geometry', () => {
|
|
285
|
+
const rect = new Rectangle2d({
|
|
286
|
+
width: 100,
|
|
287
|
+
height: 50,
|
|
288
|
+
isFilled: true,
|
|
289
|
+
excludeFromShapeBounds: true,
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
const transformed = rect.transform(Mat.Translate(50, 100))
|
|
293
|
+
const boundsVertices = transformed.getBoundsVertices()
|
|
294
|
+
|
|
295
|
+
expect(boundsVertices).toEqual([])
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
test('nested transform preserves bounds vertices behavior', () => {
|
|
299
|
+
const rect = new Rectangle2d({
|
|
300
|
+
width: 100,
|
|
301
|
+
height: 50,
|
|
302
|
+
isFilled: true,
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
const transformed1 = rect.transform(Mat.Translate(10, 20))
|
|
306
|
+
const transformed2 = transformed1.transform(Mat.Scale(2, 2))
|
|
307
|
+
const boundsVertices = transformed2.getBoundsVertices()
|
|
308
|
+
|
|
309
|
+
expect(boundsVertices).toMatchObject([
|
|
310
|
+
{ x: 20, y: 40, z: 1 },
|
|
311
|
+
{ x: 220, y: 40, z: 1 },
|
|
312
|
+
{ x: 220, y: 140, z: 1 },
|
|
313
|
+
{ x: 20, y: 140, z: 1 },
|
|
314
|
+
])
|
|
315
|
+
})
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
describe('Group2d getBoundsVertices', () => {
|
|
319
|
+
test('flattens children bounds vertices', () => {
|
|
320
|
+
const rect1 = new Rectangle2d({
|
|
321
|
+
width: 50,
|
|
322
|
+
height: 50,
|
|
323
|
+
isFilled: true,
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
const rect2 = new Rectangle2d({
|
|
327
|
+
width: 30,
|
|
328
|
+
height: 30,
|
|
329
|
+
isFilled: true,
|
|
330
|
+
}).transform(Mat.Translate(60, 60))
|
|
331
|
+
|
|
332
|
+
const group = new Group2d({
|
|
333
|
+
children: [rect1, rect2],
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
const boundsVertices = group.getBoundsVertices()
|
|
337
|
+
|
|
338
|
+
// Should include all vertices from both rectangles
|
|
339
|
+
expect(boundsVertices.length).toBe(8) // 4 vertices from each rectangle
|
|
340
|
+
|
|
341
|
+
// Check that we have vertices from both rectangles
|
|
342
|
+
expect(boundsVertices).toEqual(
|
|
343
|
+
expect.arrayContaining([
|
|
344
|
+
expect.objectContaining({ x: 0, y: 0 }), // rect1 vertices
|
|
345
|
+
expect.objectContaining({ x: 50, y: 0 }),
|
|
346
|
+
expect.objectContaining({ x: 50, y: 50 }),
|
|
347
|
+
expect.objectContaining({ x: 0, y: 50 }),
|
|
348
|
+
expect.objectContaining({ x: 60, y: 60 }), // rect2 vertices
|
|
349
|
+
expect.objectContaining({ x: 90, y: 60 }),
|
|
350
|
+
expect.objectContaining({ x: 90, y: 90 }),
|
|
351
|
+
expect.objectContaining({ x: 60, y: 90 }),
|
|
352
|
+
])
|
|
353
|
+
)
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
test('excludes children marked as excluded from bounds', () => {
|
|
357
|
+
const rect1 = new Rectangle2d({
|
|
358
|
+
width: 50,
|
|
359
|
+
height: 50,
|
|
360
|
+
isFilled: true,
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
const rect2 = new Rectangle2d({
|
|
364
|
+
width: 100,
|
|
365
|
+
height: 100,
|
|
366
|
+
isFilled: true,
|
|
367
|
+
excludeFromShapeBounds: true,
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
const group = new Group2d({
|
|
371
|
+
children: [rect1, rect2],
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
const boundsVertices = group.getBoundsVertices()
|
|
375
|
+
|
|
376
|
+
// Should only include vertices from rect1, not rect2
|
|
377
|
+
expect(boundsVertices.length).toBe(4) // Only rect1's 4 vertices
|
|
378
|
+
expect(boundsVertices).toMatchObject([
|
|
379
|
+
{ x: 0, y: 0, z: 1 },
|
|
380
|
+
{ x: 50, y: 0, z: 1 },
|
|
381
|
+
{ x: 50, y: 50, z: 1 },
|
|
382
|
+
{ x: 0, y: 50, z: 1 },
|
|
383
|
+
])
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
test('returns empty array when group itself is excluded from bounds', () => {
|
|
387
|
+
const rect1 = new Rectangle2d({
|
|
388
|
+
width: 50,
|
|
389
|
+
height: 50,
|
|
390
|
+
isFilled: true,
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
const rect2 = new Rectangle2d({
|
|
394
|
+
width: 30,
|
|
395
|
+
height: 30,
|
|
396
|
+
isFilled: true,
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
const group = new Group2d({
|
|
400
|
+
children: [rect1, rect2],
|
|
401
|
+
excludeFromShapeBounds: true,
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
const boundsVertices = group.getBoundsVertices()
|
|
405
|
+
expect(boundsVertices).toEqual([])
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
test('handles nested groups correctly', () => {
|
|
409
|
+
const rect1 = new Rectangle2d({
|
|
410
|
+
width: 50,
|
|
411
|
+
height: 50,
|
|
412
|
+
isFilled: true,
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
const rect2 = new Rectangle2d({
|
|
416
|
+
width: 30,
|
|
417
|
+
height: 30,
|
|
418
|
+
isFilled: true,
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
const innerGroup = new Group2d({
|
|
422
|
+
children: [rect2],
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
const outerGroup = new Group2d({
|
|
426
|
+
children: [rect1, innerGroup],
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
const boundsVertices = outerGroup.getBoundsVertices()
|
|
430
|
+
|
|
431
|
+
// Should include vertices from both rectangles
|
|
432
|
+
expect(boundsVertices.length).toBe(8) // 4 vertices from each rectangle
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
test('handles all children excluded from bounds', () => {
|
|
436
|
+
const rect1 = new Rectangle2d({
|
|
437
|
+
width: 50,
|
|
438
|
+
height: 50,
|
|
439
|
+
isFilled: true,
|
|
440
|
+
excludeFromShapeBounds: true,
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
const rect2 = new Rectangle2d({
|
|
444
|
+
width: 30,
|
|
445
|
+
height: 30,
|
|
446
|
+
isFilled: true,
|
|
447
|
+
excludeFromShapeBounds: true,
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
const group = new Group2d({
|
|
451
|
+
children: [rect1, rect2],
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
const boundsVertices = group.getBoundsVertices()
|
|
455
|
+
expect(boundsVertices).toEqual([])
|
|
456
|
+
})
|
|
457
|
+
})
|
|
458
|
+
|
|
39
459
|
function expectApproxMatch(a: VecLike, b: VecLike) {
|
|
40
460
|
expect(a.x).toBeCloseTo(b.x, 0.0001)
|
|
41
461
|
expect(a.y).toBeCloseTo(b.y, 0.0001)
|
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
intersectLineSegmentPolyline,
|
|
10
10
|
intersectPolys,
|
|
11
11
|
linesIntersect,
|
|
12
|
+
polygonIntersectsPolyline,
|
|
13
|
+
polygonsIntersect,
|
|
12
14
|
} from '../intersect'
|
|
13
15
|
import { approximately, pointInPolygon } from '../utils'
|
|
14
16
|
|
|
@@ -48,6 +50,7 @@ export interface TransformedGeometry2dOptions {
|
|
|
48
50
|
isInternal?: boolean
|
|
49
51
|
debugColor?: string
|
|
50
52
|
ignore?: boolean
|
|
53
|
+
excludeFromShapeBounds?: boolean
|
|
51
54
|
}
|
|
52
55
|
|
|
53
56
|
/** @public */
|
|
@@ -64,11 +67,17 @@ export abstract class Geometry2d {
|
|
|
64
67
|
isLabel = false
|
|
65
68
|
isEmptyLabel = false
|
|
66
69
|
isInternal = false
|
|
70
|
+
excludeFromShapeBounds = false
|
|
67
71
|
debugColor?: string
|
|
68
72
|
ignore?: boolean
|
|
69
73
|
|
|
70
74
|
constructor(opts: Geometry2dOptions) {
|
|
71
|
-
const {
|
|
75
|
+
const {
|
|
76
|
+
isLabel = false,
|
|
77
|
+
isEmptyLabel = false,
|
|
78
|
+
isInternal = false,
|
|
79
|
+
excludeFromShapeBounds = false,
|
|
80
|
+
} = opts
|
|
72
81
|
this.isFilled = opts.isFilled
|
|
73
82
|
this.isClosed = opts.isClosed
|
|
74
83
|
this.debugColor = opts.debugColor
|
|
@@ -76,6 +85,7 @@ export abstract class Geometry2d {
|
|
|
76
85
|
this.isLabel = isLabel
|
|
77
86
|
this.isEmptyLabel = isEmptyLabel
|
|
78
87
|
this.isInternal = isInternal
|
|
88
|
+
this.excludeFromShapeBounds = excludeFromShapeBounds
|
|
79
89
|
}
|
|
80
90
|
|
|
81
91
|
isExcludedByFilter(filters?: Geometry2dFilters) {
|
|
@@ -227,25 +237,6 @@ export abstract class Geometry2d {
|
|
|
227
237
|
return distanceAlongRoute / length
|
|
228
238
|
}
|
|
229
239
|
|
|
230
|
-
/** @deprecated Iterate the vertices instead. */
|
|
231
|
-
nearestPointOnLineSegment(A: VecLike, B: VecLike): Vec {
|
|
232
|
-
const { vertices } = this
|
|
233
|
-
let nearest: Vec | undefined
|
|
234
|
-
let dist = Infinity
|
|
235
|
-
let d: number, p: Vec, q: Vec
|
|
236
|
-
for (let i = 0; i < vertices.length; i++) {
|
|
237
|
-
p = vertices[i]
|
|
238
|
-
q = Vec.NearestPointOnLineSegment(A, B, p, true)
|
|
239
|
-
d = Vec.Dist2(p, q)
|
|
240
|
-
if (d < dist) {
|
|
241
|
-
dist = d
|
|
242
|
-
nearest = q
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
if (!nearest) throw Error('nearest point not found')
|
|
246
|
-
return nearest
|
|
247
|
-
}
|
|
248
|
-
|
|
249
240
|
isPointInBounds(point: VecLike, margin = 0) {
|
|
250
241
|
const { bounds } = this
|
|
251
242
|
return !(
|
|
@@ -256,6 +247,53 @@ export abstract class Geometry2d {
|
|
|
256
247
|
)
|
|
257
248
|
}
|
|
258
249
|
|
|
250
|
+
overlapsPolygon(_polygon: VecLike[]): boolean {
|
|
251
|
+
const polygon = _polygon.map((v) => Vec.From(v))
|
|
252
|
+
|
|
253
|
+
// Otherwise, check if the geometry itself overlaps the polygon
|
|
254
|
+
const { vertices, center, isFilled, isEmptyLabel, isClosed } = this
|
|
255
|
+
|
|
256
|
+
// We'll do things in order of cheapest to most expensive checks
|
|
257
|
+
|
|
258
|
+
// Skip empty labels
|
|
259
|
+
if (isEmptyLabel) return false
|
|
260
|
+
|
|
261
|
+
// If any of the geometry's vertices are inside the polygon, it's inside
|
|
262
|
+
if (vertices.some((v) => pointInPolygon(v, polygon))) {
|
|
263
|
+
return true
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// If the geometry is filled and closed and its center is inside the polygon, it's inside
|
|
267
|
+
if (isClosed) {
|
|
268
|
+
if (isFilled) {
|
|
269
|
+
// If closed and filled, check if the center is inside the polygon
|
|
270
|
+
if (pointInPolygon(center, polygon)) {
|
|
271
|
+
return true
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ..then, slightly more expensive check, see the geometry covers the entire polygon but not its center
|
|
275
|
+
if (polygon.every((v) => pointInPolygon(v, vertices))) {
|
|
276
|
+
return true
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// If any the geometry's vertices intersect the edge of the polygon, it's inside.
|
|
281
|
+
// for example when a rotated rectangle is moved over the corner of a parent rectangle
|
|
282
|
+
// If the geometry is closed, intersect as a polygon
|
|
283
|
+
if (polygonsIntersect(polygon, vertices)) {
|
|
284
|
+
return true
|
|
285
|
+
}
|
|
286
|
+
} else {
|
|
287
|
+
// If the geometry is not closed, intersect as a polyline
|
|
288
|
+
if (polygonIntersectsPolyline(polygon, vertices)) {
|
|
289
|
+
return true
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// If none of the above checks passed, the geometry is outside the polygon
|
|
294
|
+
return false
|
|
295
|
+
}
|
|
296
|
+
|
|
259
297
|
transform(transform: MatModel, opts?: TransformedGeometry2dOptions): Geometry2d {
|
|
260
298
|
return new TransformedGeometry2d(this, transform, opts)
|
|
261
299
|
}
|
|
@@ -271,8 +309,23 @@ export abstract class Geometry2d {
|
|
|
271
309
|
return this._vertices
|
|
272
310
|
}
|
|
273
311
|
|
|
312
|
+
getBoundsVertices(): Vec[] {
|
|
313
|
+
if (this.excludeFromShapeBounds) return []
|
|
314
|
+
return this.vertices
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
private _boundsVertices: Vec[] | undefined
|
|
318
|
+
|
|
319
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
320
|
+
get boundsVertices(): Vec[] {
|
|
321
|
+
if (!this._boundsVertices) {
|
|
322
|
+
this._boundsVertices = this.getBoundsVertices()
|
|
323
|
+
}
|
|
324
|
+
return this._boundsVertices
|
|
325
|
+
}
|
|
326
|
+
|
|
274
327
|
getBounds() {
|
|
275
|
-
return Box.FromPoints(this.
|
|
328
|
+
return Box.FromPoints(this.boundsVertices)
|
|
276
329
|
}
|
|
277
330
|
|
|
278
331
|
private _bounds: Box | undefined
|
|
@@ -399,6 +452,10 @@ export class TransformedGeometry2d extends Geometry2d {
|
|
|
399
452
|
return this.geometry.getVertices(filters).map((v) => Mat.applyToPoint(this.matrix, v))
|
|
400
453
|
}
|
|
401
454
|
|
|
455
|
+
getBoundsVertices(): Vec[] {
|
|
456
|
+
return this.geometry.getBoundsVertices().map((v) => Mat.applyToPoint(this.matrix, v))
|
|
457
|
+
}
|
|
458
|
+
|
|
402
459
|
nearestPoint(point: VecLike, filters?: Geometry2dFilters): Vec {
|
|
403
460
|
return Mat.applyToPoint(
|
|
404
461
|
this.matrix,
|
|
@@ -114,6 +114,11 @@ export class Group2d extends Geometry2d {
|
|
|
114
114
|
})
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
override getBoundsVertices(): Vec[] {
|
|
118
|
+
if (this.excludeFromShapeBounds) return []
|
|
119
|
+
return this.children.flatMap((child) => child.getBoundsVertices())
|
|
120
|
+
}
|
|
121
|
+
|
|
117
122
|
override intersectPolygon(polygon: VecLike[], filters?: Geometry2dFilters) {
|
|
118
123
|
return this.children.flatMap((child) => {
|
|
119
124
|
if (child.isExcludedByFilter(filters)) return EMPTY_ARRAY
|
|
@@ -205,7 +210,7 @@ export class Group2d extends Geometry2d {
|
|
|
205
210
|
path += child.toSimpleSvgPath()
|
|
206
211
|
}
|
|
207
212
|
|
|
208
|
-
const corners = Box.FromPoints(this.
|
|
213
|
+
const corners = Box.FromPoints(this.boundsVertices).corners
|
|
209
214
|
// draw just a few pixels around each corner, e.g. an L shape for the bottom left
|
|
210
215
|
|
|
211
216
|
for (let i = 0, n = corners.length; i < n; i++) {
|
|
@@ -236,4 +241,8 @@ export class Group2d extends Geometry2d {
|
|
|
236
241
|
getSvgPathData(): string {
|
|
237
242
|
return this.children.map((c, i) => (c.isLabel ? '' : c.getSvgPathData(i === 0))).join(' ')
|
|
238
243
|
}
|
|
244
|
+
|
|
245
|
+
overlapsPolygon(polygon: VecLike[]): boolean {
|
|
246
|
+
return this.children.some((child) => child.overlapsPolygon(polygon))
|
|
247
|
+
}
|
|
239
248
|
}
|