@navaramap/three 0.0.7 → 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-VfUncKuS.js +25 -0
- package/dist/assets/index-VfUncKuS.js.map +1 -0
- package/dist/assets/{navara_wasm_font_worker_bg-XvE_oYPr.wasm → navara_wasm_font_worker_bg-CKqHBoCR.wasm} +0 -0
- package/dist/assets/{navara_wasm_worker_bg-Dg9fnGLP.wasm → navara_wasm_worker_bg-CT26EH41.wasm} +0 -0
- package/dist/index.d.ts +460 -43
- package/dist/index.js +3732 -3584
- package/dist/index.js.map +1 -1
- package/dist/index.umd.cjs +230 -209
- package/dist/index.umd.cjs.map +1 -1
- package/package.json +17 -17
- package/src/camera.ts +11 -4
- package/src/core/InstancedMeshDesc.ts +1 -1
- package/src/core/MeshDesc.test.ts +396 -0
- package/src/core/MeshDesc.ts +219 -8
- package/src/core/errors.ts +14 -0
- package/src/core/index.ts +2 -0
- package/src/evaluations/FeatureEvaluator.ts +17 -10
- package/src/event/context.ts +6 -0
- 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/event/index.ts +7 -0
- package/src/index.ts +174 -40
- 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/billboardAtlas.ts +8 -1
- package/src/mesh/sprite/instancedSprite.ts +173 -65
- package/src/pick/pickHelper.test.ts +302 -0
- package/src/pick/pickHelper.ts +119 -20
- package/src/plugins/AttributionPlugin/index.ts +1 -1
- package/src/renderOrder.ts +10 -0
- package/src/terrain/sampleTerrainMostDetailed.test.ts +6 -6
- package/src/terrain/sampleTerrainMostDetailed.ts +7 -5
- package/src/terrain/terrainHeightEvent.test.ts +32 -0
- package/src/terrain/terrainHeightEvent.ts +22 -0
- package/src/type/index.ts +22 -1
- package/dist/assets/fontWorker-DaqfXmZf.js +0 -2
- package/dist/assets/fontWorker-DaqfXmZf.js.map +0 -1
- package/dist/assets/index-Dydat1h1.js +0 -25
- package/dist/assets/index-Dydat1h1.js.map +0 -1
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,
|
|
@@ -131,6 +131,7 @@ import {
|
|
|
131
131
|
type SampledTerrainPosition,
|
|
132
132
|
type TerrainSampleSource,
|
|
133
133
|
} from "./terrain/sampleTerrainMostDetailed";
|
|
134
|
+
import { hasObservedTerrainHeight } from "./terrain/terrainHeightEvent";
|
|
134
135
|
import type { TextureOptions } from "./textures";
|
|
135
136
|
import { TileTextureCompositor } from "./tileTexture";
|
|
136
137
|
import {
|
|
@@ -156,6 +157,30 @@ import WorkerURL from "./worker?url&worker";
|
|
|
156
157
|
|
|
157
158
|
export type { CameraOptions, CameraEvent } from "./camera";
|
|
158
159
|
|
|
160
|
+
/** Easing presets applied to a `flyTo` flight's normalized time. */
|
|
161
|
+
export type FlyToEasing =
|
|
162
|
+
"linear" | "quadInOut" | "cubicInOut" | "cubicOut" | "quinticInOut";
|
|
163
|
+
|
|
164
|
+
export type FlyToOptions = {
|
|
165
|
+
/** Animation duration in milliseconds. Defaults to a duration derived from
|
|
166
|
+
* the flight distance (2–3 s). Pass `0` to jump instantly. */
|
|
167
|
+
duration?: number;
|
|
168
|
+
/** Maximum height during the flight arc in meters. */
|
|
169
|
+
maxHeight?: number;
|
|
170
|
+
/** Easing preset. Defaults to `"quinticInOut"`, or `"cubicOut"` when
|
|
171
|
+
* descending from above 11,500 m. */
|
|
172
|
+
easing?: FlyToEasing;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// Must mirror `FlyToEasing`'s `TryFrom<u8>` mapping on the Rust side.
|
|
176
|
+
const FLY_TO_EASING_CODE: Record<FlyToEasing, number> = {
|
|
177
|
+
linear: 0,
|
|
178
|
+
quadInOut: 1,
|
|
179
|
+
cubicInOut: 2,
|
|
180
|
+
cubicOut: 3,
|
|
181
|
+
quinticInOut: 4,
|
|
182
|
+
};
|
|
183
|
+
|
|
159
184
|
export { ColorMap, EventHandler, Plugin } from "@navaramap/core";
|
|
160
185
|
export type {
|
|
161
186
|
Nullable,
|
|
@@ -284,7 +309,11 @@ export type Options = {
|
|
|
284
309
|
atmosphere?: AtmosphereOptions;
|
|
285
310
|
/** Background color of the scene. Defaults to dark color (0x0a0a0f). */
|
|
286
311
|
backgroundColor?: CoreColor;
|
|
287
|
-
/**
|
|
312
|
+
/**
|
|
313
|
+
* Feature picking configuration (default: true). Enables the `featureClick`
|
|
314
|
+
* event (click) and the `featureHover` / `featureEnter` / `featureLeave`
|
|
315
|
+
* events (mousemove).
|
|
316
|
+
*/
|
|
288
317
|
picking?: boolean;
|
|
289
318
|
/** When true, renders every frame. When false, renders only on changes or when forceUpdate() is called. */
|
|
290
319
|
animation?: boolean;
|
|
@@ -388,8 +417,24 @@ export type MapMouseEvent = {
|
|
|
388
417
|
export type ViewEvents = {
|
|
389
418
|
/** Emitted when the view is resized. Receives width and height in pixels. */
|
|
390
419
|
resize: (w: number, h: number) => void;
|
|
391
|
-
/**
|
|
392
|
-
|
|
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;
|
|
393
438
|
/** Emitted when a layer event occurs. Receives event type, layer ID, and event arguments. */
|
|
394
439
|
layer: <K extends keyof LayerEvent>(
|
|
395
440
|
k: K,
|
|
@@ -474,6 +519,12 @@ export default class ThreeView<
|
|
|
474
519
|
private _declutterRetryDelay = 0;
|
|
475
520
|
private _isIdle = false;
|
|
476
521
|
private _uniforms: CommonUniforms;
|
|
522
|
+
/** Pending `flyTo` promises keyed by flight id, settled by
|
|
523
|
+
* `camera_flight_ended` events from the engine. */
|
|
524
|
+
private _pendingFlights = new Map<number, (completed: boolean) => void>();
|
|
525
|
+
|
|
526
|
+
/** Currently hovered feature, used to diff-synthesize hover events. */
|
|
527
|
+
private _hoveredFeature: Nullable<PickedFeature> = null;
|
|
477
528
|
|
|
478
529
|
private _meshes: MeshCache = new Map();
|
|
479
530
|
private _abortControllers: AbortControllers = new Map();
|
|
@@ -1055,7 +1106,9 @@ export default class ThreeView<
|
|
|
1055
1106
|
tGlobeNormal: { value: null },
|
|
1056
1107
|
tSkyEnvMap: { value: null },
|
|
1057
1108
|
inverseProjectionMatrix: { value: null },
|
|
1058
|
-
//
|
|
1109
|
+
// Vertical fov in radians; synced with the engine on each frustum event.
|
|
1110
|
+
// Plain arithmetic: the WASM-backed degreeToRadian is not available
|
|
1111
|
+
// until init().
|
|
1059
1112
|
fov: { value: (this._camera.raw.fov * Math.PI) / 180 },
|
|
1060
1113
|
screenHeightPx: { value: height },
|
|
1061
1114
|
time: { value: 0 },
|
|
@@ -1493,6 +1546,13 @@ export default class ThreeView<
|
|
|
1493
1546
|
viewEvents: this,
|
|
1494
1547
|
layersManager: this.layersManager,
|
|
1495
1548
|
viewContext: this.viewContext,
|
|
1549
|
+
flightHandler: (id, completed) => {
|
|
1550
|
+
const resolve = this._pendingFlights.get(id);
|
|
1551
|
+
if (resolve) {
|
|
1552
|
+
this._pendingFlights.delete(id);
|
|
1553
|
+
resolve(completed);
|
|
1554
|
+
}
|
|
1555
|
+
},
|
|
1496
1556
|
layerHandler: this._layerHandler,
|
|
1497
1557
|
fontManager: this._fontManager,
|
|
1498
1558
|
textureFragmentIndex: this._textureFragmentIndex,
|
|
@@ -1525,6 +1585,17 @@ export default class ThreeView<
|
|
|
1525
1585
|
this._camera.raw,
|
|
1526
1586
|
this._meshes,
|
|
1527
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
|
+
),
|
|
1528
1599
|
// {
|
|
1529
1600
|
// debug: true,
|
|
1530
1601
|
// },
|
|
@@ -1603,6 +1674,11 @@ export default class ThreeView<
|
|
|
1603
1674
|
dispose() {
|
|
1604
1675
|
this._disposed = true;
|
|
1605
1676
|
this._initialized = false;
|
|
1677
|
+
// Settle pending flyTo promises so callers never hang on a disposed view.
|
|
1678
|
+
for (const resolve of this._pendingFlights.values()) {
|
|
1679
|
+
resolve(false);
|
|
1680
|
+
}
|
|
1681
|
+
this._pendingFlights.clear();
|
|
1606
1682
|
if (this._declutterRetry !== undefined) {
|
|
1607
1683
|
clearTimeout(this._declutterRetry);
|
|
1608
1684
|
this._declutterRetry = undefined;
|
|
@@ -1768,7 +1844,10 @@ export default class ThreeView<
|
|
|
1768
1844
|
const pixelRatio = this._renderer.getPixelRatio();
|
|
1769
1845
|
|
|
1770
1846
|
// Ref: https://github.com/CesiumGS/cesium/blob/2cf09cb06e4f7ea767da39befabcfc3444b02c49/packages/engine/Source/Core/PerspectiveFrustum.js#L208-L218
|
|
1771
|
-
|
|
1847
|
+
// raw.fov mirrors the engine's vertical fov (degrees) via the frustum
|
|
1848
|
+
// event pump. Plain arithmetic: this must not depend on the WASM-backed
|
|
1849
|
+
// degreeToRadian helper.
|
|
1850
|
+
const fovY = (this._camera.raw.fov * Math.PI) / 180;
|
|
1772
1851
|
const top = this._camera.raw.near * Math.tan(0.5 * fovY);
|
|
1773
1852
|
const bottom = -top;
|
|
1774
1853
|
const right = this._camera.raw.aspect * top;
|
|
@@ -1793,8 +1872,8 @@ export default class ThreeView<
|
|
|
1793
1872
|
this._uniforms.inverseProjectionMatrix.value =
|
|
1794
1873
|
this._camera.raw.projectionMatrixInverse;
|
|
1795
1874
|
|
|
1796
|
-
//
|
|
1797
|
-
this._uniforms.fov.value =
|
|
1875
|
+
// Vertical fov in radians (raw.fov is kept in sync with the engine).
|
|
1876
|
+
this._uniforms.fov.value = fovY;
|
|
1798
1877
|
this._uniforms.screenHeightPx.value = viewport?.height ?? 0;
|
|
1799
1878
|
}
|
|
1800
1879
|
|
|
@@ -2486,30 +2565,48 @@ export default class ThreeView<
|
|
|
2486
2565
|
|
|
2487
2566
|
/**
|
|
2488
2567
|
* Animates the camera to fly to a target position.
|
|
2568
|
+
*
|
|
2569
|
+
* The flight follows an arced path: the default duration is derived from
|
|
2570
|
+
* the flight distance (2–3 s), the default easing is `quinticInOut`
|
|
2571
|
+
* (`cubicOut` when descending from high altitude), and longitudes take the
|
|
2572
|
+
* short way across the antimeridian.
|
|
2573
|
+
*
|
|
2489
2574
|
* @param camPos - Target position with required lng/lat (degrees). Provide either `height` (meters above the ellipsoid)
|
|
2490
2575
|
* or `distance` (meters from the target point along the camera forward direction). Optional pitch, heading, roll (degrees).
|
|
2491
|
-
* @param
|
|
2492
|
-
* @
|
|
2576
|
+
* @param options - Flight options (`duration`, `maxHeight`, `easing`)
|
|
2577
|
+
* @returns A promise resolving to `true` when the flight completes, or
|
|
2578
|
+
* `false` when it is interrupted by another camera operation, such as a
|
|
2579
|
+
* newer `flyTo` or a `setCamera` call.
|
|
2493
2580
|
*/
|
|
2494
2581
|
flyTo(
|
|
2495
2582
|
camPos: CameraPosition &
|
|
2496
2583
|
Required<Pick<CameraPosition, "lng" | "lat">> &
|
|
2497
2584
|
({ height: number } | { distance: number }),
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
) {
|
|
2585
|
+
options: FlyToOptions = {},
|
|
2586
|
+
): Promise<boolean> {
|
|
2501
2587
|
const pos = camPos as CameraPosition & { lng: number; lat: number };
|
|
2502
2588
|
const position = new Float64Array([pos.lng, pos.lat, pos.height ?? 0]);
|
|
2503
2589
|
|
|
2504
|
-
this._core?.flyTo(
|
|
2590
|
+
const id = this._core?.flyTo(
|
|
2505
2591
|
position,
|
|
2506
2592
|
pos.pitch,
|
|
2507
2593
|
pos.heading,
|
|
2508
2594
|
pos.roll,
|
|
2509
|
-
duration,
|
|
2510
|
-
maxHeight,
|
|
2595
|
+
options.duration,
|
|
2596
|
+
options.maxHeight,
|
|
2511
2597
|
pos.distance,
|
|
2598
|
+
options.easing === undefined
|
|
2599
|
+
? undefined
|
|
2600
|
+
: FLY_TO_EASING_CODE[options.easing],
|
|
2512
2601
|
);
|
|
2602
|
+
|
|
2603
|
+
if (id === undefined) {
|
|
2604
|
+
return Promise.resolve(false);
|
|
2605
|
+
}
|
|
2606
|
+
|
|
2607
|
+
return new Promise<boolean>((resolve) => {
|
|
2608
|
+
this._pendingFlights.set(id, resolve);
|
|
2609
|
+
});
|
|
2513
2610
|
}
|
|
2514
2611
|
|
|
2515
2612
|
/**
|
|
@@ -2558,11 +2655,11 @@ export default class ThreeView<
|
|
|
2558
2655
|
|
|
2559
2656
|
/**
|
|
2560
2657
|
* Samples the terrain height at a given geodetic position synchronously.
|
|
2561
|
-
* @param pos - Geodetic position (lat in
|
|
2658
|
+
* @param pos - Geodetic position (lat in degrees, lng in degrees)
|
|
2562
2659
|
* @returns Terrain height in meters, or undefined if terrain data not loaded
|
|
2563
2660
|
*/
|
|
2564
2661
|
sampleTerrainHeight(pos: LatLng): number | undefined {
|
|
2565
|
-
const lle = new LLE(pos.lat, pos.lng, 0);
|
|
2662
|
+
const lle = new LLE(degreeToRadian(pos.lat), degreeToRadian(pos.lng), 0);
|
|
2566
2663
|
return this._core?.sampleTerrainHeight(lle);
|
|
2567
2664
|
}
|
|
2568
2665
|
|
|
@@ -2580,7 +2677,7 @@ export default class ThreeView<
|
|
|
2580
2677
|
* sampled.
|
|
2581
2678
|
*
|
|
2582
2679
|
* @param source - A registered terrain source (handle or id)
|
|
2583
|
-
* @param positions - Geodetic positions (lat/lng in
|
|
2680
|
+
* @param positions - Geodetic positions (lat/lng in degrees)
|
|
2584
2681
|
* @param options - Optional abort signal and fixed-level sampling
|
|
2585
2682
|
* @returns One result per input position, in the same order
|
|
2586
2683
|
*/
|
|
@@ -2606,7 +2703,7 @@ export default class ThreeView<
|
|
|
2606
2703
|
|
|
2607
2704
|
/**
|
|
2608
2705
|
* Observes terrain height changes at a position. Callback is invoked each time terrain data updates.
|
|
2609
|
-
* @param pos - Geodetic position to observe (lat in
|
|
2706
|
+
* @param pos - Geodetic position to observe (lat in degrees, lng in degrees)
|
|
2610
2707
|
* @param cb - Callback function receiving the terrain height in meters
|
|
2611
2708
|
* @returns Cleanup function to stop observing
|
|
2612
2709
|
*/
|
|
@@ -2618,11 +2715,11 @@ export default class ThreeView<
|
|
|
2618
2715
|
return () => {};
|
|
2619
2716
|
}
|
|
2620
2717
|
|
|
2621
|
-
const lle = new LLE(pos.lat, pos.lng, 0);
|
|
2718
|
+
const lle = new LLE(degreeToRadian(pos.lat), degreeToRadian(pos.lng), 0);
|
|
2622
2719
|
const entityBits = this._core.registerSampleTerrainHeightEvent(lle);
|
|
2623
2720
|
|
|
2624
2721
|
const callFunc = (ev: TerrainHeightUpdatedEvent) => {
|
|
2625
|
-
if (ev
|
|
2722
|
+
if (hasObservedTerrainHeight(ev, entityBits)) {
|
|
2626
2723
|
cb(ev.height);
|
|
2627
2724
|
}
|
|
2628
2725
|
};
|
|
@@ -2715,6 +2812,29 @@ export default class ThreeView<
|
|
|
2715
2812
|
this.resize(width, height, pixelRatio);
|
|
2716
2813
|
};
|
|
2717
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
|
+
|
|
2718
2838
|
/**
|
|
2719
2839
|
* Handles pick events and emits the picked feature information.
|
|
2720
2840
|
* @param pickArr - Array of picked batch IDs
|
|
@@ -2723,28 +2843,34 @@ export default class ThreeView<
|
|
|
2723
2843
|
this._renderFlag.forceUpdate = true;
|
|
2724
2844
|
|
|
2725
2845
|
if (pickArr.length > 0) {
|
|
2726
|
-
|
|
2727
|
-
const prop = this._core?.readPropertyByGlobalBatchId(batchId);
|
|
2728
|
-
if (prop) {
|
|
2729
|
-
const pickedFeature: PickedFeature = {
|
|
2730
|
-
properties: prop.properties,
|
|
2731
|
-
batchId,
|
|
2732
|
-
layerId: prop.layerId,
|
|
2733
|
-
};
|
|
2734
|
-
this.emit("pick", pickedFeature);
|
|
2735
|
-
} else {
|
|
2736
|
-
const emptyFeature: PickedFeature = {
|
|
2737
|
-
properties: {},
|
|
2738
|
-
batchId,
|
|
2739
|
-
layerId: undefined,
|
|
2740
|
-
};
|
|
2741
|
-
this.emit("pick", emptyFeature);
|
|
2742
|
-
}
|
|
2846
|
+
this.emit("featureClick", this.resolvePickedFeature(pickArr[0]));
|
|
2743
2847
|
} else {
|
|
2744
|
-
this.emit("
|
|
2848
|
+
this.emit("featureClick", null);
|
|
2745
2849
|
}
|
|
2746
2850
|
}
|
|
2747
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
|
+
|
|
2748
2874
|
/**
|
|
2749
2875
|
* Gets whether continuous animation mode is enabled.
|
|
2750
2876
|
*/
|
|
@@ -2758,6 +2884,14 @@ export default class ThreeView<
|
|
|
2758
2884
|
this._renderFlag.animation = v;
|
|
2759
2885
|
}
|
|
2760
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
|
+
|
|
2761
2895
|
/**
|
|
2762
2896
|
* Gets the current screen size in pixels.
|
|
2763
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
|