@navaramap/three 0.1.0 → 0.1.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/README.md +4 -4
- package/dist/index.d.ts +91 -26
- package/dist/index.js +261 -213
- package/dist/index.js.map +1 -1
- package/dist/index.umd.cjs +31 -31
- package/dist/index.umd.cjs.map +1 -1
- package/package.json +12 -12
- package/src/core/BaseDesc.ts +25 -1
- package/src/core/MeshDesc.ts +2 -1
- package/src/index.ts +174 -77
- package/src/material/enhancer/model/modelBaseEnhancer/shader.ts +2 -2
- package/src/material/enhancer/polygon/polygonBaseEnhancer/shader.ts +2 -2
- package/src/pick/pickHelper.test.ts +182 -55
- package/src/pick/pickHelper.ts +144 -56
- package/src/plugins/AttributionPlugin/index.ts +1 -1
package/src/pick/pickHelper.ts
CHANGED
|
@@ -19,22 +19,40 @@ export type PickHelperOptions = {
|
|
|
19
19
|
};
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
|
-
* Travel tolerance in CSS pixels between
|
|
22
|
+
* Travel tolerance in CSS pixels between pointerdown and pointerup to count as a click.
|
|
23
23
|
* Follows Cesium's `_clickPixelTolerance` (MapLibre uses 3px).
|
|
24
24
|
* ref: https://github.com/CesiumGS/cesium/blob/9fda7ab97a762e40c74b1d0e1814c98a2de43337/packages/engine/Source/Core/ScreenSpaceEventHandler.js#L1012
|
|
25
25
|
* ref: https://github.com/maplibre/maplibre-gl-js/blob/3a0a4f795fef5b2a29034d71833475589c344eaf/src/ui/map.ts#L542
|
|
26
26
|
*/
|
|
27
|
-
const CLICK_PIXEL_TOLERANCE = 5;
|
|
27
|
+
export const CLICK_PIXEL_TOLERANCE = 5;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Travel tolerance for touch taps. Finger contact jitters far more than a
|
|
31
|
+
* mouse, so follows MapLibre's tap `MAX_DIST` instead.
|
|
32
|
+
* ref: https://github.com/maplibre/maplibre-gl-js/blob/3a0a4f795fef5b2a29034d71833475589c344eaf/src/ui/handler/tap_recognizer.ts
|
|
33
|
+
*/
|
|
34
|
+
export const TAP_PIXEL_TOLERANCE = 30;
|
|
28
35
|
|
|
29
36
|
export function isClickGesture(
|
|
30
37
|
downPosition: Vector2,
|
|
31
38
|
upPosition: Vector2,
|
|
39
|
+
tolerance: number = CLICK_PIXEL_TOLERANCE,
|
|
32
40
|
): boolean {
|
|
33
|
-
return downPosition.distanceTo(upPosition) <
|
|
41
|
+
return downPosition.distanceTo(upPosition) < tolerance;
|
|
34
42
|
}
|
|
35
43
|
|
|
36
44
|
/**
|
|
37
|
-
*
|
|
45
|
+
* Pick search radius in CSS pixels around the pointer. The pick reads a
|
|
46
|
+
* (2r+1)² window and takes the non-zero batch id closest to its center, so
|
|
47
|
+
* small features (points, thin lines) don't demand pixel-perfect aim.
|
|
48
|
+
*/
|
|
49
|
+
export const PICK_RADIUS = 3;
|
|
50
|
+
|
|
51
|
+
/** Pick search radius for touch: fingertips land less precisely than a cursor. */
|
|
52
|
+
export const TOUCH_PICK_RADIUS = 10;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* GPU picking using a dedicated render pass over a small search window.
|
|
38
56
|
*
|
|
39
57
|
* The pick pass does NOT reuse the main render pipeline (globe / MRT /
|
|
40
58
|
* draped / copy passes). Instead, every pickable raw — regardless of
|
|
@@ -48,11 +66,14 @@ export function isClickGesture(
|
|
|
48
66
|
* transforms, so world matrices of the re-parented objects are
|
|
49
67
|
* unaffected.
|
|
50
68
|
*
|
|
51
|
-
* Two gestures trigger a pick: a click (
|
|
52
|
-
* {@link CLICK_PIXEL_TOLERANCE}
|
|
53
|
-
* pick per animation frame. Both are
|
|
54
|
-
* matching `shouldClickPick` /
|
|
55
|
-
* (i.e. someone is listening for the
|
|
69
|
+
* Two gestures trigger a pick: a click or touch tap (pointerdown/pointerup
|
|
70
|
+
* within {@link CLICK_PIXEL_TOLERANCE} / {@link TAP_PIXEL_TOLERANCE}) and
|
|
71
|
+
* hovering, throttled to at most one pick per animation frame. Both are
|
|
72
|
+
* lazy: a pick runs only while the matching `shouldClickPick` /
|
|
73
|
+
* `shouldHoverPick` gate returns true (i.e. someone is listening for the
|
|
74
|
+
* result). The gestures are detected from pointer events, which fire for
|
|
75
|
+
* touch as well. The camera-input handlers call `preventDefault()` on
|
|
76
|
+
* touch events, so the compatibility mouse events never do.
|
|
56
77
|
*/
|
|
57
78
|
export class PickHelper {
|
|
58
79
|
private element: HTMLElement;
|
|
@@ -76,12 +97,13 @@ export class PickHelper {
|
|
|
76
97
|
/** Hover picks run only while this returns true (e.g. hover listeners exist). */
|
|
77
98
|
private shouldHoverPick: () => boolean;
|
|
78
99
|
|
|
79
|
-
private
|
|
80
|
-
private
|
|
81
|
-
private
|
|
82
|
-
private
|
|
100
|
+
private pointerDownPosition?: Vector2;
|
|
101
|
+
private pointerDownHandler: (event: PointerEvent) => void;
|
|
102
|
+
private pointerUpHandler: (event: PointerEvent) => void;
|
|
103
|
+
private pointerCancelHandler: () => void;
|
|
104
|
+
private hoverMoveHandler: (event: PointerEvent) => void;
|
|
83
105
|
private hoverLeaveHandler: () => void;
|
|
84
|
-
/** Latest
|
|
106
|
+
/** Latest pointermove position awaiting a hover pick, in client coords. */
|
|
85
107
|
private hoverPosition?: Vector2;
|
|
86
108
|
private hoverRafId?: number;
|
|
87
109
|
|
|
@@ -106,10 +128,13 @@ export class PickHelper {
|
|
|
106
128
|
this.shouldClickPick = shouldClickPick;
|
|
107
129
|
this.shouldHoverPick = shouldHoverPick;
|
|
108
130
|
|
|
109
|
-
this.
|
|
110
|
-
|
|
111
|
-
this.
|
|
112
|
-
this.
|
|
131
|
+
this.pointerDownHandler = (event: PointerEvent) =>
|
|
132
|
+
this.onPointerDown(event);
|
|
133
|
+
this.pointerUpHandler = (event: PointerEvent) => this.onPointerUp(event);
|
|
134
|
+
this.pointerCancelHandler = () => this.onPointerCancel();
|
|
135
|
+
this.hoverMoveHandler = (event: PointerEvent) =>
|
|
136
|
+
this.onHoverPointerMove(event);
|
|
137
|
+
this.hoverLeaveHandler = () => this.onHoverPointerLeave();
|
|
113
138
|
|
|
114
139
|
const width = this._renderer.getContext().drawingBufferWidth;
|
|
115
140
|
const height = this._renderer.getContext().drawingBufferHeight;
|
|
@@ -130,48 +155,75 @@ export class PickHelper {
|
|
|
130
155
|
}
|
|
131
156
|
}
|
|
132
157
|
|
|
133
|
-
private
|
|
134
|
-
|
|
158
|
+
private onPointerDown(event: PointerEvent) {
|
|
159
|
+
// A second concurrent pointer (e.g. the pinch-zoom finger) turns the
|
|
160
|
+
// gesture into something other than a tap; abandon the pending click.
|
|
161
|
+
if (!event.isPrimary) {
|
|
162
|
+
this.pointerDownPosition = undefined;
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
this.pointerDownPosition = new Vector2(event.clientX, event.clientY);
|
|
135
166
|
}
|
|
136
167
|
|
|
137
|
-
private
|
|
138
|
-
|
|
139
|
-
|
|
168
|
+
private onPointerUp(event: PointerEvent) {
|
|
169
|
+
if (!event.isPrimary) return;
|
|
170
|
+
|
|
171
|
+
const downPosition = this.pointerDownPosition;
|
|
172
|
+
this.pointerDownPosition = undefined;
|
|
140
173
|
if (!downPosition) return;
|
|
141
174
|
|
|
142
175
|
// Skip the GPU pick entirely when nobody is listening for the result.
|
|
143
176
|
if (!this.shouldClickPick()) return;
|
|
144
177
|
|
|
178
|
+
const tolerance =
|
|
179
|
+
event.pointerType === "touch"
|
|
180
|
+
? TAP_PIXEL_TOLERANCE
|
|
181
|
+
: CLICK_PIXEL_TOLERANCE;
|
|
145
182
|
if (
|
|
146
|
-
isClickGesture(
|
|
183
|
+
isClickGesture(
|
|
184
|
+
downPosition,
|
|
185
|
+
new Vector2(event.clientX, event.clientY),
|
|
186
|
+
tolerance,
|
|
187
|
+
)
|
|
147
188
|
) {
|
|
148
|
-
this.
|
|
189
|
+
this.onClickPick(event);
|
|
149
190
|
}
|
|
150
191
|
}
|
|
151
192
|
|
|
193
|
+
/** The browser took over the gesture (scroll, app switch): not a click. */
|
|
194
|
+
private onPointerCancel() {
|
|
195
|
+
this.pointerDownPosition = undefined;
|
|
196
|
+
}
|
|
197
|
+
|
|
152
198
|
enablePick(bPick: boolean) {
|
|
153
199
|
if (bPick) {
|
|
154
|
-
this.element.addEventListener("
|
|
155
|
-
this.element.addEventListener("
|
|
156
|
-
this.element.addEventListener("
|
|
157
|
-
this.element.addEventListener("
|
|
200
|
+
this.element.addEventListener("pointerdown", this.pointerDownHandler);
|
|
201
|
+
this.element.addEventListener("pointerup", this.pointerUpHandler);
|
|
202
|
+
this.element.addEventListener("pointercancel", this.pointerCancelHandler);
|
|
203
|
+
this.element.addEventListener("pointermove", this.hoverMoveHandler);
|
|
204
|
+
this.element.addEventListener("pointerleave", this.hoverLeaveHandler);
|
|
158
205
|
} else {
|
|
159
|
-
this.element.removeEventListener("
|
|
160
|
-
this.element.removeEventListener("
|
|
161
|
-
this.element.removeEventListener(
|
|
162
|
-
|
|
206
|
+
this.element.removeEventListener("pointerdown", this.pointerDownHandler);
|
|
207
|
+
this.element.removeEventListener("pointerup", this.pointerUpHandler);
|
|
208
|
+
this.element.removeEventListener(
|
|
209
|
+
"pointercancel",
|
|
210
|
+
this.pointerCancelHandler,
|
|
211
|
+
);
|
|
212
|
+
this.element.removeEventListener("pointermove", this.hoverMoveHandler);
|
|
213
|
+
this.element.removeEventListener("pointerleave", this.hoverLeaveHandler);
|
|
163
214
|
this.cancelPendingHoverPick();
|
|
164
215
|
}
|
|
165
216
|
}
|
|
166
217
|
|
|
167
218
|
/**
|
|
168
219
|
* Hover picking: schedules at most one GPU pick per animation frame from
|
|
169
|
-
* the latest
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
* listeners pay
|
|
220
|
+
* the latest pointermove position. Suppressed while any button is pressed
|
|
221
|
+
* (Camera drags would otherwise trigger a pick per frame. For touch,
|
|
222
|
+
* contact itself sets `buttons`, so touch never hover-picks) and while
|
|
223
|
+
* `shouldHoverPick` returns false, so pages without hover listeners pay
|
|
224
|
+
* nothing.
|
|
173
225
|
*/
|
|
174
|
-
private
|
|
226
|
+
private onHoverPointerMove(event: PointerEvent) {
|
|
175
227
|
if (event.buttons !== 0 || !this.shouldHoverPick()) return;
|
|
176
228
|
|
|
177
229
|
if (!this.hoverPosition) this.hoverPosition = new Vector2();
|
|
@@ -184,7 +236,7 @@ export class PickHelper {
|
|
|
184
236
|
if (!position) return;
|
|
185
237
|
this.hoverPosition = undefined;
|
|
186
238
|
|
|
187
|
-
// Listeners may have been removed between the
|
|
239
|
+
// Listeners may have been removed between the pointermove that
|
|
188
240
|
// scheduled this frame and now. Re-check so no orphan pick runs.
|
|
189
241
|
if (!this.shouldHoverPick()) return;
|
|
190
242
|
|
|
@@ -193,7 +245,7 @@ export class PickHelper {
|
|
|
193
245
|
});
|
|
194
246
|
}
|
|
195
247
|
|
|
196
|
-
private
|
|
248
|
+
private onHoverPointerLeave() {
|
|
197
249
|
this.cancelPendingHoverPick();
|
|
198
250
|
// Report "nothing hovered" so diff-based enter/leave synthesis can
|
|
199
251
|
// close out the current hover when the cursor exits the canvas.
|
|
@@ -303,17 +355,24 @@ export class PickHelper {
|
|
|
303
355
|
this.debugBufferView.render(this._renderer, this.debugRenderTarget);
|
|
304
356
|
}
|
|
305
357
|
|
|
306
|
-
private
|
|
307
|
-
const
|
|
358
|
+
private onClickPick(event: PointerEvent) {
|
|
359
|
+
const radius =
|
|
360
|
+
event.pointerType === "touch" ? TOUCH_PICK_RADIUS : PICK_RADIUS;
|
|
361
|
+
const batchId = this.pickBatchIdAt(event.clientX, event.clientY, radius);
|
|
308
362
|
const pickArr = batchId > 0 ? [batchId] : [];
|
|
309
363
|
this.onPickCallback(pickArr);
|
|
310
364
|
}
|
|
311
365
|
|
|
312
366
|
/**
|
|
313
|
-
* Runs the
|
|
314
|
-
* returns the
|
|
367
|
+
* Runs the pick render in a search window around the given client
|
|
368
|
+
* coordinates and returns the non-zero batch ID closest to its center
|
|
369
|
+
* (0 when nothing pickable is inside the window).
|
|
315
370
|
*/
|
|
316
|
-
private pickBatchIdAt(
|
|
371
|
+
private pickBatchIdAt(
|
|
372
|
+
clientX: number,
|
|
373
|
+
clientY: number,
|
|
374
|
+
radiusCss: number = PICK_RADIUS,
|
|
375
|
+
): number {
|
|
317
376
|
const rect = this.element.getBoundingClientRect();
|
|
318
377
|
const x = clientX - rect.left;
|
|
319
378
|
const y = clientY - rect.top;
|
|
@@ -338,29 +397,58 @@ export class PickHelper {
|
|
|
338
397
|
);
|
|
339
398
|
const readY = fullHeight - 1 - pixelY;
|
|
340
399
|
|
|
400
|
+
// The search window in device pixels, clamped to the viewport.
|
|
401
|
+
const radius = Math.max(0, Math.round(radiusCss * pixelRatio));
|
|
402
|
+
const minX = Math.max(0, pixelX - radius);
|
|
403
|
+
const maxX = Math.min(fullWidth - 1, pixelX + radius);
|
|
404
|
+
const minY = Math.max(0, readY - radius);
|
|
405
|
+
const maxY = Math.min(fullHeight - 1, readY + radius);
|
|
406
|
+
const width = maxX - minX + 1;
|
|
407
|
+
const height = maxY - minY + 1;
|
|
408
|
+
|
|
341
409
|
// Keep the camera projection unchanged for wide lines / screen-space
|
|
342
|
-
// expanded shaders, and limit fragment work to
|
|
343
|
-
this._renderer.setScissor(
|
|
410
|
+
// expanded shaders, and limit fragment work to the window via scissor.
|
|
411
|
+
this._renderer.setScissor(minX, minY, width, height);
|
|
344
412
|
this._renderer.setScissorTest(true);
|
|
345
413
|
|
|
346
414
|
this.processRender(this.pickRenderTarget, pickingCoord);
|
|
347
415
|
|
|
416
|
+
if (this.pixelBuffer.length < width * height * 4) {
|
|
417
|
+
this.pixelBuffer = new Uint8Array(width * height * 4);
|
|
418
|
+
}
|
|
348
419
|
this._renderer.readRenderTargetPixels(
|
|
349
420
|
this.pickRenderTarget,
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
421
|
+
minX,
|
|
422
|
+
minY,
|
|
423
|
+
width,
|
|
424
|
+
height,
|
|
354
425
|
this.pixelBuffer,
|
|
355
426
|
);
|
|
356
427
|
|
|
357
428
|
this._renderer.setScissorTest(false);
|
|
358
429
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
)
|
|
430
|
+
// The non-zero batch id nearest to the pointer wins, so a small feature
|
|
431
|
+
// beside a large one is still pickable by aiming at it.
|
|
432
|
+
let bestId = 0;
|
|
433
|
+
let bestDistSq = Infinity;
|
|
434
|
+
for (let row = 0; row < height; row++) {
|
|
435
|
+
for (let col = 0; col < width; col++) {
|
|
436
|
+
const i = (row * width + col) * 4;
|
|
437
|
+
const id =
|
|
438
|
+
(this.pixelBuffer[i] << 16) +
|
|
439
|
+
(this.pixelBuffer[i + 1] << 8) +
|
|
440
|
+
this.pixelBuffer[i + 2];
|
|
441
|
+
if (id === 0) continue;
|
|
442
|
+
const dx = minX + col - pixelX;
|
|
443
|
+
const dy = minY + row - readY;
|
|
444
|
+
const distSq = dx * dx + dy * dy;
|
|
445
|
+
if (distSq < bestDistSq) {
|
|
446
|
+
bestDistSq = distSq;
|
|
447
|
+
bestId = id;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return bestId;
|
|
364
452
|
}
|
|
365
453
|
|
|
366
454
|
public dispose() {
|