@navaramap/three 0.0.8 → 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.
Files changed (39) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +20 -8
  3. package/dist/assets/fontWorker-C5cHhkWQ.js +2 -0
  4. package/dist/assets/fontWorker-C5cHhkWQ.js.map +1 -0
  5. package/dist/assets/{index-DaejBdUz.js → index-VfUncKuS.js} +2 -2
  6. package/dist/assets/index-VfUncKuS.js.map +1 -0
  7. package/dist/assets/{navara_wasm_font_worker_bg-CSmoDs_Q.wasm → navara_wasm_font_worker_bg-CKqHBoCR.wasm} +0 -0
  8. package/dist/assets/{navara_wasm_worker_bg-C1dUd_Nk.wasm → navara_wasm_worker_bg-CT26EH41.wasm} +0 -0
  9. package/dist/index.d.ts +298 -54
  10. package/dist/index.js +1569 -1396
  11. package/dist/index.js.map +1 -1
  12. package/dist/index.umd.cjs +90 -69
  13. package/dist/index.umd.cjs.map +1 -1
  14. package/package.json +12 -12
  15. package/src/core/BaseDesc.ts +25 -1
  16. package/src/core/MeshDesc.test.ts +6 -8
  17. package/src/core/MeshDesc.ts +4 -6
  18. package/src/evaluations/FeatureEvaluator.ts +17 -10
  19. package/src/event/feature.ts +15 -2
  20. package/src/event/features/billboard.test.ts +62 -0
  21. package/src/event/features/billboard.ts +4 -2
  22. package/src/index.ts +266 -98
  23. package/src/material/enhancer/model/modelBaseEnhancer/shader.ts +2 -2
  24. package/src/material/enhancer/polygon/polygonBaseEnhancer/shader.ts +2 -2
  25. package/src/material/macro/tileShader.ts +12 -2
  26. package/src/mesh/polyline.ts +32 -4
  27. package/src/mesh/sdfText/batchedSdfText.test.ts +65 -0
  28. package/src/mesh/sdfText/batchedSdfText.ts +121 -43
  29. package/src/mesh/sprite/instancedSprite.ts +173 -65
  30. package/src/pick/pickHelper.test.ts +389 -4
  31. package/src/pick/pickHelper.ts +212 -37
  32. package/src/plugins/AttributionPlugin/index.ts +1 -1
  33. package/src/renderOrder.ts +10 -0
  34. package/src/terrain/sampleTerrainMostDetailed.test.ts +6 -6
  35. package/src/terrain/sampleTerrainMostDetailed.ts +7 -5
  36. package/src/type/index.ts +22 -1
  37. package/dist/assets/fontWorker-CkLGtrXf.js +0 -2
  38. package/dist/assets/fontWorker-CkLGtrXf.js.map +0 -1
  39. package/dist/assets/index-DaejBdUz.js.map +0 -1
@@ -1,10 +1,12 @@
1
- import { Vector2 } from "three";
2
- import { describe, expect, it } from "vitest";
1
+ import { PerspectiveCamera, Vector2, type WebGLRenderer } from "three";
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
3
 
4
- import { isClickGesture } from "./pickHelper";
4
+ import type { MeshCache } from "../type";
5
+
6
+ import { isClickGesture, PickHelper } from "./pickHelper";
5
7
 
