s3m-three 0.4.25 → 0.4.27
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/browser/assets/{s3mWorker-jc9Oly5K.js → s3mWorker-BfSBNvF5.js} +1 -1
- package/dist/browser/s3m-three.iife.ext-three.js +119 -36
- package/dist/browser/s3m-three.iife.js +119 -36
- package/dist/core/BakeMercator.d.ts +54 -0
- package/dist/core/BakeMercator.d.ts.map +1 -0
- package/dist/core/BakeMercator.js +65 -0
- package/dist/core/BakeMercator.js.map +1 -0
- package/dist/core/CustomAttributes.d.ts +7 -0
- package/dist/core/CustomAttributes.d.ts.map +1 -0
- package/dist/core/CustomAttributes.js +48 -0
- package/dist/core/CustomAttributes.js.map +1 -0
- package/dist/core/Draco.d.ts.map +1 -1
- package/dist/core/Draco.js +149 -2
- package/dist/core/Draco.js.map +1 -1
- package/dist/core/MeshOpt.d.ts.map +1 -1
- package/dist/core/MeshOpt.js +2 -0
- package/dist/core/MeshOpt.js.map +1 -1
- package/dist/core/S3M1Parser.d.ts.map +1 -1
- package/dist/core/S3M1Parser.js +114 -27
- package/dist/core/S3M1Parser.js.map +1 -1
- package/dist/core/S3MBParser.d.ts +5 -2
- package/dist/core/S3MBParser.d.ts.map +1 -1
- package/dist/core/S3MBParser.js +27 -12
- package/dist/core/S3MBParser.js.map +1 -1
- package/dist/core/TileGeometry.d.ts +8 -0
- package/dist/core/TileGeometry.d.ts.map +1 -1
- package/dist/core/TileGeometry.js +45 -7
- package/dist/core/TileGeometry.js.map +1 -1
- package/dist/core/types.d.ts +1 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.d.ts +8 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/render/DatasetManager.d.ts +27 -0
- package/dist/render/DatasetManager.d.ts.map +1 -1
- package/dist/render/DatasetManager.js +18 -4
- package/dist/render/DatasetManager.js.map +1 -1
- package/dist/render/GeoAdapter.d.ts +49 -24
- package/dist/render/GeoAdapter.d.ts.map +1 -1
- package/dist/render/GeoAdapter.js +96 -8
- package/dist/render/GeoAdapter.js.map +1 -1
- package/dist/render/GeometryBuilder.d.ts +17 -0
- package/dist/render/GeometryBuilder.d.ts.map +1 -1
- package/dist/render/GeometryBuilder.js +67 -4
- package/dist/render/GeometryBuilder.js.map +1 -1
- package/dist/render/MotionBlurPass.d.ts +130 -0
- package/dist/render/MotionBlurPass.d.ts.map +1 -0
- package/dist/render/MotionBlurPass.js +394 -0
- package/dist/render/MotionBlurPass.js.map +1 -0
- package/dist/render/S3MViewer.d.ts +99 -2
- package/dist/render/S3MViewer.d.ts.map +1 -1
- package/dist/render/S3MViewer.js +345 -13
- package/dist/render/S3MViewer.js.map +1 -1
- package/dist/render/StyleMaterial.d.ts +5 -0
- package/dist/render/StyleMaterial.d.ts.map +1 -1
- package/dist/render/StyleMaterial.js +21 -0
- package/dist/render/StyleMaterial.js.map +1 -1
- package/dist/render/tilePlacement.d.ts +84 -0
- package/dist/render/tilePlacement.d.ts.map +1 -0
- package/dist/render/tilePlacement.js +71 -0
- package/dist/render/tilePlacement.js.map +1 -0
- package/dist/render/viewer/EdgeDecorator.d.ts +1 -0
- package/dist/render/viewer/EdgeDecorator.d.ts.map +1 -1
- package/dist/render/viewer/EdgeDecorator.js +3 -0
- package/dist/render/viewer/EdgeDecorator.js.map +1 -1
- package/dist/render/viewer/PickService.d.ts +13 -0
- package/dist/render/viewer/PickService.d.ts.map +1 -1
- package/dist/render/viewer/PickService.js +24 -15
- package/dist/render/viewer/PickService.js.map +1 -1
- package/dist/render/viewer/types.d.ts +75 -0
- package/dist/render/viewer/types.d.ts.map +1 -1
- package/dist/render/viewer/types.js.map +1 -1
- package/dist/tileset/S3MTileset.d.ts +62 -1
- package/dist/tileset/S3MTileset.d.ts.map +1 -1
- package/dist/tileset/S3MTileset.js +273 -6
- package/dist/tileset/S3MTileset.js.map +1 -1
- package/dist/worker/S3MWorkerClient.d.ts +1 -1
- package/dist/worker/S3MWorkerClient.d.ts.map +1 -1
- package/dist/worker/S3MWorkerClient.js +2 -2
- package/dist/worker/S3MWorkerClient.js.map +1 -1
- package/dist/worker/s3mWorker.js +844 -635
- package/dist/worker/s3mWorker.js.map +1 -1
- package/docs/API.md +36 -5
- package/docs/FORMAT-NOTES.md +10 -0
- package/package.json +1 -1
- package/src/core/BakeMercator.ts +96 -0
- package/src/core/CustomAttributes.ts +53 -0
- package/src/core/Draco.ts +168 -6
- package/src/core/MeshOpt.ts +2 -0
- package/src/core/S3M1Parser.ts +102 -22
- package/src/core/S3MBParser.ts +33 -14
- package/src/core/TileGeometry.ts +60 -7
- package/src/core/types.ts +1 -0
- package/src/index.ts +15 -3
- package/src/render/DatasetManager.ts +43 -5
- package/src/render/GeoAdapter.ts +139 -11
- package/src/render/GeometryBuilder.ts +90 -4
- package/src/render/MotionBlurPass.ts +453 -0
- package/src/render/S3MViewer.ts +384 -17
- package/src/render/StyleMaterial.ts +26 -0
- package/src/render/tilePlacement.ts +121 -0
- package/src/render/viewer/EdgeDecorator.ts +3 -0
- package/src/render/viewer/PickService.ts +44 -16
- package/src/render/viewer/types.ts +73 -0
- package/src/tileset/S3MTileset.ts +300 -6
- package/src/vendor/draco_decoder.module.d.ts +15 -1
- package/src/worker/S3MWorkerClient.ts +7 -2
- package/src/worker/s3mWorker.ts +6 -2
|
@@ -32,6 +32,7 @@ import type {
|
|
|
32
32
|
} from '../core/S3MBlockParser.js';
|
|
33
33
|
import type { Anchor, ParsedTile, Stats, StyleUniforms } from '../core/types.js';
|
|
34
34
|
import type { TileGeometryData } from '../core/TileGeometry.js';
|
|
35
|
+
import { AUTO_BAKE_RADIUS_THRESHOLD } from '../core/BakeMercator.js';
|
|
35
36
|
import type { TextureCreateOptions } from '../render/TextureFactory.js';
|
|
36
37
|
import type { Group, Texture } from 'three';
|
|
37
38
|
|
|
@@ -195,6 +196,10 @@ export function parseConfigText(text: string): Record<string, unknown> {
|
|
|
195
196
|
|
|
196
197
|
return {
|
|
197
198
|
fileType: tagVal('FileType') ?? undefined,
|
|
199
|
+
// 地形/影像缓存的判别字段(见 classifyDatasetKind):FileExtentName=Terrainz 是地形,
|
|
200
|
+
// <sml:Type>Image</sml:Type> 是影像(影像的 FileExtentName 是 jpg_png 这种扩展名样式,不可靠)
|
|
201
|
+
fileExtentName: tagVal('FileExtentName') ?? undefined,
|
|
202
|
+
cacheType: tagVal('Type') ?? undefined,
|
|
198
203
|
position: { x: posX, y: posY, z: posZ, units: 'Degree' },
|
|
199
204
|
geoBounds: { left, right, top, bottom },
|
|
200
205
|
heightRange: Number.isFinite(minH) || Number.isFinite(maxH) ? { min: minH, max: maxH } : undefined,
|
|
@@ -205,6 +210,53 @@ export function parseConfigText(text: string): Record<string, unknown> {
|
|
|
205
210
|
return JSON.parse(trimmed) as Record<string, unknown>;
|
|
206
211
|
}
|
|
207
212
|
|
|
213
|
+
/** config 能认出的数据集类别(判别规则见 classifyDatasetKind) */
|
|
214
|
+
export type DatasetKind = 'model' | 'terrain' | 'imagery';
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* 按 config 的类型标记识别数据集类别。
|
|
218
|
+
*
|
|
219
|
+
* 同一个 `…/datas/<名>/config` URL 底下可能是模型、地形、影像三种完全不同的缓存
|
|
220
|
+
* (FORMAT-NOTES 第二章)。地形/影像是按 Level0 金字塔寻址的栅格缓存,**没有瓦片清单**,
|
|
221
|
+
* 当模型加载只会一路走到「配置中缺少 root tile」,把「类型不支持」伪装成「配置缺字段」
|
|
222
|
+
* (issue #7:混合服务第一个数据集 beijingdem 是 Terrainz 地形缓存)。
|
|
223
|
+
*
|
|
224
|
+
* 判别字段取自实测(supermapol 3D-OlympicGreen_Plan,2026-10-30):
|
|
225
|
+
* 地形 XML: FileExtentName = Terrainz
|
|
226
|
+
* 影像 XML: FileExtentName = jpg_png(扩展名样式,不可靠)+ Type = Image
|
|
227
|
+
* 模型 XML: FileType = OSGBFile / OSGBCacheFile,无 FileExtentName / Type
|
|
228
|
+
*
|
|
229
|
+
* 保守原则:只有认出确凿的非模型标记才判 terrain / imagery,其余(含 JSON 配置、
|
|
230
|
+
* 无标记的 XML)一律 'model' —— 保证「认不出」时行为与旧版一致(不变式 ①)。
|
|
231
|
+
*/
|
|
232
|
+
export function classifyDatasetKind(config: Record<string, unknown> | null | undefined): DatasetKind {
|
|
233
|
+
if (!config || typeof config !== 'object') return 'model';
|
|
234
|
+
if (String(config.fileExtentName ?? '').trim().toLowerCase() === 'terrainz') return 'terrain';
|
|
235
|
+
if (String(config.cacheType ?? '').trim().toLowerCase() === 'image') return 'imagery';
|
|
236
|
+
return 'model';
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** 拼一句能区分 config 形态的上下文(FileExtentName / Type / fileType / dataType),供报错使用 */
|
|
240
|
+
function configTypeHint(config: Record<string, unknown>): string {
|
|
241
|
+
const parts: string[] = [];
|
|
242
|
+
if (config.fileExtentName) parts.push(`FileExtentName=${config.fileExtentName}`);
|
|
243
|
+
if (config.cacheType) parts.push(`Type=${config.cacheType}`);
|
|
244
|
+
if (config.fileType) parts.push(`fileType=${config.fileType}`);
|
|
245
|
+
if (config.dataType) parts.push(`dataType=${config.dataType}`);
|
|
246
|
+
return parts.length ? parts.join(', ') : '无类型标记';
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** 从 configUrl 里抠出数据集名(…/datas/<名>/config);抠不出就原样返回,报错时能对上服务里的数据集 */
|
|
250
|
+
function datasetNameOf(configUrl: string): string {
|
|
251
|
+
const m = /\/datas\/([^/?#]+)/i.exec(configUrl);
|
|
252
|
+
if (!m) return configUrl;
|
|
253
|
+
try {
|
|
254
|
+
return decodeURIComponent(m[1]!);
|
|
255
|
+
} catch {
|
|
256
|
+
return m[1]!;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
208
260
|
export interface TileNode {
|
|
209
261
|
url: string;
|
|
210
262
|
tile: ParsedTile;
|
|
@@ -253,10 +305,14 @@ export type BuildTileFn = (
|
|
|
253
305
|
colorPipeline?: 'linear' | 'direct';
|
|
254
306
|
/** 纹理创建参数(透传给 buildTileObjects → createTexture) */
|
|
255
307
|
textureOptions?: TextureCreateOptions;
|
|
308
|
+
bakeMercator?: boolean;
|
|
309
|
+
tileAnchor?: { lon: number; lat: number; height?: number };
|
|
310
|
+
tileCenter?: { x: number; y: number; z?: number };
|
|
256
311
|
}
|
|
257
312
|
) => Promise<{ group: Group; stats: Partial<Stats> }>;
|
|
258
313
|
|
|
259
314
|
export interface S3MTilesetOptions {
|
|
315
|
+
bakeMercator?: boolean;
|
|
260
316
|
configUrl: string;
|
|
261
317
|
maxTiles?: number;
|
|
262
318
|
maxConcurrent?: number;
|
|
@@ -337,6 +393,18 @@ export interface S3MTilesetOptions {
|
|
|
337
393
|
* 'silent' —— 只记录到 `stats.degradations`,由 UI 自己决定怎么提示。
|
|
338
394
|
*/
|
|
339
395
|
unsupported?: UnsupportedPolicy;
|
|
396
|
+
/**
|
|
397
|
+
* LOD 细化过渡策略:
|
|
398
|
+
* - 'binary'(默认):整节点二值切换。所有子瓦片全部就绪或失败后,父瓦片整块隐藏、子瓦片整块接替。
|
|
399
|
+
* - 'incremental':增量细化(primary / fallback 双列表 + LEQUAL 深度推移)。
|
|
400
|
+
* 子瓦片就绪一个显示一个,未就绪区域由父级(fallback)兜底填充,消除整块跳变与慢 child 阻塞。
|
|
401
|
+
*/
|
|
402
|
+
lodTransition?: 'binary' | 'incremental';
|
|
403
|
+
/**
|
|
404
|
+
* 增量细化模式下 fallback 兜底瓦片的材质深度偏移系数。
|
|
405
|
+
* 默认 { factor: 1, units: 2 },通过向远处推移深度配合 gl.LEQUAL 消除共面 z-fighting。
|
|
406
|
+
*/
|
|
407
|
+
fallbackOffset?: { factor?: number; units?: number };
|
|
340
408
|
}
|
|
341
409
|
|
|
342
410
|
/** 估算单个纹理占用的显存字节数 */
|
|
@@ -454,6 +522,10 @@ export class S3MTileset {
|
|
|
454
522
|
*/
|
|
455
523
|
maxMemory: number;
|
|
456
524
|
lodRangeScale: number;
|
|
525
|
+
lodTransition: 'binary' | 'incremental';
|
|
526
|
+
fallbackOffset: { factor: number; units: number };
|
|
527
|
+
readonly primaryNodes = new Set<string>();
|
|
528
|
+
readonly fallbackNodes = new Set<string>();
|
|
457
529
|
|
|
458
530
|
/** 数据集配置声明的高度范围(米),若配置未声明则返回 null */
|
|
459
531
|
get heightRange(): { min: number; max: number } | null {
|
|
@@ -484,8 +556,16 @@ export class S3MTileset {
|
|
|
484
556
|
/** 请求量的硬上限(软上限 maxTiles 的超额余量),见构造函数注释 */
|
|
485
557
|
hardCap = 96;
|
|
486
558
|
/** 本数据集是 S3M 1.x(SuperMapCache,.s3m):子瓦地址规则与 3.x 不同 */
|
|
487
|
-
|
|
559
|
+
isS3MV1 = false;
|
|
488
560
|
private _buildTile: BuildTileFn | null;
|
|
561
|
+
private _bakeMercator?: boolean;
|
|
562
|
+
rootRadius?: number;
|
|
563
|
+
|
|
564
|
+
shouldBakeMercator(radius?: number): boolean {
|
|
565
|
+
if (this._bakeMercator !== undefined) return this._bakeMercator;
|
|
566
|
+
const r = radius ?? this.rootRadius ?? 0;
|
|
567
|
+
return r > AUTO_BAKE_RADIUS_THRESHOLD;
|
|
568
|
+
}
|
|
489
569
|
private readonly _polygonOffset: { factor: number; units: number } | undefined;
|
|
490
570
|
private _materialQuality: 'pbr' | 'lambert' | undefined;
|
|
491
571
|
private _colorPipeline: 'linear' | 'direct' | undefined;
|
|
@@ -499,6 +579,11 @@ export class S3MTileset {
|
|
|
499
579
|
constructor(options: S3MTilesetOptions) {
|
|
500
580
|
this.configUrl = options.configUrl;
|
|
501
581
|
this.lodRangeScale = options.lodRangeScale ?? 3.0;
|
|
582
|
+
this.lodTransition = options.lodTransition ?? 'binary';
|
|
583
|
+
this.fallbackOffset = {
|
|
584
|
+
factor: options.fallbackOffset?.factor ?? 1,
|
|
585
|
+
units: options.fallbackOffset?.units ?? 2,
|
|
586
|
+
};
|
|
502
587
|
this._onTile = options.onTile ?? (() => {});
|
|
503
588
|
this._onTileRemoved = options.onTileRemoved ?? (() => {});
|
|
504
589
|
this._onError = options.onError ?? ((e) => console.warn(e));
|
|
@@ -509,6 +594,7 @@ export class S3MTileset {
|
|
|
509
594
|
|
|
510
595
|
// 渲染层可注入:Node 环境下不需要 three,也能跑 URL / 解析链路
|
|
511
596
|
this._buildTile = options.buildTile ?? null;
|
|
597
|
+
this._bakeMercator = options.bakeMercator;
|
|
512
598
|
this._polygonOffset = options.polygonOffset;
|
|
513
599
|
this._materialQuality = options.materialQuality;
|
|
514
600
|
this._colorPipeline = options.colorPipeline;
|
|
@@ -539,11 +625,31 @@ export class S3MTileset {
|
|
|
539
625
|
const text = await res.text();
|
|
540
626
|
const config = parseConfigText(text);
|
|
541
627
|
this.config = config;
|
|
628
|
+
|
|
629
|
+
// 地形/影像缓存(金字塔栅格,无瓦片清单)不能当模型加载:走到下面「缺少 root tile」
|
|
630
|
+
// 那步只会得到一句把「类型不支持」伪装成「配置缺字段」的报错(issue #7),
|
|
631
|
+
// 所以在锚点解析之前先按类型标记拒掉,把病因说清楚。
|
|
632
|
+
const kind = classifyDatasetKind(config);
|
|
633
|
+
if (kind !== 'model') {
|
|
634
|
+
const err = new Error(
|
|
635
|
+
`数据集 ${datasetNameOf(this.configUrl)} 是${kind === 'terrain' ? '地形' : '影像'}缓存(${configTypeHint(config)}),本层仅支持模型类 S3M 数据集`
|
|
636
|
+
) as Error & { datasetKind: DatasetKind };
|
|
637
|
+
// 程序化判别用:调用方(如数据集列表 UI)可以据此标注/跳过该数据集
|
|
638
|
+
err.datasetKind = kind;
|
|
639
|
+
throw err;
|
|
640
|
+
}
|
|
641
|
+
|
|
542
642
|
this.anchor = this._resolveAnchor(config);
|
|
543
643
|
|
|
544
644
|
// iServer 的 realspace 配置用 rootTiles(可多棵树),桌面导出的用 tiles
|
|
545
645
|
const roots = (config.rootTiles ?? config.tiles ?? []) as Array<{ url: string }>;
|
|
546
|
-
if (!roots.length)
|
|
646
|
+
if (!roots.length) {
|
|
647
|
+
// 带上 config 的类型标记:地形/影像缓存(本应在前一步被拒)与「configUrl 误指向
|
|
648
|
+
// …/datas 清单」「桌面导出漏瓦片清单」是三种不同的病因,文案必须能区分
|
|
649
|
+
throw new Error(
|
|
650
|
+
`配置中缺少 root tile(${configTypeHint(config)})——若是地形/影像数据集,或 configUrl 误指向 …/datas 数据集清单,本层仅支持模型类 S3M 数据集`
|
|
651
|
+
);
|
|
652
|
+
}
|
|
547
653
|
|
|
548
654
|
this.rootUrls.length = 0;
|
|
549
655
|
for (const r of roots) {
|
|
@@ -832,6 +938,9 @@ export class S3MTileset {
|
|
|
832
938
|
if (refined !== allowRefined) continue;
|
|
833
939
|
// 正在显示且没有可见子瓦接手 → 踢了就是洞
|
|
834
940
|
if (!allowUncovered && node.group.visible) {
|
|
941
|
+
if (this.lodTransition === 'incremental') {
|
|
942
|
+
continue;
|
|
943
|
+
}
|
|
835
944
|
const covered = childUrls.some((u) => {
|
|
836
945
|
const c = this.nodes.get(u);
|
|
837
946
|
return !!c && c.group.visible;
|
|
@@ -941,7 +1050,7 @@ export class S3MTileset {
|
|
|
941
1050
|
}
|
|
942
1051
|
if (this._workerClient) {
|
|
943
1052
|
try {
|
|
944
|
-
return await this._workerClient.parseTile(buffer, true);
|
|
1053
|
+
return await this._workerClient.parseTile(buffer, true, true, this.config?.vertexAttributeDescript);
|
|
945
1054
|
} catch (e) {
|
|
946
1055
|
if (e instanceof WorkerDeadError) {
|
|
947
1056
|
// 脚本加载失败 / 崩溃属于环境问题,永久退回主线程;瓦片本身的问题照常抛
|
|
@@ -951,7 +1060,7 @@ export class S3MTileset {
|
|
|
951
1060
|
}
|
|
952
1061
|
}
|
|
953
1062
|
}
|
|
954
|
-
return { tile: await parseS3MB(buffer) };
|
|
1063
|
+
return { tile: await parseS3MB(buffer, { vertexAttributeDescript: this.config?.vertexAttributeDescript }) };
|
|
955
1064
|
}
|
|
956
1065
|
|
|
957
1066
|
/**
|
|
@@ -1279,6 +1388,11 @@ export class S3MTileset {
|
|
|
1279
1388
|
// 解析成功但能力不完整的(目前只有 3DGS),在花力气建几何之前先决定处置
|
|
1280
1389
|
this._noteDegradations(tile);
|
|
1281
1390
|
|
|
1391
|
+
const lodForBake = this._pickLod(tile);
|
|
1392
|
+
if (this.rootRadius === undefined && lodForBake?.boundingSphere?.radius) {
|
|
1393
|
+
this.rootRadius = lodForBake.boundingSphere.radius;
|
|
1394
|
+
}
|
|
1395
|
+
const shouldBake = this.shouldBakeMercator(lodForBake?.boundingSphere?.radius);
|
|
1282
1396
|
const build = await this._builder();
|
|
1283
1397
|
const buildRes = await build(tile, {
|
|
1284
1398
|
textureCache: this.textureCache,
|
|
@@ -1288,6 +1402,9 @@ export class S3MTileset {
|
|
|
1288
1402
|
materialQuality: this._materialQuality,
|
|
1289
1403
|
colorPipeline: this._colorPipeline,
|
|
1290
1404
|
textureOptions: this._textureOptions,
|
|
1405
|
+
bakeMercator: shouldBake,
|
|
1406
|
+
tileAnchor: this.anchor,
|
|
1407
|
+
tileCenter: lodForBake?.boundingSphere?.center,
|
|
1291
1408
|
});
|
|
1292
1409
|
const { group, stats } = buildRes;
|
|
1293
1410
|
const texKeys = (buildRes as any).textureKeys ?? (group.userData?.textureKeys as string[] | undefined) ?? Object.keys(tile.textures ?? {});
|
|
@@ -1499,10 +1616,21 @@ export class S3MTileset {
|
|
|
1499
1616
|
if (!p || !this.nodes.has(p)) roots.add(url);
|
|
1500
1617
|
}
|
|
1501
1618
|
|
|
1502
|
-
|
|
1503
|
-
if (this.
|
|
1619
|
+
if (this.lodTransition === 'incremental') {
|
|
1620
|
+
if (this._updateVisibilityIncremental(roots, visited)) {
|
|
1504
1621
|
hasChanges = true;
|
|
1505
1622
|
}
|
|
1623
|
+
} else {
|
|
1624
|
+
for (const rootUrl of roots) {
|
|
1625
|
+
if (this._updateNodeVisibility(rootUrl, true, visited)) {
|
|
1626
|
+
hasChanges = true;
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
this.primaryNodes.clear();
|
|
1630
|
+
for (const [url, node] of this.nodes) {
|
|
1631
|
+
if (node.group.visible) this.primaryNodes.add(url);
|
|
1632
|
+
}
|
|
1633
|
+
this.fallbackNodes.clear();
|
|
1506
1634
|
}
|
|
1507
1635
|
|
|
1508
1636
|
// 防御:若有任何未被 root 树访问到的游离瓦片,全部隐藏
|
|
@@ -1512,6 +1640,9 @@ export class S3MTileset {
|
|
|
1512
1640
|
node.group.visible = false;
|
|
1513
1641
|
hasChanges = true;
|
|
1514
1642
|
}
|
|
1643
|
+
if (this.lodTransition === 'incremental') {
|
|
1644
|
+
this._restoreNodePolygonOffset(node);
|
|
1645
|
+
}
|
|
1515
1646
|
}
|
|
1516
1647
|
}
|
|
1517
1648
|
|
|
@@ -1603,6 +1734,169 @@ export class S3MTileset {
|
|
|
1603
1734
|
return changed;
|
|
1604
1735
|
}
|
|
1605
1736
|
|
|
1737
|
+
|
|
1738
|
+
getLodTransition(): 'binary' | 'incremental' {
|
|
1739
|
+
return this.lodTransition;
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
setLodTransition(mode: 'binary' | 'incremental'): void {
|
|
1743
|
+
if (this.lodTransition === mode) return;
|
|
1744
|
+
this.lodTransition = mode;
|
|
1745
|
+
if (mode === 'binary') {
|
|
1746
|
+
for (const node of this.nodes.values()) {
|
|
1747
|
+
this._restoreNodePolygonOffset(node);
|
|
1748
|
+
}
|
|
1749
|
+
this.fallbackNodes.clear();
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
private _applyNodeFallbackPolygonOffset(node: TileNode, depth: number): boolean {
|
|
1754
|
+
const factor = this.fallbackOffset.factor;
|
|
1755
|
+
const units = Math.max(2, (10 - Math.min(depth, 8)) * this.fallbackOffset.units);
|
|
1756
|
+
let changed = false;
|
|
1757
|
+
|
|
1758
|
+
node.group.traverse((obj) => {
|
|
1759
|
+
const target = obj as { isMesh?: boolean; isPoints?: boolean; isLine?: boolean; material?: any };
|
|
1760
|
+
if ((target.isMesh || target.isPoints || target.isLine) && target.material) {
|
|
1761
|
+
const materials = Array.isArray(target.material) ? target.material : [target.material];
|
|
1762
|
+
for (const mat of materials) {
|
|
1763
|
+
if (!mat) continue;
|
|
1764
|
+
if (!mat.userData) mat.userData = {};
|
|
1765
|
+
if (!mat.userData._basePolygonOffset) {
|
|
1766
|
+
mat.userData._basePolygonOffset = {
|
|
1767
|
+
polygonOffset: mat.polygonOffset === true,
|
|
1768
|
+
factor: mat.polygonOffsetFactor ?? 0,
|
|
1769
|
+
units: mat.polygonOffsetUnits ?? 0,
|
|
1770
|
+
};
|
|
1771
|
+
}
|
|
1772
|
+
const base = mat.userData._basePolygonOffset;
|
|
1773
|
+
const targetFactor = base.factor + factor;
|
|
1774
|
+
const targetUnits = base.units + units;
|
|
1775
|
+
if (
|
|
1776
|
+
mat.polygonOffset !== true ||
|
|
1777
|
+
mat.polygonOffsetFactor !== targetFactor ||
|
|
1778
|
+
mat.polygonOffsetUnits !== targetUnits
|
|
1779
|
+
) {
|
|
1780
|
+
mat.polygonOffset = true;
|
|
1781
|
+
mat.polygonOffsetFactor = targetFactor;
|
|
1782
|
+
mat.polygonOffsetUnits = targetUnits;
|
|
1783
|
+
changed = true;
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
});
|
|
1788
|
+
return changed;
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
private _restoreNodePolygonOffset(node: TileNode): boolean {
|
|
1792
|
+
let changed = false;
|
|
1793
|
+
node.group.traverse((obj) => {
|
|
1794
|
+
const target = obj as { isMesh?: boolean; isPoints?: boolean; isLine?: boolean; material?: any };
|
|
1795
|
+
if ((target.isMesh || target.isPoints || target.isLine) && target.material) {
|
|
1796
|
+
const materials = Array.isArray(target.material) ? target.material : [target.material];
|
|
1797
|
+
for (const mat of materials) {
|
|
1798
|
+
if (mat?.userData?._basePolygonOffset) {
|
|
1799
|
+
const base = mat.userData._basePolygonOffset;
|
|
1800
|
+
if (
|
|
1801
|
+
mat.polygonOffset !== base.polygonOffset ||
|
|
1802
|
+
mat.polygonOffsetFactor !== base.factor ||
|
|
1803
|
+
mat.polygonOffsetUnits !== base.units
|
|
1804
|
+
) {
|
|
1805
|
+
mat.polygonOffset = base.polygonOffset;
|
|
1806
|
+
mat.polygonOffsetFactor = base.factor;
|
|
1807
|
+
mat.polygonOffsetUnits = base.units;
|
|
1808
|
+
changed = true;
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
});
|
|
1814
|
+
return changed;
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
private _updateVisibilityIncremental(roots: Iterable<string>, visited: Set<string>): boolean {
|
|
1818
|
+
const nextPrimary = new Set<string>();
|
|
1819
|
+
const nextFallback = new Set<string>();
|
|
1820
|
+
const nodeDepths = new Map<string, number>();
|
|
1821
|
+
|
|
1822
|
+
const traverse = (url: string, depth: number) => {
|
|
1823
|
+
if (visited.has(url)) return;
|
|
1824
|
+
visited.add(url);
|
|
1825
|
+
nodeDepths.set(url, depth);
|
|
1826
|
+
|
|
1827
|
+
const node = this.nodes.get(url);
|
|
1828
|
+
if (!node) return;
|
|
1829
|
+
|
|
1830
|
+
const childUrls = node.childUrls ?? (node.childUrl ? [node.childUrl] : []);
|
|
1831
|
+
const hasChildren = childUrls.length > 0;
|
|
1832
|
+
const readyChildren = childUrls.filter((cUrl) => this.nodes.has(cUrl));
|
|
1833
|
+
const missingChildren = childUrls.filter(
|
|
1834
|
+
(cUrl) => !this.nodes.has(cUrl) && !this.failed.has(cUrl)
|
|
1835
|
+
);
|
|
1836
|
+
|
|
1837
|
+
const canRefine = !!node.needRefine && hasChildren && readyChildren.length > 0;
|
|
1838
|
+
|
|
1839
|
+
if (canRefine) {
|
|
1840
|
+
if (missingChildren.length > 0) {
|
|
1841
|
+
nextFallback.add(url);
|
|
1842
|
+
}
|
|
1843
|
+
for (const cUrl of readyChildren) {
|
|
1844
|
+
traverse(cUrl, depth + 1);
|
|
1845
|
+
}
|
|
1846
|
+
} else {
|
|
1847
|
+
nextPrimary.add(url);
|
|
1848
|
+
}
|
|
1849
|
+
};
|
|
1850
|
+
|
|
1851
|
+
for (const rootUrl of roots) {
|
|
1852
|
+
traverse(rootUrl, 0);
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
let changed = false;
|
|
1856
|
+
|
|
1857
|
+
for (const [url, node] of this.nodes) {
|
|
1858
|
+
if (!visited.has(url)) continue;
|
|
1859
|
+
|
|
1860
|
+
if (nextPrimary.has(url)) {
|
|
1861
|
+
if (!node.group.visible) {
|
|
1862
|
+
node.group.visible = true;
|
|
1863
|
+
changed = true;
|
|
1864
|
+
}
|
|
1865
|
+
const depth = nodeDepths.get(url) ?? 0;
|
|
1866
|
+
const targetOrder = 100 + depth;
|
|
1867
|
+
if (node.group.renderOrder !== targetOrder) {
|
|
1868
|
+
node.group.renderOrder = targetOrder;
|
|
1869
|
+
changed = true;
|
|
1870
|
+
}
|
|
1871
|
+
if (this._restoreNodePolygonOffset(node)) changed = true;
|
|
1872
|
+
} else if (nextFallback.has(url)) {
|
|
1873
|
+
if (!node.group.visible) {
|
|
1874
|
+
node.group.visible = true;
|
|
1875
|
+
changed = true;
|
|
1876
|
+
}
|
|
1877
|
+
const depth = nodeDepths.get(url) ?? 0;
|
|
1878
|
+
const targetOrder = depth;
|
|
1879
|
+
if (node.group.renderOrder !== targetOrder) {
|
|
1880
|
+
node.group.renderOrder = targetOrder;
|
|
1881
|
+
changed = true;
|
|
1882
|
+
}
|
|
1883
|
+
if (this._applyNodeFallbackPolygonOffset(node, depth)) changed = true;
|
|
1884
|
+
} else {
|
|
1885
|
+
if (node.group.visible) {
|
|
1886
|
+
node.group.visible = false;
|
|
1887
|
+
changed = true;
|
|
1888
|
+
}
|
|
1889
|
+
if (this._restoreNodePolygonOffset(node)) changed = true;
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
this.primaryNodes.clear();
|
|
1894
|
+
for (const u of nextPrimary) this.primaryNodes.add(u);
|
|
1895
|
+
this.fallbackNodes.clear();
|
|
1896
|
+
for (const u of nextFallback) this.fallbackNodes.add(u);
|
|
1897
|
+
|
|
1898
|
+
return changed;
|
|
1899
|
+
}
|
|
1606
1900
|
dispose(): void {
|
|
1607
1901
|
this._workerClient?.dispose();
|
|
1608
1902
|
for (const node of this.nodes.values()) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
2
|
* Draco 几何解码器(google/draco,wasm 内联)的类型声明
|
|
3
3
|
*
|
|
4
4
|
* 只声明 S3M 用到的部分。两个坑:
|
|
@@ -41,6 +41,11 @@ export interface DracoPointCloud {
|
|
|
41
41
|
__destroy__(): void;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
|
|
45
|
+
/** draco::Mesh - inherits from PointCloud, additionally provides face info (used for TRIANGULAR_MESH branch) */
|
|
46
|
+
export interface DracoMesh extends DracoPointCloud {
|
|
47
|
+
num_faces(): number;
|
|
48
|
+
}
|
|
44
49
|
export interface DracoStatus {
|
|
45
50
|
ok(): boolean;
|
|
46
51
|
error_msg(): string;
|
|
@@ -54,6 +59,14 @@ export interface DracoDecoder {
|
|
|
54
59
|
byteLength: number,
|
|
55
60
|
out: DracoPointCloud
|
|
56
61
|
): DracoStatus;
|
|
62
|
+
/** 解码三角网格(TRIANGULAR_MESH 分支) */
|
|
63
|
+
DecodeArrayToMesh(
|
|
64
|
+
data: Uint8Array,
|
|
65
|
+
byteLength: number,
|
|
66
|
+
out: DracoMesh
|
|
67
|
+
): DracoStatus;
|
|
68
|
+
/** 从已解码的 Mesh 取第 faceIndex 个面的 3 个顶点索引到 DracoInt32Array */
|
|
69
|
+
GetFaceFromMesh(mesh: DracoMesh, faceIndex: number, out: DracoInt32Array): boolean;
|
|
57
70
|
/** 按属性在几何里的序号取 */
|
|
58
71
|
GetAttribute(pc: DracoPointCloud, index: number): DracoPointAttribute;
|
|
59
72
|
/** 按 unique_id 取;不存在时返回 null */
|
|
@@ -113,6 +126,7 @@ export interface DracoUInt32Array {
|
|
|
113
126
|
export interface DracoModule {
|
|
114
127
|
Decoder: new () => DracoDecoder;
|
|
115
128
|
PointCloud: new () => DracoPointCloud;
|
|
129
|
+
Mesh: new () => DracoMesh;
|
|
116
130
|
DracoFloat32Array: new () => DracoFloat32Array;
|
|
117
131
|
DracoInt8Array: new () => DracoInt8Array;
|
|
118
132
|
DracoUInt8Array: new () => DracoUInt8Array;
|
|
@@ -88,12 +88,17 @@ export class S3MWorkerClient {
|
|
|
88
88
|
* 解析一块瓦片。buffer 会被 structured clone(不 transfer),主线程保留原 buffer 以便崩溃后降级重解。
|
|
89
89
|
* assemble = true 时顺带在 Worker 里做纯数据几何组装,返回 { tile, geometry } 一次带回。
|
|
90
90
|
*/
|
|
91
|
-
parseTile(
|
|
91
|
+
parseTile(
|
|
92
|
+
buffer: ArrayBuffer,
|
|
93
|
+
assemble = false,
|
|
94
|
+
batching = true,
|
|
95
|
+
vertexAttributeDescript?: unknown
|
|
96
|
+
): Promise<{ tile: ParsedTile; geometry?: TileGeometryData }> {
|
|
92
97
|
if (this.dead) return Promise.reject(new WorkerDeadError('s3m 解析 Worker 已终止'));
|
|
93
98
|
const id = ++this._seq;
|
|
94
99
|
return new Promise<{ tile: ParsedTile; geometry?: TileGeometryData }>((resolve, reject) => {
|
|
95
100
|
this._pending.set(id, { resolve: resolve as (v: unknown) => void, reject });
|
|
96
|
-
this._worker.postMessage({ type: 'parse', id, buffer, assemble, batching });
|
|
101
|
+
this._worker.postMessage({ type: 'parse', id, buffer, assemble, batching, vertexAttributeDescript });
|
|
97
102
|
});
|
|
98
103
|
}
|
|
99
104
|
|
package/src/worker/s3mWorker.ts
CHANGED
|
@@ -28,6 +28,7 @@ type ParseRequest = {
|
|
|
28
28
|
/** 解析后顺带做纯数据几何组装(解交错 / 法线 / 合批),一次往返带回 */
|
|
29
29
|
assemble?: boolean;
|
|
30
30
|
batching?: boolean;
|
|
31
|
+
vertexAttributeDescript?: unknown;
|
|
31
32
|
};
|
|
32
33
|
type TexRequest = { type: 'tex'; id: number; tex: S3MTexture; compressed?: boolean; maxSize?: number };
|
|
33
34
|
type Request = ParseRequest | TexRequest;
|
|
@@ -52,10 +53,13 @@ ctx.onmessage = async (e: MessageEvent<Request>) => {
|
|
|
52
53
|
const { id } = msg;
|
|
53
54
|
try {
|
|
54
55
|
if (msg.type === 'parse') {
|
|
55
|
-
const tile = await parseS3MB(msg.buffer);
|
|
56
|
+
const tile = await parseS3MB(msg.buffer, { vertexAttributeDescript: msg.vertexAttributeDescript });
|
|
56
57
|
if (msg.assemble) {
|
|
57
58
|
// 解析 + 组装一起做,避免 tile 回主线程后再整块克隆回来一次
|
|
58
|
-
const geometry = assembleTileGeometry(tile, {
|
|
59
|
+
const geometry = assembleTileGeometry(tile, {
|
|
60
|
+
batching: msg.batching,
|
|
61
|
+
vertexAttributeDescript: msg.vertexAttributeDescript,
|
|
62
|
+
});
|
|
59
63
|
ctx.postMessage(
|
|
60
64
|
{ id, ok: true, tile, geometry } satisfies ParseResponse,
|
|
61
65
|
[...collectTransferables(tile), ...collectTransferables(geometry)]
|