3dtiles-inspector 0.2.2 → 0.2.4

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.
@@ -0,0 +1,348 @@
1
+ import { updateOverlayRect } from './geometry.js';
2
+
3
+ export const SCREEN_EDIT_EDGE_HIT_SIZE = 8;
4
+ export const SCREEN_EDIT_CORNER_HIT_SIZE = 16;
5
+ const SCREEN_EDIT_EDGE_HANDLE_MAX_LENGTH = 26;
6
+ const SCREEN_EDIT_EDGE_HANDLE_MIN_LENGTH = 6;
7
+ const SCREEN_EDIT_EDGE_HANDLE_ACTIVE_SCALE_X = 1.5;
8
+ const SCREEN_EDIT_EDGE_HANDLE_ACTIVE_SCALE_Y = 1.5;
9
+ const SCREEN_EDIT_GRID_DIVISIONS = 8;
10
+ const SCREEN_EDIT_MIN_CONVEX_CROSS_ABS = 1e-3;
11
+
12
+ export const SCREEN_EDIT_HANDLE_PARTS = [
13
+ 'top-left',
14
+ 'top',
15
+ 'top-right',
16
+ 'right',
17
+ 'bottom-right',
18
+ 'bottom',
19
+ 'bottom-left',
20
+ 'left',
21
+ ];
22
+ export const SCREEN_EDIT_CORNER_PARTS = [
23
+ 'top-left',
24
+ 'top-right',
25
+ 'bottom-right',
26
+ 'bottom-left',
27
+ ];
28
+ export const SCREEN_EDIT_EDGE_PARTS = ['top', 'right', 'bottom', 'left'];
29
+ export const SCREEN_EDIT_PART_POINT_INDICES = {
30
+ 'bottom-left': [3],
31
+ 'bottom-right': [2],
32
+ bottom: [2, 3],
33
+ left: [3, 0],
34
+ right: [1, 2],
35
+ top: [0, 1],
36
+ 'top-left': [0],
37
+ 'top-right': [1],
38
+ };
39
+
40
+ function clampValue(value, min, max) {
41
+ return Math.min(max, Math.max(min, value));
42
+ }
43
+
44
+ export function copyClientPoint(point) {
45
+ return {
46
+ x: Number(point?.x) || 0,
47
+ y: Number(point?.y) || 0,
48
+ };
49
+ }
50
+
51
+ export function copyClientPoints(points) {
52
+ return points.map(copyClientPoint);
53
+ }
54
+
55
+ function getClientPointsBounds(points) {
56
+ const xs = points.map((point) => point.x);
57
+ const ys = points.map((point) => point.y);
58
+ return {
59
+ maxX: Math.max(...xs),
60
+ maxY: Math.max(...ys),
61
+ minX: Math.min(...xs),
62
+ minY: Math.min(...ys),
63
+ };
64
+ }
65
+
66
+ export function getClientRectPoints(rect) {
67
+ return [
68
+ { x: Number(rect?.minX) || 0, y: Number(rect?.minY) || 0 },
69
+ { x: Number(rect?.maxX) || 0, y: Number(rect?.minY) || 0 },
70
+ { x: Number(rect?.maxX) || 0, y: Number(rect?.maxY) || 0 },
71
+ { x: Number(rect?.minX) || 0, y: Number(rect?.maxY) || 0 },
72
+ ];
73
+ }
74
+
75
+ function clampClientPoint(point, domRect) {
76
+ return {
77
+ x: clampValue(point.x, domRect.left, domRect.right),
78
+ y: clampValue(point.y, domRect.top, domRect.bottom),
79
+ };
80
+ }
81
+
82
+ export function clampClientPoints(points, domElement) {
83
+ const domRect = domElement.getBoundingClientRect();
84
+ return points.map((point) => clampClientPoint(point, domRect));
85
+ }
86
+
87
+ function getPointTurnCross(a, b, c) {
88
+ return (b.x - a.x) * (c.y - b.y) - (b.y - a.y) * (c.x - b.x);
89
+ }
90
+
91
+ export function isConvexClientQuad(points) {
92
+ if (!Array.isArray(points) || points.length !== 4) {
93
+ return false;
94
+ }
95
+
96
+ let turnSign = 0;
97
+ for (let index = 0; index < points.length; index++) {
98
+ const cross = getPointTurnCross(
99
+ points[index],
100
+ points[(index + 1) % points.length],
101
+ points[(index + 2) % points.length],
102
+ );
103
+ if (Math.abs(cross) <= SCREEN_EDIT_MIN_CONVEX_CROSS_ABS) {
104
+ return false;
105
+ }
106
+
107
+ const nextSign = Math.sign(cross);
108
+ if (turnSign === 0) {
109
+ turnSign = nextSign;
110
+ } else if (nextSign !== turnSign) {
111
+ return false;
112
+ }
113
+ }
114
+
115
+ return true;
116
+ }
117
+
118
+ export function getPartPoint(points, part) {
119
+ const indices = SCREEN_EDIT_PART_POINT_INDICES[part] || [];
120
+ if (indices.length === 0) {
121
+ return { x: 0, y: 0 };
122
+ }
123
+ const x =
124
+ indices.reduce((total, index) => total + points[index].x, 0) /
125
+ indices.length;
126
+ const y =
127
+ indices.reduce((total, index) => total + points[index].y, 0) /
128
+ indices.length;
129
+ return { x, y };
130
+ }
131
+
132
+ function getPartAngle(points, part) {
133
+ const indices = SCREEN_EDIT_PART_POINT_INDICES[part] || [];
134
+ if (indices.length !== 2) {
135
+ return null;
136
+ }
137
+
138
+ const start = points[indices[0]];
139
+ const end = points[indices[1]];
140
+ return Math.atan2(end.y - start.y, end.x - start.x);
141
+ }
142
+
143
+ function getPartLength(points, part) {
144
+ const indices = SCREEN_EDIT_PART_POINT_INDICES[part] || [];
145
+ if (indices.length !== 2) {
146
+ return null;
147
+ }
148
+
149
+ const start = points[indices[0]];
150
+ const end = points[indices[1]];
151
+ return Math.hypot(end.x - start.x, end.y - start.y);
152
+ }
153
+
154
+ function interpolatePoint(a, b, t) {
155
+ return {
156
+ x: a.x + (b.x - a.x) * t,
157
+ y: a.y + (b.y - a.y) * t,
158
+ };
159
+ }
160
+
161
+ function updateScreenEditGrid(grid, localPoints, visible) {
162
+ if (!grid) {
163
+ return;
164
+ }
165
+
166
+ grid.replaceChildren();
167
+ grid.hidden = !visible;
168
+ if (!visible) {
169
+ return;
170
+ }
171
+
172
+ const [topLeft, topRight, bottomRight, bottomLeft] = localPoints;
173
+ for (let index = 1; index < SCREEN_EDIT_GRID_DIVISIONS; index++) {
174
+ const t = index / SCREEN_EDIT_GRID_DIVISIONS;
175
+ const top = interpolatePoint(topLeft, topRight, t);
176
+ const bottom = interpolatePoint(bottomLeft, bottomRight, t);
177
+ const left = interpolatePoint(topLeft, bottomLeft, t);
178
+ const right = interpolatePoint(topRight, bottomRight, t);
179
+ const verticalLine = document.createElementNS(
180
+ 'http://www.w3.org/2000/svg',
181
+ 'line',
182
+ );
183
+ const horizontalLine = document.createElementNS(
184
+ 'http://www.w3.org/2000/svg',
185
+ 'line',
186
+ );
187
+ verticalLine.setAttribute('x1', String(top.x));
188
+ verticalLine.setAttribute('y1', String(top.y));
189
+ verticalLine.setAttribute('x2', String(bottom.x));
190
+ verticalLine.setAttribute('y2', String(bottom.y));
191
+ horizontalLine.setAttribute('x1', String(left.x));
192
+ horizontalLine.setAttribute('y1', String(left.y));
193
+ horizontalLine.setAttribute('x2', String(right.x));
194
+ horizontalLine.setAttribute('y2', String(right.y));
195
+ grid.append(verticalLine, horizontalLine);
196
+ }
197
+ }
198
+
199
+ function ensureEditableRectHandles(rectEl) {
200
+ if (!rectEl || rectEl.dataset.editHandlesReady === 'true') {
201
+ return;
202
+ }
203
+
204
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
205
+ const polygon = document.createElementNS(
206
+ 'http://www.w3.org/2000/svg',
207
+ 'polygon',
208
+ );
209
+ const grid = document.createElementNS('http://www.w3.org/2000/svg', 'g');
210
+ svg.classList.add('screen-selection-edit-svg');
211
+ grid.classList.add('screen-selection-edit-grid');
212
+ polygon.classList.add('screen-selection-edit-polygon');
213
+ svg.append(polygon, grid);
214
+ rectEl.appendChild(svg);
215
+
216
+ SCREEN_EDIT_HANDLE_PARTS.forEach((part) => {
217
+ const handle = document.createElement('span');
218
+ handle.classList.add(
219
+ 'screen-selection-edit-handle',
220
+ `screen-selection-edit-${part}`,
221
+ );
222
+ handle.dataset.editPart = part;
223
+ rectEl.appendChild(handle);
224
+ });
225
+ rectEl.dataset.editHandlesReady = 'true';
226
+ }
227
+
228
+ export function pointSegmentDistanceSq(point, start, end) {
229
+ const dx = end.x - start.x;
230
+ const dy = end.y - start.y;
231
+ const lengthSq = dx * dx + dy * dy;
232
+ if (lengthSq <= 1e-12) {
233
+ const px = point.x - start.x;
234
+ const py = point.y - start.y;
235
+ return px * px + py * py;
236
+ }
237
+
238
+ const t = clampValue(
239
+ ((point.x - start.x) * dx + (point.y - start.y) * dy) / lengthSq,
240
+ 0,
241
+ 1,
242
+ );
243
+ const x = start.x + dx * t;
244
+ const y = start.y + dy * t;
245
+ const px = point.x - x;
246
+ const py = point.y - y;
247
+ return px * px + py * py;
248
+ }
249
+
250
+ export function createScreenEditOverlay({ overlayEl, rectEl }) {
251
+ let activePart = null;
252
+
253
+ function applyActivePart() {
254
+ SCREEN_EDIT_HANDLE_PARTS.forEach((part) => {
255
+ const handle = rectEl?.querySelector(`[data-edit-part="${part}"]`);
256
+ handle?.classList.toggle('active', part === activePart);
257
+ });
258
+ }
259
+
260
+ function render(clientPoints, { showGrid = false } = {}) {
261
+ ensureEditableRectHandles(rectEl);
262
+ rectEl?.classList.add('editable');
263
+ rectEl?.classList.toggle('drawing', showGrid);
264
+ const bounds = getClientPointsBounds(clientPoints);
265
+ updateOverlayRect(overlayEl, rectEl, bounds);
266
+
267
+ const width = Math.max(1, bounds.maxX - bounds.minX);
268
+ const height = Math.max(1, bounds.maxY - bounds.minY);
269
+ const localPoints = clientPoints.map((point) => ({
270
+ x: point.x - bounds.minX,
271
+ y: point.y - bounds.minY,
272
+ }));
273
+ const svg = rectEl?.querySelector('.screen-selection-edit-svg');
274
+ const polygon = rectEl?.querySelector('.screen-selection-edit-polygon');
275
+ const grid = rectEl?.querySelector('.screen-selection-edit-grid');
276
+ svg?.setAttribute('viewBox', `0 0 ${width} ${height}`);
277
+ polygon?.setAttribute(
278
+ 'points',
279
+ localPoints.map((point) => `${point.x},${point.y}`).join(' '),
280
+ );
281
+ updateScreenEditGrid(grid, localPoints, showGrid);
282
+
283
+ SCREEN_EDIT_HANDLE_PARTS.forEach((part) => {
284
+ const point = getPartPoint(localPoints, part);
285
+ const handle = rectEl?.querySelector(`[data-edit-part="${part}"]`);
286
+ if (!handle) {
287
+ return;
288
+ }
289
+ handle.style.left = `${point.x}px`;
290
+ handle.style.top = `${point.y}px`;
291
+ const angle = getPartAngle(localPoints, part);
292
+ const length = getPartLength(localPoints, part);
293
+ if (length != null) {
294
+ const maxVisualLength = Math.max(0, length - 2);
295
+ const visualLength = Math.max(
296
+ 0,
297
+ Math.min(SCREEN_EDIT_EDGE_HANDLE_MAX_LENGTH, maxVisualLength),
298
+ );
299
+ const activeScaleX =
300
+ visualLength > 0
301
+ ? Math.min(
302
+ SCREEN_EDIT_EDGE_HANDLE_ACTIVE_SCALE_X,
303
+ Math.max(1, maxVisualLength / visualLength),
304
+ )
305
+ : 1;
306
+ handle.style.width = `${visualLength}px`;
307
+ handle.style.height = `${
308
+ length <= SCREEN_EDIT_EDGE_HANDLE_MIN_LENGTH ? 2 : 4
309
+ }px`;
310
+ handle.style.setProperty(
311
+ '--screen-selection-edit-active-scale-x',
312
+ String(activeScaleX),
313
+ );
314
+ handle.style.setProperty(
315
+ '--screen-selection-edit-active-scale-y',
316
+ String(SCREEN_EDIT_EDGE_HANDLE_ACTIVE_SCALE_Y),
317
+ );
318
+ } else {
319
+ handle.style.width = '';
320
+ handle.style.height = '';
321
+ handle.style.removeProperty('--screen-selection-edit-active-scale-x');
322
+ handle.style.removeProperty('--screen-selection-edit-active-scale-y');
323
+ }
324
+ handle.style.transform =
325
+ angle == null
326
+ ? 'translate(-50%, -50%)'
327
+ : `translate(-50%, -50%) rotate(${angle}rad)`;
328
+ });
329
+ applyActivePart();
330
+ }
331
+
332
+ function clear() {
333
+ activePart = null;
334
+ rectEl?.classList.remove('drawing', 'editable');
335
+ applyActivePart();
336
+ }
337
+
338
+ function setActivePart(part) {
339
+ activePart = SCREEN_EDIT_HANDLE_PARTS.includes(part) ? part : null;
340
+ applyActivePart();
341
+ }
342
+
343
+ return {
344
+ clear,
345
+ render,
346
+ setActivePart,
347
+ };
348
+ }
@@ -44,6 +44,16 @@ function clientPointToNdc(point, domRect) {
44
44
  };
