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.
Files changed (109) hide show
  1. package/dist/browser/assets/{s3mWorker-jc9Oly5K.js → s3mWorker-BfSBNvF5.js} +1 -1
  2. package/dist/browser/s3m-three.iife.ext-three.js +119 -36
  3. package/dist/browser/s3m-three.iife.js +119 -36
  4. package/dist/core/BakeMercator.d.ts +54 -0
  5. package/dist/core/BakeMercator.d.ts.map +1 -0
  6. package/dist/core/BakeMercator.js +65 -0
  7. package/dist/core/BakeMercator.js.map +1 -0
  8. package/dist/core/CustomAttributes.d.ts +7 -0
  9. package/dist/core/CustomAttributes.d.ts.map +1 -0
  10. package/dist/core/CustomAttributes.js +48 -0
  11. package/dist/core/CustomAttributes.js.map +1 -0
  12. package/dist/core/Draco.d.ts.map +1 -1
  13. package/dist/core/Draco.js +149 -2
  14. package/dist/core/Draco.js.map +1 -1
  15. package/dist/core/MeshOpt.d.ts.map +1 -1
  16. package/dist/core/MeshOpt.js +2 -0
  17. package/dist/core/MeshOpt.js.map +1 -1
  18. package/dist/core/S3M1Parser.d.ts.map +1 -1
  19. package/dist/core/S3M1Parser.js +114 -27
  20. package/dist/core/S3M1Parser.js.map +1 -1
  21. package/dist/core/S3MBParser.d.ts +5 -2
  22. package/dist/core/S3MBParser.d.ts.map +1 -1
  23. package/dist/core/S3MBParser.js +27 -12
  24. package/dist/core/S3MBParser.js.map +1 -1
  25. package/dist/core/TileGeometry.d.ts +8 -0
  26. package/dist/core/TileGeometry.d.ts.map +1 -1
  27. package/dist/core/TileGeometry.js +45 -7
  28. package/dist/core/TileGeometry.js.map +1 -1
  29. package/dist/core/types.d.ts +1 -0
  30. package/dist/core/types.d.ts.map +1 -1
  31. package/dist/index.d.ts +8 -5
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +4 -2
  34. package/dist/index.js.map +1 -1
  35. package/dist/render/DatasetManager.d.ts +27 -0
  36. package/dist/render/DatasetManager.d.ts.map +1 -1
  37. package/dist/render/DatasetManager.js +18 -4
  38. package/dist/render/DatasetManager.js.map +1 -1
  39. package/dist/render/GeoAdapter.d.ts +49 -24
  40. package/dist/render/GeoAdapter.d.ts.map +1 -1
  41. package/dist/render/GeoAdapter.js +96 -8
  42. package/dist/render/GeoAdapter.js.map +1 -1
  43. package/dist/render/GeometryBuilder.d.ts +17 -0
  44. package/dist/render/GeometryBuilder.d.ts.map +1 -1
  45. package/dist/render/GeometryBuilder.js +67 -4
  46. package/dist/render/GeometryBuilder.js.map +1 -1
  47. package/dist/render/MotionBlurPass.d.ts +130 -0
  48. package/dist/render/MotionBlurPass.d.ts.map +1 -0
  49. package/dist/render/MotionBlurPass.js +394 -0
  50. package/dist/render/MotionBlurPass.js.map +1 -0
  51. package/dist/render/S3MViewer.d.ts +99 -2
  52. package/dist/render/S3MViewer.d.ts.map +1 -1
  53. package/dist/render/S3MViewer.js +345 -13
  54. package/dist/render/S3MViewer.js.map +1 -1
  55. package/dist/render/StyleMaterial.d.ts +5 -0
  56. package/dist/render/StyleMaterial.d.ts.map +1 -1
  57. package/dist/render/StyleMaterial.js +21 -0
  58. package/dist/render/StyleMaterial.js.map +1 -1
  59. package/dist/render/tilePlacement.d.ts +84 -0
  60. package/dist/render/tilePlacement.d.ts.map +1 -0
  61. package/dist/render/tilePlacement.js +71 -0
  62. package/dist/render/tilePlacement.js.map +1 -0
  63. package/dist/render/viewer/EdgeDecorator.d.ts +1 -0
  64. package/dist/render/viewer/EdgeDecorator.d.ts.map +1 -1
  65. package/dist/render/viewer/EdgeDecorator.js +3 -0
  66. package/dist/render/viewer/EdgeDecorator.js.map +1 -1
  67. package/dist/render/viewer/PickService.d.ts +13 -0
  68. package/dist/render/viewer/PickService.d.ts.map +1 -1
  69. package/dist/render/viewer/PickService.js +24 -15
  70. package/dist/render/viewer/PickService.js.map +1 -1
  71. package/dist/render/viewer/types.d.ts +75 -0
  72. package/dist/render/viewer/types.d.ts.map +1 -1
  73. package/dist/render/viewer/types.js.map +1 -1
  74. package/dist/tileset/S3MTileset.d.ts +62 -1
  75. package/dist/tileset/S3MTileset.d.ts.map +1 -1
  76. package/dist/tileset/S3MTileset.js +273 -6
  77. package/dist/tileset/S3MTileset.js.map +1 -1
  78. package/dist/worker/S3MWorkerClient.d.ts +1 -1
  79. package/dist/worker/S3MWorkerClient.d.ts.map +1 -1
  80. package/dist/worker/S3MWorkerClient.js +2 -2
  81. package/dist/worker/S3MWorkerClient.js.map +1 -1
  82. package/dist/worker/s3mWorker.js +844 -635
  83. package/dist/worker/s3mWorker.js.map +1 -1
  84. package/docs/API.md +36 -5
  85. package/docs/FORMAT-NOTES.md +10 -0
  86. package/package.json +1 -1
  87. package/src/core/BakeMercator.ts +96 -0
  88. package/src/core/CustomAttributes.ts +53 -0
  89. package/src/core/Draco.ts +168 -6
  90. package/src/core/MeshOpt.ts +2 -0
  91. package/src/core/S3M1Parser.ts +102 -22
  92. package/src/core/S3MBParser.ts +33 -14
  93. package/src/core/TileGeometry.ts +60 -7
  94. package/src/core/types.ts +1 -0
  95. package/src/index.ts +15 -3
  96. package/src/render/DatasetManager.ts +43 -5
  97. package/src/render/GeoAdapter.ts +139 -11
  98. package/src/render/GeometryBuilder.ts +90 -4
  99. package/src/render/MotionBlurPass.ts +453 -0
  100. package/src/render/S3MViewer.ts +384 -17
  101. package/src/render/StyleMaterial.ts +26 -0
  102. package/src/render/tilePlacement.ts +121 -0
  103. package/src/render/viewer/EdgeDecorator.ts +3 -0
  104. package/src/render/viewer/PickService.ts +44 -16
  105. package/src/render/viewer/types.ts +73 -0
  106. package/src/tileset/S3MTileset.ts +300 -6
  107. package/src/vendor/draco_decoder.module.d.ts +15 -1
  108. package/src/worker/S3MWorkerClient.ts +7 -2
  109. package/src/worker/s3mWorker.ts +6 -2
