@navaramap/three 0.0.8 → 0.1.0
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/LICENSE +1 -1
- package/README.md +20 -8
- package/dist/assets/fontWorker-C5cHhkWQ.js +2 -0
- package/dist/assets/fontWorker-C5cHhkWQ.js.map +1 -0
- package/dist/assets/{index-DaejBdUz.js → index-VfUncKuS.js} +2 -2
- package/dist/assets/index-VfUncKuS.js.map +1 -0
- package/dist/assets/{navara_wasm_font_worker_bg-CSmoDs_Q.wasm → navara_wasm_font_worker_bg-CKqHBoCR.wasm} +0 -0
- package/dist/assets/{navara_wasm_worker_bg-C1dUd_Nk.wasm → navara_wasm_worker_bg-CT26EH41.wasm} +0 -0
- package/dist/index.d.ts +213 -34
- package/dist/index.js +1476 -1351
- package/dist/index.js.map +1 -1
- package/dist/index.umd.cjs +79 -58
- package/dist/index.umd.cjs.map +1 -1
- package/package.json +12 -12
- package/src/core/MeshDesc.test.ts +6 -8
- package/src/core/MeshDesc.ts +2 -5
- package/src/evaluations/FeatureEvaluator.ts +17 -10
- package/src/event/feature.ts +15 -2
- package/src/event/features/billboard.test.ts +62 -0
- package/src/event/features/billboard.ts +4 -2
- package/src/index.ts +98 -27
- package/src/material/macro/tileShader.ts +12 -2
- package/src/mesh/polyline.ts +32 -4
- package/src/mesh/sdfText/batchedSdfText.test.ts +65 -0
- package/src/mesh/sdfText/batchedSdfText.ts +121 -43
- package/src/mesh/sprite/instancedSprite.ts +173 -65
- package/src/pick/pickHelper.test.ts +261 -3
- package/src/pick/pickHelper.ts +94 -7
- package/src/renderOrder.ts +10 -0
- package/src/terrain/sampleTerrainMostDetailed.test.ts +6 -6
- package/src/terrain/sampleTerrainMostDetailed.ts +7 -5
- package/src/type/index.ts +22 -1
- package/dist/assets/fontWorker-CkLGtrXf.js +0 -2
- package/dist/assets/fontWorker-CkLGtrXf.js.map +0 -1
- package/dist/assets/index-DaejBdUz.js.map +0 -1
|
@@ -1,7 +1,9 @@
|
|
|
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 {
|
|
4
|
+
import type { MeshCache } from "../type";
|
|
5
|
+
|
|
6
|
+
import { isClickGesture, PickHelper } from "./pickHelper";
|
|
5
7
|
|
|
6
8
|
describe("isClickGesture", () => {
|
|
7
9
|
it("accepts a mouseup at the exact mousedown position", () => {
|
|
@@ -42,3 +44,259 @@ describe("isClickGesture", () => {
|
|
|
42
44
|
expect(isClickGesture(new Vector2(0, 0), new Vector2(4, 4))).toBe(false);
|
|
43
45
|
});
|
|
44
46
|
});
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Renderer stub: just enough of the WebGLRenderer surface for PickHelper.
|
|
50
|
+
* `pixel` is what the next readback returns; `scissors`/`renderCount`
|
|
51
|
+
* record the GPU work a pick performed.
|
|
52
|
+
*/
|
|
53
|
+
const createRendererStub = () => {
|
|
54
|
+
const state = {
|
|
55
|
+
pixel: [0, 0, 0] as [number, number, number],
|
|
56
|
+
renderCount: 0,
|
|
57
|
+
scissors: [] as { x: number; y: number; w: number; h: number }[],
|
|
58
|
+
};
|
|
59
|
+
const renderer = {
|
|
60
|
+
getContext: () => ({ drawingBufferWidth: 800, drawingBufferHeight: 600 }),
|
|
61
|
+
getPixelRatio: () => 1,
|
|
62
|
+
getClearColor: () => undefined,
|
|
63
|
+
getClearAlpha: () => 1,
|
|
64
|
+
getRenderTarget: () => null,
|
|
65
|
+
setClearColor: () => undefined,
|
|
66
|
+
setRenderTarget: () => undefined,
|
|
67
|
+
clear: () => undefined,
|
|
68
|
+
autoClear: true,
|
|
69
|
+
render: () => {
|
|
70
|
+
state.renderCount++;
|
|
71
|
+
},
|
|
72
|
+
setScissor: (x: number, y: number, w: number, h: number) => {
|
|
73
|
+
state.scissors.push({ x, y, w, h });
|
|
74
|
+
},
|
|
75
|
+
setScissorTest: () => undefined,
|
|
76
|
+
readRenderTargetPixels: (
|
|
77
|
+
_target: unknown,
|
|
78
|
+
_x: number,
|
|
79
|
+
_y: number,
|
|
80
|
+
_w: number,
|
|
81
|
+
_h: number,
|
|
82
|
+
buffer: Uint8Array,
|
|
83
|
+
) => {
|
|
84
|
+
[buffer[0], buffer[1], buffer[2]] = state.pixel;
|
|
85
|
+
buffer[3] = 255;
|
|
86
|
+
},
|
|
87
|
+
} as unknown as WebGLRenderer;
|
|
88
|
+
return { renderer, state };
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
describe("PickHelper", () => {
|
|
92
|
+
let rafCallbacks: Map<number, FrameRequestCallback>;
|
|
93
|
+
let nextRafId: number;
|
|
94
|
+
|
|
95
|
+
beforeEach(() => {
|
|
96
|
+
rafCallbacks = new Map();
|
|
97
|
+
nextRafId = 0;
|
|
98
|
+
vi.stubGlobal("requestAnimationFrame", (cb: FrameRequestCallback) => {
|
|
99
|
+
rafCallbacks.set(++nextRafId, cb);
|
|
100
|
+
return nextRafId;
|
|
101
|
+
});
|
|
102
|
+
vi.stubGlobal("cancelAnimationFrame", (id: number) => {
|
|
103
|
+
rafCallbacks.delete(id);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
afterEach(() => {
|
|
108
|
+
vi.unstubAllGlobals();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
/** Runs every rAF callback queued so far (one simulated frame). */
|
|
112
|
+
const runFrame = () => {
|
|
113
|
+
const callbacks = [...rafCallbacks.values()];
|
|
114
|
+
rafCallbacks.clear();
|
|
115
|
+
for (const cb of callbacks) cb(0);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const setup = (opts?: { clickable?: boolean; hoverable?: boolean }) => {
|
|
119
|
+
const element = document.createElement("div");
|
|
120
|
+
const { renderer, state } = createRendererStub();
|
|
121
|
+
const picks: number[][] = [];
|
|
122
|
+
const hovers: number[][] = [];
|
|
123
|
+
// Mutable so tests can flip a gate mid-flight (listener removal).
|
|
124
|
+
const gates = {
|
|
125
|
+
clickable: opts?.clickable ?? true,
|
|
126
|
+
hoverable: opts?.hoverable ?? true,
|
|
127
|
+
};
|
|
128
|
+
const helper = new PickHelper(
|
|
129
|
+
element,
|
|
130
|
+
renderer,
|
|
131
|
+
new PerspectiveCamera(),
|
|
132
|
+
new Map() as MeshCache,
|
|
133
|
+
(arr) => picks.push(arr),
|
|
134
|
+
(arr) => hovers.push(arr),
|
|
135
|
+
() => gates.clickable,
|
|
136
|
+
() => gates.hoverable,
|
|
137
|
+
);
|
|
138
|
+
helper.enablePick(true);
|
|
139
|
+
|
|
140
|
+
const mouse = (type: string, x: number, y: number, buttons = 0) =>
|
|
141
|
+
element.dispatchEvent(
|
|
142
|
+
new MouseEvent(type, { clientX: x, clientY: y, buttons }),
|
|
143
|
+
);
|
|
144
|
+
return { helper, element, state, picks, hovers, gates, mouse };
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
describe("click picking", () => {
|
|
148
|
+
it("decodes the batch id from the pick render", () => {
|
|
149
|
+
const { state, picks, mouse } = setup();
|
|
150
|
+
state.pixel = [0, 1, 2]; // (0 << 16) + (1 << 8) + 2
|
|
151
|
+
|
|
152
|
+
mouse("mousedown", 100, 100);
|
|
153
|
+
mouse("mouseup", 100, 100);
|
|
154
|
+
|
|
155
|
+
expect(picks).toEqual([[258]]);
|
|
156
|
+
expect(state.renderCount).toBe(1);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("reports an empty pick for a miss (batch id 0)", () => {
|
|
160
|
+
const { picks, mouse } = setup();
|
|
161
|
+
|
|
162
|
+
mouse("mousedown", 100, 100);
|
|
163
|
+
mouse("mouseup", 100, 100);
|
|
164
|
+
|
|
165
|
+
expect(picks).toEqual([[]]);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("limits GPU work to the clicked pixel via scissor", () => {
|
|
169
|
+
const { state, mouse } = setup();
|
|
170
|
+
|
|
171
|
+
mouse("mousedown", 30, 40);
|
|
172
|
+
mouse("mouseup", 30, 40);
|
|
173
|
+
|
|
174
|
+
// y is flipped into WebGL space: 600 - 1 - 40.
|
|
175
|
+
expect(state.scissors).toEqual([{ x: 30, y: 559, w: 1, h: 1 }]);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("does not pick when the gesture is a drag", () => {
|
|
179
|
+
const { state, picks, mouse } = setup();
|
|
180
|
+
|
|
181
|
+
mouse("mousedown", 100, 100);
|
|
182
|
+
mouse("mouseup", 150, 150);
|
|
183
|
+
|
|
184
|
+
expect(picks).toEqual([]);
|
|
185
|
+
expect(state.renderCount).toBe(0);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("skips the pick render entirely when nobody listens", () => {
|
|
189
|
+
const { state, picks, mouse } = setup({ clickable: false });
|
|
190
|
+
|
|
191
|
+
mouse("mousedown", 100, 100);
|
|
192
|
+
mouse("mouseup", 100, 100);
|
|
193
|
+
|
|
194
|
+
expect(picks).toEqual([]);
|
|
195
|
+
expect(state.renderCount).toBe(0);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe("hover picking", () => {
|
|
200
|
+
it("picks at most once per animation frame, at the latest position", () => {
|
|
201
|
+
const { state, hovers, mouse } = setup();
|
|
202
|
+
state.pixel = [0, 0, 7];
|
|
203
|
+
|
|
204
|
+
mouse("mousemove", 10, 10);
|
|
205
|
+
mouse("mousemove", 20, 20);
|
|
206
|
+
mouse("mousemove", 30, 30);
|
|
207
|
+
expect(hovers).toEqual([]); // nothing until the frame fires
|
|
208
|
+
|
|
209
|
+
runFrame();
|
|
210
|
+
|
|
211
|
+
expect(hovers).toEqual([[7]]);
|
|
212
|
+
expect(state.renderCount).toBe(1);
|
|
213
|
+
expect(state.scissors).toEqual([{ x: 30, y: 569, w: 1, h: 1 }]);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("reports an empty hover for a miss", () => {
|
|
217
|
+
const { hovers, mouse } = setup();
|
|
218
|
+
|
|
219
|
+
mouse("mousemove", 10, 10);
|
|
220
|
+
runFrame();
|
|
221
|
+
|
|
222
|
+
expect(hovers).toEqual([[]]);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("is suppressed while a mouse button is pressed", () => {
|
|
226
|
+
const { state, hovers, mouse } = setup();
|
|
227
|
+
|
|
228
|
+
mouse("mousemove", 10, 10, 1);
|
|
229
|
+
runFrame();
|
|
230
|
+
|
|
231
|
+
expect(hovers).toEqual([]);
|
|
232
|
+
expect(state.renderCount).toBe(0);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("re-checks the listener gate when the frame fires", () => {
|
|
236
|
+
const { state, hovers, gates, mouse } = setup();
|
|
237
|
+
state.pixel = [0, 0, 7];
|
|
238
|
+
|
|
239
|
+
mouse("mousemove", 10, 10);
|
|
240
|
+
// Listeners are removed between the mousemove and the frame.
|
|
241
|
+
gates.hoverable = false;
|
|
242
|
+
runFrame();
|
|
243
|
+
|
|
244
|
+
expect(hovers).toEqual([]);
|
|
245
|
+
expect(state.renderCount).toBe(0);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("schedules nothing when no hover listener is registered", () => {
|
|
249
|
+
const { state, hovers, mouse } = setup({ hoverable: false });
|
|
250
|
+
|
|
251
|
+
mouse("mousemove", 10, 10);
|
|
252
|
+
|
|
253
|
+
expect(rafCallbacks.size).toBe(0);
|
|
254
|
+
runFrame();
|
|
255
|
+
expect(hovers).toEqual([]);
|
|
256
|
+
expect(state.renderCount).toBe(0);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("mouseleave reports nothing hovered and cancels the pending pick", () => {
|
|
260
|
+
const { state, hovers, element, mouse } = setup();
|
|
261
|
+
state.pixel = [0, 0, 7];
|
|
262
|
+
|
|
263
|
+
mouse("mousemove", 10, 10);
|
|
264
|
+
element.dispatchEvent(new MouseEvent("mouseleave"));
|
|
265
|
+
|
|
266
|
+
expect(hovers).toEqual([[]]);
|
|
267
|
+
|
|
268
|
+
runFrame();
|
|
269
|
+
// The queued pick was cancelled: no render, no second callback.
|
|
270
|
+
expect(hovers).toEqual([[]]);
|
|
271
|
+
expect(state.renderCount).toBe(0);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("mouseleave stays silent without hover listeners", () => {
|
|
275
|
+
const { hovers, element } = setup({ hoverable: false });
|
|
276
|
+
|
|
277
|
+
element.dispatchEvent(new MouseEvent("mouseleave"));
|
|
278
|
+
|
|
279
|
+
expect(hovers).toEqual([]);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
describe("enablePick(false)", () => {
|
|
284
|
+
it("detaches every handler and cancels pending hover picks", () => {
|
|
285
|
+
const { helper, state, picks, hovers, mouse } = setup();
|
|
286
|
+
state.pixel = [0, 0, 7];
|
|
287
|
+
mouse("mousemove", 10, 10);
|
|
288
|
+
|
|
289
|
+
helper.enablePick(false);
|
|
290
|
+
|
|
291
|
+
expect(rafCallbacks.size).toBe(0);
|
|
292
|
+
mouse("mousedown", 100, 100);
|
|
293
|
+
mouse("mouseup", 100, 100);
|
|
294
|
+
mouse("mousemove", 20, 20);
|
|
295
|
+
runFrame();
|
|
296
|
+
|
|
297
|
+
expect(picks).toEqual([]);
|
|
298
|
+
expect(hovers).toEqual([]);
|
|
299
|
+
expect(state.renderCount).toBe(0);
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
});
|
package/src/pick/pickHelper.ts
CHANGED
|
@@ -47,6 +47,12 @@ export function isClickGesture(
|
|
|
47
47
|
* Re-parenting is safe because pass scenes all have identity world
|
|
48
48
|
* transforms, so world matrices of the re-parented objects are
|
|
49
49
|
* unaffected.
|
|
50
|
+
*
|
|
51
|
+
* Two gestures trigger a pick: a click (mousedown/mouseup within
|
|
52
|
+
* {@link CLICK_PIXEL_TOLERANCE}) and hovering, throttled to at most one
|
|
53
|
+
* pick per animation frame. Both are lazy: a pick runs only while the
|
|
54
|
+
* matching `shouldClickPick` / `shouldHoverPick` gate returns true
|
|
55
|
+
* (i.e. someone is listening for the result).
|
|
50
56
|
*/
|
|
51
57
|
export class PickHelper {
|
|
52
58
|
private element: HTMLElement;
|
|
@@ -64,9 +70,20 @@ export class PickHelper {
|
|
|
64
70
|
/** Full-size render target used for click picking with scissor restriction. */
|
|
65
71
|
private pickRenderTarget: WebGLRenderTarget;
|
|
66
72
|
|
|
73
|
+
private onHoverCallback: (pickArr: number[]) => void;
|
|
74
|
+
/** Click picks run only while this returns true (e.g. featureClick listeners exist). */
|
|
75
|
+
private shouldClickPick: () => boolean;
|
|
76
|
+
/** Hover picks run only while this returns true (e.g. hover listeners exist). */
|
|
77
|
+
private shouldHoverPick: () => boolean;
|
|
78
|
+
|
|
67
79
|
private mouseDownPosition?: Vector2;
|
|
68
80
|
private mouseDownHandler: (event: MouseEvent) => void;
|
|
69
81
|
private mouseUpHandler: (event: MouseEvent) => void;
|
|
82
|
+
private hoverMoveHandler: (event: MouseEvent) => void;
|
|
83
|
+
private hoverLeaveHandler: () => void;
|
|
84
|
+
/** Latest mousemove position awaiting a hover pick, in client coords. */
|
|
85
|
+
private hoverPosition?: Vector2;
|
|
86
|
+
private hoverRafId?: number;
|
|
70
87
|
|
|
71
88
|
constructor(
|
|
72
89
|
element: HTMLElement,
|
|
@@ -74,6 +91,9 @@ export class PickHelper {
|
|
|
74
91
|
camera: PerspectiveCamera,
|
|
75
92
|
meshes: MeshCache,
|
|
76
93
|
onPickCallback: (pickArr: number[]) => void,
|
|
94
|
+
onHoverCallback: (pickArr: number[]) => void,
|
|
95
|
+
shouldClickPick: () => boolean,
|
|
96
|
+
shouldHoverPick: () => boolean,
|
|
77
97
|
options?: PickHelperOptions,
|
|
78
98
|
) {
|
|
79
99
|
this.element = element;
|
|
@@ -82,9 +102,14 @@ export class PickHelper {
|
|
|
82
102
|
this._camera = camera;
|
|
83
103
|
this._meshes = meshes;
|
|
84
104
|
this.onPickCallback = onPickCallback;
|
|
105
|
+
this.onHoverCallback = onHoverCallback;
|
|
106
|
+
this.shouldClickPick = shouldClickPick;
|
|
107
|
+
this.shouldHoverPick = shouldHoverPick;
|
|
85
108
|
|
|
86
109
|
this.mouseDownHandler = (event: MouseEvent) => this.onMouseDown(event);
|
|
87
110
|
this.mouseUpHandler = (event: MouseEvent) => this.onMouseUp(event);
|
|
111
|
+
this.hoverMoveHandler = (event: MouseEvent) => this.onHoverMouseMove(event);
|
|
112
|
+
this.hoverLeaveHandler = () => this.onHoverMouseLeave();
|
|
88
113
|
|
|
89
114
|
const width = this._renderer.getContext().drawingBufferWidth;
|
|
90
115
|
const height = this._renderer.getContext().drawingBufferHeight;
|
|
@@ -114,6 +139,9 @@ export class PickHelper {
|
|
|
114
139
|
this.mouseDownPosition = undefined;
|
|
115
140
|
if (!downPosition) return;
|
|
116
141
|
|
|
142
|
+
// Skip the GPU pick entirely when nobody is listening for the result.
|
|
143
|
+
if (!this.shouldClickPick()) return;
|
|
144
|
+
|
|
117
145
|
if (
|
|
118
146
|
isClickGesture(downPosition, new Vector2(event.clientX, event.clientY))
|
|
119
147
|
) {
|
|
@@ -125,10 +153,61 @@ export class PickHelper {
|
|
|
125
153
|
if (bPick) {
|
|
126
154
|
this.element.addEventListener("mousedown", this.mouseDownHandler);
|
|
127
155
|
this.element.addEventListener("mouseup", this.mouseUpHandler);
|
|
156
|
+
this.element.addEventListener("mousemove", this.hoverMoveHandler);
|
|
157
|
+
this.element.addEventListener("mouseleave", this.hoverLeaveHandler);
|
|
128
158
|
} else {
|
|
129
159
|
this.element.removeEventListener("mousedown", this.mouseDownHandler);
|
|
130
160
|
this.element.removeEventListener("mouseup", this.mouseUpHandler);
|
|
161
|
+
this.element.removeEventListener("mousemove", this.hoverMoveHandler);
|
|
162
|
+
this.element.removeEventListener("mouseleave", this.hoverLeaveHandler);
|
|
163
|
+
this.cancelPendingHoverPick();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Hover picking: schedules at most one GPU pick per animation frame from
|
|
169
|
+
* the latest mousemove position. Suppressed while any mouse button is
|
|
170
|
+
* pressed (camera drags would otherwise trigger a pick per frame) and
|
|
171
|
+
* while `shouldHoverPick` returns false, so pages without hover
|
|
172
|
+
* listeners pay nothing.
|
|
173
|
+
*/
|
|
174
|
+
private onHoverMouseMove(event: MouseEvent) {
|
|
175
|
+
if (event.buttons !== 0 || !this.shouldHoverPick()) return;
|
|
176
|
+
|
|
177
|
+
if (!this.hoverPosition) this.hoverPosition = new Vector2();
|
|
178
|
+
this.hoverPosition.set(event.clientX, event.clientY);
|
|
179
|
+
|
|
180
|
+
if (this.hoverRafId !== undefined) return;
|
|
181
|
+
this.hoverRafId = requestAnimationFrame(() => {
|
|
182
|
+
this.hoverRafId = undefined;
|
|
183
|
+
const position = this.hoverPosition;
|
|
184
|
+
if (!position) return;
|
|
185
|
+
this.hoverPosition = undefined;
|
|
186
|
+
|
|
187
|
+
// Listeners may have been removed between the mousemove that
|
|
188
|
+
// scheduled this frame and now. Re-check so no orphan pick runs.
|
|
189
|
+
if (!this.shouldHoverPick()) return;
|
|
190
|
+
|
|
191
|
+
const batchId = this.pickBatchIdAt(position.x, position.y);
|
|
192
|
+
this.onHoverCallback(batchId > 0 ? [batchId] : []);
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private onHoverMouseLeave() {
|
|
197
|
+
this.cancelPendingHoverPick();
|
|
198
|
+
// Report "nothing hovered" so diff-based enter/leave synthesis can
|
|
199
|
+
// close out the current hover when the cursor exits the canvas.
|
|
200
|
+
if (this.shouldHoverPick()) {
|
|
201
|
+
this.onHoverCallback([]);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private cancelPendingHoverPick() {
|
|
206
|
+
if (this.hoverRafId !== undefined) {
|
|
207
|
+
cancelAnimationFrame(this.hoverRafId);
|
|
208
|
+
this.hoverRafId = undefined;
|
|
131
209
|
}
|
|
210
|
+
this.hoverPosition = undefined;
|
|
132
211
|
}
|
|
133
212
|
|
|
134
213
|
/**
|
|
@@ -225,9 +304,19 @@ export class PickHelper {
|
|
|
225
304
|
}
|
|
226
305
|
|
|
227
306
|
private onMouseClick(event: MouseEvent) {
|
|
307
|
+
const batchId = this.pickBatchIdAt(event.clientX, event.clientY);
|
|
308
|
+
const pickArr = batchId > 0 ? [batchId] : [];
|
|
309
|
+
this.onPickCallback(pickArr);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Runs the single-pixel pick render at the given client coordinates and
|
|
314
|
+
* returns the decoded batch ID (0 when nothing pickable is hit).
|
|
315
|
+
*/
|
|
316
|
+
private pickBatchIdAt(clientX: number, clientY: number): number {
|
|
228
317
|
const rect = this.element.getBoundingClientRect();
|
|
229
|
-
const x =
|
|
230
|
-
const y =
|
|
318
|
+
const x = clientX - rect.left;
|
|
319
|
+
const y = clientY - rect.top;
|
|
231
320
|
|
|
232
321
|
const pixelRatio = this._renderer.getPixelRatio();
|
|
233
322
|
const fullWidth = this._renderer.getContext().drawingBufferWidth;
|
|
@@ -267,13 +356,11 @@ export class PickHelper {
|
|
|
267
356
|
|
|
268
357
|
this._renderer.setScissorTest(false);
|
|
269
358
|
|
|
270
|
-
|
|
359
|
+
return (
|
|
271
360
|
(this.pixelBuffer[0] << 16) +
|
|
272
361
|
(this.pixelBuffer[1] << 8) +
|
|
273
|
-
this.pixelBuffer[2]
|
|
274
|
-
|
|
275
|
-
const pickArr = batchId > 0 ? [batchId] : [];
|
|
276
|
-
this.onPickCallback(pickArr);
|
|
362
|
+
this.pixelBuffer[2]
|
|
363
|
+
);
|
|
277
364
|
}
|
|
278
365
|
|
|
279
366
|
public dispose() {
|
package/src/renderOrder.ts
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
1
|
export const FEATURE_RENDER_ORDER = 1;
|
|
2
|
+
/**
|
|
3
|
+
* Polylines (strokes) must draw after polygon fills. In the per-tile bake
|
|
4
|
+
* scene, sibling fill/stroke meshes are created asynchronously and land in
|
|
5
|
+
* the scene in arbitrary order, so a fill baked later paints over the stroke.
|
|
6
|
+
* In the MRT scene, polyline materials skip the depth test but still write
|
|
7
|
+
* depth, so a coplanar fill drawn after a stroke z-fights against the
|
|
8
|
+
* stroke's depth per fragment (mottled boundaries). Encoding the stacking in
|
|
9
|
+
* renderOrder makes both cases deterministic.
|
|
10
|
+
*/
|
|
11
|
+
export const STROKE_RENDER_ORDER = FEATURE_RENDER_ORDER + 1;
|
|
2
12
|
export const SKY_RENDER_ORDER = 100;
|
|
3
13
|
export const STARS_RENDER_ORDER = SKY_RENDER_ORDER + 1;
|
|
@@ -37,6 +37,7 @@ vi.mock("@navaramap/engine-api", () => {
|
|
|
37
37
|
};
|
|
38
38
|
return {
|
|
39
39
|
default: vi.fn().mockResolvedValue(undefined),
|
|
40
|
+
angleToRadian: (deg: number) => (deg * Math.PI) / 180,
|
|
40
41
|
terrainPositionsToTiles: vi.fn(
|
|
41
42
|
(geographic: boolean, level: number, positions: Float64Array) => {
|
|
42
43
|
const result = new Uint32Array(positions.length);
|
|
@@ -87,12 +88,11 @@ vi.mock("@navaramap/engine-api", () => {
|
|
|
87
88
|
};
|
|
88
89
|
});
|
|
89
90
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
};
|
|
95
|
-
const TOKYO = { lat: (35.68 * Math.PI) / 180, lng: (139.76 * Math.PI) / 180 };
|
|
91
|
+
// Public inputs are degrees; the mocked engine functions still receive the
|
|
92
|
+
// radians the implementation converts to.
|
|
93
|
+
const KARIYA = { lat: 35.0, lng: 137.0 };
|
|
94
|
+
const NEARBY = { lat: 35.001, lng: 137.001 };
|
|
95
|
+
const TOKYO = { lat: 35.68, lng: 139.76 };
|
|
96
96
|
|
|
97
97
|
const SOURCE: QuantizedMeshSource = {
|
|
98
98
|
type: "quantized-mesh",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { LatLng } from "@navaramap/core";
|
|
2
2
|
import {
|
|
3
|
+
angleToRadian,
|
|
3
4
|
sampleQuantizedMeshHeights,
|
|
4
5
|
sampleRasterDemHeights,
|
|
5
6
|
terrainPositionsToTiles,
|
|
@@ -29,9 +30,9 @@ export type SampleTerrainOptions = {
|
|
|
29
30
|
};
|
|
30
31
|
|
|
31
32
|
export type SampledTerrainPosition = {
|
|
32
|
-
/** Latitude in
|
|
33
|
+
/** Latitude in degrees, echoed from the input. */
|
|
33
34
|
lat: number;
|
|
34
|
-
/** Longitude in
|
|
35
|
+
/** Longitude in degrees, echoed from the input. */
|
|
35
36
|
lng: number;
|
|
36
37
|
/**
|
|
37
38
|
* Terrain height in meters, or `undefined` when no tile covering the
|
|
@@ -104,7 +105,7 @@ type SourceConfig = {
|
|
|
104
105
|
*
|
|
105
106
|
* @param source - A `quantized-mesh` or `raster-dem` source description (the
|
|
106
107
|
* same shape `addSource` accepts)
|
|
107
|
-
* @param positions - Geodetic positions (lat/lng in
|
|
108
|
+
* @param positions - Geodetic positions (lat/lng in degrees)
|
|
108
109
|
* @param options - Optional abort signal and fixed-level sampling
|
|
109
110
|
* @returns One result per input position, in the same order (the input
|
|
110
111
|
* objects are not mutated)
|
|
@@ -123,10 +124,11 @@ export async function sampleTerrainMostDetailed(
|
|
|
123
124
|
// Fixed-level sampling never falls back to parents.
|
|
124
125
|
const floorLevel = options?.level != null ? startLevel : config.minZoom;
|
|
125
126
|
|
|
127
|
+
// The engine samplers work in radians; inputs and echoed outputs are degrees.
|
|
126
128
|
const flat = new Float64Array(positions.length * 2);
|
|
127
129
|
positions.forEach((p, i) => {
|
|
128
|
-
flat[i * 2] = p.lng;
|
|
129
|
-
flat[i * 2 + 1] = p.lat;
|
|
130
|
+
flat[i * 2] = angleToRadian(p.lng);
|
|
131
|
+
flat[i * 2 + 1] = angleToRadian(p.lat);
|
|
130
132
|
});
|
|
131
133
|
|
|
132
134
|
// Group positions by their containing tile at the starting level; each
|
package/src/type/index.ts
CHANGED
|
@@ -75,6 +75,27 @@ type ConvertColorFields<T> = {
|
|
|
75
75
|
*/
|
|
76
76
|
type WithColorSupport<T> = ConvertColorFields<T>;
|
|
77
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Source geometry categories a vector appearance can consume via its
|
|
80
|
+
* `geometryTypes` option. Each appearance defaults to its native geometry only
|
|
81
|
+
* (`"point"` for point/billboard/text, `"line"` for polyline); adding entries
|
|
82
|
+
* derives extra representations, e.g. `polyline: { geometryTypes: ["line",
|
|
83
|
+
* "polygon"] }` also renders polygon boundary rings as polylines.
|
|
84
|
+
*/
|
|
85
|
+
export type SourceGeometryType = "point" | "line" | "polygon";
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Helper type to narrow the generated `geometryTypes?: string[]` material
|
|
89
|
+
* fields to the {@link SourceGeometryType} literal union.
|
|
90
|
+
*/
|
|
91
|
+
type ConvertGeometryTypeFields<T> = {
|
|
92
|
+
[K in keyof T]: K extends "geometryTypes"
|
|
93
|
+
? SourceGeometryType[] | Extract<T[K], undefined>
|
|
94
|
+
: T[K] extends object | undefined
|
|
95
|
+
? ConvertGeometryTypeFields<T[K]> | Extract<T[K], undefined>
|
|
96
|
+
: T[K];
|
|
97
|
+
};
|
|
98
|
+
|
|
78
99
|
/**
|
|
79
100
|
* A reference to a {@link Source}: either the source handle returned by
|
|
80
101
|
* `addSource`, or its `id` string.
|
|
@@ -87,7 +108,7 @@ export type SourceLayerBase<Layer extends { source?: string | undefined }> =
|
|
|
87
108
|
};
|
|
88
109
|
|
|
89
110
|
type VectorLayerBase = WithColorSupport<
|
|
90
|
-
Layer<VectorLayerDescription & { type: "vector" }
|
|
111
|
+
ConvertGeometryTypeFields<Layer<VectorLayerDescription & { type: "vector" }>>
|
|
91
112
|
>;
|
|
92
113
|
|
|
93
114
|
/**
|