45
45
  }
46
46
 
47
+ function getClientSelectionQuad(start, end) {
48
+ const clientRect = getClientSelectionRect(start, end);
49
+ return [
50
+ new Vector2(clientRect.minX, clientRect.minY),
51
+ new Vector2(clientRect.maxX, clientRect.minY),
52
+ new Vector2(clientRect.maxX, clientRect.maxY),
53
+ new Vector2(clientRect.minX, clientRect.maxY),
54
+ ];
55
+ }
56
+
47
57
  function getNdcSelectionRect(clientRect, domRect) {
48
58
  const topLeft = clientPointToNdc(
49
59
  new Vector2(clientRect.minX, clientRect.minY),
@@ -61,6 +71,21 @@ function getNdcSelectionRect(clientRect, domRect) {
61
71
  };
62
72
  }
63
73
 
74
+ function getNdcSelectionQuad(clientPoints, domRect) {
75
+ return clientPoints.map((point) => clientPointToNdc(point, domRect));
76
+ }
77
+
78
+ function getNdcBounds(points) {
79
+ const xs = points.map((point) => point.x);
80
+ const ys = points.map((point) => point.y);
81
+ return {
82
+ maxX: Math.max(...xs),
83
+ maxY: Math.max(...ys),
84
+ minX: Math.min(...xs),
85
+ minY: Math.min(...ys),
86
+ };
87
+ }
88
+
64
89
  export function updateOverlayRect(overlayEl, rectEl, clientRect) {
65
90
  if (!overlayEl || !rectEl) {
66
91
  return;
@@ -277,10 +302,12 @@ function createFarPlaneData({
277
302
  };
278
303
  }
279
304
 
280
- function createFrustumData(camera, rect, depthRange) {
305
+ function createFrustumDataFromQuad(camera, quad, depthRange) {
281
306
  const { farDepth, nearDepth } = normalizeDepthRange(camera, depthRange);
282
- const centerX = (rect.minX + rect.maxX) * 0.5;
283
- const centerY = (rect.minY + rect.maxY) * 0.5;
307
+ const centerX =
308
+ quad.reduce((total, point) => total + point.x, 0) / quad.length;
309
+ const centerY =
310
+ quad.reduce((total, point) => total + point.y, 0) / quad.length;
284
311
  const nearCenter = createPointAtViewDepth(
285
312
  camera,
286
313
  centerX,
@@ -312,56 +339,14 @@ function createFrustumData(camera, rect, depthRange) {
312
339
  plane.setFromNormalAndCoplanarPoint(selectionForward, farCenter);
313
340
 
314
341
  const farClipPlane = plane.clone();
315
- const farTopLeft = createPointOnPlane(
316
- camera,
317
- rect.minX,
318
- rect.maxY,
319
- farClipPlane,
320
- );
321
- const farTopRight = createPointOnPlane(
322
- camera,
323
- rect.maxX,
324
- rect.maxY,
325
- farClipPlane,
326
- );
327
- const farBottomRight = createPointOnPlane(
328
- camera,
329
- rect.maxX,
330
- rect.minY,
331
- farClipPlane,
332
- );
333
- const farBottomLeft = createPointOnPlane(
334
- camera,
335
- rect.minX,
336
- rect.minY,
337
- farClipPlane,
342
+ const [farTopLeft, farTopRight, farBottomRight, farBottomLeft] = quad.map(
343
+ (point) => createPointOnPlane(camera, point.x, point.y, farClipPlane),
338
344
  );
339
345
 
340
346
  plane.setFromNormalAndCoplanarPoint(selectionForward, nearCenter);
341
347
  const nearPlane = plane.clone();
342
- const nearTopLeft = createPointOnPlane(
343
- camera,
344
- rect.minX,
345
- rect.maxY,
346
- nearPlane,
347
- );
348
- const nearTopRight = createPointOnPlane(
349
- camera,
350
- rect.maxX,
351
- rect.maxY,
352
- nearPlane,
353
- );
354
- const nearBottomRight = createPointOnPlane(
355
- camera,
356
- rect.maxX,
357
- rect.minY,
358
- nearPlane,
359
- );
360
- const nearBottomLeft = createPointOnPlane(
361
- camera,
362
- rect.minX,
363
- rect.minY,
364
- nearPlane,
348
+ const [nearTopLeft, nearTopRight, nearBottomRight, nearBottomLeft] = quad.map(
349
+ (point) => createPointOnPlane(camera, point.x, point.y, nearPlane),
365
350
  );
366
351
  if (
367
352
  !farTopLeft ||
@@ -451,8 +436,22 @@ function createFrustumData(camera, rect, depthRange) {
451
436
  };
452
437
  }
453
438
 
439
+ function createFrustumData(camera, rect, depthRange) {
440
+ return createFrustumDataFromQuad(
441
+ camera,
442
+ [
443
+ { x: rect.minX, y: rect.maxY },
444
+ { x: rect.maxX, y: rect.maxY },
445
+ { x: rect.maxX, y: rect.minY },
446
+ { x: rect.minX, y: rect.minY },
447
+ ],
448
+ depthRange,
449
+ );
450
+ }
451
+
454
452
  export function createSelectionData({
455
453
  camera,
454
+ clientPoints,
456
455
  domElement,
457
456
  end,
458
457
  getDepthRange,
@@ -463,14 +462,27 @@ export function createSelectionData({
463
462
  return null;
464
463
  }
465
464
 
466
- const clientRect = getClientSelectionRect(start, end);
465
+ const hasClientQuad =
466
+ Array.isArray(clientPoints) && clientPoints.length === 4;
467
+ const points = hasClientQuad
468
+ ? clientPoints
469
+ : getClientSelectionQuad(start, end);
470
+ const clientRect = {
471
+ maxX: Math.max(...points.map((point) => point.x)),
472
+ maxY: Math.max(...points.map((point) => point.y)),
473
+ minX: Math.min(...points.map((point) => point.x)),
474
+ minY: Math.min(...points.map((point) => point.y)),
475
+ };
467
476
  const width = clientRect.maxX - clientRect.minX;
468
477
  const height = clientRect.maxY - clientRect.minY;
469
478
  if (width * width + height * height < SCREEN_SELECTION_MIN_DRAG_DISTANCE_SQ) {
470
479
  return null;
471
480
  }
472
481
 
473
- const rect = getNdcSelectionRect(clientRect, domRect);
482
+ const quad = getNdcSelectionQuad(points, domRect);
483
+ const rect = hasClientQuad
484
+ ? getNdcBounds(quad)
485
+ : getNdcSelectionRect(clientRect, domRect);
474
486
  camera.updateProjectionMatrix();
475
487
  camera.updateMatrixWorld(true);
476
488
  cameraPosition.copy(camera.position);
@@ -478,7 +490,9 @@ export function createSelectionData({
478
490
  const viewProjectionMatrix = new Matrix4()
479
491
  .multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse)
480
492
  .toArray();
481
- const frustum = createFrustumData(camera, rect, depthRange);
493
+ const frustum = hasClientQuad
494
+ ? createFrustumDataFromQuad(camera, quad, depthRange)
495
+ : createFrustumData(camera, rect, depthRange);
482
496
  if (!frustum) {
483
497
  return null;
484
498
  }
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  SCREEN_SELECTION_ACTION_EXCLUDE,
3
+ SCREEN_SELECTION_MIN_DEPTH_RANGE,
3
4
  cameraPosition,
4
5
  copyDepthRange,
5
6
  copyFarPlane,
@@ -87,6 +88,66 @@ export function getScreenSelectionPayload(selection) {
87
88
  };
88
89
  }
89
90
 
91
+ export function setScreenSelectionShape(
92
+ selection,
93
+ {
94
+ cameraPosition: sourceCameraPosition,
95
+ depthRange,
96
+ farPlane,
97
+ planeMatrices,
98
+ rect,
99
+ selectionForward: sourceSelectionForward,
100
+ viewProjectionMatrix,
101
+ },
102
+ currentTransformMatrix,
103
+ ) {
104
+ if (!selection) {
105
+ return;
106
+ }
107
+
108
+ const previousFarDepth = Number(selection.depthRange?.farDepth);
109
+ const copiedPlaneMatrices = planeMatrices.map((matrix) => matrix.slice());
110
+ const referenceTransformMatrix = copyMatrix4Array(currentTransformMatrix);
111
+
112
+ selection.basePlaneMatrices = copiedPlaneMatrices.map((matrix) =>
113
+ matrix.slice(),
114
+ );
115
+ selection.cameraPosition = copyVectorArray(sourceCameraPosition);
116
+ selection.currentTransformMatrix = referenceTransformMatrix.slice();
117
+ selection.depthRange = copyDepthRange(depthRange);
118
+ selection.farPlane = copyFarPlane(farPlane);
119
+ selection.planeMatrices = copiedPlaneMatrices;
120
+ selection.referenceTransformMatrix = referenceTransformMatrix;
121
+ selection.rect = copyRect(rect);
122
+ selection.selectionForward = copyVectorArray(sourceSelectionForward, [
123
+ 0,
124
+ 0,
125
+ -1,
126
+ ]);
127
+ selection.viewProjectionMatrix = viewProjectionMatrix.slice();
128
+
129
+ updateScreenSelectionWorldState(selection, currentTransformMatrix);
130
+
131
+ if (!Number.isFinite(previousFarDepth)) {
132
+ return;
133
+ }
134
+
135
+ const nextFarDepth = Math.min(
136
+ selection.depthRange.maxFarDepth,
137
+ Math.max(
138
+ selection.depthRange.nearDepth + SCREEN_SELECTION_MIN_DEPTH_RANGE,
139
+ previousFarDepth,
140
+ ),
141
+ );
142
+ if (Math.abs(nextFarDepth - selection.depthRange.farDepth) > 1e-9) {
143
+ setScreenSelectionFarDepth(
144
+ selection,
145
+ nextFarDepth,
146
+ currentTransformMatrix,
147
+ );
148
+ }
149
+ }
150
+
90
151
  export function updateScreenSelectionWorldState(
91
152
  selection,
92
153
  currentTransformMatrix,
@@ -11,6 +11,8 @@ export function createScreenSelectionPointerTracker({
11
11
  camera,
12
12
  domElement,
13
13
  getDepthRange,
14
+ onOverlayClear,
15
+ onOverlayUpdate,
14
16
  onSelectionCreated,
15
17
  overlayEl,
16
18
  rectEl,
@@ -20,9 +22,20 @@ export function createScreenSelectionPointerTracker({
20
22
 
21
23
  function clearDrag() {
22
24
  drag = null;
25
+ onOverlayClear?.();
23
26
  clearOverlay(overlayEl, rectEl);
24
27
  }
25
28
 
29
+ function updateDragOverlay() {
30
+ const clientRect = {
31
+ ...getClientSelectionRect(drag.start, drag.current),
32
+ };
33
+ if (onOverlayUpdate?.(clientRect) === true) {
34
+ return;
35
+ }
36
+ updateOverlayRect(overlayEl, rectEl, clientRect);
37
+ }
38
+
26
39
  function setActive(nextActive) {
27
40
  active = !!nextActive;
28
41
  if (!active) {
@@ -43,11 +56,7 @@ export function createScreenSelectionPointerTracker({
43
56
  start: dragStart.clone(),
44
57
  current: dragCurrent.clone(),
45
58
  };
46
- updateOverlayRect(
47
- overlayEl,
48
- rectEl,
49
- getClientSelectionRect(drag.start, drag.current),
50
- );
59
+ updateDragOverlay();
51
60
  domElement.setPointerCapture?.(event.pointerId);
52
61
  event.preventDefault();
53
62
  event.stopPropagation();
@@ -61,11 +70,7 @@ export function createScreenSelectionPointerTracker({
61
70
 
62
71
  const domRect = domElement.getBoundingClientRect();
63
72
  getClampedClientPoint(event, domRect, drag.current);
64
- updateOverlayRect(
65
- overlayEl,
66
- rectEl,
67
- getClientSelectionRect(drag.start, drag.current),
68
- );
73
+ updateDragOverlay();
69
74
  event.preventDefault();
70
75
  event.stopPropagation();
71
76
  return true;
@@ -76,6 +81,9 @@ export function createScreenSelectionPointerTracker({
76
81
  return false;
77
82
  }
78
83
 
84
+ const clientRect = {
85
+ ...getClientSelectionRect(drag.start, drag.current),
86
+ };
79
87
  const selection = createSelectionData({
80
88
  camera,
81
89
  domElement,
@@ -85,7 +93,7 @@ export function createScreenSelectionPointerTracker({
85
93
  });
86
94
  domElement.releasePointerCapture?.(event.pointerId);
87
95
  clearDrag();
88
- onSelectionCreated(selection);
96
+ onSelectionCreated(selection, clientRect);
89
97
  event.preventDefault();
90
98
  event.stopPropagation();
91
99
  return true;