@@ -225,14 +225,45 @@ interface MeshRecord {
225
225
  position: Float32Array;
226
226
  uv: Float32Array;
227
227
  indexCount: number;
228
- indices: Uint16Array;
228
+ indices: Uint16Array | Uint32Array;
229
+ primitiveType: number;
229
230
  materialId: string;
230
231
  /** 实例矩阵(每实例 16 float:3×4 行主序 + RGBA),没有实例化时为 undefined */
231
232
  instances?: Float32Array;
232
233
  }
233
234
 
234
- /** 索引头的第 3 个 dword,全版本固定 */
235
- const INDEX_HEADER_MARK = 0x00040100;
235
+ /**
236
+ * 校验 p 偏移处是否为合法的 S3M 1.x 索引头:
237
+ * dword[0] = 1 (idxKind)
238
+ * dword[1] = indexCount
239
+ * dword[2] = mark,按字节小端存储:
240
+ * byte 0: indexType (0=u16, 1/3=u32)
241
+ * byte 1: useIndex (恒 1)
242
+ * byte 2: primitiveType (1=POINTS, 2=LINES, 3=LINE_STRIP, 4=TRIANGLES, 5/6=STRIP/FAN, 13=EDGES)
243
+ * byte 3: reserved (恒 0)
244
+ * 校验紧随其后的索引数据(4 字节对齐)之后,记录尾标记恒为 1。
245
+ */
246
+ function isValidIndexHeader(view: DataView, p: number): boolean {
247
+ if (view.getUint32(p, true) !== 1) return false;
248
+ const count = view.getUint32(p + 4, true);
249
+ if (count === 0 || count > 10_000_000) return false;
250
+ const mark = view.getUint32(p + 8, true);
251
+ const indexType = mark & 0xff;
252
+ const useIndex = (mark >> 8) & 0xff;
253
+ const primitiveType = (mark >> 16) & 0xff;
254
+ const reserved = (mark >> 24) & 0xff;
255
+ if (useIndex !== 1 || reserved !== 0) return false;
256
+ if (indexType !== 0 && indexType !== 1 && indexType !== 3) return false;
257
+ if (![1, 2, 3, 4, 5, 6, 13].includes(primitiveType)) return false;
258
+ if (primitiveType === 4 && count % 3 !== 0) return false;
259
+ if (primitiveType === 2 && count % 2 !== 0) return false;
260
+
261
+ const wide = indexType === 1 || indexType === 3;
262
+ const indexBytes = count * (wide ? 4 : 2);
263
+ const tailOffset = (p + 12 + indexBytes + 3) & ~3;
264
+ if (tailOffset + 8 > view.byteLength) return false;
265
+ return view.getUint32(tailOffset, true) === 1;
266
+ }
236
267
 
