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
@@ -0,0 +1,121 @@
1
+ /**
2
+ * 瓦片级放置(issue #12 / #17)。
3
+ *
4
+ * ## 问题
5
+ *
6
+ * 场景只有一个切平面:`_enuMatrix` 用**场景锚点纬度**的常量 `meterInMercator`
7
+ * 把整个场景缩放进 Web 墨卡托。但墨卡托的比例 `s(φ) = 1/(2πN·cos φ)` 随纬度变化,
8
+ * 且 WGS84 椭球下东西向 (N) 与南北向 (M) 曲率半径存在各向异性偏差(赤道 0.67%,中纬度约 0.4%)。
9
+ * 所以距锚点 `d` 处的真实尺度是 `s(φ₀ + Δφ)`,相对误差随距离累积。
10
+ *
11
+ * 数据集**原点**是精确的(`layoutDataset` 用精确墨卡托差除以常量 s,两者互逆),
12
+ * 错的是数据集**内部**每一个顶点——它们被同一个常量缩放,于是整块模型被整体
13
+ * 放大/缩小,误差随离锚点的距离线性累积。
14
+ *
15
+ * ## 做法
16
+ *
17
+ * 每块瓦片用自己的中心经纬度算东西向与南北向尺度,并把它的几何绕自己的中心各向异性缩放:
18
+ *
19
+ * ```
20
+ * A(v) = position + K · (v - pivot)
21
+ * K = diag(sEast_tile / sEast_scene, sNorth_tile / sNorth_scene, sEast_tile / sEast_scene)
22
+ * ```
23
+ *
24
+ * 其中 `position` 是瓦片中心在**数据集局部 ENU**(即 `entry.root` 坐标系)里的位置,
25
+ * 用精确墨卡托反解得到;`pivot` 是中心在数据集 ENU 里的坐标(`node.sphere.center`)。
26
+ *
27
+ * 渲染时 `_enuMatrix` 依旧作用在整个场景上,于是合成结果是中心精确落位,周边顶点按自己的尺度缩放。
28
+ */
29
+ import { worldFromLngLat, enuToLngLat, type GeoAdapter } from './GeoAdapter.js';
30
+
31
+ /** 归一化世界坐标 + 米尺度(`DatasetManager.sceneAnchor` 的形状) */
32
+ export interface SceneAnchorLike {
33
+ x: number;
34
+ y: number;
35
+ z: number;
36
+ meterInMercatorEast?: number;
37
+ meterInMercatorNorth?: number;
38
+ meterInMercatorUp?: number;
39
+ meterInMercator: number;
40
+ altitude?: number;
41
+ }
42
+
43
+ /** 数据集锚点(`Anchor` 的形状:经纬度 + 椭球高) */
44
+ export interface DatasetAnchorLike {
45
+ lon: number;
46
+ lat: number;
47
+ height: number;
48
+ }
49
+
50
+ export interface TilePlacementInput {
51
+ /** 瓦片中心在数据集局部 ENU 中的位置(米),即 `node.sphere.center` */
52
+ center: { x: number; y: number; z: number };
53
+ /** 数据集锚点 */
54
+ datasetAnchor: DatasetAnchorLike;
55
+ /** 场景锚点 */
56
+ sceneAnchor: SceneAnchorLike;
57
+ /** 数据集相对场景锚点的偏移(场景 ENU 米),即 `entry.offset` */
58
+ datasetOffset: readonly [number, number, number];
59
+ /** 宿主的坐标适配器;`null` = 标准 Web 墨卡托兜底 */
60
+ geo: GeoAdapter | null;
61
+ }
62
+
63
+ export interface TilePlacement {
64
+ /** 变换枢轴,数据集局部 ENU(米) */
65
+ pivot: readonly [number, number, number];
66
+ /** 枢轴落到的位置,**数据集局部** ENU(米)——父级 `entry.root` 会再加回 `datasetOffset` */
67
+ position: readonly [number, number, number];
68
+ /** 兼容旧代码:等同于 scaleEast */
69
+ scale: number;
70
+ /** 东向相对缩放 `sEast_tile / sEast_scene` */
71
+ scaleEast: number;
72
+ /** 北向相对缩放 `sNorth_tile / sNorth_scene` */
73
+ scaleNorth: number;
74
+ /** 逐瓦片逆尺度 (1 / sTileEast, 1 / sTileNorth, 1 / sTileUp),供烘焙着色器抵消各向异性缩放 */
75
+ invTileScale?: readonly [number, number, number];
76
+ }
77
+
78
+ /**
79
+ * 算一块瓦片的放置参数。
80
+ *
81
+ * 拿不到可用锚点时返回 `null`,调用方应保持瓦片原状(矩阵不动)——这保证
82
+ * 「数据不完整」时的表现与不开本功能时一致,而不是把瓦片甩到别处去。
83
+ */
84
+ export function computeTilePlacement(input: TilePlacementInput): TilePlacement | null {
85
+ const { center, datasetAnchor, sceneAnchor, datasetOffset, geo } = input;
86
+
87
+ const sSceneEast = sceneAnchor.meterInMercatorEast ?? sceneAnchor.meterInMercator;
88
+ const sSceneNorth = sceneAnchor.meterInMercatorNorth ?? sceneAnchor.meterInMercator;
89
+ if (!Number.isFinite(sSceneEast) || sSceneEast <= 0 || !Number.isFinite(sSceneNorth) || sSceneNorth <= 0) return null;
90
+ if (!Number.isFinite(center.x) || !Number.isFinite(center.y) || !Number.isFinite(center.z)) return null;
91
+
92
+ // 1) 瓦片中心 → 经纬度(高精度 WGS84 椭球反解)
93
+ const [lng, lat] = enuToLngLat(datasetAnchor.lon, datasetAnchor.lat, center.x, center.y);
94
+
95
+ // 2) 中心处的精确墨卡托位置 + 该处的真实尺度
96
+ const altitude = datasetAnchor.height + center.z;
97
+ const tMerc = worldFromLngLat(geo, lng, lat, altitude);
98
+ const sTileEast = tMerc.meterInMercatorEast ?? tMerc.meterInMercator;
99
+ const sTileNorth = tMerc.meterInMercatorNorth ?? tMerc.meterInMercator;
100
+ if (!Number.isFinite(sTileEast) || sTileEast <= 0 || !Number.isFinite(sTileNorth) || sTileNorth <= 0) return null;
101
+
102
+ // 3) 精确的场景 ENU 位置(与 `_enuMatrix` 互逆),再扣掉父级的偏移
103
+ const position: [number, number, number] = [
104
+ (tMerc.x - sceneAnchor.x) / sSceneEast - datasetOffset[0],
105
+ -(tMerc.y - sceneAnchor.y) / sSceneNorth - datasetOffset[1],
106
+ (tMerc.z - sceneAnchor.z) / sSceneEast - datasetOffset[2],
107
+ ];
108
+
109
+ const scaleEast = sTileEast / sSceneEast;
110
+ const scaleNorth = sTileNorth / sSceneNorth;
111
+ const sTileUp = (tMerc as any).meterInMercatorUp ?? sTileEast;
112
+
113
+ return {
114
+ pivot: [center.x, center.y, center.z],
115
+ position,
116
+ scale: scaleEast,
117
+ scaleEast,
118
+ scaleNorth,
119
+ invTileScale: [1.0 / sTileEast, 1.0 / sTileNorth, 1.0 / sTileUp],
120
+ };
121
+ }
@@ -98,6 +98,7 @@ export interface EdgeHost {
98
98
  renderer: THREE.WebGLRenderer | null;
99
99
  camera: THREE.Camera | null;
100
100
  readonly authenticate: AuthenticateFn | undefined;
101
+ resolveDepthMode?(): 'scene' | 'clear';
101
102
  requestRepaint(): void;
102
103
  }
