@navaramap/three 0.0.2 → 0.0.3
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/dist/assets/{fontWorker-8oWvyvwL.js → fontWorker-wz61edjR.js} +2 -2
- package/dist/assets/{fontWorker-8oWvyvwL.js.map → fontWorker-wz61edjR.js.map} +1 -1
- package/dist/assets/{index-D9G7kIEj.js → index-ywHSGImj.js} +3 -3
- package/dist/assets/{index-D9G7kIEj.js.map → index-ywHSGImj.js.map} +1 -1
- package/dist/assets/{navara_wasm_font_worker_bg-CkZP9l2i.wasm → navara_wasm_font_worker_bg-Du9nb2Pn.wasm} +0 -0
- package/dist/assets/{navara_wasm_worker_bg-Q8zrkLCR.wasm → navara_wasm_worker_bg-Xadu67fw.wasm} +0 -0
- package/dist/index.d.ts +133 -2
- package/dist/index.js +4309 -4288
- package/dist/index.js.map +1 -1
- package/dist/index.umd.cjs +223 -210
- package/dist/index.umd.cjs.map +1 -1
- package/package.json +12 -12
- package/src/atmosphere.ts +43 -1
- package/src/event/feature.ts +43 -13
- package/src/event/features/polygon.ts +2 -1
- package/src/input.ts +48 -15
- package/src/material/enhancer/instancedSprite/instancedSpriteBaseEnhancer/mutates.test.ts +1 -0
- package/src/material/enhancer/instancedSprite/instancedSpriteBaseEnhancer/mutates.ts +2 -0
- package/src/material/enhancer/instancedSprite/instancedSpriteBaseEnhancer/types.ts +2 -0
- package/src/material/enhancer/polygon/polygonBaseEnhancer/mutates.ts +2 -0
- package/src/material/enhancer/polygon/polygonBaseEnhancer/shader.ts +5 -1
- package/src/material/enhancer/polygon/polygonBaseEnhancer/types.ts +2 -0
- package/src/material/enhancer/polyline/polylineBaseEnhancer/mutates.ts +2 -0
- package/src/material/enhancer/polyline/polylineBaseEnhancer/types.ts +2 -0
- package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/mutates.ts +2 -0
- package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/types.ts +2 -0
- package/src/material/overrideMaterialsForMRT.ts +7 -2
- package/src/mesh/batchedSdfText.ts +234 -70
- package/src/mesh/instanced.ts +1 -1
- package/src/mesh/polygon.ts +11 -4
- package/src/mesh/polyline.ts +10 -2
- package/src/mesh/sdfText.ts +6 -1
- package/src/solar.test.ts +33 -0
- package/src/solar.ts +31 -0
|
@@ -3,7 +3,7 @@ import type {
|
|
|
3
3
|
TextMaterial as NavaraTextMaterial,
|
|
4
4
|
} from "@navaramap/engine";
|
|
5
5
|
import type { FontManager } from "@navaramap/font";
|
|
6
|
-
import {
|
|
6
|
+
import { Color } from "three";
|
|
7
7
|
import invariant from "tiny-invariant";
|
|
8
8
|
|
|
9
9
|
import type {
|
|
@@ -38,6 +38,22 @@ type PositionsInfo = PositionsInfoBase &
|
|
|
38
38
|
}
|
|
39
39
|
);
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Per-feature evaluator state received before the feature's `SDFTextMesh`
|
|
43
|
+
* exists (meshes are created lazily on the first non-empty text — see
|
|
44
|
+
* `initMeshes`). Replayed onto the mesh at creation. Colors are kept as hex
|
|
45
|
+
* so a shared evaluator `Color` instance mutated later can't leak in.
|
|
46
|
+
*/
|
|
47
|
+
type PendingFeatureState = {
|
|
48
|
+
colorHex?: number;
|
|
49
|
+
show?: boolean;
|
|
50
|
+
height?: number;
|
|
51
|
+
size?: number;
|
|
52
|
+
opacity?: number;
|
|
53
|
+
declutterPriority?: number | undefined;
|
|
54
|
+
hasDeclutterPriority?: boolean;
|
|
55
|
+
};
|
|
56
|
+
|
|
41
57
|
export class BatchedSdfTextMesh
|
|
42
58
|
extends InstancedMesh<SDFTextMesh>
|
|
43
59
|
implements PickableMesh, DeclutterParticipant
|
|
@@ -59,6 +75,15 @@ export class BatchedSdfTextMesh
|
|
|
59
75
|
* and must be balanced with unloadFont() on dispose or font change.
|
|
60
76
|
*/
|
|
61
77
|
private _loadedFaceUrls: Set<string>;
|
|
78
|
+
/** Geometry-extracted anchor data; per-index meshes slice it lazily. */
|
|
79
|
+
private _positions: PositionsInfo | null = null;
|
|
80
|
+
/** Sparse batchIndex → mesh; `allMeshes` holds only created meshes. */
|
|
81
|
+
private _meshByIndex: (SDFTextMesh | undefined)[] = [];
|
|
82
|
+
/** Evaluator state received before a feature's mesh exists. */
|
|
83
|
+
private _pending = new Map<number, PendingFeatureState>();
|
|
84
|
+
/** Material/transform used when creating meshes lazily. */
|
|
85
|
+
private _material: NavaraTextMaterial;
|
|
86
|
+
private _transform: NavaraTextMesh["transform"];
|
|
62
87
|
|
|
63
88
|
constructor(
|
|
64
89
|
ctx: EventContext,
|
|
@@ -74,7 +99,12 @@ export class BatchedSdfTextMesh
|
|
|
74
99
|
invariant(ctx.fontManager);
|
|
75
100
|
this._fontManager = ctx.fontManager;
|
|
76
101
|
this._loadedFaceUrls = loadedFaceUrls ?? new Set();
|
|
77
|
-
|
|
102
|
+
// One getter call each: wasm getters clone, so these snapshots are owned
|
|
103
|
+
// by this batch and safe to use after the event object is freed.
|
|
104
|
+
this._material = m.material;
|
|
105
|
+
this._transform = m.transform;
|
|
106
|
+
this._positions = this.extractPositions(m);
|
|
107
|
+
this.initMeshes();
|
|
78
108
|
// When the shared atlas evicts glyphs, a still-in-flight glyph this batch
|
|
79
109
|
// already baked into a visible mesh may have had its rect reused. Rebuild
|
|
80
110
|
// any such stale mesh so its UVs and retains refresh.
|
|
@@ -167,73 +197,149 @@ export class BatchedSdfTextMesh
|
|
|
167
197
|
return this._highQuality;
|
|
168
198
|
}
|
|
169
199
|
|
|
170
|
-
|
|
171
|
-
|
|
200
|
+
/**
|
|
201
|
+
* A mesh per anchor is expensive (geometry + ShaderMaterial + enhancer
|
|
202
|
+
* mount), and with per-feature texts (MVT) most features never receive a
|
|
203
|
+
* non-empty text — they'd sit invisible while still costing construction and
|
|
204
|
+
* every per-frame loop. So meshes are created lazily by `_ensureMeshAt` on
|
|
205
|
+
* the first non-empty `setTextByBatchIndex`. Only a non-empty material-level
|
|
206
|
+
* default text (single-label sources) renders without the evaluator ever
|
|
207
|
+
* setting text, so only that case creates all meshes eagerly.
|
|
208
|
+
*/
|
|
209
|
+
private initMeshes() {
|
|
210
|
+
const positionInfo = this._positions;
|
|
172
211
|
if (!positionInfo) {
|
|
173
212
|
return;
|
|
174
213
|
}
|
|
175
214
|
|
|
176
|
-
|
|
177
|
-
|
|
215
|
+
if (this._material.text) {
|
|
216
|
+
// The atlas can't be reallocated within this synchronous loop, so fetch
|
|
217
|
+
// the shared textures once and hand them to every creation.
|
|
218
|
+
const shared = this._sharedTextures();
|
|
219
|
+
for (let i = 0; i < positionInfo.nPositions; i++) {
|
|
220
|
+
this._ensureMeshAt(i, shared);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
178
224
|
|
|
179
|
-
|
|
180
|
-
|
|
225
|
+
/** The font-level shared atlas textures for this batch's font+quality (the
|
|
226
|
+
* color atlas is `null` for monochrome fonts). */
|
|
227
|
+
private _sharedTextures() {
|
|
228
|
+
return {
|
|
229
|
+
tex: this._fontManager.getAtlasTexture(
|
|
230
|
+
this._fontIdentifier,
|
|
231
|
+
this._highQuality,
|
|
232
|
+
),
|
|
233
|
+
colorTex: this._fontManager.getColorAtlasTexture(
|
|
234
|
+
this._fontIdentifier,
|
|
235
|
+
this._highQuality,
|
|
236
|
+
),
|
|
237
|
+
};
|
|
238
|
+
}
|
|
181
239
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
240
|
+
/**
|
|
241
|
+
* The feature's mesh, created on first need (see `initMeshes`). `shared`
|
|
242
|
+
* lets a batch creation pass hoist the atlas lookups; when omitted (a single
|
|
243
|
+
* lazy creation) they are fetched here, since the atlas may have been
|
|
244
|
+
* (re)allocated since this batch was constructed.
|
|
245
|
+
*/
|
|
246
|
+
private _ensureMeshAt(
|
|
247
|
+
batchIndex: number,
|
|
248
|
+
shared?: ReturnType<BatchedSdfTextMesh["_sharedTextures"]>,
|
|
249
|
+
): SDFTextMesh | undefined {
|
|
250
|
+
const existing = this._meshByIndex[batchIndex];
|
|
251
|
+
if (existing) return existing;
|
|
252
|
+
|
|
253
|
+
const info = this._positions;
|
|
254
|
+
if (!info || batchIndex < 0 || batchIndex >= info.nPositions) {
|
|
255
|
+
return undefined;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const { position, positionSize, batchIDs, batchIDSize, RTE } = info;
|
|
259
|
+
const batchId = batchIDs ? batchIDs[batchIndex * batchIDSize] : undefined;
|
|
260
|
+
const posIdx = batchIndex * positionSize;
|
|
261
|
+
const pos = RTE
|
|
262
|
+
? {
|
|
263
|
+
high: position.high.subarray(posIdx, posIdx + positionSize),
|
|
264
|
+
low: position.low.subarray(posIdx, posIdx + positionSize),
|
|
265
|
+
}
|
|
266
|
+
: position.subarray(posIdx, posIdx + positionSize);
|
|
267
|
+
|
|
268
|
+
const mesh = new SDFTextMesh(
|
|
269
|
+
pos,
|
|
270
|
+
this._material,
|
|
271
|
+
this._transform,
|
|
272
|
+
this._fontManager,
|
|
186
273
|
this._fontIdentifier,
|
|
274
|
+
batchId,
|
|
275
|
+
RTE,
|
|
276
|
+
// Pin quality to the batch, not `this._material.highQuality`, which a
|
|
277
|
+
// later `_update` may have changed (quality is immutable per batch).
|
|
187
278
|
this._highQuality,
|
|
188
279
|
);
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
280
|
+
mesh.renderOrder = this.renderOrder;
|
|
281
|
+
// Re-prepare evicted glyphs through the batch so font-family faces and
|
|
282
|
+
// refcounts stay consistent; request a render once they're rebuilt.
|
|
283
|
+
mesh.setGlyphLifecycleHandlers(
|
|
284
|
+
(text) =>
|
|
285
|
+
this._fontManager.prepareText(
|
|
286
|
+
this._fontIdentifier,
|
|
287
|
+
text,
|
|
288
|
+
this._highQuality,
|
|
289
|
+
this._loadedFaceUrls,
|
|
290
|
+
),
|
|
291
|
+
() => this._needRender?.(),
|
|
192
292
|
);
|
|
193
293
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
? {
|
|
200
|
-
high: position.high.subarray(posIdx, posIdx + positionSize),
|
|
201
|
-
low: position.low.subarray(posIdx, posIdx + positionSize),
|
|
202
|
-
}
|
|
203
|
-
: position.subarray(posIdx, posIdx + positionSize);
|
|
204
|
-
|
|
205
|
-
const mesh = new SDFTextMesh(
|
|
206
|
-
pos,
|
|
207
|
-
material,
|
|
208
|
-
transform,
|
|
209
|
-
this._fontManager,
|
|
210
|
-
this._fontIdentifier,
|
|
211
|
-
batchId,
|
|
212
|
-
RTE,
|
|
213
|
-
);
|
|
214
|
-
mesh.renderOrder = this.renderOrder;
|
|
215
|
-
// Re-prepare evicted glyphs through the batch so font-family faces and
|
|
216
|
-
// refcounts stay consistent; request a render once they're rebuilt.
|
|
217
|
-
mesh.setGlyphLifecycleHandlers(
|
|
218
|
-
(text) =>
|
|
219
|
-
this._fontManager.prepareText(
|
|
220
|
-
this._fontIdentifier,
|
|
221
|
-
text,
|
|
222
|
-
this._highQuality,
|
|
223
|
-
this._loadedFaceUrls,
|
|
224
|
-
),
|
|
225
|
-
() => this._needRender?.(),
|
|
226
|
-
);
|
|
294
|
+
const { tex, colorTex } = shared ?? this._sharedTextures();
|
|
295
|
+
if (tex) {
|
|
296
|
+
mesh.setAtlasTexture(tex);
|
|
297
|
+
}
|
|
298
|
+
mesh.setColorAtlasTexture(colorTex);
|
|
227
299
|
|
|
228
|
-
|
|
229
|
-
|
|
300
|
+
mesh.update(this._material);
|
|
301
|
+
|
|
302
|
+
// Replay evaluator state that arrived before the mesh existed.
|
|
303
|
+
const pending = this._pending.get(batchIndex);
|
|
304
|
+
if (pending) {
|
|
305
|
+
this._pending.delete(batchIndex);
|
|
306
|
+
if (pending.colorHex !== undefined) {
|
|
307
|
+
mesh.setColor(new Color(pending.colorHex));
|
|
230
308
|
}
|
|
231
|
-
|
|
309
|
+
if (pending.show !== undefined) {
|
|
310
|
+
mesh._setFeatureShow(pending.show);
|
|
311
|
+
}
|
|
312
|
+
if (pending.height !== undefined) {
|
|
313
|
+
mesh.setHeight(pending.height);
|
|
314
|
+
}
|
|
315
|
+
if (pending.size !== undefined) {
|
|
316
|
+
mesh.setSize(pending.size);
|
|
317
|
+
}
|
|
318
|
+
if (pending.opacity !== undefined) {
|
|
319
|
+
mesh.setOpacity(pending.opacity);
|
|
320
|
+
}
|
|
321
|
+
if (pending.hasDeclutterPriority) {
|
|
322
|
+
mesh.setDeclutterPriority(pending.declutterPriority);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
232
325
|
|
|
233
|
-
|
|
326
|
+
this._meshByIndex[batchIndex] = mesh;
|
|
327
|
+
this.addWithBatchIndex(mesh, batchIndex);
|
|
328
|
+
return mesh;
|
|
329
|
+
}
|
|
234
330
|
|
|
235
|
-
|
|
331
|
+
/** Pending-state slot for a feature whose mesh doesn't exist yet. */
|
|
332
|
+
private _pendingAt(batchIndex: number): PendingFeatureState | undefined {
|
|
333
|
+
const info = this._positions;
|
|
334
|
+
if (!info || batchIndex < 0 || batchIndex >= info.nPositions) {
|
|
335
|
+
return undefined;
|
|
336
|
+
}
|
|
337
|
+
let state = this._pending.get(batchIndex);
|
|
338
|
+
if (!state) {
|
|
339
|
+
state = {};
|
|
340
|
+
this._pending.set(batchIndex, state);
|
|
236
341
|
}
|
|
342
|
+
return state;
|
|
237
343
|
}
|
|
238
344
|
|
|
239
345
|
async _update(m: NavaraTextMesh, needRender?: () => void) {
|
|
@@ -241,31 +347,40 @@ export class BatchedSdfTextMesh
|
|
|
241
347
|
|
|
242
348
|
const material = m.material;
|
|
243
349
|
const text = material.text ?? "";
|
|
350
|
+
// Later lazy creations must see the updated material/transform too.
|
|
351
|
+
this._material = material;
|
|
352
|
+
this._transform = m.transform;
|
|
244
353
|
|
|
245
354
|
const positionInfo = this.extractPositions(m);
|
|
246
355
|
if (positionInfo) {
|
|
247
|
-
const { position, nPositions, positionSize, RTE } = positionInfo;
|
|
248
|
-
|
|
249
356
|
invariant(
|
|
250
|
-
|
|
251
|
-
|
|
357
|
+
this._positions === null ||
|
|
358
|
+
positionInfo.nPositions === this._positions.nPositions,
|
|
359
|
+
"Number of positions in the updated geometry must match the initial geometry",
|
|
252
360
|
);
|
|
361
|
+
this._positions = positionInfo;
|
|
253
362
|
|
|
363
|
+
const { position, positionSize, RTE } = positionInfo;
|
|
254
364
|
const transform = m.transform;
|
|
255
|
-
for (
|
|
256
|
-
const posIdx =
|
|
365
|
+
for (const mesh of this.meshes()) {
|
|
366
|
+
const posIdx = (mesh.userData.batchIndex as number) * positionSize;
|
|
257
367
|
const pos = RTE
|
|
258
368
|
? {
|
|
259
369
|
high: position.high.subarray(posIdx, posIdx + positionSize),
|
|
260
370
|
low: position.low.subarray(posIdx, posIdx + positionSize),
|
|
261
371
|
}
|
|
262
372
|
: position.subarray(posIdx, posIdx + positionSize);
|
|
263
|
-
|
|
373
|
+
mesh.setPosition(pos, RTE, transform);
|
|
264
374
|
}
|
|
265
375
|
// Anchors moved (e.g. terrain height resolution) — re-place labels.
|
|
266
376
|
this._markDeclutterDirty();
|
|
267
377
|
}
|
|
268
378
|
|
|
379
|
+
// A non-empty default text renders without per-feature setText calls, so
|
|
380
|
+
// any lazily-skipped meshes must exist before `_applyUpdate` (no-op for
|
|
381
|
+
// already-created ones).
|
|
382
|
+
if (text) this.initMeshes();
|
|
383
|
+
|
|
269
384
|
const fontIdentifier = m.material.font ?? this._fontIdentifier;
|
|
270
385
|
const needFontUpdate = fontIdentifier !== this._fontIdentifier;
|
|
271
386
|
|
|
@@ -411,7 +526,12 @@ export class BatchedSdfTextMesh
|
|
|
411
526
|
}
|
|
412
527
|
|
|
413
528
|
setTextByBatchIndex(batchIndex: number, text: string) {
|
|
414
|
-
|
|
529
|
+
// An empty text on a not-yet-created mesh changes nothing (a lazily
|
|
530
|
+
// skipped mesh is exactly an invisible empty-text label); only a
|
|
531
|
+
// non-empty text forces the mesh into existence.
|
|
532
|
+
const mesh = text
|
|
533
|
+
? this._ensureMeshAt(batchIndex)
|
|
534
|
+
: this.getMeshByBatchIndex(batchIndex);
|
|
415
535
|
|
|
416
536
|
if (mesh) {
|
|
417
537
|
// If the text hasn't been prepared in the worker yet, schedule async preparation
|
|
@@ -471,57 +591,101 @@ export class BatchedSdfTextMesh
|
|
|
471
591
|
}
|
|
472
592
|
}
|
|
473
593
|
|
|
594
|
+
/**
|
|
595
|
+
* The mesh for a feature's `batchIndex`, or `undefined` if not created yet.
|
|
596
|
+
* Meshes are created lazily and pushed onto `allMeshes` in creation order, so
|
|
597
|
+
* `allMeshes[batchIndex]` (the base-class mapping) does NOT hold here; the
|
|
598
|
+
* batchIndex→mesh mapping lives in `_meshByIndex`. Overriding this one
|
|
599
|
+
* accessor keeps the base-class setters and any external caller (picking,
|
|
600
|
+
* plugins) correct without patching each call site.
|
|
601
|
+
*/
|
|
602
|
+
override getMeshByBatchIndex(batchIndex: number): SDFTextMesh | undefined {
|
|
603
|
+
return this._meshByIndex[batchIndex];
|
|
604
|
+
}
|
|
605
|
+
|
|
474
606
|
setFeatureColorByBatchIndex(batchIndex: number, color: Color) {
|
|
475
|
-
const mesh = this.
|
|
607
|
+
const mesh = this.getMeshByBatchIndex(batchIndex);
|
|
476
608
|
if (mesh) {
|
|
477
609
|
mesh.setColor(color);
|
|
610
|
+
return;
|
|
478
611
|
}
|
|
612
|
+
const pending = this._pendingAt(batchIndex);
|
|
613
|
+
if (pending) pending.colorHex = color.getHex();
|
|
479
614
|
}
|
|
480
615
|
|
|
481
616
|
setFeatureShowByBatchIndex(batchIndex: number, rawVisible: boolean) {
|
|
482
|
-
const mesh = this.
|
|
617
|
+
const mesh = this.getMeshByBatchIndex(batchIndex);
|
|
483
618
|
if (mesh) {
|
|
484
619
|
mesh._setFeatureShow(rawVisible);
|
|
485
620
|
this.markVisibility(mesh);
|
|
486
621
|
this._markDeclutterDirty();
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
// A lazily-created mesh starts hidden (empty text ⇒ `visible=false`), so a
|
|
625
|
+
// mesh-less feature is already effectively `show:false`. Only `show:true`
|
|
626
|
+
// needs buffering; buffering `show:false` would allocate a pending entry
|
|
627
|
+
// per hidden feature — the common MVT case (most features stay hidden and
|
|
628
|
+
// never get a mesh) and exactly the allocation lazy creation avoids. For
|
|
629
|
+
// `show:false` just clear any stale buffered `show`, never allocate.
|
|
630
|
+
if (rawVisible) {
|
|
631
|
+
const pending = this._pendingAt(batchIndex);
|
|
632
|
+
if (pending) pending.show = true;
|
|
633
|
+
} else {
|
|
634
|
+
const pending = this._pending.get(batchIndex);
|
|
635
|
+
if (pending) pending.show = undefined;
|
|
487
636
|
}
|
|
488
637
|
}
|
|
489
638
|
|
|
490
639
|
setFeatureHeightByBatchIndex(batchIndex: number, height: number) {
|
|
491
|
-
const mesh = this.
|
|
640
|
+
const mesh = this.getMeshByBatchIndex(batchIndex);
|
|
492
641
|
if (mesh) {
|
|
493
642
|
mesh.setHeight(height);
|
|
494
643
|
this._markDeclutterDirty();
|
|
644
|
+
return;
|
|
495
645
|
}
|
|
646
|
+
const pending = this._pendingAt(batchIndex);
|
|
647
|
+
if (pending) pending.height = height;
|
|
496
648
|
}
|
|
497
649
|
|
|
498
650
|
setFeatureSizeByBatchIndex(batchIndex: number, size: number) {
|
|
499
|
-
const mesh = this.
|
|
651
|
+
const mesh = this.getMeshByBatchIndex(batchIndex);
|
|
500
652
|
if (mesh) {
|
|
501
653
|
mesh.setSize(size);
|
|
502
654
|
this._markDeclutterDirty();
|
|
655
|
+
return;
|
|
503
656
|
}
|
|
657
|
+
const pending = this._pendingAt(batchIndex);
|
|
658
|
+
if (pending) pending.size = size;
|
|
504
659
|
}
|
|
505
660
|
|
|
506
661
|
setFeatureDeclutterPriorityByBatchIndex(
|
|
507
662
|
batchIndex: number,
|
|
508
663
|
priority: number,
|
|
509
664
|
) {
|
|
510
|
-
const mesh = this.
|
|
665
|
+
const mesh = this.getMeshByBatchIndex(batchIndex);
|
|
511
666
|
if (mesh) {
|
|
512
667
|
mesh.setDeclutterPriority(priority);
|
|
513
668
|
this._markDeclutterDirty();
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
const pending = this._pendingAt(batchIndex);
|
|
672
|
+
if (pending) {
|
|
673
|
+
pending.declutterPriority = priority;
|
|
674
|
+
pending.hasDeclutterPriority = true;
|
|
514
675
|
}
|
|
515
676
|
}
|
|
516
677
|
|
|
517
678
|
setFeatureOpacityByBatchIndex(batchIndex: number, opacity: number) {
|
|
518
|
-
const
|
|
679
|
+
const clampedOpacity = Number.isFinite(opacity)
|
|
680
|
+
? Math.max(0, Math.min(1, opacity))
|
|
681
|
+
: 1.0;
|
|
682
|
+
const mesh = this.getMeshByBatchIndex(batchIndex);
|
|
519
683
|
if (mesh) {
|
|
520
|
-
const clampedOpacity = Number.isFinite(opacity)
|
|
521
|
-
? Math.max(0, Math.min(1, opacity))
|
|
522
|
-
: 1.0;
|
|
523
684
|
mesh.setOpacity(clampedOpacity);
|
|
685
|
+
return;
|
|
524
686
|
}
|
|
687
|
+
const pending = this._pendingAt(batchIndex);
|
|
688
|
+
if (pending) pending.opacity = clampedOpacity;
|
|
525
689
|
}
|
|
526
690
|
|
|
527
691
|
dispose() {
|
package/src/mesh/instanced.ts
CHANGED
package/src/mesh/polygon.ts
CHANGED
|
@@ -282,7 +282,8 @@ export class PolygonMesh extends BatchedFeatureMesh<
|
|
|
282
282
|
this.receiveShadow = !!meshMaterial.receiveShadow;
|
|
283
283
|
|
|
284
284
|
// This mesh is texturized if it has a tile handle (terrain attachment).
|
|
285
|
-
|
|
285
|
+
// `!= null`, not truthiness: the root vector tile's handle is 0.
|
|
286
|
+
const isTexturized = tileHandle != null;
|
|
286
287
|
const material = this.material;
|
|
287
288
|
|
|
288
289
|
material.vertexColors = false;
|
|
@@ -390,9 +391,15 @@ export class PolygonMesh extends BatchedFeatureMesh<
|
|
|
390
391
|
_update(material: PolygonMaterial, active: boolean, isTexturized: boolean) {
|
|
391
392
|
const enhancer = this.getEnhancer();
|
|
392
393
|
|
|
393
|
-
// Update mesh properties (not handled by enhancer)
|
|
394
|
-
|
|
395
|
-
|
|
394
|
+
// Update mesh properties (not handled by enhancer).
|
|
395
|
+
// Draped (clamp-to-ground) polygons bake offscreen only — never the main
|
|
396
|
+
// scene — and the Rust drape resolve, not this LOD `active` flag, picks which
|
|
397
|
+
// baked tile composites per terrain region. Tying bake visibility to `active`
|
|
398
|
+
// makes the drape flicker on LOD swaps; keep a draped mesh bakeable whenever
|
|
399
|
+
// shown so every built tile stays a stable drape source (see
|
|
400
|
+
// PolylineMesh._update).
|
|
401
|
+
const shown = (material.show ?? true) && (material.surfaceShow ?? true);
|
|
402
|
+
this.visible = isTexturized ? shown : shown && active;
|
|
396
403
|
if (this._debugBoundingSphereMesh) {
|
|
397
404
|
this._debugBoundingSphereMesh.visible = this.visible;
|
|
398
405
|
}
|
package/src/mesh/polyline.ts
CHANGED
|
@@ -404,8 +404,16 @@ export class PolylineMesh extends BatchedFeatureMesh<
|
|
|
404
404
|
|
|
405
405
|
const enhancer = this.getEnhancer();
|
|
406
406
|
|
|
407
|
-
// Update mesh properties (not handled by enhancer)
|
|
408
|
-
|
|
407
|
+
// Update mesh properties (not handled by enhancer).
|
|
408
|
+
// Draped polylines render only through the offscreen bake, never the main
|
|
409
|
+
// scene, and the Rust drape resolve — not this LOD `active` flag — decides
|
|
410
|
+
// which baked tile is composited per terrain region. Tying bake visibility
|
|
411
|
+
// to `active` makes the drape flicker on every LOD swap (the tile goes
|
|
412
|
+
// invisible → bakes empty before its replacement is ready). Keep a draped
|
|
413
|
+
// mesh bakeable whenever its material is shown, so every built tile stays a
|
|
414
|
+
// stable drape source — the vector twin of a loaded raster texture.
|
|
415
|
+
const shown = material.show ?? true;
|
|
416
|
+
this.visible = enhancer.states().isTexturized ? shown : shown && active;
|
|
409
417
|
this.castShadow = !!material.castShadow;
|
|
410
418
|
this.receiveShadow = !!material.receiveShadow;
|
|
411
419
|
|
package/src/mesh/sdfText.ts
CHANGED
|
@@ -261,12 +261,17 @@ export class SDFTextMesh
|
|
|
261
261
|
fontUrl: string,
|
|
262
262
|
batchId: number | undefined,
|
|
263
263
|
RTE: boolean,
|
|
264
|
+
// Quality is fixed by the owning batch (flipping it needs a new batch), so
|
|
265
|
+
// it is passed in rather than re-derived from `material.highQuality` — the
|
|
266
|
+
// stored material can carry a newer, mismatched value by the time a mesh is
|
|
267
|
+
// created lazily, which must not diverge from the batch's atlas/shaping.
|
|
268
|
+
highQuality: boolean,
|
|
264
269
|
) {
|
|
265
270
|
super();
|
|
266
271
|
|
|
267
272
|
this._fontManager = fontManager;
|
|
268
273
|
this._fontUrl = fontUrl;
|
|
269
|
-
this._highQuality =
|
|
274
|
+
this._highQuality = highQuality;
|
|
270
275
|
this._maxWidth = material.maxWidth ?? 0;
|
|
271
276
|
this._lineHeight = material.lineHeight ?? 1.0;
|
|
272
277
|
this._textAlign = ALIGN_FACTORS[material.textAlign ?? "center"] ?? 0.5;
|
package/src/solar.test.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { AstroTime, Body, HourAngle, Observer } from "astronomy-engine";
|
|
|
2
2
|
import { describe, expect, it } from "vitest";
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
|
+
dateForSolarTime,
|
|
6
|
+
getSolarTime,
|
|
5
7
|
getSunElevation,
|
|
6
8
|
shiftDateToElevation,
|
|
7
9
|
shiftDateToHourAngle,
|
|
@@ -66,6 +68,37 @@ describe("shiftDateToElevation", () => {
|
|
|
66
68
|
});
|
|
67
69
|
});
|
|
68
70
|
|
|
71
|
+
describe("getSolarTime / dateForSolarTime", () => {
|
|
72
|
+
it("reads ~12 (solar noon) at Tokyo solar noon", () => {
|
|
73
|
+
// Tokyo solar noon ≈ 02:43 UTC on 2024-06-21
|
|
74
|
+
const date = new Date("2024-06-21T02:43:00Z");
|
|
75
|
+
expect(getSolarTime(date, 139.69)).toBeCloseTo(12, 1);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("reads ~0 (solar midnight) at Tokyo solar midnight", () => {
|
|
79
|
+
// Tokyo solar midnight ≈ 14:43 UTC on 2024-06-21
|
|
80
|
+
const date = new Date("2024-06-21T14:43:00Z");
|
|
81
|
+
const t = getSolarTime(date, 139.69);
|
|
82
|
+
expect(Math.min(t, 24 - t)).toBeCloseTo(0, 1);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("round-trips: dateForSolarTime then getSolarTime returns the input", () => {
|
|
86
|
+
for (const hours of [0, 6.3, 12, 18.75]) {
|
|
87
|
+
const date = dateForSolarTime(BASE_DATE, 139.69, hours);
|
|
88
|
+
// Compare on the 24h circle so 0 and 24 count as equal.
|
|
89
|
+
const diff = Math.abs(getSolarTime(date, 139.69) - hours);
|
|
90
|
+
expect(Math.min(diff, 24 - diff)).toBeCloseTo(0, 2);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("puts the sun above the horizon at midday and below at midnight", () => {
|
|
95
|
+
const noon = dateForSolarTime(BASE_DATE, 139.69, 12);
|
|
96
|
+
expect(getSunElevation(noon, 35.68, 139.69)).toBeGreaterThan(0);
|
|
97
|
+
const midnight = dateForSolarTime(BASE_DATE, 139.69, 0);
|
|
98
|
+
expect(getSunElevation(midnight, 35.68, 139.69)).toBeLessThan(0);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
69
102
|
describe("getSunElevation", () => {
|
|
70
103
|
it("returns positive elevation at solar noon", () => {
|
|
71
104
|
// Tokyo solar noon ≈ 02:43 UTC on 2024-06-21
|
package/src/solar.ts
CHANGED
|
@@ -20,6 +20,37 @@ export function getSunElevation(date: Date, lat: number, lng: number): number {
|
|
|
20
20
|
return Horizon(time, observer, eq.ra, eq.dec, "normal").altitude;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Returns the local apparent solar time at `lng` for `date`, in hours in the
|
|
25
|
+
* range [0, 24) where 12 is solar noon. Derived from the sun's hour angle, so
|
|
26
|
+
* it accounts for the equation of time.
|
|
27
|
+
*/
|
|
28
|
+
export function getSolarTime(date: Date, lng: number): number {
|
|
29
|
+
const ha = HourAngle(Body.Sun, new AstroTime(date), new Observer(0, lng, 0));
|
|
30
|
+
return (ha + 12) % 24;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Returns a new Date on the same UTC-anchored solar day such that the local
|
|
35
|
+
* apparent solar time at `lng` equals `hours` (0–24). Inverse of
|
|
36
|
+
* {@link getSolarTime}.
|
|
37
|
+
*/
|
|
38
|
+
export function dateForSolarTime(date: Date, lng: number, hours: number): Date {
|
|
39
|
+
const targetHourAngle = (((hours - 12) % 24) + 24) % 24;
|
|
40
|
+
const dayStart = new AstroTime(
|
|
41
|
+
new Date(
|
|
42
|
+
Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()),
|
|
43
|
+
),
|
|
44
|
+
);
|
|
45
|
+
return SearchHourAngle(
|
|
46
|
+
Body.Sun,
|
|
47
|
+
new Observer(0, lng, 0),
|
|
48
|
+
targetHourAngle,
|
|
49
|
+
dayStart,
|
|
50
|
+
1,
|
|
51
|
+
).time.date;
|
|
52
|
+
}
|
|
53
|
+
|
|
23
54
|
/**
|
|
24
55
|
* Returns a new Date such that the sun's hour angle at `targetLng` matches the
|
|
25
56
|
* hour angle currently at `fromLng`.
|