s3m-three 0.4.26 → 0.4.28

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 (77) hide show
  1. package/dist/browser/assets/s3mWorker-B2u-xt54.js +1 -0
  2. package/dist/browser/s3m-three.iife.ext-three.js +52 -40
  3. package/dist/browser/s3m-three.iife.js +52 -40
  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/S3M1Parser.d.ts +19 -0
  9. package/dist/core/S3M1Parser.d.ts.map +1 -1
  10. package/dist/core/S3M1Parser.js +179 -8
  11. package/dist/core/S3M1Parser.js.map +1 -1
  12. package/dist/core/TileGeometry.d.ts +7 -0
  13. package/dist/core/TileGeometry.d.ts.map +1 -1
  14. package/dist/core/TileGeometry.js +20 -0
  15. package/dist/core/TileGeometry.js.map +1 -1
  16. package/dist/index.d.ts +4 -3
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +2 -1
  19. package/dist/index.js.map +1 -1
  20. package/dist/render/DatasetManager.d.ts +27 -0
  21. package/dist/render/DatasetManager.d.ts.map +1 -1
  22. package/dist/render/DatasetManager.js +18 -4
  23. package/dist/render/DatasetManager.js.map +1 -1
  24. package/dist/render/GeoAdapter.d.ts +49 -24
  25. package/dist/render/GeoAdapter.d.ts.map +1 -1
  26. package/dist/render/GeoAdapter.js +96 -8
  27. package/dist/render/GeoAdapter.js.map +1 -1
  28. package/dist/render/GeometryBuilder.d.ts +17 -0
  29. package/dist/render/GeometryBuilder.d.ts.map +1 -1
  30. package/dist/render/GeometryBuilder.js +31 -2
  31. package/dist/render/GeometryBuilder.js.map +1 -1
  32. package/dist/render/S3MViewer.d.ts +80 -2
  33. package/dist/render/S3MViewer.d.ts.map +1 -1
  34. package/dist/render/S3MViewer.js +299 -9
  35. package/dist/render/S3MViewer.js.map +1 -1
  36. package/dist/render/StyleMaterial.d.ts +5 -0
  37. package/dist/render/StyleMaterial.d.ts.map +1 -1
  38. package/dist/render/StyleMaterial.js +21 -0
  39. package/dist/render/StyleMaterial.js.map +1 -1
  40. package/dist/render/tilePlacement.d.ts +84 -0
  41. package/dist/render/tilePlacement.d.ts.map +1 -0
  42. package/dist/render/tilePlacement.js +71 -0
  43. package/dist/render/tilePlacement.js.map +1 -0
  44. package/dist/render/viewer/EdgeDecorator.d.ts +1 -0
  45. package/dist/render/viewer/EdgeDecorator.d.ts.map +1 -1
  46. package/dist/render/viewer/EdgeDecorator.js +3 -0
  47. package/dist/render/viewer/EdgeDecorator.js.map +1 -1
  48. package/dist/render/viewer/PickService.d.ts +13 -0
  49. package/dist/render/viewer/PickService.d.ts.map +1 -1
  50. package/dist/render/viewer/PickService.js +24 -15
  51. package/dist/render/viewer/PickService.js.map +1 -1
  52. package/dist/render/viewer/types.d.ts +53 -0
  53. package/dist/render/viewer/types.d.ts.map +1 -1
  54. package/dist/render/viewer/types.js.map +1 -1
  55. package/dist/tileset/S3MTileset.d.ts +43 -1
  56. package/dist/tileset/S3MTileset.d.ts.map +1 -1
  57. package/dist/tileset/S3MTileset.js +217 -2
  58. package/dist/tileset/S3MTileset.js.map +1 -1
  59. package/dist/worker/s3mWorker.js +691 -508
  60. package/dist/worker/s3mWorker.js.map +1 -1
  61. package/docs/API.md +18 -0
  62. package/package.json +1 -1
  63. package/src/core/BakeMercator.ts +96 -0
  64. package/src/core/S3M1Parser.ts +179 -9
  65. package/src/core/TileGeometry.ts +26 -0
  66. package/src/index.ts +11 -1
  67. package/src/render/DatasetManager.ts +43 -5
  68. package/src/render/GeoAdapter.ts +139 -11
  69. package/src/render/GeometryBuilder.ts +53 -2
  70. package/src/render/S3MViewer.ts +335 -13
  71. package/src/render/StyleMaterial.ts +26 -0
  72. package/src/render/tilePlacement.ts +121 -0
  73. package/src/render/viewer/EdgeDecorator.ts +3 -0
  74. package/src/render/viewer/PickService.ts +44 -16
  75. package/src/render/viewer/types.ts +51 -0
  76. package/src/tileset/S3MTileset.ts +240 -3
  77. package/dist/browser/assets/s3mWorker-DHOVIrAU.js +0 -1
