@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
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@navaramap/three",
3
3
  "type": "module",
4
- "version": "0.0.8",
4
+ "version": "0.1.1",
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/core": "0.0.8",
34
- "@navaramap/engine-api": "0.0.8",
35
- "@navaramap/three-api": "0.0.8",
36
- "@navaramap/font": "0.0.8",
37
- "@navaramap/three-csm": "0.0.8",
38
- "@navaramap/worker": "0.0.8",
39
- "@navaramap/engine": "0.0.8"
33
+ "@navaramap/core": "0.1.1",
34
+ "@navaramap/engine": "0.1.1",
35
+ "@navaramap/three-api": "0.1.1",
36
+ "@navaramap/three-csm": "0.1.1",
37
+ "@navaramap/worker": "0.1.1",
38
+ "@navaramap/engine-api": "0.1.1",
39
+ "@navaramap/font": "0.1.1"
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-descs": "0.0.8",
75
- "@navaramap/three-default-plugin": "0.0.8",
76
- "@navaramap/three-plugins": "0.0.8",
77
- "@navaramap/three-react": "0.0.8"
74
+ "@navaramap/three-default-descs": "0.1.1",
75
+ "@navaramap/three-default-plugin": "0.1.1",
76
+ "@navaramap/three-react": "0.1.1",
77
+ "@navaramap/three-plugins": "0.1.1"
78
78
  },
79
79
  "scripts": {
80
80
  "start": "vite",
@@ -76,6 +76,8 @@ export abstract class BaseDesc<
76
76
 
77
77
  private _visible?: boolean;
78
78
 
79
+ private _destroyed = false;
80
+
79
81
  constructor(
80
82
  view: ThreeView,
81
83
  ctx: ViewContext,
@@ -92,8 +94,20 @@ export abstract class BaseDesc<
92
94
  /**
93
95
  * Called when the this class is added to the scene. Override this to create the Three.js objects.
94
96
  * This is where you should initialize `this._instance` and add it to the appropriate scene.
97
+ *
98
+ * May return a promise: a descriptor whose implementation loads lazily
99
+ * (e.g. an `await import(...)` of a heavy renderer dependency) can be
100
+ * `async`, awaiting the module before delegating to `super.onCreate()`
101
+ * (after checking {@link destroyed}). `addMesh`/`addLight`/`addEffect`
102
+ * still return their handle synchronously; the instance appears once the
103
+ * promise settles, and a rejection is reported through the descriptor's
104
+ * `error` event (when it declares one). Handle calls issued while the
105
+ * promise is pending are safe: `update()`/`visible` store their fields
106
+ * without touching the missing instance (the deferred `super.onCreate()`
107
+ * applies them), and per-frame/resize hooks are wired only after creation
108
+ * completes.
95
109
  */
96
- abstract onCreate(): void;
110
+ abstract onCreate(): void | Promise<void>;
97
111
 
98
112
  /**
99
113
  * Called when the configuration is updated via `BaseHandle.update()`.
@@ -122,9 +136,19 @@ export abstract class BaseDesc<
122
136
  * Override this to clean up resources. Remember to call `super.onDestroy()`.
123
137
  */
124
138
  onDestroy(): void {
139
+ this._destroyed = true;
125
140
  this._instance = undefined;
126
141
  }
127
142
 
143
+ /**
144
+ * True once {@link onDestroy} has run. An async {@link onCreate} (and the
145
+ * hook wiring that waits on it) checks this after its awaited module
146
+ * resolves, so a descriptor deleted mid-load creates nothing.
147
+ */
148
+ get destroyed(): boolean {
149
+ return this._destroyed;
150
+ }
151
+
128
152
  /**
129
153
  * Gets whether this is currently visible.
130
154
  */
@@ -274,15 +274,15 @@ describe("MeshDesc geodetic placement", () => {
274
274
  });
275
275
 
276
276
  describe("MeshDesc terrain-referenced placement", () => {
277
- type LatLngRad = { lat: number; lng: number };
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: LatLngRad) => height),
283
+ sampleTerrainHeight: vi.fn((_pos: LatLngDeg) => height),
284
284
  observeTerrainHeightAt: vi.fn(
285
- (_pos: LatLngRad, cb: (h: number) => void) => {
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 engine methods take RADIANS, while `geodetic` is degrees.
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(rad(TOKYO.lat), 9);
314
- expect(pos.lng).toBeCloseTo(rad(TOKYO.lng), 9);
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);
@@ -3,7 +3,7 @@ import {
3
3
  headingPitchRollToFixedFrame,
4
4
  type GeodeticPlacement,
5
5
  } from "@navaramap/three-api";
6
- import { Euler, MathUtils, Matrix4, Object3D, Vector3, type Mesh } from "three";
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,
@@ -601,7 +598,6 @@ export abstract class MeshDesc<
601
598
  assertNoTransformConflict(nextGeodetic, nextMatrix, nextMatrixWorld);
602
599
 
603
600
  super.onUpdateConfig(updates);
604
- invariant(this.raw);
605
601
 
606
602
  const spatialChanged =
607
603
  updates.matrix !== undefined ||
@@ -639,6 +635,8 @@ export abstract class MeshDesc<
639
635
  this.applyLit();
640
636
  }
641
637
 
638
+ if (!this.raw) return;
639
+
642
640
  if (spatialChanged) {
643
641
  // With a frame present, the effective transform depends on the
644
642
  // combination of all stored fields — recompose the whole thing.
@@ -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, info.batchId, evaluated);
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.setFeatureColorByBatchId(batchId, evaluated.color.raw);
460
+ obj.setFeatureColorByBatchIndex(batchIndex, evaluated.color.raw);
454
461
  }
455
462
  if (evaluated.show != null) {
456
- obj.setFeatureShowByBatchId(batchId, evaluated.show);
463
+ obj.setFeatureShowByBatchIndex(batchIndex, evaluated.show);
457
464
  }
458
465
  if (evaluated.height != null) {
459
- obj.setFeatureHeightByBatchId(batchId, evaluated.height);
466
+ obj.setFeatureHeightByBatchIndex(batchIndex, evaluated.height);
460
467
  }
461
468
  if (evaluated.size != null) {
462
- obj.setFeatureSizeByBatchId(batchId, evaluated.size);
469
+ obj.setFeatureSizeByBatchIndex(batchIndex, evaluated.size);
463
470
  }
464
471
  if (evaluated.opacity != null) {
465
- obj.setFeatureOpacityByBatchId(batchId, evaluated.opacity);
472
+ obj.setFeatureOpacityByBatchIndex(batchIndex, evaluated.opacity);
466
473
  }
467
474
  if (evaluated.declutterPriority != null) {
468
- obj.setFeatureDeclutterPriorityByBatchId(
469
- batchId,
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.setFeatureImageByBatchId(batchId, evaluated.image);
484
+ void obj.setFeatureImageByBatchIndex(batchIndex, evaluated.image);
478
485
  }
479
486
  return;
480
487
  }
@@ -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
- obj.renderOrder = FEATURE_RENDER_ORDER;
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
- if (!m.material.url) return;
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,