@deck.gl-community/editable-layers 9.2.0-beta.4 → 9.2.0-beta.6
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/edit-modes/draw-polygon-mode.d.ts +7 -3
- package/dist/edit-modes/draw-polygon-mode.d.ts.map +1 -1
- package/dist/edit-modes/draw-polygon-mode.js +226 -81
- package/dist/edit-modes/draw-polygon-mode.js.map +1 -1
- package/dist/index.cjs +184 -60
- package/dist/index.cjs.map +3 -3
- package/package.json +6 -2
- package/src/edit-modes/draw-polygon-mode.ts +303 -99
package/package.json
CHANGED
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
"name": "@deck.gl-community/editable-layers",
|
|
3
3
|
"description": "A suite of 3D-enabled data editing overlays, suitable for deck.gl",
|
|
4
4
|
"license": "MIT",
|
|
5
|
-
"version": "9.2.0-beta.
|
|
5
|
+
"version": "9.2.0-beta.6",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
6
9
|
"repository": {
|
|
7
10
|
"type": "git",
|
|
8
11
|
"url": "https://github.com/visgl/deck.gl-community"
|
|
@@ -39,6 +42,7 @@
|
|
|
39
42
|
"@turf/bbox-polygon": "^6.5.0",
|
|
40
43
|
"@turf/bearing": "^6.5.0",
|
|
41
44
|
"@turf/boolean-point-in-polygon": "^6.5.0",
|
|
45
|
+
"@turf/boolean-within": "^6.5.0",
|
|
42
46
|
"@turf/buffer": "^6.5.0",
|
|
43
47
|
"@turf/center": "^6.5.0",
|
|
44
48
|
"@turf/centroid": "^6.5.0",
|
|
@@ -83,5 +87,5 @@
|
|
|
83
87
|
"@luma.gl/engine": "~9.2.0",
|
|
84
88
|
"@math.gl/core": ">=4.0.1"
|
|
85
89
|
},
|
|
86
|
-
"gitHead": "
|
|
90
|
+
"gitHead": "c72a43fa7f8edfb9456b37656b095d036946b3a4"
|
|
87
91
|
}
|
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
// Copyright (c) vis.gl contributors
|
|
4
4
|
|
|
5
5
|
import lineIntersect from '@turf/line-intersect';
|
|
6
|
-
import {
|
|
6
|
+
import { polygon as turfPolygon} from '@turf/helpers';
|
|
7
|
+
import booleanWithin from "@turf/boolean-within";
|
|
8
|
+
|
|
9
|
+
|
|
7
10
|
import {
|
|
8
11
|
ClickEvent,
|
|
9
12
|
PointerMoveEvent,
|
|
@@ -13,129 +16,151 @@ import {
|
|
|
13
16
|
GuideFeature,
|
|
14
17
|
DoubleClickEvent
|
|
15
18
|
} from './types';
|
|
16
|
-
import {
|
|
19
|
+
import {Position, FeatureCollection, Geometry} from '../utils/geojson-types';
|
|
17
20
|
import {getPickedEditHandle} from './utils';
|
|
18
21
|
import {GeoJsonEditMode} from './geojson-edit-mode';
|
|
22
|
+
import { ImmutableFeatureCollection } from './immutable-feature-collection';
|
|
23
|
+
|
|
19
24
|
|
|
20
25
|
export class DrawPolygonMode extends GeoJsonEditMode {
|
|
26
|
+
|
|
27
|
+
holeSequence: Position[] = [];
|
|
28
|
+
isDrawingHole = false;
|
|
29
|
+
|
|
21
30
|
createTentativeFeature(props: ModeProps<FeatureCollection>): TentativeFeature {
|
|
22
|
-
const {lastPointerMoveEvent} = props;
|
|
31
|
+
const { lastPointerMoveEvent } = props;
|
|
23
32
|
const clickSequence = this.getClickSequence();
|
|
33
|
+
const holeSequence = this.holeSequence;
|
|
34
|
+
const lastCoords = lastPointerMoveEvent
|
|
35
|
+
? [lastPointerMoveEvent.mapCoords]
|
|
36
|
+
: [];
|
|
24
37
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
geometry: {
|
|
35
|
-
type: 'LineString',
|
|
36
|
-
coordinates: [...clickSequence, ...lastCoords]
|
|
37
|
-
}
|
|
38
|
+
let geometry: Geometry;
|
|
39
|
+
|
|
40
|
+
if (this.isDrawingHole && holeSequence.length > 1) {
|
|
41
|
+
geometry = {
|
|
42
|
+
type: "Polygon",
|
|
43
|
+
coordinates: [
|
|
44
|
+
[...clickSequence, clickSequence[0]],
|
|
45
|
+
[...holeSequence, ...lastCoords, holeSequence[0]],
|
|
46
|
+
],
|
|
38
47
|
};
|
|
39
48
|
} else if (clickSequence.length > 2) {
|
|
40
|
-
|
|
41
|
-
type:
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
+
geometry = {
|
|
50
|
+
type: "Polygon",
|
|
51
|
+
coordinates: [[...clickSequence, ...lastCoords, clickSequence[0]]],
|
|
52
|
+
};
|
|
53
|
+
} else {
|
|
54
|
+
geometry = {
|
|
55
|
+
type: "LineString",
|
|
56
|
+
coordinates: [...clickSequence, ...lastCoords],
|
|
49
57
|
};
|
|
50
58
|
}
|
|
51
59
|
|
|
52
|
-
return
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
getGuides(props: ModeProps<FeatureCollection>): GuideFeatureCollection {
|
|
56
|
-
const clickSequence = this.getClickSequence();
|
|
57
|
-
|
|
58
|
-
const guides: GuideFeatureCollection = {
|
|
59
|
-
type: 'FeatureCollection',
|
|
60
|
-
features: []
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
const tentativeFeature = this.createTentativeFeature(props);
|
|
64
|
-
if (tentativeFeature) {
|
|
65
|
-
guides.features.push(tentativeFeature);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const editHandles: GuideFeature[] = clickSequence.map((clickedCoord, index) => ({
|
|
69
|
-
type: 'Feature',
|
|
60
|
+
return {
|
|
61
|
+
type: "Feature",
|
|
70
62
|
properties: {
|
|
71
|
-
guideType:
|
|
72
|
-
editHandleType: 'existing',
|
|
73
|
-
featureIndex: -1,
|
|
74
|
-
positionIndexes: [index]
|
|
63
|
+
guideType: "tentative",
|
|
75
64
|
},
|
|
76
|
-
geometry
|
|
77
|
-
|
|
78
|
-
coordinates: clickedCoord
|
|
79
|
-
}
|
|
80
|
-
}));
|
|
81
|
-
|
|
82
|
-
guides.features.push(...editHandles);
|
|
83
|
-
|
|
84
|
-
return guides;
|
|
65
|
+
geometry,
|
|
66
|
+
};
|
|
85
67
|
}
|
|
86
68
|
|
|
87
|
-
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
69
|
+
getGuides(props: ModeProps<FeatureCollection>): GuideFeatureCollection {
|
|
70
|
+
const guides: GuideFeatureCollection = {
|
|
71
|
+
type: "FeatureCollection",
|
|
72
|
+
features: [],
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const tentative = this.createTentativeFeature(props);
|
|
76
|
+
if (tentative) guides.features.push(tentative);
|
|
77
|
+
|
|
78
|
+
const sequence = this.isDrawingHole
|
|
79
|
+
? this.holeSequence
|
|
80
|
+
: this.getClickSequence();
|
|
81
|
+
|
|
82
|
+
const handles: GuideFeature[] = sequence.map((coord, index) => ({
|
|
83
|
+
type: "Feature",
|
|
84
|
+
properties: {
|
|
85
|
+
guideType: "editHandle",
|
|
86
|
+
editHandleType: "existing",
|
|
87
|
+
featureIndex: -1,
|
|
88
|
+
positionIndexes: [index],
|
|
89
|
+
},
|
|
90
|
+
geometry: {
|
|
91
|
+
type: "Point",
|
|
92
|
+
coordinates: coord,
|
|
93
|
+
},
|
|
94
|
+
}));
|
|
95
|
+
|
|
96
|
+
guides.features.push(...handles);
|
|
97
|
+
return guides;
|
|
101
98
|
}
|
|
102
99
|
|
|
103
|
-
// eslint-disable-next-line complexity
|
|
100
|
+
// eslint-disable-next-line complexity, max-statements
|
|
104
101
|
handleClick(event: ClickEvent, props: ModeProps<FeatureCollection>) {
|
|
105
102
|
const {picks} = event;
|
|
106
103
|
const clickedEditHandle = getPickedEditHandle(picks);
|
|
107
104
|
const clickSequence = this.getClickSequence();
|
|
105
|
+
const coords = event.mapCoords;
|
|
106
|
+
|
|
107
|
+
// Check if they clicked on an edit handle to complete the polygon
|
|
108
|
+
if (
|
|
109
|
+
!this.isDrawingHole &&
|
|
110
|
+
clickSequence.length > 2 &&
|
|
111
|
+
clickedEditHandle &&
|
|
112
|
+
Array.isArray(clickedEditHandle.properties.positionIndexes) &&
|
|
113
|
+
(clickedEditHandle.properties.positionIndexes[0] === 0 ||
|
|
114
|
+
clickedEditHandle.properties.positionIndexes[0] === clickSequence.length - 1)
|
|
115
|
+
) {
|
|
116
|
+
// They clicked the first or last point, so complete the polygon
|
|
117
|
+
this.finishDrawing(props);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Check if they clicked near the first point to complete the polygon
|
|
122
|
+
if (!this.isDrawingHole && clickSequence.length > 2) {
|
|
123
|
+
if (isNearFirstPoint(coords, clickSequence[0])) {
|
|
124
|
+
this.finishDrawing(props);
|
|
125
|
+
this.resetClickSequence();
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (this.isDrawingHole) {
|
|
131
|
+
const current = this.holeSequence;
|
|
132
|
+
current.push(coords);
|
|
133
|
+
|
|
134
|
+
if (current.length > 2) {
|
|
135
|
+
const poly: Geometry = {
|
|
136
|
+
type: "Polygon",
|
|
137
|
+
coordinates: [
|
|
138
|
+
[...clickSequence, clickSequence[0]],
|
|
139
|
+
[...current, current[0]],
|
|
140
|
+
],
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
this.resetClickSequence();
|
|
144
|
+
this.holeSequence = [];
|
|
145
|
+
this.isDrawingHole = false;
|
|
108
146
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
]);
|
|
115
|
-
const otherLines = turfLineString([...clickSequence.slice(0, clickSequence.length - 1)]);
|
|
116
|
-
const intersectingPoints = lineIntersect(currentLine, otherLines);
|
|
117
|
-
if (intersectingPoints.features.length > 0) {
|
|
118
|
-
overlappingLines = true;
|
|
147
|
+
const editAction = this.getAddFeatureOrBooleanPolygonAction(
|
|
148
|
+
poly,
|
|
149
|
+
props,
|
|
150
|
+
);
|
|
151
|
+
if (editAction) props.onEdit(editAction);
|
|
119
152
|
}
|
|
153
|
+
return;
|
|
120
154
|
}
|
|
121
155
|
|
|
156
|
+
// Add the click if we didn't click on a handle
|
|
122
157
|
let positionAdded = false;
|
|
123
|
-
if (!clickedEditHandle
|
|
124
|
-
// Don't add another point right next to an existing one
|
|
158
|
+
if (!clickedEditHandle) {
|
|
125
159
|
this.addClickSequence(event);
|
|
126
160
|
positionAdded = true;
|
|
127
161
|
}
|
|
128
162
|
|
|
129
|
-
if (
|
|
130
|
-
clickSequence.length > 2 &&
|
|
131
|
-
clickedEditHandle &&
|
|
132
|
-
Array.isArray(clickedEditHandle.properties.positionIndexes) &&
|
|
133
|
-
(clickedEditHandle.properties.positionIndexes[0] === 0 ||
|
|
134
|
-
clickedEditHandle.properties.positionIndexes[0] === clickSequence.length - 1)
|
|
135
|
-
) {
|
|
136
|
-
// They clicked the first or last point (or double-clicked), so complete the polygon
|
|
137
|
-
this.finishDrawing(props);
|
|
138
|
-
} else if (positionAdded) {
|
|
163
|
+
if (positionAdded) {
|
|
139
164
|
// new tentative point
|
|
140
165
|
props.onEdit({
|
|
141
166
|
// data is the same
|
|
@@ -148,20 +173,24 @@ export class DrawPolygonMode extends GeoJsonEditMode {
|
|
|
148
173
|
}
|
|
149
174
|
}
|
|
150
175
|
|
|
151
|
-
handleDoubleClick(
|
|
176
|
+
handleDoubleClick(_event: DoubleClickEvent, props: ModeProps<FeatureCollection>) {
|
|
152
177
|
this.finishDrawing(props);
|
|
178
|
+
this.resetClickSequence();
|
|
153
179
|
}
|
|
154
180
|
|
|
155
181
|
handleKeyUp(event: KeyboardEvent, props: ModeProps<FeatureCollection>) {
|
|
156
|
-
if (event.key ===
|
|
182
|
+
if (event.key === "Enter") {
|
|
157
183
|
this.finishDrawing(props);
|
|
158
|
-
} else if (event.key === 'Escape') {
|
|
159
184
|
this.resetClickSequence();
|
|
185
|
+
} else if (event.key === "Escape") {
|
|
186
|
+
this.resetClickSequence();
|
|
187
|
+
this.holeSequence = [];
|
|
188
|
+
this.isDrawingHole = false;
|
|
189
|
+
|
|
160
190
|
props.onEdit({
|
|
161
|
-
// Because the new drawing feature is dropped, so the data will keep as the same.
|
|
162
191
|
updatedData: props.data,
|
|
163
|
-
editType:
|
|
164
|
-
editContext: {}
|
|
192
|
+
editType: "cancelFeature",
|
|
193
|
+
editContext: {},
|
|
165
194
|
});
|
|
166
195
|
}
|
|
167
196
|
}
|
|
@@ -170,4 +199,179 @@ export class DrawPolygonMode extends GeoJsonEditMode {
|
|
|
170
199
|
props.onUpdateCursor('cell');
|
|
171
200
|
super.handlePointerMove(event, props);
|
|
172
201
|
}
|
|
202
|
+
|
|
203
|
+
// eslint-disable-next-line max-statements, complexity
|
|
204
|
+
finishDrawing(props: ModeProps<FeatureCollection>) {
|
|
205
|
+
const clickSequence = this.getClickSequence();
|
|
206
|
+
const polygon = [...clickSequence, clickSequence[0]];
|
|
207
|
+
|
|
208
|
+
const newPolygon = turfPolygon([polygon]);
|
|
209
|
+
|
|
210
|
+
const canAddHole = canAddHoleToPolygon(props);
|
|
211
|
+
const canOverlap = canPolygonOverlap(props);
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
// Check if the polygon intersects itself (excluding shared start/end point)
|
|
215
|
+
if (!canOverlap) {
|
|
216
|
+
const overlapping = lineIntersect(
|
|
217
|
+
newPolygon,
|
|
218
|
+
newPolygon,
|
|
219
|
+
).features.filter(
|
|
220
|
+
(intersection) =>
|
|
221
|
+
!newPolygon.geometry.coordinates[0].some(
|
|
222
|
+
(coord) =>
|
|
223
|
+
coord[0] === intersection.geometry.coordinates[0] &&
|
|
224
|
+
coord[1] === intersection.geometry.coordinates[1],
|
|
225
|
+
),
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
if (overlapping.length > 0) {
|
|
229
|
+
// ❌ Invalid polygon: overlaps
|
|
230
|
+
props.onEdit({
|
|
231
|
+
updatedData: props.data,
|
|
232
|
+
editType: "invalidPolygon",
|
|
233
|
+
editContext: { reason: "overlaps" },
|
|
234
|
+
});
|
|
235
|
+
this.resetClickSequence();
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (canAddHole) {
|
|
241
|
+
const holeResult = this.tryAddHoleToExistingPolygon(newPolygon, polygon, props);
|
|
242
|
+
if (holeResult.handled) {
|
|
243
|
+
this.resetClickSequence();
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// If no valid hole was found, add the polygon as a new feature
|
|
249
|
+
const editAction = this.getAddFeatureOrBooleanPolygonAction(
|
|
250
|
+
{
|
|
251
|
+
type: "Polygon",
|
|
252
|
+
coordinates: [[...this.getClickSequence(), this.getClickSequence()[0]]],
|
|
253
|
+
},
|
|
254
|
+
props,
|
|
255
|
+
);
|
|
256
|
+
if (editAction) props.onEdit(editAction);
|
|
257
|
+
this.resetClickSequence();
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private tryAddHoleToExistingPolygon(
|
|
262
|
+
newPolygon: any,
|
|
263
|
+
polygon: Position[],
|
|
264
|
+
props: ModeProps<FeatureCollection>
|
|
265
|
+
): { handled: boolean } {
|
|
266
|
+
for (const [featureIndex, feature] of props.data.features.entries()) {
|
|
267
|
+
if (feature.geometry.type === "Polygon") {
|
|
268
|
+
const result = this.validateAndCreateHole(feature, featureIndex, newPolygon, polygon, props);
|
|
269
|
+
if (result.handled) {
|
|
270
|
+
return result;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return { handled: false };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private validateAndCreateHole(
|
|
279
|
+
feature: any,
|
|
280
|
+
featureIndex: number,
|
|
281
|
+
newPolygon: any,
|
|
282
|
+
polygon: Position[],
|
|
283
|
+
props: ModeProps<FeatureCollection>
|
|
284
|
+
): { handled: boolean } {
|
|
285
|
+
const outer = turfPolygon(feature.geometry.coordinates);
|
|
286
|
+
|
|
287
|
+
// Check existing holes for conflicts
|
|
288
|
+
for (let i = 1; i < feature.geometry.coordinates.length; i++) {
|
|
289
|
+
const hole = turfPolygon([feature.geometry.coordinates[i]]);
|
|
290
|
+
const intersection = lineIntersect(hole, newPolygon);
|
|
291
|
+
|
|
292
|
+
if (intersection.features.length > 0) {
|
|
293
|
+
props.onEdit({
|
|
294
|
+
updatedData: props.data,
|
|
295
|
+
editType: "invalidHole",
|
|
296
|
+
editContext: { reason: "intersects-existing-hole" },
|
|
297
|
+
});
|
|
298
|
+
return { handled: true };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (booleanWithin(hole, newPolygon) || booleanWithin(newPolygon, hole)) {
|
|
302
|
+
props.onEdit({
|
|
303
|
+
updatedData: props.data,
|
|
304
|
+
editType: "invalidHole",
|
|
305
|
+
editContext: { reason: "contains-or-contained-by-existing-hole" },
|
|
306
|
+
});
|
|
307
|
+
return { handled: true };
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Check outer polygon conflicts
|
|
312
|
+
const intersectionWithOuter = lineIntersect(outer, newPolygon);
|
|
313
|
+
if (intersectionWithOuter.features.length > 0) {
|
|
314
|
+
props.onEdit({
|
|
315
|
+
updatedData: props.data,
|
|
316
|
+
editType: "invalidPolygon",
|
|
317
|
+
editContext: { reason: "intersects-existing-polygon" },
|
|
318
|
+
});
|
|
319
|
+
return { handled: true };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (booleanWithin(outer, newPolygon)) {
|
|
323
|
+
props.onEdit({
|
|
324
|
+
updatedData: props.data,
|
|
325
|
+
editType: "invalidPolygon",
|
|
326
|
+
editContext: { reason: "contains-existing-polygon" },
|
|
327
|
+
});
|
|
328
|
+
return { handled: true };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Check if new polygon is within outer polygon (valid hole)
|
|
332
|
+
if (booleanWithin(newPolygon, outer)) {
|
|
333
|
+
const updatedData = new ImmutableFeatureCollection(props.data)
|
|
334
|
+
.replaceGeometry(featureIndex, {
|
|
335
|
+
...feature.geometry,
|
|
336
|
+
coordinates: [...feature.geometry.coordinates, polygon],
|
|
337
|
+
})
|
|
338
|
+
.getObject();
|
|
339
|
+
|
|
340
|
+
props.onEdit({
|
|
341
|
+
updatedData,
|
|
342
|
+
editType: "addHole",
|
|
343
|
+
editContext: { hole: newPolygon.geometry },
|
|
344
|
+
});
|
|
345
|
+
return { handled: true };
|
|
346
|
+
}
|
|
347
|
+
return { handled: false };
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Helper function to check if a point is near the first point in the sequence
|
|
352
|
+
function isNearFirstPoint(
|
|
353
|
+
click: Position,
|
|
354
|
+
first: Position,
|
|
355
|
+
threshold = 1e-4,
|
|
356
|
+
): boolean {
|
|
357
|
+
const dx = click[0] - first[0];
|
|
358
|
+
const dy = click[1] - first[1];
|
|
359
|
+
return dx * dx + dy * dy < threshold * threshold;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Helper function to determine if a hole can be added to a polygon
|
|
363
|
+
function canAddHoleToPolygon(
|
|
364
|
+
props: ModeProps<FeatureCollection>
|
|
365
|
+
): boolean {
|
|
366
|
+
// For simplicity, always return true in this example.
|
|
367
|
+
// Implement your own logic based on application requirements.
|
|
368
|
+
return props.modeConfig?.allowHoles ?? false;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Helper function to determine if a polygon can intersect itself
|
|
372
|
+
function canPolygonOverlap(
|
|
373
|
+
props: ModeProps<FeatureCollection>
|
|
374
|
+
): boolean {
|
|
375
|
+
// Return the value of allowSelfIntersection (defaults to false for safety)
|
|
376
|
+
return props.modeConfig?.allowSelfIntersection ?? false;
|
|
173
377
|
}
|