@@ -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
 
@@ -304,10 +305,14 @@ export type BuildTileFn = (
304
305
  colorPipeline?: 'linear' | 'direct';
305
306
  /** 纹理创建参数(透传给 buildTileObjects → createTexture) */
306
307
  textureOptions?: TextureCreateOptions;
308
+ bakeMercator?: boolean;
309
+ tileAnchor?: { lon: number; lat: number; height?: number };
310
+ tileCenter?: { x: number; y: number; z?: number };
307
311
  }
308
312
  ) => Promise<{ group: Group; stats: Partial<Stats> }>;
309
313
 
310
314
  export interface S3MTilesetOptions {
315
+ bakeMercator?: boolean;
311
316
  configUrl: string;
312
317
  maxTiles?: number;
313
318
  maxConcurrent?: number;
@@ -388,6 +393,18 @@ export interface S3MTilesetOptions {
388
393
  * 'silent' —— 只记录到 `stats.degradations`,由 UI 自己决定怎么提示。
389
394
  */
390
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 };
391
408
  }
392
409
 
393
410
  /** 估算单个纹理占用的显存字节数 */
@@ -505,6 +522,10 @@ export class S3MTileset {
505
522
  */
506
523
  maxMemory: number;
507
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>();
508
529
 
509
530
  /** 数据集配置声明的高度范围(米),若配置未声明则返回 null */
510
531
  get heightRange(): { min: number; max: number } | null {
@@ -535,8 +556,16 @@ export class S3MTileset {
535
556
  /** 请求量的硬上限(软上限 maxTiles 的超额余量),见构造函数注释 */
536
557
  hardCap = 96;
537
558
  /** 本数据集是 S3M 1.x(SuperMapCache,.s3m):子瓦地址规则与 3.x 不同 */
538
- private isS3MV1 = false;
559
+ isS3MV1 = false;
539
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
+ }
540
569
  private readonly _polygonOffset: { factor: number; units: number } | undefined;
541
570
  private _materialQuality: 'pbr' | 'lambert' | undefined;
542
571
  private _colorPipeline: 'linear' | 'direct' | undefined;
@@ -550,6 +579,11 @@ export class S3MTileset {
550
579
  constructor(options: S3MTilesetOptions) {
551
580
  this.configUrl = options.configUrl;
552
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
+ };
553
587
  this._onTile = options.onTile ?? (() => {});
554
588
  this._onTileRemoved = options.onTileRemoved ?? (() => {});
555
589
  this._onError = options.onError ?? ((e) => console.warn(e));
@@ -560,6 +594,7 @@ export class S3MTileset {
560
594
 
561
595
  // 渲染层可注入:Node 环境下不需要 three,也能跑 URL / 解析链路
562
596
  this._buildTile = options.buildTile ?? null;
597
+ this._bakeMercator = options.bakeMercator;
563
598
  this._polygonOffset = options.polygonOffset;
564
599
  this._materialQuality = options.materialQuality;
565
600
  this._colorPipeline = options.colorPipeline;
@@ -903,6 +938,9 @@ export class S3MTileset {
903
938
  if (refined !== allowRefined) continue;
904
939
  // 正在显示且没有可见子瓦接手 → 踢了就是洞
905
940
  if (!allowUncovered && node.group.visible) {
941
+ if (this.lodTransition === 'incremental') {
942
+ continue;
943
+ }
906
944
  const covered = childUrls.some((u) => {
907
945
  const c = this.nodes.get(u);
908
946
  return !!c && c.group.visible;
@@ -913,6 +951,15 @@ export class S3MTileset {
913
951
  victim = node;
914
952
  continue;
915
953
  }
954
+ // 可见性优先于时间戳:没有包围球的瓦片算不出距离,`lastSeen` 天然比别的瓦片旧,
955
+ // 只看时间戳会把「正在显示」的那块先踢掉(开洞 → 回补 → 再踢 = 闪)。
956
+ // 只有不可见的候选分不出来时,才轮到 lastSeen / distance 决定。
957
+ const nodeVisible = node.group.visible ? 1 : 0;
958
+ const victimVisible = victim.group.visible ? 1 : 0;
959
+ if (nodeVisible !== victimVisible) {
960
+ if (nodeVisible < victimVisible) victim = node;
961
+ continue;
962
+ }
916
963
  const a = node.lastSeen ?? -1;
917
964
  const b = victim.lastSeen ?? -1;
918
965
  if (a < b || (a === b && (node.distance ?? 0) > (victim.distance ?? 0))) victim = node;
@@ -1350,6 +1397,11 @@ export class S3MTileset {
1350
1397
  // 解析成功但能力不完整的(目前只有 3DGS),在花力气建几何之前先决定处置
1351
1398
  this._noteDegradations(tile);
1352
1399
 
1400
+ const lodForBake = this._pickLod(tile);
1401
+ if (this.rootRadius === undefined && lodForBake?.boundingSphere?.radius) {
1402
+ this.rootRadius = lodForBake.boundingSphere.radius;
1403
+ }
1404
+ const shouldBake = this.shouldBakeMercator(lodForBake?.boundingSphere?.radius);
1353
1405
  const build = await this._builder();
1354
1406
  const buildRes = await build(tile, {
1355
1407
  textureCache: this.textureCache,
@@ -1359,6 +1411,9 @@ export class S3MTileset {
1359
1411
  materialQuality: this._materialQuality,
1360
1412
  colorPipeline: this._colorPipeline,
1361
1413
  textureOptions: this._textureOptions,
1414
+ bakeMercator: shouldBake,
1415
+ tileAnchor: this.anchor,
1416
+ tileCenter: lodForBake?.boundingSphere?.center,
1362
1417
  });
1363
1418
  const { group, stats } = buildRes;
1364
1419
  const texKeys = (buildRes as any).textureKeys ?? (group.userData?.textureKeys as string[] | undefined) ?? Object.keys(tile.textures ?? {});
@@ -1467,6 +1522,11 @@ export class S3MTileset {
1467
1522
 
1468
1523
  for (const node of this.nodes.values()) {
1469
1524
  if (!node.sphere) {
1525
+ // 没有包围球(S3M 1.x 叶子瓦片:XML 是 `<PageLods><Geode>`,没有 PagedLOD)时
1526
+ // 算不出距离,但**仍要记「最近用过」**:`lastSeen` 恒为 undefined 会让它在
1527
+ // `_pickVictim` 里永远排最前(`?? -1`),硬上限一触发 `allowUncovered`,
1528
+ // 第一块被踢掉的就是正在显示的那块 —— 画面立刻一个洞,下一帧又加载回来。
1529
+ node.lastSeen = this._frame;
1470
1530
  node.needRefine = false;
1471
1531
  continue;
1472
1532
  }
@@ -1570,10 +1630,21 @@ export class S3MTileset {
1570
1630
  if (!p || !this.nodes.has(p)) roots.add(url);
1571
1631
  }
1572
1632
 
1573
- for (const rootUrl of roots) {
1574
- if (this._updateNodeVisibility(rootUrl, true, visited)) {
1633
+ if (this.lodTransition === 'incremental') {
1634
+ if (this._updateVisibilityIncremental(roots, visited)) {
1575
1635
  hasChanges = true;
1576
1636
  }
1637
+ } else {
1638
+ for (const rootUrl of roots) {
1639
+ if (this._updateNodeVisibility(rootUrl, true, visited)) {
1640
+ hasChanges = true;
1641
+ }
1642
+ }
1643
+ this.primaryNodes.clear();
1644
+ for (const [url, node] of this.nodes) {
1645
+ if (node.group.visible) this.primaryNodes.add(url);
1646
+ }
1647
+ this.fallbackNodes.clear();
1577
1648
  }
1578
1649
 
1579
1650
  // 防御:若有任何未被 root 树访问到的游离瓦片,全部隐藏
@@ -1583,6 +1654,9 @@ export class S3MTileset {
1583
1654
  node.group.visible = false;
1584
1655
  hasChanges = true;
1585
1656
  }
1657
+ if (this.lodTransition === 'incremental') {
1658
+ this._restoreNodePolygonOffset(node);
1659
+ }
1586
1660
  }
1587
1661
  }
1588
1662
 
@@ -1674,6 +1748,169 @@ export class S3MTileset {
1674
1748
  return changed;
1675
1749
  }
1676
1750
 
1751
+
1752
+ getLodTransition(): 'binary' | 'incremental' {
1753
+ return this.lodTransition;
1754
+ }
1755
+
1756
+ setLodTransition(mode: 'binary' | 'incremental'): void {
1757
+ if (this.lodTransition === mode) return;
1758
+ this.lodTransition = mode;
1759
+ if (mode === 'binary') {
1760
+ for (const node of this.nodes.values()) {
1761
+ this._restoreNodePolygonOffset(node);
1762
+ }
1763
+ this.fallbackNodes.clear();
1764
+ }
1765
+ }
1766
+
1767
+ private _applyNodeFallbackPolygonOffset(node: TileNode, depth: number): boolean {
1768
+ const factor = this.fallbackOffset.factor;
1769
+ const units = Math.max(2, (10 - Math.min(depth, 8)) * this.fallbackOffset.units);
1770
+ let changed = false;
1771
+
1772
+ node.group.traverse((obj) => {
1773
+ const target = obj as { isMesh?: boolean; isPoints?: boolean; isLine?: boolean; material?: any };
1774
+ if ((target.isMesh || target.isPoints || target.isLine) && target.material) {
1775
+ const materials = Array.isArray(target.material) ? target.material : [target.material];
1776
+ for (const mat of materials) {
1777
+ if (!mat) continue;
1778
+ if (!mat.userData) mat.userData = {};
1779
+ if (!mat.userData._basePolygonOffset) {
1780
+ mat.userData._basePolygonOffset = {
1781
+ polygonOffset: mat.polygonOffset === true,
1782
+ factor: mat.polygonOffsetFactor ?? 0,
1783
+ units: mat.polygonOffsetUnits ?? 0,
1784
+ };
1785
+ }
1786
+ const base = mat.userData._basePolygonOffset;
1787
+ const targetFactor = base.factor + factor;
1788
+ const targetUnits = base.units + units;
1789
+ if (
1790
+ mat.polygonOffset !== true ||
1791
+ mat.polygonOffsetFactor !== targetFactor ||
1792
+ mat.polygonOffsetUnits !== targetUnits
1793
+ ) {
1794
+ mat.polygonOffset = true;
1795
+ mat.polygonOffsetFactor = targetFactor;
1796
+ mat.polygonOffsetUnits = targetUnits;
1797
+ changed = true;
1798
+ }
1799
+ }
1800
+ }
1801
+ });
1802
+ return changed;
1803
+ }
1804
+
1805
+ private _restoreNodePolygonOffset(node: TileNode): boolean {
1806
+ let changed = false;
1807
+ node.group.traverse((obj) => {
1808
+ const target = obj as { isMesh?: boolean; isPoints?: boolean; isLine?: boolean; material?: any };
1809
+ if ((target.isMesh || target.isPoints || target.isLine) && target.material) {
1810
+ const materials = Array.isArray(target.material) ? target.material : [target.material];
1811
+ for (const mat of materials) {
1812
+ if (mat?.userData?._basePolygonOffset) {
1813
+ const base = mat.userData._basePolygonOffset;
1814
+ if (
1815
+ mat.polygonOffset !== base.polygonOffset ||
1816
+ mat.polygonOffsetFactor !== base.factor ||
1817
+ mat.polygonOffsetUnits !== base.units
1818
+ ) {
1819
+ mat.polygonOffset = base.polygonOffset;
1820
+ mat.polygonOffsetFactor = base.factor;
1821
+ mat.polygonOffsetUnits = base.units;
1822
+ changed = true;
1823
+ }
1824
+ }
1825
+ }
1826
+ }
1827
+ });
1828
+ return changed;
1829
+ }
1830
+
1831
+ private _updateVisibilityIncremental(roots: Iterable<string>, visited: Set<string>): boolean {
1832
+ const nextPrimary = new Set<string>();
1833
+ const nextFallback = new Set<string>();
1834
+ const nodeDepths = new Map<string, number>();
1835
+
1836
+ const traverse = (url: string, depth: number) => {
1837
+ if (visited.has(url)) return;
1838
+ visited.add(url);
1839
+ nodeDepths.set(url, depth);
1840
+
1841
+ const node = this.nodes.get(url);
1842
+ if (!node) return;
1843
+
1844
+ const childUrls = node.childUrls ?? (node.childUrl ? [node.childUrl] : []);
1845
+ const hasChildren = childUrls.length > 0;
1846
+ const readyChildren = childUrls.filter((cUrl) => this.nodes.has(cUrl));
1847
+ const missingChildren = childUrls.filter(
1848
+ (cUrl) => !this.nodes.has(cUrl) && !this.failed.has(cUrl)
1849
+ );
1850
+
1851
+ const canRefine = !!node.needRefine && hasChildren && readyChildren.length > 0;
1852
+
1853
+ if (canRefine) {
1854
+ if (missingChildren.length > 0) {
1855
+ nextFallback.add(url);
1856
+ }
1857
+ for (const cUrl of readyChildren) {
1858
+ traverse(cUrl, depth + 1);
1859
+ }
1860
+ } else {
1861
+ nextPrimary.add(url);
1862
+ }
1863
+ };
1864
+
1865
+ for (const rootUrl of roots) {
1866
+ traverse(rootUrl, 0);
1867
+ }
1868
+
1869
+ let changed = false;
1870
+
1871
+ for (const [url, node] of this.nodes) {
1872
+ if (!visited.has(url)) continue;
1873
+
1874
+ if (nextPrimary.has(url)) {
1875
+ if (!node.group.visible) {
1876
+ node.group.visible = true;
1877
+ changed = true;
1878
+ }
1879
+ const depth = nodeDepths.get(url) ?? 0;
1880
+ const targetOrder = 100 + depth;
1881
+ if (node.group.renderOrder !== targetOrder) {
1882
+ node.group.renderOrder = targetOrder;
1883
+ changed = true;
1884
+ }
1885
+ if (this._restoreNodePolygonOffset(node)) changed = true;
1886
+ } else if (nextFallback.has(url)) {
1887
+ if (!node.group.visible) {
1888
+ node.group.visible = true;
1889
+ changed = true;
1890
+ }
1891
+ const depth = nodeDepths.get(url) ?? 0;
1892
+ const targetOrder = depth;
1893
+ if (node.group.renderOrder !== targetOrder) {
1894
+ node.group.renderOrder = targetOrder;
1895
+ changed = true;
1896
+ }
1897
+ if (this._applyNodeFallbackPolygonOffset(node, depth)) changed = true;
1898
+ } else {
1899
+ if (node.group.visible) {
1900
+ node.group.visible = false;
1901
+ changed = true;
1902
+ }
1903
+ if (this._restoreNodePolygonOffset(node)) changed = true;
1904
+ }
1905
+ }
1906
+
1907
+ this.primaryNodes.clear();
1908
+ for (const u of nextPrimary) this.primaryNodes.add(u);
1909
+ this.fallbackNodes.clear();
1910
+ for (const u of nextFallback) this.fallbackNodes.add(u);
1911
+
1912
+ return changed;
1913
+ }
1677
1914
  dispose(): void {
1678
1915
  this._workerClient?.dispose();
1679
1916
  for (const node of this.nodes.values()) {