s3m-three 0.4.19 → 0.4.21

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 (87) hide show
  1. package/dist/browser/assets/{s3mWorker-4iTAmWIu.js → s3mWorker-DvAUA8NJ.js} +1 -1
  2. package/dist/browser/s3m-three.iife.ext-three.js +243 -243
  3. package/dist/browser/s3m-three.iife.js +227 -227
  4. package/dist/core/Draco.d.ts.map +1 -1
  5. package/dist/core/Draco.js +0 -1
  6. package/dist/core/Draco.js.map +1 -1
  7. package/dist/core/S3MBlockParser.d.ts.map +1 -1
  8. package/dist/core/S3MBlockParser.js +27 -14
  9. package/dist/core/S3MBlockParser.js.map +1 -1
  10. package/dist/core/SplatSort.d.ts +14 -1
  11. package/dist/core/SplatSort.d.ts.map +1 -1
  12. package/dist/core/SplatSort.js +51 -4
  13. package/dist/core/SplatSort.js.map +1 -1
  14. package/dist/core/TileGeometry.d.ts +10 -0
  15. package/dist/core/TileGeometry.d.ts.map +1 -1
  16. package/dist/core/TileGeometry.js +17 -14
  17. package/dist/core/TileGeometry.js.map +1 -1
  18. package/dist/render/DatasetManager.d.ts +3 -1
  19. package/dist/render/DatasetManager.d.ts.map +1 -1
  20. package/dist/render/DatasetManager.js +2 -1
  21. package/dist/render/DatasetManager.js.map +1 -1
  22. package/dist/render/FeatureStyleManager.d.ts.map +1 -1
  23. package/dist/render/GeometryBuilder.d.ts +8 -0
  24. package/dist/render/GeometryBuilder.d.ts.map +1 -1
  25. package/dist/render/GeometryBuilder.js +62 -7
  26. package/dist/render/GeometryBuilder.js.map +1 -1
  27. package/dist/render/S3MViewer.d.ts +106 -440
  28. package/dist/render/S3MViewer.d.ts.map +1 -1
  29. package/dist/render/S3MViewer.js +297 -1653
  30. package/dist/render/S3MViewer.js.map +1 -1
  31. package/dist/render/SplatSorter.d.ts.map +1 -1
  32. package/dist/render/SplatSorter.js +8 -1
  33. package/dist/render/SplatSorter.js.map +1 -1
  34. package/dist/render/viewer/EdgeDecorator.d.ts +206 -0
  35. package/dist/render/viewer/EdgeDecorator.d.ts.map +1 -0
  36. package/dist/render/viewer/EdgeDecorator.js +628 -0
  37. package/dist/render/viewer/EdgeDecorator.js.map +1 -0
  38. package/dist/render/viewer/HeightIndex.d.ts +36 -0
  39. package/dist/render/viewer/HeightIndex.d.ts.map +1 -0
  40. package/dist/render/viewer/HeightIndex.js +98 -0
  41. package/dist/render/viewer/HeightIndex.js.map +1 -0
  42. package/dist/render/viewer/LightRig.d.ts +88 -0
  43. package/dist/render/viewer/LightRig.d.ts.map +1 -0
  44. package/dist/render/viewer/LightRig.js +229 -0
  45. package/dist/render/viewer/LightRig.js.map +1 -0
  46. package/dist/render/viewer/PickService.d.ts +109 -0
  47. package/dist/render/viewer/PickService.d.ts.map +1 -0
  48. package/dist/render/viewer/PickService.js +591 -0
  49. package/dist/render/viewer/PickService.js.map +1 -0
  50. package/dist/render/viewer/SelectionDecorator.d.ts +84 -0
  51. package/dist/render/viewer/SelectionDecorator.d.ts.map +1 -0
  52. package/dist/render/viewer/SelectionDecorator.js +339 -0
  53. package/dist/render/viewer/SelectionDecorator.js.map +1 -0
  54. package/dist/render/viewer/SplatLayer.d.ts +26 -0
  55. package/dist/render/viewer/SplatLayer.d.ts.map +1 -0
  56. package/dist/render/viewer/SplatLayer.js +44 -0
  57. package/dist/render/viewer/SplatLayer.js.map +1 -0
  58. package/dist/render/viewer/types.d.ts +256 -0
  59. package/dist/render/viewer/types.d.ts.map +1 -0
  60. package/dist/render/viewer/types.js +3 -0
  61. package/dist/render/viewer/types.js.map +1 -0
  62. package/dist/tileset/S3MTileset.d.ts +30 -3
  63. package/dist/tileset/S3MTileset.d.ts.map +1 -1
  64. package/dist/tileset/S3MTileset.js +99 -5
  65. package/dist/tileset/S3MTileset.js.map +1 -1
  66. package/dist/worker/s3mWorker.js +1 -1
  67. package/dist/worker/s3mWorker.js.map +1 -1
  68. package/docs/API.md +9 -28
  69. package/docs/FORMAT-NOTES.md +38 -14
  70. package/package.json +1 -1
  71. package/src/core/Draco.ts +1 -2
  72. package/src/core/S3MBlockParser.ts +27 -16
  73. package/src/core/SplatSort.ts +67 -5
  74. package/src/core/TileGeometry.ts +24 -11
  75. package/src/render/DatasetManager.ts +5 -1
  76. package/src/render/FeatureStyleManager.ts +1 -1
  77. package/src/render/GeometryBuilder.ts +59 -7
  78. package/src/render/S3MViewer.ts +327 -1916
  79. package/src/render/SplatSorter.ts +8 -1
  80. package/src/render/viewer/EdgeDecorator.ts +722 -0
  81. package/src/render/viewer/HeightIndex.ts +94 -0
  82. package/src/render/viewer/LightRig.ts +267 -0
  83. package/src/render/viewer/PickService.ts +651 -0
  84. package/src/render/viewer/SelectionDecorator.ts +377 -0
  85. package/src/render/viewer/SplatLayer.ts +44 -0
  86. package/src/render/viewer/types.ts +264 -0
  87. package/src/tileset/S3MTileset.ts +106 -6
