s3m-three 0.3.1 → 0.3.3
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/s3m-three.iife.ext-three.js +3 -3
- package/dist/browser/s3m-three.iife.js +3 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/render/DatasetManager.d.ts +11 -0
- package/dist/render/DatasetManager.d.ts.map +1 -1
- package/dist/render/DatasetManager.js +63 -0
- package/dist/render/DatasetManager.js.map +1 -1
- package/dist/render/GeometryBuilder.d.ts +1 -0
- package/dist/render/GeometryBuilder.d.ts.map +1 -1
- package/dist/render/GeometryBuilder.js +4 -1
- package/dist/render/GeometryBuilder.js.map +1 -1
- package/dist/render/S3MViewer.d.ts +70 -3
- package/dist/render/S3MViewer.d.ts.map +1 -1
- package/dist/render/S3MViewer.js +208 -58
- package/dist/render/S3MViewer.js.map +1 -1
- package/dist/tileset/S3MTileset.d.ts +26 -1
- package/dist/tileset/S3MTileset.d.ts.map +1 -1
- package/dist/tileset/S3MTileset.js +91 -13
- package/dist/tileset/S3MTileset.js.map +1 -1
- package/docs/API.md +40 -2
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/render/DatasetManager.ts +65 -0
- package/src/render/GeometryBuilder.ts +5 -1
- package/src/render/S3MViewer.ts +249 -57
- package/src/tileset/S3MTileset.ts +104 -14
package/dist/render/S3MViewer.js
CHANGED
|
@@ -109,8 +109,24 @@ export class S3MViewer {
|
|
|
109
109
|
_needsRepaint = true;
|
|
110
110
|
_frame = 0;
|
|
111
111
|
_fps = { value: 0, last: 0 };
|
|
112
|
-
|
|
113
|
-
|
|
112
|
+
_ambientLight;
|
|
113
|
+
_hemiLight;
|
|
114
|
+
_keyLight;
|
|
115
|
+
_fillLight;
|
|
116
|
+
_lightOptions = {
|
|
117
|
+
ambientIntensity: 0.4,
|
|
118
|
+
ambientColor: 0xffffff,
|
|
119
|
+
hemisphereIntensity: 0.8,
|
|
120
|
+
hemisphereColor: 0xffffff,
|
|
121
|
+
hemisphereGroundColor: 0xb6c0cb,
|
|
122
|
+
keyLightIntensity: 1.0,
|
|
123
|
+
keyLightColor: 0xffffff,
|
|
124
|
+
fillLightIntensity: 0.35,
|
|
125
|
+
fillLightColor: 0xffffff,
|
|
126
|
+
mode: 'headlamp',
|
|
127
|
+
azimuth: 150,
|
|
128
|
+
altitude: 50,
|
|
129
|
+
};
|
|
114
130
|
_datasetQueue;
|
|
115
131
|
/** 宿主重画信号与地图朝向,构造时注入 */
|
|
116
132
|
_onRequestRepaint = () => { };
|
|
@@ -120,10 +136,19 @@ export class S3MViewer {
|
|
|
120
136
|
_requestRepaint() {
|
|
121
137
|
this._onRequestRepaint();
|
|
122
138
|
}
|
|
139
|
+
triggerRepaint() {
|
|
140
|
+
this._requestRepaint();
|
|
141
|
+
}
|
|
123
142
|
constructor(options = {}) {
|
|
124
143
|
this.options = options;
|
|
125
144
|
this.pickable = options.pickable !== false;
|
|
126
145
|
this._lodRangeScale = options.lodRangeScale ?? 1.0;
|
|
146
|
+
if (options.light) {
|
|
147
|
+
Object.assign(this._lightOptions, options.light);
|
|
148
|
+
}
|
|
149
|
+
if (options.totalMaxTiles) {
|
|
150
|
+
this._datasetManager.totalMaxTiles = options.totalMaxTiles;
|
|
151
|
+
}
|
|
127
152
|
this._onRequestRepaint = options.onRequestRepaint ?? (() => { });
|
|
128
153
|
this._getBearing = options.getBearing;
|
|
129
154
|
this.onStats = options.onStats ?? (() => { });
|
|
@@ -131,6 +156,11 @@ export class S3MViewer {
|
|
|
131
156
|
this.onPick = options.onPick ?? (() => { });
|
|
132
157
|
this.onError = options.onError ?? ((e) => console.warn('[s3m]', e));
|
|
133
158
|
this.groundManager = new GroundManager(options.ground);
|
|
159
|
+
if (!this.scene) {
|
|
160
|
+
this.scene = new THREE.Scene();
|
|
161
|
+
this.scene.add(this.groundManager.group);
|
|
162
|
+
this._addLights();
|
|
163
|
+
}
|
|
134
164
|
this._featureStyleManager = new FeatureStyleManager(() => this.style.floorHeight, () => this._requestRepaint(), () => {
|
|
135
165
|
if (this._selection)
|
|
136
166
|
this._syncSelectionStyle();
|
|
@@ -159,18 +189,23 @@ export class S3MViewer {
|
|
|
159
189
|
* 绑定宿主画布与 WebGL 上下文,建场景并启动数据集队列。
|
|
160
190
|
* MapLibre 宿主由 S3MLayer 在 onAdd 里调用;纯 three 宿主直接传自己的 canvas / gl。
|
|
161
191
|
*/
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
192
|
+
/** 创建并配置一个绑定到给定 gl 上下文的 three renderer(mount 与上下文恢复共用) */
|
|
193
|
+
_createRenderer(canvas, gl) {
|
|
194
|
+
const r = new THREE.WebGLRenderer({
|
|
165
195
|
canvas,
|
|
166
196
|
context: gl,
|
|
167
197
|
antialias: true,
|
|
168
198
|
});
|
|
169
|
-
|
|
170
|
-
|
|
199
|
+
r.autoClear = false;
|
|
200
|
+
r.setPixelRatio(1);
|
|
171
201
|
// 白模 albedo 接近 1,不做色调映射会过曝成一片纯白、丢失明暗关系
|
|
172
|
-
|
|
173
|
-
|
|
202
|
+
r.toneMapping = THREE.ACESFilmicToneMapping;
|
|
203
|
+
r.toneMappingExposure = 1.0;
|
|
204
|
+
return r;
|
|
205
|
+
}
|
|
206
|
+
mount(canvas, gl) {
|
|
207
|
+
this.canvas = canvas;
|
|
208
|
+
this.renderer = this._createRenderer(canvas, gl);
|
|
174
209
|
setMaxAnisotropy(this.renderer.capabilities.getMaxAnisotropy());
|
|
175
210
|
this.scene = new THREE.Scene();
|
|
176
211
|
this.scene.add(this.groundManager.group);
|
|
@@ -183,6 +218,38 @@ export class S3MViewer {
|
|
|
183
218
|
void this.addDataset(ds).catch((e) => this.onError(e));
|
|
184
219
|
this._recomputeAnchors();
|
|
185
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* WebGL 上下文恢复后原地换新 renderer,保留已经加载好的场景与瓦片。
|
|
223
|
+
*
|
|
224
|
+
* MapLibre 在 `webglcontextlost` 时会销毁 style 并丢弃自定义图层(自定义图层
|
|
225
|
+
* 无法序列化进 style,所以恢复后不会自动加回)。但自定义图层的 `onRemove` 不会被
|
|
226
|
+
* 调用,因此我们的 scene / 几何体 / 材质 / 纹理 / 已加载瓦片都还在内存里。
|
|
227
|
+
*
|
|
228
|
+
* three 的 GPU 状态是按 renderer 实例缓存的,所以只需把 renderer / picker 换成
|
|
229
|
+
* 绑在新 gl 上的新实例,下一帧渲染时 three 会自动把现存的几何体、材质、纹理
|
|
230
|
+
* 重新上传到新上下文 —— 不需要重建场景,也不需要重新拉瓦片。
|
|
231
|
+
*/
|
|
232
|
+
restoreContext(gl) {
|
|
233
|
+
if (!this.canvas) {
|
|
234
|
+
// 理论不会走到:没 mount 过就没有上下文可恢复
|
|
235
|
+
console.warn('[s3m] restoreContext 调用过早(尚未 mount),按首次挂载处理');
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
// 旧上下文已失效,dispose 可能抛错,忽略即可
|
|
240
|
+
this.renderer?.dispose();
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
/* noop */
|
|
244
|
+
}
|
|
245
|
+
this.renderer = this._createRenderer(this.canvas, gl);
|
|
246
|
+
setMaxAnisotropy(this.renderer.capabilities.getMaxAnisotropy());
|
|
247
|
+
this.picker?.dispose();
|
|
248
|
+
this.picker = new Picker(this.renderer);
|
|
249
|
+
// 共享 uniform / 材质引用不变,three 重新编译着色器时会自然接管;
|
|
250
|
+
// 这里只确保下一帧尽快驱动一次渲染。
|
|
251
|
+
this._requestRepaint();
|
|
252
|
+
}
|
|
186
253
|
/** 解绑场景并释放全部 GPU 资源。对应 MapLibre 图层的 onRemove()。 */
|
|
187
254
|
unmount() {
|
|
188
255
|
for (const id of [...this.datasets.keys()])
|
|
@@ -207,24 +274,64 @@ export class S3MViewer {
|
|
|
207
274
|
this.renderer = null;
|
|
208
275
|
this.canvas = null;
|
|
209
276
|
}
|
|
277
|
+
_createLights() {
|
|
278
|
+
this._ambientLight = new THREE.AmbientLight(this._lightOptions.ambientColor, this._lightOptions.ambientIntensity);
|
|
279
|
+
this._hemiLight = new THREE.HemisphereLight(this._lightOptions.hemisphereColor, this._lightOptions.hemisphereGroundColor, this._lightOptions.hemisphereIntensity);
|
|
280
|
+
this._keyLight = new THREE.DirectionalLight(this._lightOptions.keyLightColor, this._lightOptions.keyLightIntensity);
|
|
281
|
+
this._fillLight = new THREE.DirectionalLight(this._lightOptions.fillLightColor, this._lightOptions.fillLightIntensity);
|
|
282
|
+
this._updateLights();
|
|
283
|
+
}
|
|
210
284
|
_addLights() {
|
|
211
285
|
if (!this.scene)
|
|
212
286
|
return;
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
this.scene.add(
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
287
|
+
if (!this._ambientLight)
|
|
288
|
+
this._createLights();
|
|
289
|
+
this.scene.add(this._ambientLight, this._hemiLight, this._keyLight, this._fillLight);
|
|
290
|
+
}
|
|
291
|
+
_applyLightOptions() {
|
|
292
|
+
if (this._ambientLight) {
|
|
293
|
+
this._ambientLight.intensity = this._lightOptions.ambientIntensity;
|
|
294
|
+
this._ambientLight.color.set(this._lightOptions.ambientColor);
|
|
295
|
+
}
|
|
296
|
+
if (this._hemiLight) {
|
|
297
|
+
this._hemiLight.intensity = this._lightOptions.hemisphereIntensity;
|
|
298
|
+
this._hemiLight.color.set(this._lightOptions.hemisphereColor);
|
|
299
|
+
this._hemiLight.groundColor.set(this._lightOptions.hemisphereGroundColor);
|
|
300
|
+
}
|
|
301
|
+
if (this._keyLight) {
|
|
302
|
+
this._keyLight.intensity = this._lightOptions.keyLightIntensity;
|
|
303
|
+
this._keyLight.color.set(this._lightOptions.keyLightColor);
|
|
304
|
+
}
|
|
305
|
+
if (this._fillLight) {
|
|
306
|
+
this._fillLight.intensity = this._lightOptions.fillLightIntensity;
|
|
307
|
+
this._fillLight.color.set(this._lightOptions.fillLightColor);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* 动态设置光照配置
|
|
312
|
+
*/
|
|
313
|
+
setLight(options) {
|
|
314
|
+
Object.assign(this._lightOptions, options);
|
|
315
|
+
this._applyLightOptions();
|
|
316
|
+
this._updateLights();
|
|
317
|
+
this._needsRepaint = true;
|
|
318
|
+
this.triggerRepaint();
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* 获取当前光照配置快照
|
|
322
|
+
*/
|
|
323
|
+
getLight() {
|
|
324
|
+
return { ...this._lightOptions };
|
|
221
325
|
}
|
|
222
326
|
/**
|
|
223
|
-
*
|
|
224
|
-
*
|
|
327
|
+
* 更新光照位置与方向:
|
|
328
|
+
* - 'headlamp': 随地图方位角 bearing 旋转,主光始终在镜头上方,受光面面向镜头;
|
|
329
|
+
* - 'fixed': 固定世界方位角(如模拟恒定日光)。
|
|
225
330
|
*/
|
|
226
331
|
_updateLights() {
|
|
227
|
-
|
|
332
|
+
if (!this._keyLight || !this._fillLight)
|
|
333
|
+
return;
|
|
334
|
+
const bearing = this._lightOptions.mode === 'fixed' ? 0 : (((this._getBearing?.() ?? 0) * Math.PI) / 180);
|
|
228
335
|
const place = (light, azOffsetDeg, elevDeg) => {
|
|
229
336
|
if (!light)
|
|
230
337
|
return;
|
|
@@ -233,8 +340,8 @@ export class S3MViewer {
|
|
|
233
340
|
const c = Math.cos(el);
|
|
234
341
|
light.position.set(Math.sin(az) * c, Math.cos(az) * c, Math.sin(el)).multiplyScalar(1000);
|
|
235
342
|
};
|
|
236
|
-
place(this._keyLight,
|
|
237
|
-
place(this._fillLight,
|
|
343
|
+
place(this._keyLight, this._lightOptions.azimuth, this._lightOptions.altitude);
|
|
344
|
+
place(this._fillLight, this._lightOptions.azimuth + 65, Math.max(10, this._lightOptions.altitude - 20));
|
|
238
345
|
}
|
|
239
346
|
/* ---------------------------------------------------------------- 数据集 */
|
|
240
347
|
_normalizeDatasets(options) {
|
|
@@ -629,6 +736,28 @@ export class S3MViewer {
|
|
|
629
736
|
* 而 LOD 细化、瓦片重载、淘汰回补都会换掉 mesh —— 不重放的话,用户点中的那栋楼
|
|
630
737
|
* 一细化就失去高亮(状态还在 _selection.smid 里,只是没人往新 mesh 上刷)。
|
|
631
738
|
*/
|
|
739
|
+
/**
|
|
740
|
+
* 判断一块无 SMID 单体化信息的网格(如倾斜摄影或白模整块)是否空间覆盖当前选中命中点
|
|
741
|
+
*/
|
|
742
|
+
_isMeshCoveringSelection(mesh, sel) {
|
|
743
|
+
if (!sel.point)
|
|
744
|
+
return false;
|
|
745
|
+
const geom = mesh.geometry;
|
|
746
|
+
if (!geom)
|
|
747
|
+
return false;
|
|
748
|
+
if (!geom.boundingBox)
|
|
749
|
+
geom.computeBoundingBox();
|
|
750
|
+
if (!geom.boundingBox)
|
|
751
|
+
return false;
|
|
752
|
+
// 将选中的三维场景点转入网格局部坐标系进行包围盒检测
|
|
753
|
+
const pt = new THREE.Vector3(sel.point[0], sel.point[1], sel.point[2]);
|
|
754
|
+
mesh.updateWorldMatrix(true, false);
|
|
755
|
+
const inv = mesh.matrixWorld.clone().invert();
|
|
756
|
+
const localPt = pt.applyMatrix4(inv);
|
|
757
|
+
// 留出 0.5 米容差,避免刚好处于三角网格边缘缝隙时漏判
|
|
758
|
+
const box = geom.boundingBox.clone().expandByScalar(0.5);
|
|
759
|
+
return box.containsPoint(localPt);
|
|
760
|
+
}
|
|
632
761
|
_replaySelectionOnto(mesh) {
|
|
633
762
|
const sel = this._selection;
|
|
634
763
|
if (!sel)
|
|
@@ -647,8 +776,14 @@ export class S3MViewer {
|
|
|
647
776
|
if (mesh === sel.mesh)
|
|
648
777
|
batchId = sel.batchId;
|
|
649
778
|
// 这块几何里根本没有这个单体 → 不动它(否则会把无关瓦片一起点亮)
|
|
650
|
-
if (batchId < 0 && mesh !== sel.mesh)
|
|
651
|
-
|
|
779
|
+
if (batchId < 0 && mesh !== sel.mesh) {
|
|
780
|
+
if (sel.smid == null && ud.datasetId === sel.datasetId && this._isMeshCoveringSelection(mesh, sel)) {
|
|
781
|
+
batchId = -1;
|
|
782
|
+
}
|
|
783
|
+
else {
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
652
787
|
mat.userData.uSelBatch.value = batchId;
|
|
653
788
|
mat.userData.uSelWhole.value = batchId < 0 ? 1 : 0;
|
|
654
789
|
if (!this._selectedMeshes.includes(mesh))
|
|
@@ -658,46 +793,57 @@ export class S3MViewer {
|
|
|
658
793
|
this.uniforms.uSelMix.value = 1;
|
|
659
794
|
this.uniforms.uDim.value = this.style.dimOthers ? 1 : 0;
|
|
660
795
|
}
|
|
661
|
-
/**
|
|
796
|
+
/** 瓦片被淘汰:从拾取表和场景里摘掉,并深度彻底释放所有几何体与材质显存 */
|
|
662
797
|
_unregisterTile(node, entry) {
|
|
663
|
-
|
|
664
|
-
const
|
|
665
|
-
if (
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
// 以前这里一律 select(null),于是点中的那栋楼一拉近,选中状态就整个被清掉。
|
|
670
|
-
const wasSelected = this._selectedMeshes.includes(mesh);
|
|
671
|
-
if (wasSelected) {
|
|
672
|
-
this._selectedMeshes = this._selectedMeshes.filter((m) => m !== mesh);
|
|
798
|
+
node.group.traverse((child) => {
|
|
799
|
+
const obj = child;
|
|
800
|
+
if (obj.userData?.pickId != null) {
|
|
801
|
+
this._pickables.delete(obj.userData.pickId);
|
|
802
|
+
this._releasePickId(obj.userData.pickId);
|
|
803
|
+
obj.userData.pickId = undefined;
|
|
673
804
|
}
|
|
674
|
-
if (
|
|
675
|
-
const
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
// 选中身份交给承载同一 smid 的另一块 mesh(通常是接替显示的子瓦)
|
|
679
|
-
this._selection = { ...this._selection, mesh: carrier };
|
|
680
|
-
this._replaySelectionOnto(carrier);
|
|
805
|
+
if (obj.isMesh) {
|
|
806
|
+
const wasSelected = this._selectedMeshes.includes(obj);
|
|
807
|
+
if (wasSelected) {
|
|
808
|
+
this._selectedMeshes = this._selectedMeshes.filter((m) => m !== obj);
|
|
681
809
|
}
|
|
682
|
-
|
|
683
|
-
|
|
810
|
+
const currentSel = this._selection;
|
|
811
|
+
if (currentSel && currentSel.mesh === obj) {
|
|
812
|
+
const smid = currentSel.smid;
|
|
813
|
+
const carrier = (smid != null
|
|
814
|
+
? [...(this._smidMeshes.get(smid) ?? [])].find((m) => m !== obj && m.parent)
|
|
815
|
+
: this._selectedMeshes.find((m) => m !== obj && m.parent && m.parent.visible) ??
|
|
816
|
+
this._selectedMeshes.find((m) => m !== obj && m.parent));
|
|
817
|
+
if (carrier) {
|
|
818
|
+
// 选中身份交给承载同一 smid 的另一块 mesh(通常是接替显示的子瓦)
|
|
819
|
+
this._selection = { ...currentSel, mesh: carrier };
|
|
820
|
+
this._replaySelectionOnto(carrier);
|
|
821
|
+
}
|
|
822
|
+
else {
|
|
823
|
+
this.select(null);
|
|
824
|
+
}
|
|
684
825
|
}
|
|
826
|
+
this._unindexMeshSmids(obj);
|
|
827
|
+
this._removeMeshHeight(obj);
|
|
828
|
+
this._featureStyleManager.disposeMesh(obj);
|
|
829
|
+
const styled = styledMaterialOf(obj);
|
|
830
|
+
styled?.dispose?.();
|
|
685
831
|
}
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
ud.pickId = undefined;
|
|
832
|
+
// 深度释放材质(点云、普通材质、材质数组)
|
|
833
|
+
if (isOwnedPointCloudMaterial(obj.material)) {
|
|
834
|
+
obj.material.dispose();
|
|
690
835
|
}
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
836
|
+
else if (obj.material) {
|
|
837
|
+
if (Array.isArray(obj.material)) {
|
|
838
|
+
obj.material.forEach((m) => m?.dispose?.());
|
|
839
|
+
}
|
|
840
|
+
else {
|
|
841
|
+
obj.material.dispose?.();
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
// 释放几何体显存缓冲
|
|
845
|
+
obj.geometry?.dispose?.();
|
|
846
|
+
});
|
|
701
847
|
entry.root.remove(node.group);
|
|
702
848
|
}
|
|
703
849
|
/**
|
|
@@ -1738,6 +1884,10 @@ export class S3MViewer {
|
|
|
1738
1884
|
renderFrame(opts) {
|
|
1739
1885
|
if (!this.renderer || !this.scene || !this.camera || !this.sceneAnchor || !this.canvas)
|
|
1740
1886
|
return false;
|
|
1887
|
+
// 上下文丢失期间(丢失→恢复之间)不要尝试渲染,否则会刷一堆 GL 错误
|
|
1888
|
+
const gl = this.renderer.getContext();
|
|
1889
|
+
if (gl && typeof gl.isContextLost === 'function' && gl.isContextLost())
|
|
1890
|
+
return false;
|
|
1741
1891
|
// 脏标记正常由微任务刷掉;这里再兜一次底,保证「本帧画出来的」一定是最新样式
|
|
1742
1892
|
this.flushOverrides();
|
|
1743
1893
|
const canvas = this.canvas;
|