@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
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@navaramap/three",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0
|
|
4
|
+
"version": "0.1.0",
|
|
5
5
|
"main": "dist/index.umd.cjs",
|
|
6
6
|
"module": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -30,13 +30,13 @@
|
|
|
30
30
|
"astronomy-engine": "2.1.19",
|
|
31
31
|
"stats.js": "0.17.0",
|
|
32
32
|
"three-stdlib": "2.36.1",
|
|
33
|
-
"@navaramap/
|
|
34
|
-
"@navaramap/engine-api": "0.0
|
|
35
|
-
"@navaramap/
|
|
36
|
-
"@navaramap/
|
|
37
|
-
"@navaramap/
|
|
38
|
-
"@navaramap/worker": "0.0
|
|
39
|
-
"@navaramap/
|
|
33
|
+
"@navaramap/engine": "0.1.0",
|
|
34
|
+
"@navaramap/engine-api": "0.1.0",
|
|
35
|
+
"@navaramap/core": "0.1.0",
|
|
36
|
+
"@navaramap/three-csm": "0.1.0",
|
|
37
|
+
"@navaramap/font": "0.1.0",
|
|
38
|
+
"@navaramap/worker": "0.1.0",
|
|
39
|
+
"@navaramap/three-api": "0.1.0"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@playwright/test": "1.62.1",
|
|
@@ -71,10 +71,10 @@
|
|
|
71
71
|
"tweakpane": "4.0.5",
|
|
72
72
|
"vite-plugin-glsl": "1.6.0",
|
|
73
73
|
"vite-plugin-static-copy": "4.1.1",
|
|
74
|
-
"@navaramap/three-default-
|
|
75
|
-
"@navaramap/three-
|
|
76
|
-
"@navaramap/three-
|
|
77
|
-
"@navaramap/three-react": "0.0
|
|
74
|
+
"@navaramap/three-default-plugin": "0.1.0",
|
|
75
|
+
"@navaramap/three-plugins": "0.1.0",
|
|
76
|
+
"@navaramap/three-default-descs": "0.1.0",
|
|
77
|
+
"@navaramap/three-react": "0.1.0"
|
|
78
78
|
},
|
|
79
79
|
"scripts": {
|
|
80
80
|
"start": "vite",
|
|
@@ -274,15 +274,15 @@ describe("MeshDesc geodetic placement", () => {
|
|
|
274
274
|
});
|
|
275
275
|
|
|
276
276
|
describe("MeshDesc terrain-referenced placement", () => {
|
|
277
|
-
type
|
|
277
|
+
type LatLngDeg = { lat: number; lng: number };
|
|
278
278
|
|
|
279
279
|
const terrainView = (height: number | undefined) => {
|
|
280
280
|
const listeners: ((h: number) => void)[] = [];
|
|
281
281
|
const unsubscribe = vi.fn();
|
|
282
282
|
const spy = {
|
|
283
|
-
sampleTerrainHeight: vi.fn((_pos:
|
|
283
|
+
sampleTerrainHeight: vi.fn((_pos: LatLngDeg) => height),
|
|
284
284
|
observeTerrainHeightAt: vi.fn(
|
|
285
|
-
(_pos:
|
|
285
|
+
(_pos: LatLngDeg, cb: (h: number) => void) => {
|
|
286
286
|
listeners.push(cb);
|
|
287
287
|
return unsubscribe;
|
|
288
288
|
},
|
|
@@ -297,8 +297,6 @@ describe("MeshDesc terrain-referenced placement", () => {
|
|
|
297
297
|
return desc;
|
|
298
298
|
};
|
|
299
299
|
|
|
300
|
-
const rad = (deg: number) => (deg * Math.PI) / 180;
|
|
301
|
-
|
|
302
300
|
it("seeds from the resident-tile sample and subscribes", () => {
|
|
303
301
|
const { view, spy } = terrainView(300);
|
|
304
302
|
createWith(view, {
|
|
@@ -308,10 +306,10 @@ describe("MeshDesc terrain-referenced placement", () => {
|
|
|
308
306
|
expect(spy.sampleTerrainHeight).toHaveBeenCalled();
|
|
309
307
|
expect(spy.observeTerrainHeightAt).toHaveBeenCalled();
|
|
310
308
|
|
|
311
|
-
// Both
|
|
309
|
+
// Both terrain methods take the same degrees `geodetic` carries.
|
|
312
310
|
const pos = spy.sampleTerrainHeight.mock.calls[0][0];
|
|
313
|
-
expect(pos.lat).toBeCloseTo(
|
|
314
|
-
expect(pos.lng).toBeCloseTo(
|
|
311
|
+
expect(pos.lat).toBeCloseTo(TOKYO.lat, 9);
|
|
312
|
+
expect(pos.lng).toBeCloseTo(TOKYO.lng, 9);
|
|
315
313
|
|
|
316
314
|
// terrain 300 + requested 10
|
|
317
315
|
expect(lastPlacement()?.height).toBeCloseTo(310, 9);
|
package/src/core/MeshDesc.ts
CHANGED
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
headingPitchRollToFixedFrame,
|
|
4
4
|
type GeodeticPlacement,
|
|
5
5
|
} from "@navaramap/three-api";
|
|
6
|
-
import { Euler,
|
|
6
|
+
import { Euler, Matrix4, Object3D, Vector3, type Mesh } from "three";
|
|
7
7
|
import invariant from "tiny-invariant";
|
|
8
8
|
|
|
9
9
|
import type ThreeView from "../index";
|
|
@@ -369,10 +369,7 @@ export abstract class MeshDesc<
|
|
|
369
369
|
return;
|
|
370
370
|
}
|
|
371
371
|
|
|
372
|
-
const pos = {
|
|
373
|
-
lat: MathUtils.degToRad(g.lat),
|
|
374
|
-
lng: MathUtils.degToRad(g.lng),
|
|
375
|
-
};
|
|
372
|
+
const pos = { lat: g.lat, lng: g.lng };
|
|
376
373
|
this.terrainHeight = this.view.sampleTerrainHeight(pos) ?? 0;
|
|
377
374
|
this.terrainUnsubscribe = this.view.observeTerrainHeightAt(
|
|
378
375
|
pos,
|
|
@@ -99,6 +99,11 @@ export type { GeometryType };
|
|
|
99
99
|
* Information about a feature batch, passed to evaluation callbacks.
|
|
100
100
|
*/
|
|
101
101
|
export type FeatureInfo = {
|
|
102
|
+
/**
|
|
103
|
+
* The feature's canonical global batch id (its first instance's id when a
|
|
104
|
+
* feature spans several instances, e.g. MultiPoint). Pick events report the
|
|
105
|
+
* same canonical id, so picks and evaluations correlate through this field.
|
|
106
|
+
*/
|
|
102
107
|
batchId: number;
|
|
103
108
|
properties: Record<string, unknown> | undefined;
|
|
104
109
|
layerId: LayerId | undefined;
|
|
@@ -431,7 +436,7 @@ export class FeatureEvaluator {
|
|
|
431
436
|
) {
|
|
432
437
|
const evaluate = (info: FeatureInfo, batchIndex: number) => {
|
|
433
438
|
const evaluated = f(info);
|
|
434
|
-
this.applyEvaluatedValues(batchIndex,
|
|
439
|
+
this.applyEvaluatedValues(batchIndex, evaluated);
|
|
435
440
|
};
|
|
436
441
|
|
|
437
442
|
if (options?.filters) {
|
|
@@ -443,30 +448,32 @@ export class FeatureEvaluator {
|
|
|
443
448
|
|
|
444
449
|
private applyEvaluatedValues(
|
|
445
450
|
batchIndex: number,
|
|
446
|
-
batchId: number,
|
|
447
451
|
evaluated: Partial<EvaluatedValue>,
|
|
448
452
|
) {
|
|
449
453
|
const obj = this.obj;
|
|
450
454
|
|
|
455
|
+
// Sprites apply by batch index: a feature can own several instances
|
|
456
|
+
// (MultiPoint, points derived from line/polygon vertices), so a
|
|
457
|
+
// batch-id keyed lookup would style only one of them.
|
|
451
458
|
if (obj instanceof InstancedSpriteMesh) {
|
|
452
459
|
if (evaluated.color != null) {
|
|
453
|
-
obj.
|
|
460
|
+
obj.setFeatureColorByBatchIndex(batchIndex, evaluated.color.raw);
|
|
454
461
|
}
|
|
455
462
|
if (evaluated.show != null) {
|
|
456
|
-
obj.
|
|
463
|
+
obj.setFeatureShowByBatchIndex(batchIndex, evaluated.show);
|
|
457
464
|
}
|
|
458
465
|
if (evaluated.height != null) {
|
|
459
|
-
obj.
|
|
466
|
+
obj.setFeatureHeightByBatchIndex(batchIndex, evaluated.height);
|
|
460
467
|
}
|
|
461
468
|
if (evaluated.size != null) {
|
|
462
|
-
obj.
|
|
469
|
+
obj.setFeatureSizeByBatchIndex(batchIndex, evaluated.size);
|
|
463
470
|
}
|
|
464
471
|
if (evaluated.opacity != null) {
|
|
465
|
-
obj.
|
|
472
|
+
obj.setFeatureOpacityByBatchIndex(batchIndex, evaluated.opacity);
|
|
466
473
|
}
|
|
467
474
|
if (evaluated.declutterPriority != null) {
|
|
468
|
-
obj.
|
|
469
|
-
|
|
475
|
+
obj.setFeatureDeclutterPriorityByBatchIndex(
|
|
476
|
+
batchIndex,
|
|
470
477
|
evaluated.declutterPriority,
|
|
471
478
|
);
|
|
472
479
|
}
|
|
@@ -474,7 +481,7 @@ export class FeatureEvaluator {
|
|
|
474
481
|
// Async by nature (the image may need fetching); the atlas dedupes
|
|
475
482
|
// loads by URL so evaluating many features costs one fetch per image.
|
|
476
483
|
// A nullish url clears the override back to the material default.
|
|
477
|
-
void obj.
|
|
484
|
+
void obj.setFeatureImageByBatchIndex(batchIndex, evaluated.image);
|
|
478
485
|
}
|
|
479
486
|
return;
|
|
480
487
|
}
|
package/src/event/feature.ts
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
PolygonMesh,
|
|
15
15
|
PolylineMesh,
|
|
16
16
|
} from "../mesh";
|
|
17
|
-
import { FEATURE_RENDER_ORDER } from "../renderOrder";
|
|
17
|
+
import { FEATURE_RENDER_ORDER, STROKE_RENDER_ORDER } from "../renderOrder";
|
|
18
18
|
import type { Scenes } from "../scene";
|
|
19
19
|
|
|
20
20
|
import type { EventContext } from "./context";
|
|
@@ -190,7 +190,20 @@ export async function processRenderableFeatureAdded(
|
|
|
190
190
|
setTransform(obj, transform);
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
-
|
|
193
|
+
// Strokes draw after fills: sibling meshes are created asynchronously in
|
|
194
|
+
// arbitrary order, and a fill drawn after a coplanar stroke either paints
|
|
195
|
+
// over it (bake scene) or z-fights against its depth (MRT scene), so the
|
|
196
|
+
// stacking must come from renderOrder, not insertion order. Only polylines
|
|
197
|
+
// stroking polygon rings sit on a sibling fill; standalone line strokes
|
|
198
|
+
// keep the base order — elevated, they would deterministically composite
|
|
199
|
+
// over every marker and outline they merely cross (PolylineMesh skips the
|
|
200
|
+
// depth test in the MRT scene).
|
|
201
|
+
const strokesSiblingFills =
|
|
202
|
+
polyline?.material.geometryTypes?.includes("polygon") ?? false;
|
|
203
|
+
obj.renderOrder =
|
|
204
|
+
obj instanceof PolylineMesh && strokesSiblingFills
|
|
205
|
+
? STROKE_RENDER_ORDER
|
|
206
|
+
: FEATURE_RENDER_ORDER;
|
|
194
207
|
|
|
195
208
|
// Add to MRT scene if not draped (draped features render to texturized scene)
|
|
196
209
|
const isDraped =
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { BillboardMesh as NavaraBillboardMesh } from "@navaramap/engine";
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
import type { EventContext } from "../context";
|
|
5
|
+
|
|
6
|
+
import { renderBillboard } from "./billboard";
|
|
7
|
+
|
|
8
|
+
const init = vi.fn().mockResolvedValue(undefined);
|
|
9
|
+
const setActive = vi.fn();
|
|
10
|
+
|
|
11
|
+
vi.mock("../../mesh", () => ({
|
|
12
|
+
InstancedSpriteMesh: class {
|
|
13
|
+
_init = init;
|
|
14
|
+
setActive = setActive;
|
|
15
|
+
},
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
describe("renderBillboard", () => {
|
|
19
|
+
const ctx = {} as EventContext;
|
|
20
|
+
|
|
21
|
+
// `url` is omitted from the material entirely when not passed, mirroring the
|
|
22
|
+
// engine's `string | undefined` accessor. The engine itself currently always
|
|
23
|
+
// sends a string (`BillboardMaterial::url` is a `String` defaulting to `""`,
|
|
24
|
+
// converted as `Some(url)`), so `""` is the shape a real url-less layer
|
|
25
|
+
// produces — both are covered below.
|
|
26
|
+
const createMesh = (url?: string) =>
|
|
27
|
+
({
|
|
28
|
+
material: url === undefined ? {} : { url },
|
|
29
|
+
active: true,
|
|
30
|
+
}) as unknown as NavaraBillboardMesh;
|
|
31
|
+
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
init.mockClear();
|
|
34
|
+
setActive.mockClear();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("creates a mesh when the material carries a default image", async () => {
|
|
38
|
+
const obj = await renderBillboard(ctx, createMesh("marker.png"));
|
|
39
|
+
|
|
40
|
+
expect(obj).toBeDefined();
|
|
41
|
+
expect(init).toHaveBeenCalledOnce();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Regression: the mesh used to be skipped when the material had no `url`,
|
|
45
|
+
// so a layer that styles every feature through a FeatureEvaluator
|
|
46
|
+
// (`{ image }`) never got a mesh — and never a `featureCreated` event to
|
|
47
|
+
// attach the evaluator to. Empty atlas rects keep such instances invisible
|
|
48
|
+
// until an image is packed, so the mesh is safe to create eagerly.
|
|
49
|
+
it("creates a mesh when the material's default image is empty", async () => {
|
|
50
|
+
const obj = await renderBillboard(ctx, createMesh(""));
|
|
51
|
+
|
|
52
|
+
expect(obj).toBeDefined();
|
|
53
|
+
expect(init).toHaveBeenCalledOnce();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("creates a mesh when the material has no `url` at all", async () => {
|
|
57
|
+
const obj = await renderBillboard(ctx, createMesh());
|
|
58
|
+
|
|
59
|
+
expect(obj).toBeDefined();
|
|
60
|
+
expect(init).toHaveBeenCalledOnce();
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -8,8 +8,10 @@ export async function renderBillboard(
|
|
|
8
8
|
ctx: EventContext,
|
|
9
9
|
m: NavaraBillboardMesh,
|
|
10
10
|
) {
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
// No `url` check: a billboard layer may carry no material-level image and
|
|
12
|
+
// get every image per-feature from a FeatureEvaluator (`{ image }`). The
|
|
13
|
+
// mesh starts with empty atlas rects, which the shader culls, so instances
|
|
14
|
+
// stay invisible until an image is packed for them.
|
|
13
15
|
const mesh = new InstancedSpriteMesh({
|
|
14
16
|
renderOrder: FEATURE_RENDER_ORDER,
|
|
15
17
|
ctx,
|
package/src/index.ts
CHANGED
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
type FontWorkerMemoryStats,
|
|
24
24
|
} from "@navaramap/font";
|
|
25
25
|
import FontWorkerURL from "@navaramap/font/fontWorker?worker&url";
|
|
26
|
-
import { initNavaraApi } from "@navaramap/three-api";
|
|
26
|
+
import { initNavaraApi, degreeToRadian } from "@navaramap/three-api";
|
|
27
27
|
import {
|
|
28
28
|
initializeWorkerPool,
|
|
29
29
|
probeWorkerPoolHeap,
|
|
@@ -309,7 +309,11 @@ export type Options = {
|
|
|
309
309
|
atmosphere?: AtmosphereOptions;
|
|
310
310
|
/** Background color of the scene. Defaults to dark color (0x0a0a0f). */
|
|
311
311
|
backgroundColor?: CoreColor;
|
|
312
|
-
/**
|
|
312
|
+
/**
|
|
313
|
+
* Feature picking configuration (default: true). Enables the `featureClick`
|
|
314
|
+
* event (click) and the `featureHover` / `featureEnter` / `featureLeave`
|
|
315
|
+
* events (mousemove).
|
|
316
|
+
*/
|
|
313
317
|
picking?: boolean;
|
|
314
318
|
/** When true, renders every frame. When false, renders only on changes or when forceUpdate() is called. */
|
|
315
319
|
animation?: boolean;
|
|
@@ -413,8 +417,24 @@ export type MapMouseEvent = {
|
|
|
413
417
|
export type ViewEvents = {
|
|
414
418
|
/** Emitted when the view is resized. Receives width and height in pixels. */
|
|
415
419
|
resize: (w: number, h: number) => void;
|
|
416
|
-
/**
|
|
417
|
-
|
|
420
|
+
/**
|
|
421
|
+
* Emitted when a feature is clicked (click-gesture GPU pick). Receives the
|
|
422
|
+
* clicked feature, or null when empty space was clicked. For raw click
|
|
423
|
+
* coordinates use the `click` event instead.
|
|
424
|
+
*/
|
|
425
|
+
featureClick: (info: Nullable<PickedFeature>) => void;
|
|
426
|
+
/**
|
|
427
|
+
* Emitted when the hovered feature changes (mousemove picking). Receives
|
|
428
|
+
* the newly hovered feature, or null when the pointer leaves all pickable
|
|
429
|
+
* features. Hover picking runs a GPU pick per frame while the pointer
|
|
430
|
+
* moves, and is active only while at least one `featureHover` /
|
|
431
|
+
* `featureEnter` / `featureLeave` listener is registered.
|
|
432
|
+
*/
|
|
433
|
+
featureHover: (info: Nullable<PickedFeature>) => void;
|
|
434
|
+
/** Emitted when the pointer starts hovering a pickable feature. See `featureHover` for activation. */
|
|
435
|
+
featureEnter: (info: PickedFeature) => void;
|
|
436
|
+
/** Emitted when the pointer stops hovering the previously hovered feature. See `featureHover` for activation. */
|
|
437
|
+
featureLeave: (info: PickedFeature) => void;
|
|
418
438
|
/** Emitted when a layer event occurs. Receives event type, layer ID, and event arguments. */
|
|
419
439
|
layer: <K extends keyof LayerEvent>(
|
|
420
440
|
k: K,
|
|
@@ -503,6 +523,9 @@ export default class ThreeView<
|
|
|
503
523
|
* `camera_flight_ended` events from the engine. */
|
|
504
524
|
private _pendingFlights = new Map<number, (completed: boolean) => void>();
|
|
505
525
|
|
|
526
|
+
/** Currently hovered feature, used to diff-synthesize hover events. */
|
|
527
|
+
private _hoveredFeature: Nullable<PickedFeature> = null;
|
|
528
|
+
|
|
506
529
|
private _meshes: MeshCache = new Map();
|
|
507
530
|
private _abortControllers: AbortControllers = new Map();
|
|
508
531
|
private _workerPoolPromises: WorkerPoolPromises = new Map();
|
|
@@ -1562,6 +1585,17 @@ export default class ThreeView<
|
|
|
1562
1585
|
this._camera.raw,
|
|
1563
1586
|
this._meshes,
|
|
1564
1587
|
this.onPick.bind(this),
|
|
1588
|
+
this.onHoverPick.bind(this),
|
|
1589
|
+
// Picks cost a GPU render (plus a re-render to restore draped
|
|
1590
|
+
// atlases), so both gestures stay off until someone actually
|
|
1591
|
+
// listens for the result.
|
|
1592
|
+
() => !!this.size("featureClick"),
|
|
1593
|
+
() =>
|
|
1594
|
+
!!(
|
|
1595
|
+
this.size("featureHover") ||
|
|
1596
|
+
this.size("featureEnter") ||
|
|
1597
|
+
this.size("featureLeave")
|
|
1598
|
+
),
|
|
1565
1599
|
// {
|
|
1566
1600
|
// debug: true,
|
|
1567
1601
|
// },
|
|
@@ -2621,11 +2655,11 @@ export default class ThreeView<
|
|
|
2621
2655
|
|
|
2622
2656
|
/**
|
|
2623
2657
|
* Samples the terrain height at a given geodetic position synchronously.
|
|
2624
|
-
* @param pos - Geodetic position (lat in
|
|
2658
|
+
* @param pos - Geodetic position (lat in degrees, lng in degrees)
|
|
2625
2659
|
* @returns Terrain height in meters, or undefined if terrain data not loaded
|
|
2626
2660
|
*/
|
|
2627
2661
|
sampleTerrainHeight(pos: LatLng): number | undefined {
|
|
2628
|
-
const lle = new LLE(pos.lat, pos.lng, 0);
|
|
2662
|
+
const lle = new LLE(degreeToRadian(pos.lat), degreeToRadian(pos.lng), 0);
|
|
2629
2663
|
return this._core?.sampleTerrainHeight(lle);
|
|
2630
2664
|
}
|
|
2631
2665
|
|
|
@@ -2643,7 +2677,7 @@ export default class ThreeView<
|
|
|
2643
2677
|
* sampled.
|
|
2644
2678
|
*
|
|
2645
2679
|
* @param source - A registered terrain source (handle or id)
|
|
2646
|
-
* @param positions - Geodetic positions (lat/lng in
|
|
2680
|
+
* @param positions - Geodetic positions (lat/lng in degrees)
|
|
2647
2681
|
* @param options - Optional abort signal and fixed-level sampling
|
|
2648
2682
|
* @returns One result per input position, in the same order
|
|
2649
2683
|
*/
|
|
@@ -2669,7 +2703,7 @@ export default class ThreeView<
|
|
|
2669
2703
|
|
|
2670
2704
|
/**
|
|
2671
2705
|
* Observes terrain height changes at a position. Callback is invoked each time terrain data updates.
|
|
2672
|
-
* @param pos - Geodetic position to observe (lat in
|
|
2706
|
+
* @param pos - Geodetic position to observe (lat in degrees, lng in degrees)
|
|
2673
2707
|
* @param cb - Callback function receiving the terrain height in meters
|
|
2674
2708
|
* @returns Cleanup function to stop observing
|
|
2675
2709
|
*/
|
|
@@ -2681,7 +2715,7 @@ export default class ThreeView<
|
|
|
2681
2715
|
return () => {};
|
|
2682
2716
|
}
|
|
2683
2717
|
|
|
2684
|
-
const lle = new LLE(pos.lat, pos.lng, 0);
|
|
2718
|
+
const lle = new LLE(degreeToRadian(pos.lat), degreeToRadian(pos.lng), 0);
|
|
2685
2719
|
const entityBits = this._core.registerSampleTerrainHeightEvent(lle);
|
|
2686
2720
|
|
|
2687
2721
|
const callFunc = (ev: TerrainHeightUpdatedEvent) => {
|
|
@@ -2778,6 +2812,29 @@ export default class ThreeView<
|
|
|
2778
2812
|
this.resize(width, height, pixelRatio);
|
|
2779
2813
|
};
|
|
2780
2814
|
|
|
2815
|
+
/**
|
|
2816
|
+
* Resolves a picked batch ID into feature information via the engine's
|
|
2817
|
+
* property store.
|
|
2818
|
+
*/
|
|
2819
|
+
private resolvePickedFeature(batchId: number): PickedFeature {
|
|
2820
|
+
const prop = this._core?.readPropertyByGlobalBatchId(batchId);
|
|
2821
|
+
if (prop) {
|
|
2822
|
+
return {
|
|
2823
|
+
properties: prop.properties,
|
|
2824
|
+
// The GPU pick returns the clicked instance's id; report the
|
|
2825
|
+
// feature's canonical batch id instead so it matches the id the
|
|
2826
|
+
// evaluator exposes (a MultiPoint feature spans many instances).
|
|
2827
|
+
batchId: prop.batchId ?? batchId,
|
|
2828
|
+
layerId: prop.layerId,
|
|
2829
|
+
};
|
|
2830
|
+
}
|
|
2831
|
+
return {
|
|
2832
|
+
properties: {},
|
|
2833
|
+
batchId,
|
|
2834
|
+
layerId: undefined,
|
|
2835
|
+
};
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2781
2838
|
/**
|
|
2782
2839
|
* Handles pick events and emits the picked feature information.
|
|
2783
2840
|
* @param pickArr - Array of picked batch IDs
|
|
@@ -2786,28 +2843,34 @@ export default class ThreeView<
|
|
|
2786
2843
|
this._renderFlag.forceUpdate = true;
|
|
2787
2844
|
|
|
2788
2845
|
if (pickArr.length > 0) {
|
|
2789
|
-
|
|
2790
|
-
const prop = this._core?.readPropertyByGlobalBatchId(batchId);
|
|
2791
|
-
if (prop) {
|
|
2792
|
-
const pickedFeature: PickedFeature = {
|
|
2793
|
-
properties: prop.properties,
|
|
2794
|
-
batchId,
|
|
2795
|
-
layerId: prop.layerId,
|
|
2796
|
-
};
|
|
2797
|
-
this.emit("pick", pickedFeature);
|
|
2798
|
-
} else {
|
|
2799
|
-
const emptyFeature: PickedFeature = {
|
|
2800
|
-
properties: {},
|
|
2801
|
-
batchId,
|
|
2802
|
-
layerId: undefined,
|
|
2803
|
-
};
|
|
2804
|
-
this.emit("pick", emptyFeature);
|
|
2805
|
-
}
|
|
2846
|
+
this.emit("featureClick", this.resolvePickedFeature(pickArr[0]));
|
|
2806
2847
|
} else {
|
|
2807
|
-
this.emit("
|
|
2848
|
+
this.emit("featureClick", null);
|
|
2808
2849
|
}
|
|
2809
2850
|
}
|
|
2810
2851
|
|
|
2852
|
+
/**
|
|
2853
|
+
* Handles hover pick results and synthesizes `featureHover` / `featureEnter` /
|
|
2854
|
+
* `featureLeave` events by diffing against the previously hovered feature.
|
|
2855
|
+
* @param pickArr - Array of picked batch IDs
|
|
2856
|
+
*/
|
|
2857
|
+
private onHoverPick(pickArr: number[]) {
|
|
2858
|
+
// The pick render re-bakes draped vector atlases in pick mode; a main
|
|
2859
|
+
// render is needed to restore their normal colors.
|
|
2860
|
+
this._renderFlag.forceUpdate = true;
|
|
2861
|
+
|
|
2862
|
+
const batchId = pickArr.length > 0 ? pickArr[0] : 0;
|
|
2863
|
+
const prev = this._hoveredFeature;
|
|
2864
|
+
if ((prev?.batchId ?? 0) === batchId) return;
|
|
2865
|
+
|
|
2866
|
+
const feature = batchId > 0 ? this.resolvePickedFeature(batchId) : null;
|
|
2867
|
+
this._hoveredFeature = feature;
|
|
2868
|
+
|
|
2869
|
+
if (prev) this.emit("featureLeave", prev);
|
|
2870
|
+
if (feature) this.emit("featureEnter", feature);
|
|
2871
|
+
this.emit("featureHover", feature);
|
|
2872
|
+
}
|
|
2873
|
+
|
|
2811
2874
|
/**
|
|
2812
2875
|
* Gets whether continuous animation mode is enabled.
|
|
2813
2876
|
*/
|
|
@@ -2821,6 +2884,14 @@ export default class ThreeView<
|
|
|
2821
2884
|
this._renderFlag.animation = v;
|
|
2822
2885
|
}
|
|
2823
2886
|
|
|
2887
|
+
/**
|
|
2888
|
+
* Gets the canvas element the view renders into. Useful for styling,
|
|
2889
|
+
* e.g. switching the cursor from a `featureHover` event listener.
|
|
2890
|
+
*/
|
|
2891
|
+
get canvas(): HTMLCanvasElement {
|
|
2892
|
+
return this._renderer.domElement;
|
|
2893
|
+
}
|
|
2894
|
+
|
|
2824
2895
|
/**
|
|
2825
2896
|
* Gets the current screen size in pixels.
|
|
2826
2897
|
*/
|
|
@@ -97,8 +97,18 @@ export function generateTileMapFragment(
|
|
|
97
97
|
}`
|
|
98
98
|
: "";
|
|
99
99
|
return `
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
// The atlases are linear-filtered for display, but the pick pass reads
|
|
101
|
+
// encoded batch ids out of atlasColor: interpolating id colors at feature
|
|
102
|
+
// edges decodes to batch ids that don't exist (a thin draped polyline is
|
|
103
|
+
// almost all edge pixels). Snap to the texel center (nearest sampling)
|
|
104
|
+
// while picking.
|
|
105
|
+
vec2 nvr_atlasUv = vOrigUv;
|
|
106
|
+
if (uPickable > 0.) {
|
|
107
|
+
vec2 nvr_atlasSize = vec2(textureSize(uColorAtlas, 0));
|
|
108
|
+
nvr_atlasUv = (floor(vOrigUv * nvr_atlasSize) + 0.5) / nvr_atlasSize;
|
|
109
|
+
}
|
|
110
|
+
vec4 atlasColor = texture2D(uColorAtlas, nvr_atlasUv);
|
|
111
|
+
vec4 atlasAttr = texture2D(uAttrAtlas, nvr_atlasUv);
|
|
102
112
|
|
|
103
113
|
// attr.a encodes (winningSlot + 1) / 255 — 0 means no contributing slot.
|
|
104
114
|
int winIdx = int(round(atlasAttr.a * 255.0)) - 1;
|
package/src/mesh/polyline.ts
CHANGED
|
@@ -32,6 +32,16 @@ import { setupRTECallback } from "./rtcRteHelper";
|
|
|
32
32
|
// Sentinel value for picking coordinate when not picking (reused to avoid allocations)
|
|
33
33
|
const PICKING_COORD_SENTINEL = new Vector2(-1, -1);
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Minimum stroke width used while rendering the pick pass. A hairline stroke
|
|
37
|
+
* covers too few pick-buffer texels to click reliably — on the draped path the
|
|
38
|
+
* 512px tile atlas resolves a thin line to scattered partial texels, so the
|
|
39
|
+
* decoded batch id is background almost everywhere the line is visible.
|
|
40
|
+
* Fattening the stroke only for the pick render gives lines the same "click
|
|
41
|
+
* tolerance" pointing devices get elsewhere; the visible width is untouched.
|
|
42
|
+
*/
|
|
43
|
+
const MIN_PICK_WIDTH = 10;
|
|
44
|
+
|
|
35
45
|
type Attributes = BatchedFeatureAttributes<{
|
|
36
46
|
position: BufferAttribute;
|
|
37
47
|
// RTE mode attributes (only present when useRTE=true)
|
|
@@ -490,11 +500,21 @@ export class PolylineMesh extends BatchedFeatureMesh<
|
|
|
490
500
|
return this.getEnhancer().states().effectIdsMask;
|
|
491
501
|
}
|
|
492
502
|
|
|
503
|
+
/** Visible stroke width to restore after a pick-pass width bump. */
|
|
504
|
+
private _pickSavedWidth?: number;
|
|
505
|
+
|
|
493
506
|
onBeforePicking(pickingCoord?: Vector2) {
|
|
494
|
-
this.getEnhancer()
|
|
507
|
+
const enhancer = this.getEnhancer();
|
|
508
|
+
const width = enhancer.states().width;
|
|
509
|
+
if (width < MIN_PICK_WIDTH) {
|
|
510
|
+
this._pickSavedWidth = width;
|
|
511
|
+
enhancer.update({ base: { pickable: true, width: MIN_PICK_WIDTH } });
|
|
512
|
+
} else {
|
|
513
|
+
enhancer.update({ base: { pickable: true } });
|
|
514
|
+
}
|
|
495
515
|
this.needsUpdate();
|
|
496
516
|
|
|
497
|
-
const mutates =
|
|
517
|
+
const mutates = enhancer.mutates();
|
|
498
518
|
if (pickingCoord) {
|
|
499
519
|
mutates.setPickingCoord(pickingCoord);
|
|
500
520
|
} else {
|
|
@@ -503,9 +523,17 @@ export class PolylineMesh extends BatchedFeatureMesh<
|
|
|
503
523
|
}
|
|
504
524
|
|
|
505
525
|
onAfterPicking() {
|
|
506
|
-
this.getEnhancer()
|
|
526
|
+
const enhancer = this.getEnhancer();
|
|
527
|
+
if (this._pickSavedWidth !== undefined) {
|
|
528
|
+
enhancer.update({
|
|
529
|
+
base: { pickable: false, width: this._pickSavedWidth },
|
|
530
|
+
});
|
|
531
|
+
this._pickSavedWidth = undefined;
|
|
532
|
+
} else {
|
|
533
|
+
enhancer.update({ base: { pickable: false } });
|
|
534
|
+
}
|
|
507
535
|
this.needsUpdate();
|
|
508
|
-
|
|
536
|
+
enhancer.mutates().setPickingCoord(PICKING_COORD_SENTINEL);
|
|
509
537
|
}
|
|
510
538
|
|
|
511
539
|
_getDefaultBatchAttributeValues(): DefaultBatchAttributeValues {
|
|
@@ -212,6 +212,71 @@ describe("BatchedSdfTextMesh material updates vs per-feature values", () => {
|
|
|
212
212
|
});
|
|
213
213
|
});
|
|
214
214
|
|
|
215
|
+
// A feature owns several anchors for MultiPoint geometry and for labels
|
|
216
|
+
// derived from line/polygon vertices via `geometryTypes`. Per-feature setters
|
|
217
|
+
// address features (batch indices), so they must fan out to every anchor the
|
|
218
|
+
// feature owns via the geometry's per-anchor `batch_index` buffer.
|
|
219
|
+
describe("BatchedSdfTextMesh multi-instance fan-out", () => {
|
|
220
|
+
/** Feature 0 owns anchors 0 and 1 (MultiPoint); feature 1 owns anchor 2. */
|
|
221
|
+
function multiInstanceMeshEvent(mat: NavaraTextMaterial): NavaraTextMesh {
|
|
222
|
+
return {
|
|
223
|
+
material: mat,
|
|
224
|
+
transform: { tx: 0, ty: 0, tz: 0 },
|
|
225
|
+
geometry: {
|
|
226
|
+
batch_ids: { data: new Float32Array([7, 8, 9]), size: 1 },
|
|
227
|
+
batch_index: { data: new Uint32Array([0, 0, 1]), size: 1 },
|
|
228
|
+
position: {
|
|
229
|
+
data: new Float32Array([0, 0, 0, 10, 0, 0, 20, 0, 0]),
|
|
230
|
+
size: 3,
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
} as unknown as NavaraTextMesh;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function makeMultiInstanceMesh() {
|
|
237
|
+
const fontManager = makeFontManager();
|
|
238
|
+
const ctx = {
|
|
239
|
+
buf: {
|
|
240
|
+
removeF32: (d: Float32Array) => d,
|
|
241
|
+
u32: (d: Uint32Array) => d,
|
|
242
|
+
},
|
|
243
|
+
fontManager,
|
|
244
|
+
renderFlag: { forceUpdate: false },
|
|
245
|
+
} as unknown as EventContext;
|
|
246
|
+
const mesh = new BatchedSdfTextMesh(
|
|
247
|
+
ctx,
|
|
248
|
+
multiInstanceMeshEvent(material()),
|
|
249
|
+
"font",
|
|
250
|
+
{ layerId: "layer" },
|
|
251
|
+
);
|
|
252
|
+
return { mesh, fontManager };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
it("applies text and styles to every anchor the feature owns", () => {
|
|
256
|
+
const { mesh } = makeMultiInstanceMesh();
|
|
257
|
+
mesh.setTextByBatchIndex(0, "AB");
|
|
258
|
+
mesh.setFeatureSizeByBatchIndex(0, 32);
|
|
259
|
+
|
|
260
|
+
// Feature 0's two anchors got labels (slots 0 and 1), both styled.
|
|
261
|
+
expect(showOf(mesh, 0)).toBe(1);
|
|
262
|
+
expect(showOf(mesh, 1)).toBe(1);
|
|
263
|
+
expect(sizeOf(mesh, 0)).toBe(32);
|
|
264
|
+
expect(sizeOf(mesh, 1)).toBe(32);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("does not leak styling into other features' anchors", () => {
|
|
268
|
+
const { mesh } = makeMultiInstanceMesh();
|
|
269
|
+
mesh.setTextByBatchIndex(0, "AB");
|
|
270
|
+
mesh.setTextByBatchIndex(1, "CD");
|
|
271
|
+
mesh.setFeatureShowByBatchIndex(1, false);
|
|
272
|
+
|
|
273
|
+
expect(showOf(mesh, 0)).toBe(1);
|
|
274
|
+
expect(showOf(mesh, 1)).toBe(1);
|
|
275
|
+
// Feature 1 = anchor 2 = the third label slot.
|
|
276
|
+
expect(showOf(mesh, 2)).toBe(0);
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
215
280
|
// Unprepared text costs a worker round-trip and font-face fetches, and a
|
|
216
281
|
// low-zoom tile spans far more world than the screen shows. With declutter on,
|
|
217
282
|
// `setTextByBatchIndex` therefore parks preparation until a placement pass
|