@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.
@@ -6,7 +6,7 @@ import type { MeshCache } from "../type";
6
6
  import { isClickGesture, PickHelper } from "./pickHelper";
7
7
 
8
8
  describe("isClickGesture", () => {
9
- it("accepts a mouseup at the exact mousedown position", () => {
9
+ it("accepts a pointerup at the exact pointerdown position", () => {
10
10
  expect(isClickGesture(new Vector2(100, 100), new Vector2(100, 100))).toBe(
11
11
  true,
12
12
  );
@@ -43,6 +43,15 @@ describe("isClickGesture", () => {
43
43
  // Each axis moved less than 5px but the combined travel exceeds it.
44
44
  expect(isClickGesture(new Vector2(0, 0), new Vector2(4, 4))).toBe(false);
45
45
  });
46
+
47
+ it("accepts a wider travel when given an explicit tolerance (touch taps)", () => {
48
+ expect(isClickGesture(new Vector2(0, 0), new Vector2(20, 0), 30)).toBe(
49
+ true,
50
+ );
51
+ expect(isClickGesture(new Vector2(0, 0), new Vector2(30, 0), 30)).toBe(
52
+ false,
53
+ );
54
+ });
46
55
  });
47
56
 
48
57
  /**
@@ -53,6 +62,9 @@ describe("isClickGesture", () => {
53
62
  const createRendererStub = () => {
54
63
  const state = {
55
64
  pixel: [0, 0, 0] as [number, number, number],
65
+ /** Per-pixel override for the readback, keyed by device coords. */
66
+ pixelAt: undefined as
67
+ ((x: number, y: number) => [number, number, number]) | undefined,
56
68
  renderCount: 0,
57
69
  scissors: [] as { x: number; y: number; w: number; h: number }[],
58
70
  };
@@ -75,14 +87,22 @@ const createRendererStub = () => {
75
87
  setScissorTest: () => undefined,
76
88
  readRenderTargetPixels: (
77
89
  _target: unknown,
78
- _x: number,
79
- _y: number,
80
- _w: number,
81
- _h: number,
90
+ x: number,
91
+ y: number,
92
+ w: number,
93
+ h: number,
82
94
  buffer: Uint8Array,
83
95
  ) => {
84
- [buffer[0], buffer[1], buffer[2]] = state.pixel;
85
- buffer[3] = 255;
96
+ for (let row = 0; row < h; row++) {
97
+ for (let col = 0; col < w; col++) {
98
+ const i = (row * w + col) * 4;
99
+ const [r, g, b] = state.pixelAt?.(x + col, y + row) ?? state.pixel;
100
+ buffer[i] = r;
101
+ buffer[i + 1] = g;
102
+ buffer[i + 2] = b;
103
+ buffer[i + 3] = 255;
104
+ }
105
+ }
86
106
  },
87
107
  } as unknown as WebGLRenderer;
88
108
  return { renderer, state };
@@ -137,59 +157,165 @@ describe("PickHelper", () => {
137
157
  );
138
158
  helper.enablePick(true);
139
159
 
140
- const mouse = (type: string, x: number, y: number, buttons = 0) =>
160
+ const pointer = (
161
+ type: string,
162
+ x: number,
163
+ y: number,
164
+ init?: PointerEventInit,
165
+ ) =>
141
166
  element.dispatchEvent(
142
- new MouseEvent(type, { clientX: x, clientY: y, buttons }),
167
+ new PointerEvent(type, {
168
+ clientX: x,
169
+ clientY: y,
170
+ isPrimary: true,
171
+ pointerType: "mouse",
172
+ ...init,
173
+ }),
143
174
  );
144
- return { helper, element, state, picks, hovers, gates, mouse };
175
+ return { helper, element, state, picks, hovers, gates, pointer };
145
176
  };
146
177
 
147
178
  describe("click picking", () => {
148
179
  it("decodes the batch id from the pick render", () => {
149
- const { state, picks, mouse } = setup();
180
+ const { state, picks, pointer } = setup();
150
181
  state.pixel = [0, 1, 2]; // (0 << 16) + (1 << 8) + 2
151
182
 
152
- mouse("mousedown", 100, 100);
153
- mouse("mouseup", 100, 100);
183
+ pointer("pointerdown", 100, 100);
184
+ pointer("pointerup", 100, 100);
154
185
 
155
186
  expect(picks).toEqual([[258]]);
156
187
  expect(state.renderCount).toBe(1);
157
188
  });
158
189
 
159
190
  it("reports an empty pick for a miss (batch id 0)", () => {
160
- const { picks, mouse } = setup();
191
+ const { picks, pointer } = setup();
161
192
 
162
- mouse("mousedown", 100, 100);
163
- mouse("mouseup", 100, 100);
193
+ pointer("pointerdown", 100, 100);
194
+ pointer("pointerup", 100, 100);
164
195
 
165
196
  expect(picks).toEqual([[]]);
166
197
  });
167
198
 
168
- it("limits GPU work to the clicked pixel via scissor", () => {
169
- const { state, mouse } = setup();
199
+ it("limits GPU work to the search window via scissor", () => {
200
+ const { state, pointer } = setup();
170
201
 
171
- mouse("mousedown", 30, 40);
172
- mouse("mouseup", 30, 40);
202
+ pointer("pointerdown", 30, 40);
203
+ pointer("pointerup", 30, 40);
173
204
 
174
- // y is flipped into WebGL space: 600 - 1 - 40.
175
- expect(state.scissors).toEqual([{ x: 30, y: 559, w: 1, h: 1 }]);
205
+ // A (2 * PICK_RADIUS + 1)² window centered on the click; y is flipped
206
+ // into WebGL space: 600 - 1 - 40.
207
+ expect(state.scissors).toEqual([{ x: 27, y: 556, w: 7, h: 7 }]);
208
+ });
209
+
210
+ it("picks the non-zero id closest to the pointer within the window", () => {
211
+ const { state, picks, pointer } = setup();
212
+ // Two hits inside the window around (100, 100) (device y = 499):
213
+ // id 5 at 3px away, id 9 at 1px away. The closer one wins.
214
+ state.pixelAt = (x, y) => {
215
+ if (x === 103 && y === 499) return [0, 0, 5];
216
+ if (x === 101 && y === 499) return [0, 0, 9];
217
+ return [0, 0, 0];
218
+ };
219
+
220
+ pointer("pointerdown", 100, 100);
221
+ pointer("pointerup", 100, 100);
222
+
223
+ expect(picks).toEqual([[9]]);
176
224
  });
177
225
 
178
226
  it("does not pick when the gesture is a drag", () => {
179
- const { state, picks, mouse } = setup();
227
+ const { state, picks, pointer } = setup();
180
228
 
181
- mouse("mousedown", 100, 100);
182
- mouse("mouseup", 150, 150);
229
+ pointer("pointerdown", 100, 100);
230
+ pointer("pointerup", 150, 150);
183
231
 
184
232
  expect(picks).toEqual([]);
185
233
  expect(state.renderCount).toBe(0);
186
234
  });
187
235
 
188
236
  it("skips the pick render entirely when nobody listens", () => {
189
- const { state, picks, mouse } = setup({ clickable: false });
237
+ const { state, picks, pointer } = setup({ clickable: false });
238
+
239
+ pointer("pointerdown", 100, 100);
240
+ pointer("pointerup", 100, 100);
241
+
242
+ expect(picks).toEqual([]);
243
+ expect(state.renderCount).toBe(0);
244
+ });
245
+ });
246
+
247
+ describe("touch tap picking", () => {
248
+ const touch = { pointerType: "touch" } as const;
249
+
250
+ it("picks on a single-finger tap", () => {
251
+ const { state, picks, pointer } = setup();
252
+ state.pixel = [0, 0, 9];
253
+
254
+ pointer("pointerdown", 100, 100, touch);
255
+ pointer("pointerup", 100, 100, touch);
256
+
257
+ expect(picks).toEqual([[9]]);
258
+ expect(state.renderCount).toBe(1);
259
+ });
260
+
261
+ it("tolerates finger jitter beyond the mouse click tolerance", () => {
262
+ const { picks, pointer } = setup();
263
+
264
+ // 20px of travel: a drag for a mouse, still a tap for a finger.
265
+ pointer("pointerdown", 100, 100, touch);
266
+ pointer("pointerup", 120, 100, touch);
267
+
268
+ expect(picks).toEqual([[]]);
269
+ });
270
+
271
+ it("rejects travel beyond the tap tolerance", () => {
272
+ const { state, picks, pointer } = setup();
273
+
274
+ pointer("pointerdown", 100, 100, touch);
275
+ pointer("pointerup", 140, 100, touch);
276
+
277
+ expect(picks).toEqual([]);
278
+ expect(state.renderCount).toBe(0);
279
+ });
280
+
281
+ it("keeps the stricter tolerance for the mouse", () => {
282
+ const { state, picks, pointer } = setup();
283
+
284
+ pointer("pointerdown", 100, 100);
285
+ pointer("pointerup", 120, 100);
286
+
287
+ expect(picks).toEqual([]);
288
+ expect(state.renderCount).toBe(0);
289
+ });
290
+
291
+ it("does not pick when a second finger joins (pinch)", () => {
292
+ const { state, picks, pointer } = setup();
293
+
294
+ pointer("pointerdown", 100, 100, touch);
295
+ pointer("pointerdown", 120, 100, { ...touch, isPrimary: false });
296
+ pointer("pointerup", 120, 100, { ...touch, isPrimary: false });
297
+ pointer("pointerup", 100, 100, touch);
298
+
299
+ expect(picks).toEqual([]);
300
+ expect(state.renderCount).toBe(0);
301
+ });
302
+
303
+ it("searches a wider window than the mouse (fingers are less precise)", () => {
304
+ const { state, pointer } = setup();
305
+
306
+ pointer("pointerdown", 100, 100, touch);
307
+ pointer("pointerup", 100, 100, touch);
308
+
309
+ // (2 * TOUCH_PICK_RADIUS + 1)² centered on the tap (device y = 499).
310
+ expect(state.scissors).toEqual([{ x: 90, y: 489, w: 21, h: 21 }]);
311
+ });
312
+
313
+ it("does not pick when the browser cancels the gesture", () => {
314
+ const { state, picks, pointer } = setup();
190
315
 
191
- mouse("mousedown", 100, 100);
192
- mouse("mouseup", 100, 100);
316
+ pointer("pointerdown", 100, 100, touch);
317
+ pointer("pointercancel", 100, 100, touch);
318
+ pointer("pointerup", 100, 100, touch);
193
319
 
194
320
  expect(picks).toEqual([]);
195
321
  expect(state.renderCount).toBe(0);
@@ -198,34 +324,35 @@ describe("PickHelper", () => {
198
324
 
199
325
  describe("hover picking", () => {
200
326
  it("picks at most once per animation frame, at the latest position", () => {
201
- const { state, hovers, mouse } = setup();
327
+ const { state, hovers, pointer } = setup();
202
328
  state.pixel = [0, 0, 7];
203
329
 
204
- mouse("mousemove", 10, 10);
205
- mouse("mousemove", 20, 20);
206
- mouse("mousemove", 30, 30);
330
+ pointer("pointermove", 10, 10);
331
+ pointer("pointermove", 20, 20);
332
+ pointer("pointermove", 30, 30);
207
333
  expect(hovers).toEqual([]); // nothing until the frame fires
208
334
 
209
335
  runFrame();
210
336
 
211
337
  expect(hovers).toEqual([[7]]);
212
338
  expect(state.renderCount).toBe(1);
213
- expect(state.scissors).toEqual([{ x: 30, y: 569, w: 1, h: 1 }]);
339
+ expect(state.scissors).toEqual([{ x: 27, y: 566, w: 7, h: 7 }]);
214
340
  });
215
341
 
216
342
  it("reports an empty hover for a miss", () => {
217
- const { hovers, mouse } = setup();
343
+ const { hovers, pointer } = setup();
218
344
 
219
- mouse("mousemove", 10, 10);
345
+ pointer("pointermove", 10, 10);
220
346
  runFrame();
221
347
 
222
348
  expect(hovers).toEqual([[]]);
223
349
  });
224
350
 
225
- it("is suppressed while a mouse button is pressed", () => {
226
- const { state, hovers, mouse } = setup();
351
+ it("is suppressed while a button is pressed (including touch contact)", () => {
352
+ const { state, hovers, pointer } = setup();
227
353
 
228
- mouse("mousemove", 10, 10, 1);
354
+ pointer("pointermove", 10, 10, { buttons: 1 });
355
+ pointer("pointermove", 10, 10, { buttons: 1, pointerType: "touch" });
229
356
  runFrame();
230
357
 
231
358
  expect(hovers).toEqual([]);
@@ -233,11 +360,11 @@ describe("PickHelper", () => {
233
360
  });
234
361
 
235
362
  it("re-checks the listener gate when the frame fires", () => {
236
- const { state, hovers, gates, mouse } = setup();
363
+ const { state, hovers, gates, pointer } = setup();
237
364
  state.pixel = [0, 0, 7];
238
365
 
239
- mouse("mousemove", 10, 10);
240
- // Listeners are removed between the mousemove and the frame.
366
+ pointer("pointermove", 10, 10);
367
+ // Listeners are removed between the pointermove and the frame.
241
368
  gates.hoverable = false;
242
369
  runFrame();
243
370
 
@@ -246,9 +373,9 @@ describe("PickHelper", () => {
246
373
  });
247
374
 
248
375
  it("schedules nothing when no hover listener is registered", () => {
249
- const { state, hovers, mouse } = setup({ hoverable: false });
376
+ const { state, hovers, pointer } = setup({ hoverable: false });
250
377
 
251
- mouse("mousemove", 10, 10);
378
+ pointer("pointermove", 10, 10);
252
379
 
253
380
  expect(rafCallbacks.size).toBe(0);
254
381
  runFrame();
@@ -256,12 +383,12 @@ describe("PickHelper", () => {
256
383
  expect(state.renderCount).toBe(0);
257
384
  });
258
385
 
259
- it("mouseleave reports nothing hovered and cancels the pending pick", () => {
260
- const { state, hovers, element, mouse } = setup();
386
+ it("pointerleave reports nothing hovered and cancels the pending pick", () => {
387
+ const { state, hovers, pointer } = setup();
261
388
  state.pixel = [0, 0, 7];
262
389
 
263
- mouse("mousemove", 10, 10);
264
- element.dispatchEvent(new MouseEvent("mouseleave"));
390
+ pointer("pointermove", 10, 10);
391
+ pointer("pointerleave", 10, 10);
265
392
 
266
393
  expect(hovers).toEqual([[]]);
267
394
 
@@ -271,10 +398,10 @@ describe("PickHelper", () => {
271
398
  expect(state.renderCount).toBe(0);
272
399
  });
273
400
 
274
- it("mouseleave stays silent without hover listeners", () => {
275
- const { hovers, element } = setup({ hoverable: false });
401
+ it("pointerleave stays silent without hover listeners", () => {
402
+ const { hovers, pointer } = setup({ hoverable: false });
276
403
 
277
- element.dispatchEvent(new MouseEvent("mouseleave"));
404
+ pointer("pointerleave", 10, 10);
278
405
 
279
406
  expect(hovers).toEqual([]);
280
407
  });
@@ -282,16 +409,16 @@ describe("PickHelper", () => {
282
409
 
283
410
  describe("enablePick(false)", () => {
284
411
  it("detaches every handler and cancels pending hover picks", () => {
285
- const { helper, state, picks, hovers, mouse } = setup();
412
+ const { helper, state, picks, hovers, pointer } = setup();
286
413
  state.pixel = [0, 0, 7];
287
- mouse("mousemove", 10, 10);
414
+ pointer("pointermove", 10, 10);
288
415
 
289
416
  helper.enablePick(false);
290
417
 
291
418
  expect(rafCallbacks.size).toBe(0);
292
- mouse("mousedown", 100, 100);
293
- mouse("mouseup", 100, 100);
294
- mouse("mousemove", 20, 20);
419
+ pointer("pointerdown", 100, 100);
420
+ pointer("pointerup", 100, 100);
421
+ pointer("pointermove", 20, 20);
295
422
  runFrame();
296
423
 
297
424
  expect(picks).toEqual([]);