237
268
  /**
238
269
  * v1 里每个实例记录固定 20 个 float(80 字节):
@@ -321,11 +352,7 @@ function readInstances(
321
352
  function findIndexHeader(view: DataView, from: number): number {
322
353
  const limit = view.byteLength - 12;
323
354
  for (let p = from; p <= limit; p += 4) {
324
- if (view.getUint32(p, true) !== 1) continue;
325
- const count = view.getUint32(p + 4, true);
326
- if (count === 0 || count % 3 !== 0) continue;
327
- if (view.getUint32(p + 8, true) !== INDEX_HEADER_MARK) continue;
328
- return p;
355
+ if (isValidIndexHeader(view, p)) return p;
329
356
  }
330
357
  return -1;
331
358
  }
@@ -389,6 +416,39 @@ function applyQuantBox(position: Float32Array, box: Float32Array): boolean {
389
416
  return true;
390
417
  }
391
418
 
419
+ /**
420
+ * 从版本 1 属性块中尝试提取 UV 坐标:
421
+ * 倾斜摄影(OSGB)等数据中,UV 块通常紧贴在索引头前:
422
+ * 头: [count: u32, dim: u32 = 2]
423
+ * 数据: V * 8 字节 float32 (u, v)
424
+ */
425
+ function findV1UV(view: DataView, from: number, to: number, V: number): Float32Array | null {
426
+ const head = to - V * 8 - 8;
427
+ if (head >= from && view.getUint32(head, true) === V && view.getUint32(head + 4, true) === 2) {
428
+ let uv = new Float32Array(V * 2);
429
+ for (let i = 0; i < V * 2; i++) {
430
+ uv[i] = view.getFloat32(head + 8 + i * 4, true);
431
+ }
432
+ return uv;
433
+ }
434
+ for (let p = to - V * 8 - 8; p >= from; p -= 4) {
435
+ if (view.getUint32(p, true) === V && view.getUint32(p + 4, true) === 2) {
436
+ let uv = new Float32Array(V * 2);
437
+ let ok = true;
438
+ for (let i = 0; i < V * 2; i++) {
439
+ const val = view.getFloat32(p + 8 + i * 4, true);
440
+ if (!Number.isFinite(val) || Math.abs(val) > 1e4) {
441
+ ok = false;
442
+ break;
443
+ }
444
+ uv[i] = val;
445
+ }
446
+ if (ok) return uv;
447
+ }
448
+ }
449
+ return null;
450
+ }
451
+
392
452
  function readGeometry(
393
453
  br: BinaryReader,
394
454
  geoSize: number
@@ -433,7 +493,7 @@ function readGeometry(
433
493
  position[i * 3 + 2] = z;
434
494
  }
435
495
 
436
- const uv = new Float32Array(V * 2);
496
+ let uv = new Float32Array(V * 2);
437
497
  let instances: Float32Array | undefined;
438
498
  if (v0UvAt16) {
439
499
  // 版本 0:UV 数组头 [0,0,0,kind=1,count=V,flag=2]
@@ -456,7 +516,12 @@ function readGeometry(
456
516
  throw new Error(`[s3m] S3M 1.x 属性段无法定位索引头:网格 ${geoName}(偏移 ${br.offset}/${geoSize})`);
457
517
  }
458
518
  if (at !== br.offset) {
459
- skippedAttrs++;
519
+ const foundUv = findV1UV(br.view, br.offset, at, V);
520
+ if (foundUv) {
521
+ uv = foundUv;
522
+ } else {
523
+ skippedAttrs++;
524
+ }
460
525
  // 优先认实例化:柱状图是真·实例化的(一份模板 + N 个变换矩阵),
461
526
  // 顶点本来就该保持单位盒,靠矩阵摆位/缩放/上色。
462
527
  const inst = readInstances(br.view, br.offset, at);
@@ -471,15 +536,29 @@ function readGeometry(
471
536
  }
472
537
  }
473
538
 
474
- // 索引头 [1, indexCount, 0x00040100]
539
+ // 索引头 [1, indexCount, mark]
475
540
  const idxKind = br.u32();
476
541
  const indexCount = br.u32();
477
- br.u32(); // 常量标记
478
- if (idxKind !== 1 || indexCount === 0 || indexCount % 3 !== 0) {
479
- throw new Error(`[s3m] S3M 1.x 索引头不符:网格 ${geoName} kind=${idxKind} count=${indexCount}`);
542
+ const mark = br.u32();
543
+ const indexType = mark & 0xff;
544
+ const useIndex = (mark >> 8) & 0xff;
545
+ const primitiveType = (mark >> 16) & 0xff;
546
+ if (idxKind !== 1 || indexCount === 0 || useIndex !== 1) {
547
+ throw new Error(`[s3m] S3M 1.x 索引头不符:网格 ${geoName} kind=${idxKind} count=${indexCount} mark=0x${mark.toString(16)}`);
548
+ }
549
+ if (primitiveType === 4 && indexCount % 3 !== 0) {
550
+ throw new Error(`[s3m] S3M 1.x 索引头不符:网格 ${geoName} 三角形索引数 ${indexCount} 非 3 的倍数`);
551
+ }
552
+ if (primitiveType === 2 && indexCount % 2 !== 0) {
553
+ throw new Error(`[s3m] S3M 1.x 索引头不符:网格 ${geoName} 线段索引数 ${indexCount} 非 2 的倍数`);
554
+ }
555
+ const wide = indexType === 1 || indexType === 3;
556
+ const indices = wide ? new Uint32Array(indexCount) : new Uint16Array(indexCount);
557
+ if (wide) {
558
+ for (let i = 0; i < indexCount; i++) indices[i] = br.u32();
559
+ } else {
560
+ for (let i = 0; i < indexCount; i++) indices[i] = br.u16();
480
561
  }
481
- const indices = new Uint16Array(indexCount);
482
- for (let i = 0; i < indexCount; i++) indices[i] = br.u16();
483
562
  if (br.offset > geoSize) throw new Error(`[s3m] S3M 1.x 索引越界:网格 ${geoName}`);
484
563
  br.align4();
485
564
 
@@ -495,6 +574,7 @@ function readGeometry(
495
574
  uv,
496
575
  indexCount,
497
576
  indices,
577
+ primitiveType,
498
578
  materialId,
499
579
  ...(instances ? { instances } : {}),
500
580
  });
@@ -511,10 +591,10 @@ function readGeometry(
511
591
  */
512
592
  export function pixelFormatOfBpp(bpp: number, rawFormat: number): number {
513
593
  if (!Number.isFinite(bpp) || bpp <= 0) return rawFormat;
514
- if (Math.abs(bpp - 0.5) < 0.02) return 17; // DXT1
515
- if (Math.abs(bpp - 1) < 0.02) return 21; // DXT5
516
- if (Math.abs(bpp - 4) < 0.02) return 14; // BGRA
517
- if (Math.abs(bpp - 3) < 0.02) return 10; // RGB
594
+ if (Math.abs(bpp - 0.5) < 0.05 || (bpp >= 0.6 && bpp <= 0.75)) return 17; // DXT1(含全 mip 链 ~0.667 B/px)
595
+ if (Math.abs(bpp - 1) < 0.05 || (bpp >= 1.25 && bpp <= 1.5)) return 21; // DXT5(含全 mip 链 ~1.333~1.44 B/px)
596
+ if (Math.abs(bpp - 4) < 0.05) return 14; // BGRA
597
+ if (Math.abs(bpp - 3) < 0.05) return 10; // RGB
518
598
  console.warn(`[s3m] S3M 1.x 纹理密度 ${bpp.toFixed(3)} B/px 不在已知档位,按原 format=${rawFormat} 交给上层判定`);
519
599
  return rawFormat;
520
600
  }
@@ -668,9 +748,9 @@ export async function parseS3M1(arrayBuffer: ArrayBuffer, options: ParseS3M1Opti
668
748
  }
669
749
  const pack: IndexPackage = {
670
750
  indexCount: mesh.indexCount,
671
- indexType: 0, // 0 = UNSIGNED_SHORT
751
+ indexType: mesh.indices instanceof Uint32Array ? 1 : 0,
672
752
  useIndex: 1,
673
- primitiveType: 4, // TRIANGLES
753
+ primitiveType: mesh.primitiveType ?? 4,
674
754
  indices: new Uint8Array(mesh.indices.buffer, mesh.indices.byteOffset, mesh.indices.byteLength),
675
755
  passNames: [mesh.materialId],
676
756
  };
@@ -12,6 +12,7 @@
12
12
  import { BinaryReader } from './BinaryReader.js';
13
13
  import { parseDracoGeometry } from './Draco.js';
14
14
  import { parseMeshOptGeometry } from './MeshOpt.js';
15
+ import { applyCustomAttributeSemantics } from './CustomAttributes.js';
15
16
  import { isS3M1Buffer, parseS3M1 } from './S3M1Parser.js';
16
17
  import type { DegradationKind } from './degradations.js';
17
18
  import type {
@@ -53,6 +54,8 @@ export interface ParseOptions {
53
54
  inflate?: InflateFn;
54
55
  /** 逐段追踪偏移,调试解析用 */
55
56
  trace?: (section: string, detail?: unknown, offset?: number) => void;
57
+ /** config.json 声明的顶点属性描述清单(用于语义重映射与维度判定) */
58
+ vertexAttributeDescript?: unknown;
56
59
  }
57
60
 
58
61
  /** 默认解压:zlib(文件头 78 9C / 78 DA)。浏览器与 Node 18+ 均可用。 */
@@ -73,9 +76,8 @@ function pushAttr(
73
76
  offset = 0,
74
77
  divisor?: number,
75
78
  dtype: AttrDType = 'f32'
76
- ): void {
77
- vp.attrLocation[semantic] = vp.vertexAttributes.length;
78
- vp.vertexAttributes.push({
79
+ ): VertexAttribute {
80
+ const attr: VertexAttribute = {
79
81
  semantic,
80
82
  data,
81
83
  components,
@@ -83,7 +85,10 @@ function pushAttr(
83
85
  offsetInBytes: offset,
84
86
  divisor,
85
87
  dtype,
86
- });
88
+ };
89
+ vp.attrLocation[semantic] = vp.vertexAttributes.length;
90
+ vp.vertexAttributes.push(attr);
91
+ return attr;
87
92
  }
88
93
 
89
94
  function newVertexPackage(): VertexPackage {
@@ -248,15 +253,19 @@ function parseInstanceInfo(r: BinaryReader, vp: VertexPackage, version: number):
248
253
  }
249
254
  }
250
255
 
251
- function parseVertexAttrExtension(r: BinaryReader, vp: VertexPackage): void {
256
+ export { applyCustomAttributeSemantics } from './CustomAttributes.js';
257
+
258
+ function parseVertexAttrExtension(r: BinaryReader, vp: VertexPackage): VertexAttribute[] {
252
259
  const n = r.u32();
260
+ const attrs: VertexAttribute[] = [];
253
261
  for (let i = 0; i < n; i++) {
254
262
  const count = r.u32();
255
263
  const dim = r.u16();
256
264
  const type = r.u16();
257
265
  const size = AttrTypeSize[type] ?? 4;
258
- pushAttr(vp, `custom${i}`, r.bytes(count * dim * size), dim, dim * size);
266
+ attrs.push(pushAttr(vp, `custom${i}`, r.bytes(count * dim * size), dim, dim * size));
259
267
  }
268
+ return attrs;
260
269
  }
261
270
 
262
271
  function parseTangent(r: BinaryReader): void {
@@ -267,7 +276,7 @@ function parseTangent(r: BinaryReader): void {
267
276
  r.skip(n * dim * 4);
268
277
  }
269
278
 
270
- function parseStandardSkeleton(r: BinaryReader, vp: VertexPackage, version: number): void {
279
+ function parseStandardSkeleton(r: BinaryReader, vp: VertexPackage, version: number, options?: ParseOptions): void {
271
280
  if (version >= 3) r.u32(); // streamSize
272
281
 
273
282
  parseVertex(r, vp);
@@ -278,9 +287,17 @@ function parseStandardSkeleton(r: BinaryReader, vp: VertexPackage, version: numb
278
287
  parseInstanceInfo(r, vp, version);
279
288
 
280
289
  if (version >= 3) {
281
- parseVertexAttrExtension(r, vp);
290
+ const extAttrs = parseVertexAttrExtension(r, vp);
282
291
  const descript = r.string();
283
- vp.customVertexAttribute = descript ? JSON.parse(descript) : undefined;
292
+ try {
293
+ vp.customVertexAttribute = descript ? JSON.parse(descript) : undefined;
294
+ } catch {
295
+ vp.customVertexAttribute = descript;
296
+ }
297
+ if (!vp.customVertexAttribute && options?.vertexAttributeDescript) {
298
+ vp.customVertexAttribute = options.vertexAttributeDescript;
299
+ }
300
+ applyCustomAttributeSemantics(vp, extAttrs, options?.vertexAttributeDescript);
284
301
  r.align4();
285
302
  parseTangent(r);
286
303
  }
@@ -386,7 +403,8 @@ function isFiniteBox(box: { center: Vec3; xExtent: Vec3; yExtent: Vec3; zExtent:
386
403
  export async function parseSkeletonEntries(
387
404
  r: BinaryReader,
388
405
  version: number,
389
- trace?: ParseOptions['trace']
406
+ trace?: ParseOptions['trace'],
407
+ options?: ParseOptions
390
408
  ): Promise<Record<string, GeoPackageEntry>> {
391
409
  const count = r.u32();
392
410
  const geoPackage: Record<string, GeoPackageEntry> = {};
@@ -419,7 +437,7 @@ export async function parseSkeletonEntries(
419
437
  let indexPackages: IndexPackage[] = [];
420
438
 
421
439
  if (tag === VertexTag.STANDARD) {
422
- parseStandardSkeleton(r, vp, version);
440
+ parseStandardSkeleton(r, vp, version, options);
423
441
  indexPackages = parseIndexPackages(r, version);
424
442
  } else if (tag === VertexTag.COMPRESSED) {
425
443
  if (version >= 3) {
@@ -462,10 +480,11 @@ export async function parseSkeletonEntries(
462
480
  export async function parseSkeletons(
463
481
  r: BinaryReader,
464
482
  version: number,
465
- trace?: ParseOptions['trace']
483
+ trace?: ParseOptions['trace'],
484
+ options?: ParseOptions
466
485
  ): Promise<Record<string, GeoPackageEntry>> {
467
486
  r.u32(); // streamSize
468
- const geoPackage = await parseSkeletonEntries(r, version, trace);
487
+ const geoPackage = await parseSkeletonEntries(r, version, trace, options);
469
488
 
470
489
  // v1/v2 在骨架数组之后还有一段附加流。官方写法是
471
490
  // bytesOffset += secColorSize; bytesOffset += 4;
@@ -649,7 +668,7 @@ export async function parseS3MB(arrayBuffer: ArrayBuffer, options: ParseOptions
649
668
  trace?.('fileOptions', fileOptions, t.offset);
650
669
  const groupNode = parseGroupNode(t, version);
651
670
  trace?.('groupNode', groupNode.pageLods.length, t.offset);
652
- const geoPackage = await parseSkeletons(t, version, trace);
671
+ const geoPackage = await parseSkeletons(t, version, trace, options);
653
672
  trace?.('skeletons', Object.keys(geoPackage).length, t.offset);
654
673
  const textures = parseTextures(t);
655
674
  trace?.('textures', Object.keys(textures).length, t.offset);
@@ -20,6 +20,8 @@ import type {
20
20
  ParsedTile,
21
21
  VertexAttribute,
22
22
  } from './types.js';
23
+ import { applyCustomAttributeSemantics } from './CustomAttributes.js';
24
+ import { bakeMercatorOffsets, type TileAnchorLike, type TileCenterLike } from './BakeMercator.js';
23
25
 
24
26
  /**
25
27
  * S3M 规范里 `DrawPrimitiveType` 的两个特殊值(原住 GeometryBuilder,随组装层
@@ -248,6 +250,8 @@ export interface PrimitiveAttributes {
248
250
  * 只有存在该流时才产生;解码语义见 core/TexCoordZ.ts。
249
251
  */
250
252
  aTexCoordZ?: PlainAttribute;
253
+ /** 逐顶点精确 Web 墨卡托偏移(Issue #15) */
254
+ aMercatorOffset?: PlainAttribute;
251
255
  }
252
256
 
253
257
  export interface PrimitiveData {
@@ -297,6 +301,11 @@ export interface AssembleOptions {
297
301
  * `useWorker: false` 或 Worker 崩溃降级时它落在主线程上。
298
302
  */
299
303
  dedupeCoincident?: boolean;
304
+ /** 是否烘焙精确 Web 墨卡托偏移(Issue #15) */
305
+ bakeMercator?: boolean;
306
+ tileAnchor?: TileAnchorLike;
307
+ tileCenter?: TileCenterLike;
308
+ vertexAttributeDescript?: unknown;
300
309
  }
301
310
 
302
311
  /* ------------------------------------------------------------------ 合批合并 */
@@ -556,8 +565,15 @@ interface SkeletonAsm {
556
565
  }
557
566
 
558
567
  /** 每骨架只解一次交错流(同一骨架被多个 pack / geode 引用时共享同一份数据) */
559
- function assembleSkeleton(name: string, geo: GeoPackageEntry, tile: ParsedTile, skipped: string[]): SkeletonAsm | null {
568
+ function assembleSkeleton(
569
+ name: string,
570
+ geo: GeoPackageEntry,
571
+ tile: ParsedTile,
572
+ skipped: string[],
573
+ options?: AssembleOptions
574
+ ): SkeletonAsm | null {
560
575
  const vp = geo.vertexPackage;
576
+ applyCustomAttributeSemantics(vp, undefined, (tile as any).vertexAttributeDescript ?? options?.vertexAttributeDescript);
561
577
  const positionAttr = vp.vertexAttributes.find((a) => a.semantic === 'position');
562
578
  if (!positionAttr) {
563
579
  skipped.push(`${name} 无 position`);
@@ -585,11 +601,29 @@ function assembleSkeleton(name: string, geo: GeoPackageEntry, tile: ParsedTile,
585
601
  else if (attr.semantic === 'normal') normalData = deinterleave(attr);
586
602
  else if (attr.semantic === 'uv') uvData = deinterleave(attr);
587
603
  else if (attr.semantic === 'uv2') uv1Data = deinterleave(attr);
588
- // custom0 = uv 的第三分量,官方 aTextureCoordMatrix:决定「这张贴图用不用、
589
- // 用图集的哪个子区域、按 2 的几次幂缩放」。BIM/OSGB 转出来的数据依赖它,
590
- // 丢掉就会把本该纯色的墙面贴上整张材质图。语义见 core/TexCoordZ.ts。
591
- else if (attr.semantic === 'custom0' && (attr.components ?? 1) >= 1) {
592
- texCoordZData = deinterleave(attr);
604
+ // uv 第三分量(官方 aTextureCoordMatrix):决定「这张贴图用不用、
605
+ // 用图集的哪个子区域、按 2 的几次幂缩放」。语义见 core/TexCoordZ.ts。
606
+ // 注意:按语义查槽(找 TextureCoordMatrix),不要固定取 custom0;
607
+ // 绝对不能把 VertexWeight(高度值)等其它语义属性当成贴图矩阵;
608
+ // 若全为哨兵(-99999)则视为无第三分量,跳过采样/图集选择逻辑。
609
+ else if (
610
+ (attr.semantic === 'TextureCoordMatrix' ||
611
+ attr.semantic === 'aTextureCoordMatrix' ||
612
+ (attr.semantic === 'custom0' && !vp.vertexAttributes.some((a) => a.semantic === 'TextureCoordMatrix' || a.semantic === 'VertexWeight'))) &&
613
+ (attr.components ?? 1) >= 1
614
+ ) {
615
+ const plain = deinterleave(attr);
616
+ const arr = plain.array as Float32Array;
617
+ let hasValid = false;
618
+ for (let i = 0; i < arr.length; i++) {
619
+ if (arr[i]! >= -99900) {
620
+ hasValid = true;
621
+ break;
622
+ }
623
+ }
624
+ if (hasValid) {
625
+ texCoordZData = plain;
626
+ }
593
627
  }
594
628
  }
595
629
 
@@ -752,7 +786,7 @@ export function assembleTileGeometry(tile: ParsedTile, options: AssembleOptions
752
786
 
753
787
  const skeletons = new Map<string, SkeletonAsm>();
754
788
  for (const [name, geo] of Object.entries(tile.geoPackage ?? {})) {
755
- const asm = assembleSkeleton(name, geo, tile, skipped);
789
+ const asm = assembleSkeleton(name, geo, tile, skipped, options);
756
790
  if (asm) {
757
791
  skeletons.set(name, asm);
758
792
  stats.vertices += geo.vertexPackage.verticesCount || 0;
@@ -878,5 +912,24 @@ export function assembleTileGeometry(tile: ParsedTile, options: AssembleOptions
878
912
  }
879
913
  }
880
914
 
915
+ // 逐顶点烘焙精确 Web 墨卡托(Issue #15)
916
+ if (options.bakeMercator && options.tileAnchor) {
917
+ for (const p of primitives) {
918
+ const positions = p.attributes.position?.array;
919
+ if (!positions || p.vertexCount === 0) continue;
920
+ const center = options.tileCenter ?? {
921
+ x: p.bounds.sphereCenter[0],
922
+ y: p.bounds.sphereCenter[1],
923
+ z: p.bounds.sphereCenter[2],
924
+ };
925
+ const offsets = bakeMercatorOffsets(positions, p.vertexCount, options.tileAnchor, center);
926
+ p.attributes.aMercatorOffset = {
927
+ array: offsets,
928
+ components: 3,
929
+ normalized: false,
930
+ };
931
+ }
932
+ }
933
+
881
934
  return { primitives, stats };
882
935
  }
package/src/core/types.ts CHANGED
@@ -201,6 +201,7 @@ export interface ParsedTile {
201
201
  materials: unknown;
202
202
  pickInfo: unknown;
203
203
  extensions?: unknown;
204
+ vertexAttributeDescript?: unknown;
204
205
  /**
205
206
  * 解析成功、但渲染能力不完整的项(机器可读)。
206
207
  *
package/src/index.ts CHANGED
@@ -51,17 +51,25 @@ export type {
51
51
 
52
52
  // —— 地理坐标适配(宿主无关接口 + 墨卡托兜底)
53
53
  export {
54
+ WGS84_A,
55
+ WGS84_B,
56
+ WGS84_F,
57
+ WGS84_E2,
54
58
  EARTH_RADIUS,
59
+ primeVerticalRadius,
60
+ meridianRadius,
61
+ enuToLngLat,
62
+ lngLatToEnu,
55
63
  mercatorFromLngLat,
56
64
  lngLatFromMercator,
57
65
  worldFromLngLat,
58
66
  lngLatFromWorld,
59
67
  } from './render/GeoAdapter.js';
60
- export type { GeoAdapter } from './render/GeoAdapter.js';
68
+ export type { GeoAdapter, WorldCoordinateResult } from './render/GeoAdapter.js';
61
69
 
62
70
  // —— 瓦片层
63
- export { S3MTileset } from './tileset/S3MTileset.js';
64
- export type { TileNode, BuildTileFn, S3MTilesetOptions } from './tileset/S3MTileset.js';
71
+ export { S3MTileset, parseConfigText, classifyDatasetKind } from './tileset/S3MTileset.js';
72
+ export type { TileNode, BuildTileFn, S3MTilesetOptions, DatasetKind } from './tileset/S3MTileset.js';
65
73
 
66
74
  // —— 渲染层
67
75
  export { S3MViewer } from './render/S3MViewer.js';
@@ -110,8 +118,10 @@ export type {
110
118
  GroundOptions,
111
119
  LightOptions,
112
120
  PathFollowOptions,
121
+ DepthMode,
113
122
  } from './render/S3MViewer.js';
114
123
 
124
+ export { normalizeDepthMode } from './render/S3MViewer.js';
115
125
  export { LIGHT_PRESETS, DEFAULT_LIGHT_PRESET, SUPERMAP_LIGHT_PRESET, resolveLightingPreset } from './render/lightingPreset.js';
116
126
  export type { LightingPreset, PresetLightIntensities } from './render/lightingPreset.js';
117
127
 
@@ -138,6 +148,8 @@ export { setMaterialQuality, getMaterialQuality } from './render/StyleMaterial.j
138
148
 
139
149
  export { Picker } from './render/Picker.js';
140
150
  export { GpuEdgePass } from './render/GpuEdgePass.js';
151
+ export { MotionBlurPass, normalizeMotionBlur, autoMotionBlurDamp } from './render/MotionBlurPass.js';
152
+ export type { MotionBlurOptions, MotionBlurConfig, MotionBlurParams } from './render/MotionBlurPass.js';
141
153
 
142
154
  /* 三维空间分析内核:纯数学,不依赖 WebGL,可单独用于服务端 / Worker */
143
155
  export * from './analysis/index.js';
@@ -14,6 +14,7 @@ import type { Anchor } from '../core/types.js';
14
14
  import type { UnsupportedPolicy } from '../core/degradations.js';
15
15
  import { worldFromLngLat, type GeoAdapter } from './GeoAdapter.js';
16
16
  import type { TextureCreateOptions } from './TextureFactory.js';
17
+ import type { DepthMode } from './viewer/types.js';
17
18
 
18
19
  export type DatasetAlignment = 'origin' | 'bottom' | 'top' | 'center';
19
20
 
@@ -42,6 +43,11 @@ export interface S3MDatasetOptions {
42
43
  heightOffset?: number;
43
44
  /** 是否用 Web Worker 解析经典 .s3mb 瓦片(默认开,崩溃自动降级主线程),见 S3MTilesetOptions.useWorker */
44
45
  useWorker?: boolean;
46
+ /**
47
+ * 本数据集推荐的深度模式('scene' | 'clear' | 'auto')。
48
+ * 当图层/Viewer 的 depthMode 为 'auto' 时,若任一可见数据集标注为 'clear',则整层以 'clear' 绘制。
49
+ */
50
+ depthMode?: DepthMode;
45
51
  /** 三角面材质深度偏移(共面摩尔纹 / 重合面归属),见 BuildTileOptions.polygonOffset */
46
52
  polygonOffset?: { factor: number; units: number };
47
53
  /**
@@ -49,6 +55,7 @@ export interface S3MDatasetOptions {
49
55
  * 只影响该数据集**之后创建**的材质。
50
56
  */
51
57
  materialQuality?: 'pbr' | 'lambert';
58
+ bakeMercator?: boolean;
52
59
  /**
53
60
  * 是否对该数据集生成几何棱线(CPU 端 EdgesGeometry)。
54
61
  * - 不填:跟随全局 `style.edges`
@@ -65,6 +72,13 @@ export interface S3MDatasetOptions {
65
72
  * 目前只有 3DGS:按点渲染,做不出泼溅。见 `S3MTilesetOptions.unsupported`。
66
73
  */
67
74
  unsupported?: UnsupportedPolicy;
75
+ /**
76
+ * LOD 细化过渡策略:
77
+ * - 'binary'(默认):整节点二值切换
78
+ * - 'incremental':增量细化(primary / fallback 双列表 + LEQUAL 深度推移)
79
+ */
80
+ lodTransition?: 'binary' | 'incremental';
81
+ fallbackOffset?: { factor?: number; units?: number };
68
82
  }
69
83
 
70
84
  export interface S3MDatasetEntry {
@@ -97,13 +111,32 @@ export interface DatasetManagerCallbacks {
97
111
 
98
112
  export class DatasetManager {
99
113
  readonly datasets = new Map<string, S3MDatasetEntry>();
100
- sceneAnchor: { x: number; y: number; z: number; meterInMercator: number; altitude?: number } | null = null;
114
+ sceneAnchor: {
115
+ x: number;
116
+ y: number;
117
+ z: number;
118
+ meterInMercatorEast?: number;
119
+ meterInMercatorNorth?: number;
120
+ meterInMercatorUp?: number;
121
+ meterInMercator: number;
122
+ altitude?: number;
123
+ } | null = null;
101
124
  totalMaxTiles: number = 0;
102
125
  /**
103
126
  * 图层级默认光照模型档位(`undefined` = 跟随模块级默认)。
104
127
  * 数据集自己指定了 `materialQuality` 就用自己的。
105
128
  */
106
129
  defaultMaterialQuality: 'pbr' | 'lambert' | undefined = undefined;
130
+ defaultLodTransition: 'binary' | 'incremental' = 'binary';
131
+ defaultFallbackOffset?: { factor?: number; units?: number };
132
+
133
+ setDefaultLodTransition(mode: 'binary' | 'incremental'): void {
134
+ this.defaultLodTransition = mode;
135
+ for (const entry of this.datasets.values()) {
136
+ entry.tileset?.setLodTransition(mode);
137
+ }
138
+ }
139
+ defaultBakeMercator: boolean | undefined = undefined;
107
140
 
108
141
  /**
109
142
  * 图层档位变更:同步给已存在的数据集(它们之后加载的瓦片才用新档位;
@@ -264,6 +297,8 @@ export class DatasetManager {
264
297
  maxMemory: ds.maxMemory ?? maxMemory,
265
298
  maxImageCache: ds.maxImageCache ?? maxImageCache,
266
299
  lodRangeScale: ds.lodRangeScale ?? defaultLodRangeScale,
300
+ lodTransition: ds.lodTransition ?? this.defaultLodTransition,
301
+ fallbackOffset: ds.fallbackOffset ?? this.defaultFallbackOffset,
267
302
  buildTile: buildTile ?? null,
268
303
  polygonOffset: ds.polygonOffset,
269
304
  // 数据集 > 图层 > 模块级默认(undefined 一路传下去,由 createStyledMaterial 回落)
@@ -272,6 +307,7 @@ export class DatasetManager {
272
307
  colorPipeline: this.defaultColorPipeline,
273
308
  textureOptions: this.defaultTextureOptions,
274
309
  useWorker: ds.useWorker,
310
+ bakeMercator: ds.bakeMercator ?? this.defaultBakeMercator,
275
311
  authenticate: ds.authenticate ?? authenticate,
276
312
  unsupported: ds.unsupported,
277
313
  uniforms: uniforms,
@@ -359,12 +395,14 @@ export class DatasetManager {
359
395
  if (!this.sceneAnchor) {
360
396
  this.sceneAnchor = worldFromLngLat(geo, entry.anchor.lon, entry.anchor.lat, entry.anchor.height ?? 0);
361
397
  }
362
- const s = this.sceneAnchor.meterInMercator;
398
+ const sEast = this.sceneAnchor.meterInMercatorEast ?? this.sceneAnchor.meterInMercator;
399
+ const sNorth = this.sceneAnchor.meterInMercatorNorth ?? this.sceneAnchor.meterInMercator;
400
+ const sUp = sEast;
363
401
  const a = worldFromLngLat(geo, entry.anchor.lon, entry.anchor.lat, entry.anchor.height ?? 0);
364
402
  entry.offset = [
365
- (a.x - this.sceneAnchor.x) / s,
366
- -(a.y - this.sceneAnchor.y) / s,
367
- (a.z - this.sceneAnchor.z) / s + (entry.heightOffset ?? 0),
403
+ (a.x - this.sceneAnchor.x) / sEast,
404
+ -(a.y - this.sceneAnchor.y) / sNorth,
405
+ (a.z - this.sceneAnchor.z) / sUp + (entry.heightOffset ?? 0),
368
406
  ];
369
407
  entry.root.position.set(entry.offset[0], entry.offset[1], entry.offset[2]);
370
408
  entry.root.updateMatrixWorld(true);