6
8
  describe("isClickGesture", () => {
7
- it("accepts a mouseup at the exact mousedown position", () => {
9
+ it("accepts a pointerup at the exact pointerdown position", () => {
8
10
  expect(isClickGesture(new Vector2(100, 100), new Vector2(100, 100))).toBe(
9
11
  true,
10
12
  );
@@ -41,4 +43,387 @@ describe("isClickGesture", () => {
41
43
  // Each axis moved less than 5px but the combined travel exceeds it.
42
44
  expect(isClickGesture(new Vector2(0, 0), new Vector2(4, 4))).toBe(false);
43
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
+ });
55
+ });
56
+
57
+ /**
58
+ * Renderer stub: just enough of the WebGLRenderer surface for PickHelper.
59
+ * `pixel` is what the next readback returns; `scissors`/`renderCount`
60
+ * record the GPU work a pick performed.
61
+ */
62
+ const createRendererStub = () => {
63
+ const state = {
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,
68
+ renderCount: 0,
69
+ scissors: [] as { x: number; y: number; w: number; h: number }[],
70
+ };
71
+ const renderer = {
72
+ getContext: () => ({ drawingBufferWidth: 800, drawingBufferHeight: 600 }),
73
+ getPixelRatio: () => 1,
74
+ getClearColor: () => undefined,
75
+ getClearAlpha: () => 1,
76
+ getRenderTarget: () => null,
77
+ setClearColor: () => undefined,
78
+ setRenderTarget: () => undefined,
79
+ clear: () => undefined,
80
+ autoClear: true,
81
+ render: () => {
82
+ state.renderCount++;
83
+ },
84
+ setScissor: (x: number, y: number, w: number, h: number) => {
85
+ state.scissors.push({ x, y, w, h });
86
+ },
87
+ setScissorTest: () => undefined,
88
+ readRenderTargetPixels: (
89
+ _target: unknown,
90
+ x: number,
91
+ y: number,
92
+ w: number,
93
+ h: number,
94
+ buffer: Uint8Array,
95
+ ) => {
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
+ }
106
+ },
107
+ } as unknown as WebGLRenderer;
108
+ return { renderer, state };
109
+ };
110
+
111
+ describe("PickHelper", () => {
112
+ let rafCallbacks: Map<number, FrameRequestCallback>;
113
+ let nextRafId: number;
114
+
115
+ beforeEach(() => {
116
+ rafCallbacks = new Map();
117
+ nextRafId = 0;
118
+ vi.stubGlobal("requestAnimationFrame", (cb: FrameRequestCallback) => {
119
+ rafCallbacks.set(++nextRafId, cb);
120
+ return nextRafId;
121
+ });
122
+ vi.stubGlobal("cancelAnimationFrame", (id: number) => {
123
+ rafCallbacks.delete(id);
124
+ });
125
+ });
126
+
127
+ afterEach(() => {
128
+ vi.unstubAllGlobals();
129
+ });
130
+
131
+ /** Runs every rAF callback queued so far (one simulated frame). */
132
+ const runFrame = () => {
133
+ const callbacks = [...rafCallbacks.values()];
134
+ rafCallbacks.clear();
135
+ for (const cb of callbacks) cb(0);
136
+ };
137
+
138
+ const setup = (opts?: { clickable?: boolean; hoverable?: boolean }) => {
139
+ const element = document.createElement("div");
140
+ const { renderer, state } = createRendererStub();
141
+ const picks: number[][] = [];
142
+ const hovers: number[][] = [];
143
+ // Mutable so tests can flip a gate mid-flight (listener removal).
144
+ const gates = {
145
+ clickable: opts?.clickable ?? true,
146
+ hoverable: opts?.hoverable ?? true,
147
+ };
148
+ const helper = new PickHelper(
149
+ element,
150
+ renderer,
151
+ new PerspectiveCamera(),
152
+ new Map() as MeshCache,
153
+ (arr) => picks.push(arr),
154
+ (arr) => hovers.push(arr),
155
+ () => gates.clickable,
156
+ () => gates.hoverable,
157
+ );
158
+ helper.enablePick(true);
159
+
160
+ const pointer = (
161
+ type: string,
162
+ x: number,
163
+ y: number,
164
+ init?: PointerEventInit,
165
+ ) =>
166
+ element.dispatchEvent(
167
+ new PointerEvent(type, {
168
+ clientX: x,
169
+ clientY: y,
170
+ isPrimary: true,
171
+ pointerType: "mouse",
172
+ ...init,
173
+ }),
174
+ );
175
+ return { helper, element, state, picks, hovers, gates, pointer };
176
+ };
177
+
178
+ describe("click picking", () => {
179
+ it("decodes the batch id from the pick render", () => {
180
+ const { state, picks, pointer } = setup();
181
+ state.pixel = [0, 1, 2]; // (0 << 16) + (1 << 8) + 2
182
+
183
+ pointer("pointerdown", 100, 100);
184
+ pointer("pointerup", 100, 100);
185
+
186
+ expect(picks).toEqual([[258]]);
187
+ expect(state.renderCount).toBe(1);
188
+ });
189
+
190
+ it("reports an empty pick for a miss (batch id 0)", () => {
191
+ const { picks, pointer } = setup();
192
+
193
+ pointer("pointerdown", 100, 100);
194
+ pointer("pointerup", 100, 100);
195
+
196
+ expect(picks).toEqual([[]]);
197
+ });
198
+
199
+ it("limits GPU work to the search window via scissor", () => {
200
+ const { state, pointer } = setup();
201
+
202
+ pointer("pointerdown", 30, 40);
203
+ pointer("pointerup", 30, 40);
204
+
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]]);
224
+ });
225
+
226
+ it("does not pick when the gesture is a drag", () => {
227
+ const { state, picks, pointer } = setup();
228
+
229
+ pointer("pointerdown", 100, 100);
230
+ pointer("pointerup", 150, 150);
231
+
232
+ expect(picks).toEqual([]);
233
+ expect(state.renderCount).toBe(0);
234
+ });
235
+
236
+ it("skips the pick render entirely when nobody listens", () => {
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();
315
+
316
+ pointer("pointerdown", 100, 100, touch);
317
+ pointer("pointercancel", 100, 100, touch);
318
+ pointer("pointerup", 100, 100, touch);
319
+
320
+ expect(picks).toEqual([]);
321
+ expect(state.renderCount).toBe(0);
322
+ });
323
+ });
324
+
325
+ describe("hover picking", () => {
326
+ it("picks at most once per animation frame, at the latest position", () => {
327
+ const { state, hovers, pointer } = setup();
328
+ state.pixel = [0, 0, 7];
329
+
330
+ pointer("pointermove", 10, 10);
331
+ pointer("pointermove", 20, 20);
332
+ pointer("pointermove", 30, 30);
333
+ expect(hovers).toEqual([]); // nothing until the frame fires
334
+
335
+ runFrame();
336
+
337
+ expect(hovers).toEqual([[7]]);
338
+ expect(state.renderCount).toBe(1);
339
+ expect(state.scissors).toEqual([{ x: 27, y: 566, w: 7, h: 7 }]);
340
+ });
341
+
342
+ it("reports an empty hover for a miss", () => {
343
+ const { hovers, pointer } = setup();
344
+
345
+ pointer("pointermove", 10, 10);
346
+ runFrame();
347
+
348
+ expect(hovers).toEqual([[]]);
349
+ });
350
+
351
+ it("is suppressed while a button is pressed (including touch contact)", () => {
352
+ const { state, hovers, pointer } = setup();
353
+
354
+ pointer("pointermove", 10, 10, { buttons: 1 });
355
+ pointer("pointermove", 10, 10, { buttons: 1, pointerType: "touch" });
356
+ runFrame();
357
+
358
+ expect(hovers).toEqual([]);
359
+ expect(state.renderCount).toBe(0);
360
+ });
361
+
362
+ it("re-checks the listener gate when the frame fires", () => {
363
+ const { state, hovers, gates, pointer } = setup();
364
+ state.pixel = [0, 0, 7];
365
+
366
+ pointer("pointermove", 10, 10);
367
+ // Listeners are removed between the pointermove and the frame.
368
+ gates.hoverable = false;
369
+ runFrame();
370
+
371
+ expect(hovers).toEqual([]);
372
+ expect(state.renderCount).toBe(0);
373
+ });
374
+
375
+ it("schedules nothing when no hover listener is registered", () => {
376
+ const { state, hovers, pointer } = setup({ hoverable: false });
377
+
378
+ pointer("pointermove", 10, 10);
379
+
380
+ expect(rafCallbacks.size).toBe(0);
381
+ runFrame();
382
+ expect(hovers).toEqual([]);
383
+ expect(state.renderCount).toBe(0);
384
+ });
385
+
386
+ it("pointerleave reports nothing hovered and cancels the pending pick", () => {
387
+ const { state, hovers, pointer } = setup();
388
+ state.pixel = [0, 0, 7];
389
+
390
+ pointer("pointermove", 10, 10);
391
+ pointer("pointerleave", 10, 10);
392
+
393
+ expect(hovers).toEqual([[]]);
394
+
395
+ runFrame();
396
+ // The queued pick was cancelled: no render, no second callback.
397
+ expect(hovers).toEqual([[]]);
398
+ expect(state.renderCount).toBe(0);
399
+ });
400
+
401
+ it("pointerleave stays silent without hover listeners", () => {
402
+ const { hovers, pointer } = setup({ hoverable: false });
403
+
404
+ pointer("pointerleave", 10, 10);
405
+
406
+ expect(hovers).toEqual([]);
407
+ });
408
+ });
409
+
410
+ describe("enablePick(false)", () => {
411
+ it("detaches every handler and cancels pending hover picks", () => {
412
+ const { helper, state, picks, hovers, pointer } = setup();
413
+ state.pixel = [0, 0, 7];
414
+ pointer("pointermove", 10, 10);
415
+
416
+ helper.enablePick(false);
417
+
418
+ expect(rafCallbacks.size).toBe(0);
419
+ pointer("pointerdown", 100, 100);
420
+ pointer("pointerup", 100, 100);
421
+ pointer("pointermove", 20, 20);
422
+ runFrame();
423
+
424
+ expect(picks).toEqual([]);
425
+ expect(hovers).toEqual([]);
426
+ expect(state.renderCount).toBe(0);
427
+ });
428
+ });
44
429
  });