@@ -0,0 +1,94 @@
1
+ /**
2
+ * 世界高度范围的增量维护。
3
+ *
4
+ * 原来每注册一块瓦片都要把所有网格重新遍历一遍、每个都 clone 一个 Box3 再乘矩阵,
5
+ * 加载 n 块瓦片就是 O(n²) 次矩阵乘 + n² 个垃圾对象。现在只在网格进出的记一次,
6
+ * 全局值从缓存里滚:只有被移除的那块正好卡在边界上时,才回退到一次 O(n) 重算。
7
+ *
8
+ * 结果写进共享 uniform `uHeightMin` / `uHeightMax`,供着色器做高度渐变 / 拔高。
9
+ */
10
+ import * as THREE from 'three';
11
+ import type { StyleUniforms } from '../../core/types.js';
12
+
13
+ export class HeightIndex {
14
+ /** 每块网格的世界高度范围 */
15
+ private readonly _meshHeight = new Map<THREE.Mesh, { min: number; max: number }>();
16
+ private _min = Infinity;
17
+ private _max = -Infinity;
18
+ /** 换算世界包围盒用的临时对象,避免每次 new */
19
+ private readonly _box = new THREE.Box3();
20
+
21
+ constructor(private readonly uniforms: StyleUniforms) {}
22
+
23
+ get size(): number {
24
+ return this._meshHeight.size;
25
+ }
26
+
27
+ add(mesh: THREE.Mesh): void {
28
+ const bb = mesh.geometry?.boundingBox;
29
+ if (!bb) return;
30
+ this._box.copy(bb).applyMatrix4(mesh.matrixWorld);
31
+ const min = this._box.min.z;
32
+ const max = this._box.max.z;
33
+ if (!Number.isFinite(min) || !Number.isFinite(max)) return;
34
+ this._meshHeight.set(mesh, { min, max });
35
+ if (min < this._min) this._min = min;
36
+ if (max > this._max) this._max = max;
37
+ this._push();
38
+ }
39
+
40
+ remove(mesh: THREE.Mesh): void {
41
+ const rec = this._meshHeight.get(mesh);
42
+ if (!rec) return;
43
+ this._meshHeight.delete(mesh);
44
+ // 只有正好卡在边界上才需要重算,否则滚动的 min/max 依然成立
45
+ if (rec.min === this._min || rec.max === this._max) this._recompute();
46
+ else this._push();
47
+ }
48
+
49
+ /** 数据集整体位移(换高程偏移 / 重新布局)后,缓存的世界高度全部失效 */
50
+ refreshAll(): void {
51
+ if (this._meshHeight.size === 0) return;
52
+ const meshes = [...this._meshHeight.keys()];
53
+ this._meshHeight.clear();
54
+ this._min = Infinity;
55
+ this._max = -Infinity;
56
+ for (const mesh of meshes) this.add(mesh);
57
+ this._push();
58
+ }
59
+
60
+ /** 场景清空时重置 */
61
+ clear(): void {
62
+ this._meshHeight.clear();
63
+ this._min = Infinity;
64
+ this._max = -Infinity;
65
+ }
66
+
67
+ /** 当前高度范围(未初始化时是 Infinity / -Infinity,调用方自己兜底) */
68
+ get range(): { min: number; max: number } {
69
+ return { min: this._min, max: this._max };
70
+ }
71
+
72
+ private _recompute(): void {
73
+ let min = Infinity;
74
+ let max = -Infinity;
75
+ for (const h of this._meshHeight.values()) {
76
+ if (h.min < min) min = h.min;
77
+ if (h.max > max) max = h.max;
78
+ }
79
+ this._min = min;
80
+ this._max = max;
81
+ this._push();
82
+ }
83
+
84
+ private _push(): void {
85
+ let min = this._min;
86
+ let max = this._max;
87
+ if (!Number.isFinite(min) || !Number.isFinite(max) || max <= min) {
88
+ min = 0;
89
+ max = 100;
90
+ }
91
+ this.uniforms.uHeightMin.value = min;
92
+ this.uniforms.uHeightMax.value = max;
93
+ }
94
+ }
@@ -0,0 +1,267 @@
1
+ /**
2
+ * 光照与色调映射:四个光源 + 光照预设 + 环境贴图。
3
+ *
4
+ * 从 `S3MViewer` 里整体搬出来的第一块 —— 它只依赖 `scene` / `renderer` / 共享
5
+ * uniform / 宿主方位角,不碰几何、瓦片、拾取,是耦合最低的一块。
6
+ *
7
+ * ## 两个模块级全局态
8
+ *
9
+ * `setColorPipeline` / `setSrgbTextureDecode` / `setAnisotropyCap` 都是模块级设置,
10
+ * 影响**之后创建**的材质与纹理。所以切预设时已加载的瓦片要重载才换观感,
11
+ * 这是已知的架构债(一个页面两个 viewer 用不同预设会互相污染),
12
+ * 拆分没有改变这一点。
13
+ */
14
+ import * as THREE from 'three';
15
+ import { resolveLightingPreset, type LightingPreset } from '../lightingPreset.js';
16
+ import { setColorPipeline } from '../StyleMaterial.js';
17
+ import { setAnisotropyCap, setSrgbTextureDecode } from '../TextureFactory.js';
18
+ import type { LightOptions, S3MViewerOptions } from '../S3MViewer.js';
19
+ import type { StyleUniforms } from '../../core/types.js';
20
+
21
+ /** 四个光源的强度 / 颜色 / 朝向。`mainIntensity` / `mainColor` 是 keyLight 的别名 */
22
+ type LightState = Required<Omit<LightOptions, 'mainIntensity' | 'mainColor'>>;
23
+
24
+ const DEFAULT_LIGHT: LightState = {
25
+ ambientIntensity: 0.4,
26
+ ambientColor: 0xffffff,
27
+ hemisphereIntensity: 0.8,
28
+ hemisphereColor: 0xffffff,
29
+ hemisphereGroundColor: 0xb6c0cb,
30
+ keyLightIntensity: 1.0,
31
+ keyLightColor: 0xffffff,
32
+ fillLightIntensity: 0.35,
33
+ fillLightColor: 0xffffff,
34
+ mode: 'headlamp',
35
+ azimuth: 150,
36
+ altitude: 50,
37
+ };
38
+
39
+ /**
40
+ * 宿主侧依赖。全部用取值函数而不是对象引用 —— 宿主可能在 mount 之前构造 viewer,
41
+ * 而 `scene` / `renderer` 在 mount 时才存在,存快照会拿到 null。
42
+ */
43
+ export interface LightRigContext {
44
+ scene: () => THREE.Scene | null;
45
+ renderer: () => THREE.WebGLRenderer | null;
46
+ uniforms: StyleUniforms;
47
+ options: S3MViewerOptions;
48
+ /** 地图方位角(度),让主光跟着视角走;纯 three 宿主可以不传 */
49
+ getBearing?: () => number | undefined;
50
+ /** 宿主需要重画的信号 */
51
+ repaint: () => void;
52
+ }
53
+
54
+ export class LightRig {
55
+ ambient: THREE.AmbientLight | null = null;
56
+ hemi: THREE.HemisphereLight | null = null;
57
+ key: THREE.DirectionalLight | null = null;
58
+ fill: THREE.DirectionalLight | null = null;
59
+
60
+ private readonly _state: LightState;
61
+ /** 环境贴图的 PMREM 渲染目标(setEnvironment 建的,dispose 时释放) */
62
+ private _envRT: THREE.WebGLRenderTarget | null = null;
63
+
64
+ /** @internal 环境贴图 RT,供单测观察「有没有释放旧的」 */
65
+ get envRT(): THREE.WebGLRenderTarget | null {
66
+ return this._envRT;
67
+ }
68
+ set envRT(v: THREE.WebGLRenderTarget | null) {
69
+ this._envRT = v;
70
+ }
71
+
72
+ constructor(private readonly ctx: LightRigContext) {
73
+ this._state = { ...DEFAULT_LIGHT };
74
+ const options = ctx.options;
75
+ if (options.lightingPreset && options.lightingPreset !== 'default') {
76
+ Object.assign(this._state, resolveLightingPreset(options.lightingPreset));
77
+ this.applyColorPipeline(options.lightingPreset);
78
+ }
79
+ if (options.light) Object.assign(this._state, options.light);
80
+ }
81
+
82
+ /* ---------------------------------------------------------------- 场景挂载 */
83
+
84
+ private _create(): void {
85
+ this.ambient = new THREE.AmbientLight(this._state.ambientColor as THREE.ColorRepresentation, this._state.ambientIntensity);
86
+ this.hemi = new THREE.HemisphereLight(
87
+ this._state.hemisphereColor as THREE.ColorRepresentation,
88
+ this._state.hemisphereGroundColor as THREE.ColorRepresentation,
89
+ this._state.hemisphereIntensity
90
+ );
91
+ this.key = new THREE.DirectionalLight(this._state.keyLightColor as THREE.ColorRepresentation, this._state.keyLightIntensity);
92
+ this.fill = new THREE.DirectionalLight(this._state.fillLightColor as THREE.ColorRepresentation, this._state.fillLightIntensity);
93
+ this.update();
94
+ }
95
+
96
+ /** 把四个光源挂进场景;重复调用是幂等的 */
97
+ addToScene(): void {
98
+ const scene = this.ctx.scene();
99
+ if (!scene) return;
100
+ if (!this.ambient) this._create();
101
+ scene.add(this.ambient!, this.hemi!, this.key!, this.fill!);
102
+ }
103
+
104
+ /** 光源实例是否已建(构造后、mount 前是 false) */
105
+ get created(): boolean {
106
+ return this.ambient !== null;
107
+ }
108
+
109
+ /* ---------------------------------------------------------------- 参数 */
110
+
111
+ private _applyOptions(): void {
112
+ const s = this._state;
113
+ if (this.ambient) {
114
+ this.ambient.intensity = s.ambientIntensity;
115
+ this.ambient.color.set(s.ambientColor as THREE.ColorRepresentation);
116
+ }
117
+ if (this.hemi) {
118
+ this.hemi.intensity = s.hemisphereIntensity;
119
+ this.hemi.color.set(s.hemisphereColor as THREE.ColorRepresentation);
120
+ this.hemi.groundColor.set(s.hemisphereGroundColor as THREE.ColorRepresentation);
121
+ }
122
+ if (this.key) {
123
+ this.key.intensity = s.keyLightIntensity;
124
+ this.key.color.set(s.keyLightColor as THREE.ColorRepresentation);
125
+ }
126
+ if (this.fill) {
127
+ this.fill.intensity = s.fillLightIntensity;
128
+ this.fill.color.set(s.fillLightColor as THREE.ColorRepresentation);
129
+ }
130
+ }
131
+
132
+ /**
133
+ * 更新光照位置与方向:
134
+ * - `'headlamp'`:随地图方位角 bearing 旋转,主光始终在镜头上方,受光面面向镜头;
135
+ * - `'fixed'`:固定世界方位角(如模拟恒定日光)。
136
+ */
137
+ update(): void {
138
+ if (!this.key || !this.fill) return;
139
+ const s = this._state;
140
+ const bearing = s.mode === 'fixed' ? 0 : ((this.ctx.getBearing?.() ?? 0) * Math.PI) / 180;
141
+ const place = (light: THREE.DirectionalLight | null, azOffsetDeg: number, elevDeg: number): void => {
142
+ if (!light) return;
143
+ const az = bearing + (azOffsetDeg * Math.PI) / 180;
144
+ const el = (elevDeg * Math.PI) / 180;
145
+ const c = Math.cos(el);
146
+ light.position.set(Math.sin(az) * c, Math.cos(az) * c, Math.sin(el)).multiplyScalar(1000);
147
+ };
148
+ place(this.key, s.azimuth, s.altitude);
149
+ place(this.fill, s.azimuth + 65, Math.max(10, s.altitude - 20));
150
+ // direct 管线的官方 Lambert 用主光方向(共享 uniform,所有材质同步生效)
151
+ const sun = this.ctx.uniforms?.uSunDirection;
152
+ if (sun) (sun.value as THREE.Vector3).copy(this.key.position).normalize();
153
+ }
154
+
155
+ /**
156
+ * supermap 完整版 = 光照强度 + 颜色管线一起切:
157
+ * direct 管线下纹理不做 sRGB 解码、片元按官方常数 Lambert 直乘(见 lightingPreset.ts)。
158
+ * 两个都是模块级全局态,只影响之后创建的材质 / 纹理 —— 已加载瓦片要重载才换。
159
+ */
160
+ applyColorPipeline(preset: LightingPreset): void {
161
+ const direct = preset === 'supermap';
162
+ setColorPipeline(direct ? 'direct' : 'linear');
163
+ setSrgbTextureDecode(!direct);
164
+ // 官方不开各向异性:远处直接采高 mip 平均成纯色("灰色滤镜"的一部分)。
165
+ // 我们默认 Infinity(倾斜摄影立面清晰度优先),supermap 模式必须收回。
166
+ setAnisotropyCap(direct ? 1 : Infinity);
167
+ }
168
+
169
+ setLightingPreset(preset: LightingPreset): void {
170
+ Object.assign(this._state, resolveLightingPreset(preset));
171
+ this.applyColorPipeline(preset);
172
+ this.ctx.options.lightingPreset = preset;
173
+ this._applyOptions();
174
+ this.update();
175
+ this.ctx.repaint();
176
+ }
177
+
178
+ setLight(options: Partial<LightOptions>): void {
179
+ const normalized: Partial<LightOptions> = { ...options };
180
+ if (normalized.mainColor !== undefined && normalized.keyLightColor === undefined) {
181
+ normalized.keyLightColor = normalized.mainColor;
182
+ }
183
+ if (normalized.mainIntensity !== undefined && normalized.keyLightIntensity === undefined) {
184
+ normalized.keyLightIntensity = normalized.mainIntensity;
185
+ }
186
+ Object.assign(this._state, normalized);
187
+ this._applyOptions();
188
+ this.update();
189
+ this.ctx.repaint();
190
+ }
191
+
192
+ getLight(): LightOptions {
193
+ return { ...this._state };
194
+ }
195
+
196
+ setToneMapping(toneMapping: THREE.ToneMapping, exposure = 1.0): void {
197
+ this.ctx.options.toneMapping = toneMapping;
198
+ this.ctx.options.toneMappingExposure = exposure;
199
+ const r = this.ctx.renderer();
200
+ if (r) {
201
+ r.toneMapping = toneMapping;
202
+ r.toneMappingExposure = exposure;
203
+ this.ctx.repaint();
204
+ }
205
+ }
206
+
207
+ getToneMapping(): { toneMapping: THREE.ToneMapping; toneMappingExposure: number } {
208
+ const r = this.ctx.renderer();
209
+ return {
210
+ toneMapping: r ? r.toneMapping : (this.ctx.options.toneMapping ?? THREE.NoToneMapping),
211
+ toneMappingExposure: r ? r.toneMappingExposure : (this.ctx.options.toneMappingExposure ?? 1.0),
212
+ };
213
+ }
214
+
215
+ /* ---------------------------------------------------------------- 环境贴图 */
216
+
217
+ /**
218
+ * 挂 / 摘环境贴图。
219
+ *
220
+ * 金属度(`setStyle({ metalness })`)**必须**有环境图才看得出质感:
221
+ * PBR 的金属几乎没有漫反射,画面全靠 `scene.environment` 里的环境反射,
222
+ * 没有它时 metalness 一高就整片发黑,只剩两盏平行灯打成的小高光点。
223
+ *
224
+ * @param source 等距柱状图(.hdr / .jpg / .png,2:1)地址,或已加载的 `THREE.Texture`;
225
+ * `null` = 关掉环境图
226
+ * @param intensity 环境光强度(`scene.environmentIntensity`,默认 1)
227
+ *
228
+ * 传入 URL 时纹理由内部加载并转成 PMREM(预滤波 mipmap);传 Texture 时不会接管它的生命周期。
229
+ */
230
+ async setEnvironment(source: string | THREE.Texture | null, intensity = 1): Promise<void> {
231
+ const renderer = this.ctx.renderer();
232
+ const scene = this.ctx.scene();
233
+ if (!renderer || !scene) return;
234
+ scene.environmentIntensity = Math.max(0, intensity);
235
+
236
+ if (source == null) {
237
+ scene.environment = null;
238
+ this._envRT?.dispose();
239
+ this._envRT = null;
240
+ this.ctx.repaint();
241
+ return;
242
+ }
243
+
244
+ const fromUrl = typeof source === 'string';
245
+ const tex = fromUrl ? await new THREE.TextureLoader().loadAsync(source) : source;
246
+ // PMREM 只认等距柱状图;普通贴图也按 2:1 全景处理
247
+ tex.mapping = THREE.EquirectangularReflectionMapping;
248
+
249
+ const pmrem = new THREE.PMREMGenerator(renderer);
250
+ pmrem.compileEquirectangularShader();
251
+ const rt = pmrem.fromEquirectangular(tex);
252
+ pmrem.dispose();
253
+ if (fromUrl) tex.dispose();
254
+
255
+ scene.environment = rt.texture;
256
+ // 上一张(同一个 viewer 反复换)要释放,否则显存里会一直堆
257
+ this._envRT?.dispose();
258
+ this._envRT = rt;
259
+ this.ctx.repaint();
260
+ }
261
+
262
+ /** 释放环境贴图与光源(unmount 时调) */
263
+ dispose(): void {
264
+ this._envRT?.dispose();
265
+ this._envRT = null;
266
+ }
267
+ }