103
104
 
@@ -219,6 +220,8 @@ export class EdgeDecorator {
219
220
  /** 一帧的 GPU 描边,必须画在主场景之后(依赖本帧已写好的深度) */
220
221
  renderGpuEdges(canvas: HTMLCanvasElement): void {
221
222
  if (!this._modes.gpu) return;
223
+ // clear 深度模式下宿主深度已丢失,GPU 描边会穿透地形,必须跳过
224
+ if (this.host.resolveDepthMode?.() === 'clear') return;
222
225
  const renderer = this.host.renderer;
223
226
  const scene = this.host.scene;
224
227
  const camera = this.host.camera;
@@ -49,11 +49,30 @@ export interface PickHost {
49
49
  readonly scene: THREE.Scene | null;
50
50
  readonly canvas: HTMLCanvasElement | null;
51
51
  readonly style: { floorHeight: number };
52
- readonly sceneAnchor: { x: number; y: number; z: number; meterInMercator: number; altitude?: number } | null;
52
+ readonly sceneAnchor: {
53
+ x: number;
54
+ y: number;
55
+ z: number;
56
+ meterInMercatorEast?: number;
57
+ meterInMercatorNorth?: number;
58
+ meterInMercator: number;
59
+ altitude?: number;
60
+ } | null;
53
61
  readonly datasets: Map<string, { name: string }>;
54
62
  readonly featureStyleManager: FeatureStyleManager;
55
63
  readonly smidMeshes: Map<number, Set<THREE.Mesh>>;
56
64
  lngLatFromWorld(x: number, y: number): [number, number];
65
+ /**
66
+ * 世界坐标 → **规范**场景 ENU(把瓦片级放置的缩放反解掉)。
67
+ *
68
+ * `perTilePlacement` 开启时瓦片几何是绕自身中心缩放 `k` 倍后摆到正确位置的,
69
+ * 命中点的世界坐标因此不再等于「数据本身的 ENU」。拾取要报的是坐标本身,
70
+ * 不能带渲染相对底图的缩放,否则同一个屋顶在 20 km 外会比 1 km 内偏出几米,
71
+ * 量算、贴地、外部标注全都跟着错。
72
+ *
73
+ * 未开启本功能、或网格不在已放置的瓦片里时**原样返回**(默认路径零副作用)。
74
+ */
75
+ toCanonicalScene(p: THREE.Vector3, mesh?: THREE.Object3D | null): THREE.Vector3;
57
76
  gpuPathPoseAt(smid: number): { pos: [number, number, number]; yaw: number } | null;
58
77
  isFeatureHidden(smid: number | null, batchId: number): boolean;
59
78
  }
@@ -219,8 +238,10 @@ export class PickService {
219
238
  if (box && this.host.sceneAnchor) {
220
239
  const v = new THREE.Vector3((box.minX + box.maxX) / 2, (box.minY + box.maxY) / 2, (box.minZ + box.maxZ) / 2);
221
240
  v.applyMatrix4(ref.mesh.matrixWorld);
222
- const s = this.host.sceneAnchor.meterInMercator;
223
- centerLngLat = this.host.lngLatFromWorld(v.x * s + this.host.sceneAnchor.x, -v.y * s + this.host.sceneAnchor.y);
241
+ const c = this.host.toCanonicalScene(v, ref.mesh);
242
+ const sEast = this.host.sceneAnchor.meterInMercatorEast ?? this.host.sceneAnchor.meterInMercator;
243
+ const sNorth = this.host.sceneAnchor.meterInMercatorNorth ?? this.host.sceneAnchor.meterInMercator;
244
+ centerLngLat = this.host.lngLatFromWorld(c.x * sEast + this.host.sceneAnchor.x, -c.y * sNorth + this.host.sceneAnchor.y);
224
245
  }
225
246
 
226
247
  // 精确命中点求交计算
@@ -237,7 +258,8 @@ export class PickService {
237
258
  const off = smid != null ? this.host.featureStyleManager.getFeatureEffectiveOffset(smid) : {};
238
259
  const trans = off.translation;
239
260
  const elev = off.elevation;
240
- const finalPos = hitPoint.clone();
261
+ // 先反解瓦片级放置,再叠单体位移 —— 位移是「米」,必须落在规范 ENU 里
262
+ const finalPos = this.host.toCanonicalScene(hitPoint, ref.mesh).clone();
241
263
 
242
264
  if (off.rotation) {
243
265
  const rotMat = resolveRotationMatrix(off.rotation);
@@ -256,8 +278,9 @@ export class PickService {
256
278
  else if (elev) finalPos.z += elev;
257
279
 
258
280
  point = [finalPos.x, finalPos.y, finalPos.z];
259
- const s = this.host.sceneAnchor.meterInMercator;
260
- lngLat = this.host.lngLatFromWorld(finalPos.x * s + this.host.sceneAnchor.x, -finalPos.y * s + this.host.sceneAnchor.y);
281
+ const sEast = this.host.sceneAnchor.meterInMercatorEast ?? this.host.sceneAnchor.meterInMercator;
282
+ const sNorth = this.host.sceneAnchor.meterInMercatorNorth ?? this.host.sceneAnchor.meterInMercator;
283
+ lngLat = this.host.lngLatFromWorld(finalPos.x * sEast + this.host.sceneAnchor.x, -finalPos.y * sNorth + this.host.sceneAnchor.y);
261
284
  altitude = finalPos.z + (this.host.sceneAnchor.altitude ?? 0);
262
285
  } else if (centerLngLat && box && this.host.sceneAnchor) {
263
286
  lngLat = centerLngLat;
@@ -321,16 +344,18 @@ export class PickService {
321
344
  floorHeight: this.host.style.floorHeight,
322
345
  });
323
346
 
324
- const s = anchor.meterInMercator;
347
+ const sEast = anchor.meterInMercatorEast ?? anchor.meterInMercator;
348
+ const sNorth = anchor.meterInMercatorNorth ?? anchor.meterInMercator;
325
349
  let point: [number, number, number] | null = null;
326
350
  let lngLat: [number, number] | null = null;
327
351
  let altitude: number | null = null;
328
352
  let centerLngLat: [number, number] | null = null;
329
353
 
330
354
  if (hitPoint) {
331
- point = [hitPoint.x, hitPoint.y, hitPoint.z];
332
- lngLat = this.host.lngLatFromWorld(hitPoint.x * s + anchor.x, -hitPoint.y * s + anchor.y);
333
- altitude = hitPoint.z + (anchor.altitude ?? 0);
355
+ const c = this.host.toCanonicalScene(hitPoint, mesh);
356
+ point = [c.x, c.y, c.z];
357
+ lngLat = this.host.lngLatFromWorld(c.x * sEast + anchor.x, -c.y * sNorth + anchor.y);
358
+ altitude = c.z + (anchor.altitude ?? 0);
334
359
  // 泼溅命中的就是一个高斯,没有「单体中心」可言,用命中点当中心
335
360
  centerLngLat = lngLat;
336
361
  } else if (box) {
@@ -339,8 +364,9 @@ export class PickService {
339
364
  (box.minY + box.maxY) / 2,
340
365
  (box.minZ + box.maxZ) / 2
341
366
  ).applyMatrix4(mesh.matrixWorld);
342
- centerLngLat = this.host.lngLatFromWorld(v.x * s + anchor.x, -v.y * s + anchor.y);
343
- altitude = v.z + (anchor.altitude ?? 0);
367
+ const c = this.host.toCanonicalScene(v, mesh);
368
+ centerLngLat = this.host.lngLatFromWorld(c.x * sEast + anchor.x, -c.y * sNorth + anchor.y);
369
+ altitude = c.z + (anchor.altitude ?? 0);
344
370
  }
345
371
 
346
372
  return {
@@ -395,8 +421,9 @@ export class PickService {
395
421
  const target = new THREE.Vector3();
396
422
  const intersect = ray.intersectPlane(groundPlane, target);
397
423
  if (intersect) {
398
- const s = this.host.sceneAnchor.meterInMercator;
399
- const lngLat = this.host.lngLatFromWorld(intersect.x * s + this.host.sceneAnchor.x, -intersect.y * s + this.host.sceneAnchor.y);
424
+ const sEast = this.host.sceneAnchor.meterInMercatorEast ?? this.host.sceneAnchor.meterInMercator;
425
+ const sNorth = this.host.sceneAnchor.meterInMercatorNorth ?? this.host.sceneAnchor.meterInMercator;
426
+ const lngLat = this.host.lngLatFromWorld(intersect.x * sEast + this.host.sceneAnchor.x, -intersect.y * sNorth + this.host.sceneAnchor.y);
400
427
  const altitude = intersect.z + (this.host.sceneAnchor.altitude ?? 0);
401
428
  return {
402
429
  lngLat,
@@ -643,8 +670,9 @@ export class PickService {
643
670
  if (gpuPose) c.add(new THREE.Vector3(gpuPose.pos[0], gpuPose.pos[1], gpuPose.pos[2]));
644
671
 
645
672
  c.applyMatrix4(mesh.matrixWorld);
646
- const s = this.host.sceneAnchor.meterInMercator;
647
- const [lng, lat] = this.host.lngLatFromWorld(c.x * s + this.host.sceneAnchor.x, -c.y * s + this.host.sceneAnchor.y);
673
+ const sEast = this.host.sceneAnchor.meterInMercatorEast ?? this.host.sceneAnchor.meterInMercator;
674
+ const sNorth = this.host.sceneAnchor.meterInMercatorNorth ?? this.host.sceneAnchor.meterInMercator;
675
+ const [lng, lat] = this.host.lngLatFromWorld(c.x * sEast + this.host.sceneAnchor.x, -c.y * sNorth + this.host.sceneAnchor.y);
648
676
  if (lng < minLng) minLng = lng;
649
677
  if (lng > maxLng) maxLng = lng;
650
678
  if (lat < minLat) minLat = lat;
@@ -77,7 +77,23 @@ export interface LightOptions {
77
77
  altitude?: number;
78
78
  }
79
79
 
80
+ /**
81
+ * 本层绘制前如何处理宿主已有的深度缓冲。
82
+ * - `'scene'` 保留(默认)。地形能遮挡本层几何,适用于建筑、管线等离散对象。
83
+ * - `'clear'` 先清深度再画,本层只与自己做深度测试。适用于贴地地面类数据(倾斜摄影、大范围 DEM 化模型等),消除共面 z-fighting。
84
+ * - `'auto'` 按数据集类型推断:有 ground 覆盖 / OSGB 倾斜摄影 / 地面类网格 → `'clear'`,否则 `'scene'`。
85
+ */
86
+ export type DepthMode = 'scene' | 'clear' | 'auto';
87
+
80
88
  export interface S3MViewerOptions {
89
+ /**
90
+ * 本层绘制前如何处理宿主已有的深度缓冲。
91
+ * - `'scene'` 保留(默认)。地形能遮挡本层几何,适用于建筑、管线等离散对象。
92
+ * - `'clear'` 先清深度再画,本层只与自己做深度测试。适用于贴地地面类数据。
93
+ * - `'auto'` 按数据集类型推断:有 ground 覆盖 / OSGB 倾斜摄影 / 地面类网格 → `'clear'`,否则 `'scene'`。
94
+ * 默认 `'scene'`,与改动前行为逐字一致。
95
+ */
96
+ depthMode?: DepthMode;
81
97
  /**
82
98
  * 光照预设:
83
99
  * - `'default'`(默认):现有 PBR 打光,亮部线性总量 ≈ 2.55;
@@ -134,6 +150,34 @@ export interface S3MViewerOptions {
134
150
  * 所以默认不启用,由接入方按设备档位自行决定。运行时可用 `setLodPixelRatioCap()` 调整。
135
151
  */
136
152
  lodPixelRatioCap?: number;
153
+ /**
154
+ * 瓦片级放置(默认 `true`)。
155
+ *
156
+ * 开启后每块瓦片用自己的中心纬度算 `meterInMercator`,几何绕自身中心缩放,
157
+ * 修正「全场景共用一个切平面」在大范围下的尺度误差。
158
+ *
159
+ * 误差量级(北纬 40°,距场景锚点的距离 → 相对尺度的偏差):
160
+ * `5 km → 0.065%`、`20 km → 0.26%(约 26 m)`、`200 km → 2.8%(约 2.8 km)`。
161
+ * 城市级(几公里)随手开无妨;多数据集相距几十公里、或单个数据集跨 20 km 以上时
162
+ * 必须开,否则远处的模型会整体偏大/偏小。
163
+ *
164
+ * 代价与遗留:
165
+ * - 每块瓦片的 `Object3D` 走 `matrixAutoUpdate = false`,多一次矩阵乘;
166
+ * - 场景 ENU 仍是**一个平面**,所以只是把误差按瓦片重新分摊(瓦片跨 2 km 时
167
+ * 剩余约 0.5 m),彻底消除要逐顶点烘焙墨卡托;
168
+ * - 拾取 / 量算的 `lngLat ↔ 场景 ENU` 换算仍用场景锚点的常量 s,在本模式下
169
+ * 保持修复前的误差量级(几何与反解不再自洽,但都不比之前差)。
170
+ *
171
+ * 见 `render/tilePlacement.ts` 的文件头。
172
+ */
173
+ perTilePlacement?: boolean;
174
+ /**
175
+ * 是否对瓦片逐顶点烘焙精确 Web 墨卡托偏移(消除全球大尺度下的视差游动/呼吸效应)。
176
+ * - undefined(默认):自动判定。当瓦片集根节点包围球半径 > 25 km 时自动启用,<= 25 km 时保持关闭(逐字节保持原有 ENU 顶点缓冲不变)。
177
+ * - true:强制开启。
178
+ * - false:强制关闭。
179
+ */
180
+ bakeMercator?: boolean;
137
181
  /**
138
182
  * 每个数据集驻留三角形总数的软上限(默认不限制)。
139
183
  * 见 `S3MTilesetOptions.maxTriangles`:瓦片数管不住的场景(精模、大面片)用这条刹车。
@@ -237,6 +281,13 @@ export interface S3MViewerOptions {
237
281
  * 编译着色器、卡一帧。预热在首帧之后异步做,期间跳一两帧渲染,肉眼不可见。
238
282
  */
239
283
  prewarmShading?: boolean;
284
+ /**
285
+ * LOD 细化过渡策略:
286
+ * - 'binary'(默认):整节点二值切换
287
+ * - 'incremental':增量细化(primary / fallback 双列表 + LEQUAL 深度推移)
288
+ */
289
+ lodTransition?: 'binary' | 'incremental';
290
+ fallbackOffset?: { factor?: number; units?: number };
240
291
  }
241
292
 
242
293
  /* ------------------------------------------------------------------ 内部类型 */