s3m-three 0.4.18 → 0.4.20
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/assets/s3mWorker-DvAUA8NJ.js +1 -0
- package/dist/browser/s3m-three.iife.ext-three.js +619 -304
- package/dist/browser/s3m-three.iife.js +647 -332
- package/dist/core/Draco.d.ts.map +1 -1
- package/dist/core/Draco.js +0 -1
- package/dist/core/Draco.js.map +1 -1
- package/dist/core/GaussianSplat.d.ts +64 -0
- package/dist/core/GaussianSplat.d.ts.map +1 -0
- package/dist/core/GaussianSplat.js +139 -0
- package/dist/core/GaussianSplat.js.map +1 -0
- package/dist/core/S3M1Parser.d.ts +1 -1
- package/dist/core/S3M1Parser.d.ts.map +1 -1
- package/dist/core/S3M1Parser.js +320 -25
- package/dist/core/S3M1Parser.js.map +1 -1
- package/dist/core/S3MBParser.d.ts.map +1 -1
- package/dist/core/S3MBParser.js +13 -0
- package/dist/core/S3MBParser.js.map +1 -1
- package/dist/core/S3MBlockParser.d.ts.map +1 -1
- package/dist/core/S3MBlockParser.js +83 -28
- package/dist/core/S3MBlockParser.js.map +1 -1
- package/dist/core/SplatLod.d.ts +36 -0
- package/dist/core/SplatLod.d.ts.map +1 -0
- package/dist/core/SplatLod.js +54 -0
- package/dist/core/SplatLod.js.map +1 -0
- package/dist/core/SplatProjection.d.ts +75 -0
- package/dist/core/SplatProjection.d.ts.map +1 -0
- package/dist/core/SplatProjection.js +162 -0
- package/dist/core/SplatProjection.js.map +1 -0
- package/dist/core/SplatSort.d.ts +26 -0
- package/dist/core/SplatSort.d.ts.map +1 -0
- package/dist/core/SplatSort.js +61 -0
- package/dist/core/SplatSort.js.map +1 -0
- package/dist/core/TexCoordZ.d.ts +94 -0
- package/dist/core/TexCoordZ.d.ts.map +1 -0
- package/dist/core/TexCoordZ.js +125 -0
- package/dist/core/TexCoordZ.js.map +1 -0
- package/dist/core/TextureDecode.d.ts +19 -0
- package/dist/core/TextureDecode.d.ts.map +1 -1
- package/dist/core/TextureDecode.js +53 -11
- package/dist/core/TextureDecode.js.map +1 -1
- package/dist/core/TileGeometry.d.ts +39 -0
- package/dist/core/TileGeometry.d.ts.map +1 -1
- package/dist/core/TileGeometry.js +133 -14
- package/dist/core/TileGeometry.js.map +1 -1
- package/dist/core/degradations.d.ts +37 -0
- package/dist/core/degradations.d.ts.map +1 -0
- package/dist/core/degradations.js +32 -0
- package/dist/core/degradations.js.map +1 -0
- package/dist/core/types.d.ts +36 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -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 +2 -0
- package/dist/render/DatasetManager.js.map +1 -1
- package/dist/render/FeatureStyleManager.d.ts.map +1 -1
- package/dist/render/FeatureStyleManager.js +7 -5
- package/dist/render/FeatureStyleManager.js.map +1 -1
- package/dist/render/GeometryBuilder.d.ts +31 -0
- package/dist/render/GeometryBuilder.d.ts.map +1 -1
- package/dist/render/GeometryBuilder.js +229 -6
- package/dist/render/GeometryBuilder.js.map +1 -1
- package/dist/render/Picker.d.ts.map +1 -1
- package/dist/render/Picker.js +19 -5
- package/dist/render/Picker.js.map +1 -1
- package/dist/render/S3MViewer.d.ts +106 -379
- package/dist/render/S3MViewer.d.ts.map +1 -1
- package/dist/render/S3MViewer.js +324 -1494
- package/dist/render/S3MViewer.js.map +1 -1
- package/dist/render/SplatMaterial.d.ts +103 -0
- package/dist/render/SplatMaterial.d.ts.map +1 -0
- package/dist/render/SplatMaterial.js +388 -0
- package/dist/render/SplatMaterial.js.map +1 -0
- package/dist/render/SplatSorter.d.ts +14 -0
- package/dist/render/SplatSorter.d.ts.map +1 -0
- package/dist/render/SplatSorter.js +66 -0
- package/dist/render/SplatSorter.js.map +1 -0
- package/dist/render/StyleMaterial.d.ts +48 -0
- package/dist/render/StyleMaterial.d.ts.map +1 -1
- package/dist/render/StyleMaterial.js +292 -69
- package/dist/render/StyleMaterial.js.map +1 -1
- package/dist/render/TextureFactory.d.ts +10 -0
- package/dist/render/TextureFactory.d.ts.map +1 -1
- package/dist/render/TextureFactory.js +35 -5
- package/dist/render/TextureFactory.js.map +1 -1
- package/dist/render/lightingPreset.d.ts +53 -0
- package/dist/render/lightingPreset.d.ts.map +1 -0
- package/dist/render/lightingPreset.js +36 -0
- package/dist/render/lightingPreset.js.map +1 -0
- package/dist/render/materialAlpha.d.ts +51 -0
- package/dist/render/materialAlpha.d.ts.map +1 -0
- package/dist/render/materialAlpha.js +68 -0
- package/dist/render/materialAlpha.js.map +1 -0
- package/dist/render/viewer/EdgeDecorator.d.ts +206 -0
- package/dist/render/viewer/EdgeDecorator.d.ts.map +1 -0
- package/dist/render/viewer/EdgeDecorator.js +628 -0
- package/dist/render/viewer/EdgeDecorator.js.map +1 -0
- package/dist/render/viewer/HeightIndex.d.ts +36 -0
- package/dist/render/viewer/HeightIndex.d.ts.map +1 -0
- package/dist/render/viewer/HeightIndex.js +98 -0
- package/dist/render/viewer/HeightIndex.js.map +1 -0
- package/dist/render/viewer/LightRig.d.ts +88 -0
- package/dist/render/viewer/LightRig.d.ts.map +1 -0
- package/dist/render/viewer/LightRig.js +229 -0
- package/dist/render/viewer/LightRig.js.map +1 -0
- package/dist/render/viewer/PickService.d.ts +109 -0
- package/dist/render/viewer/PickService.d.ts.map +1 -0
- package/dist/render/viewer/PickService.js +591 -0
- package/dist/render/viewer/PickService.js.map +1 -0
- package/dist/render/viewer/SelectionDecorator.d.ts +84 -0
- package/dist/render/viewer/SelectionDecorator.d.ts.map +1 -0
- package/dist/render/viewer/SelectionDecorator.js +339 -0
- package/dist/render/viewer/SelectionDecorator.js.map +1 -0
- package/dist/render/viewer/SplatLayer.d.ts +26 -0
- package/dist/render/viewer/SplatLayer.d.ts.map +1 -0
- package/dist/render/viewer/SplatLayer.js +44 -0
- package/dist/render/viewer/SplatLayer.js.map +1 -0
- package/dist/render/viewer/types.d.ts +250 -0
- package/dist/render/viewer/types.d.ts.map +1 -0
- package/dist/render/viewer/types.js +3 -0
- package/dist/render/viewer/types.js.map +1 -0
- package/dist/tileset/S3MTileset.d.ts +42 -1
- package/dist/tileset/S3MTileset.d.ts.map +1 -1
- package/dist/tileset/S3MTileset.js +52 -2
- package/dist/tileset/S3MTileset.js.map +1 -1
- package/dist/vendor/browser-node-stub.js +10 -0
- package/dist/worker/s3mWorker.js +1166 -979
- package/dist/worker/s3mWorker.js.map +1 -1
- package/docs/API.md +55 -1
- package/docs/FORMAT-NOTES.md +362 -0
- package/package.json +1 -1
- package/src/core/Draco.ts +1 -2
- package/src/core/GaussianSplat.ts +175 -0
- package/src/core/S3M1Parser.ts +330 -24
- package/src/core/S3MBParser.ts +18 -0
- package/src/core/S3MBlockParser.ts +83 -27
- package/src/core/SplatLod.ts +83 -0
- package/src/core/SplatProjection.ts +231 -0
- package/src/core/SplatSort.ts +92 -0
- package/src/core/TexCoordZ.ts +160 -0
- package/src/core/TextureDecode.ts +63 -11
- package/src/core/TileGeometry.ts +151 -14
- package/src/core/degradations.ts +74 -0
- package/src/core/types.ts +32 -0
- package/src/index.ts +8 -1
- package/src/render/DatasetManager.ts +10 -0
- package/src/render/FeatureStyleManager.ts +8 -6
- package/src/render/GeometryBuilder.ts +271 -7
- package/src/render/Picker.ts +21 -5
- package/src/render/S3MViewer.ts +358 -1727
- package/src/render/SplatMaterial.ts +433 -0
- package/src/render/SplatSorter.ts +66 -0
- package/src/render/StyleMaterial.ts +339 -69
- package/src/render/TextureFactory.ts +39 -5
- package/src/render/lightingPreset.ts +76 -0
- package/src/render/materialAlpha.ts +75 -0
- package/src/render/viewer/EdgeDecorator.ts +722 -0
- package/src/render/viewer/HeightIndex.ts +94 -0
- package/src/render/viewer/LightRig.ts +267 -0
- package/src/render/viewer/PickService.ts +651 -0
- package/src/render/viewer/SelectionDecorator.ts +377 -0
- package/src/render/viewer/SplatLayer.ts +44 -0
- package/src/render/viewer/types.ts +258 -0
- package/src/tileset/S3MTileset.ts +78 -2
- package/src/vendor/browser-node-stub.js +10 -0
- package/dist/browser/assets/s3mWorker-BjPOUgjg.js +0 -1
package/dist/render/S3MViewer.js
CHANGED
|
@@ -22,27 +22,28 @@
|
|
|
22
22
|
*/
|
|
23
23
|
import * as THREE from 'three';
|
|
24
24
|
import { usableGeoBounds } from '../tileset/S3MTileset.js';
|
|
25
|
-
import {
|
|
25
|
+
import { setSplatViewport } from './GeometryBuilder.js';
|
|
26
26
|
import { setMaxAnisotropy, setCompressedTexturesSupported, setTextureOffThreadDecode, setMaxTextureSize as setTextureSizeCap } from './TextureFactory.js';
|
|
27
27
|
import { getMaterialQuality, setMaterialQuality } from './StyleMaterial.js';
|
|
28
|
+
import { LightRig } from './viewer/LightRig.js';
|
|
29
|
+
import { HeightIndex } from './viewer/HeightIndex.js';
|
|
30
|
+
import { SplatLayer } from './viewer/SplatLayer.js';
|
|
31
|
+
import { EdgeDecorator } from './viewer/EdgeDecorator.js';
|
|
32
|
+
import { PickService } from './viewer/PickService.js';
|
|
33
|
+
import { SelectionDecorator } from './viewer/SelectionDecorator.js';
|
|
28
34
|
import { decodeTextureWithWorker } from '../worker/S3MWorkerClient.js';
|
|
29
|
-
import { STYLE, STYLE_LABELS, SELECT_MODE, DEFAULT_STYLE, createSharedUniforms, createStyledMaterial, createOutlineMaterial,
|
|
35
|
+
import { STYLE, STYLE_LABELS, SELECT_MODE, DEFAULT_STYLE, createSharedUniforms, createStyledMaterial, createOutlineMaterial, restoreBaseBlendState, } from './StyleMaterial.js';
|
|
30
36
|
import { applyPointCloudStyle, isOwnedPointCloudMaterial } from './PointCloudMaterial.js';
|
|
31
37
|
import { Picker } from './Picker.js';
|
|
32
|
-
import { GpuEdgePass } from './GpuEdgePass.js';
|
|
33
38
|
import { Animator } from '../anim/Animator.js';
|
|
34
39
|
import { Path } from '../anim/Path.js';
|
|
35
40
|
import { GPUPathManager } from './GPUPathManager.js';
|
|
36
|
-
import { buildFeatureEdgeSegments, buildPositionBatchMap, buildThickLineGeometry, createLineSharedUniforms, createThickLineMaterial, positionQuantKey, } from './EdgeLines.js';
|
|
37
|
-
import { mergeVerticesByPosition } from './MergeVertices.js';
|
|
38
|
-
import { resolveFeature, measureSegments, buildAttributes, buildInstanceLookup, pickEntryId, } from '../core/FeatureInfo.js';
|
|
39
41
|
import { AttributeQuery } from '../core/AttributeQuery.js';
|
|
40
42
|
import { lngLatFromWorld } from './GeoAdapter.js';
|
|
41
43
|
import { DatasetManager, } from './DatasetManager.js';
|
|
42
|
-
import { FeatureStyleManager,
|
|
44
|
+
import { FeatureStyleManager, extractEulerAngles, } from './FeatureStyleManager.js';
|
|
43
45
|
import { GroundManager, } from './GroundManager.js';
|
|
44
46
|
export { GroundManager, DatasetManager, FeatureStyleManager };
|
|
45
|
-
const MAX_PICK_ID = 65534;
|
|
46
47
|
function styledMaterialOf(mesh) {
|
|
47
48
|
const m = Array.isArray(mesh.material) ? mesh.material[0] : mesh.material;
|
|
48
49
|
const styled = m;
|
|
@@ -82,6 +83,8 @@ const STYLE_SCALAR_KEYS = [
|
|
|
82
83
|
'texMix',
|
|
83
84
|
'roofThreshold',
|
|
84
85
|
'ambientGain',
|
|
86
|
+
'metalness',
|
|
87
|
+
'roughness',
|
|
85
88
|
'edgeWidth',
|
|
86
89
|
'edgeOpacity',
|
|
87
90
|
'edgeOffset',
|
|
@@ -92,7 +95,7 @@ const STYLE_SCALAR_KEYS = [
|
|
|
92
95
|
const STYLE_COLOR_KEYS = ['colorA', 'colorB', 'colorC', 'selectColor', 'edgeColor'];
|
|
93
96
|
/** 可动画的单体属性 */
|
|
94
97
|
const FEATURE_PROPS = ['translation', 'elevation', 'opacity', 'color', 'rotation'];
|
|
95
|
-
|
|
98
|
+
/** 单体沿折线运动的参数 */
|
|
96
99
|
export class S3MViewer {
|
|
97
100
|
options;
|
|
98
101
|
pickable;
|
|
@@ -120,12 +123,8 @@ export class S3MViewer {
|
|
|
120
123
|
edgeMaterial;
|
|
121
124
|
/** 选中棱线材质(同上,选中棱线的逐网格材质共享它的对象) */
|
|
122
125
|
selectionEdgeMaterial;
|
|
123
|
-
/**
|
|
124
|
-
|
|
125
|
-
/** 选中棱线的外观 uniform */
|
|
126
|
-
_selEdgeStyleUniforms;
|
|
127
|
-
/** 全局外扩轮廓的外观 uniform:逐网格轮廓壳共享这些对象 */
|
|
128
|
-
_hullStyleUniforms;
|
|
126
|
+
/** 棱线装饰(见 viewer/EdgeDecorator.ts) */
|
|
127
|
+
_edges;
|
|
129
128
|
/** 3D 物理地表面 / 基坑开挖管理器 */
|
|
130
129
|
groundManager;
|
|
131
130
|
/** 多数据集生命周期与锚点管理器 */
|
|
@@ -146,29 +145,15 @@ export class S3MViewer {
|
|
|
146
145
|
set sceneAnchor(val) {
|
|
147
146
|
this._datasetManager.sceneAnchor = val;
|
|
148
147
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
_freePickIds = [];
|
|
148
|
+
/** 拾取表 / pickId 池 / 射线求交(见 viewer/PickService.ts) */
|
|
149
|
+
_picks;
|
|
152
150
|
/** SMID → 包含它的网格。改单个单体样式时只重算这几块,不用全量扫 */
|
|
153
151
|
_smidMeshes = new Map();
|
|
154
152
|
/** 每块网格的世界高度范围,用来增量维护 uHeightMin / uHeightMax */
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
_selection = null;
|
|
160
|
-
_selectedMeshes = [];
|
|
161
|
-
_outlineMesh = null;
|
|
162
|
-
_outlineMeshes = [];
|
|
163
|
-
_selectionEdges = null;
|
|
164
|
-
_selectionEdgeMeshes = [];
|
|
165
|
-
_edgesEnabled = false;
|
|
166
|
-
/** 当前生效的棱线画法(line / offset / hull / gpu),决定要不要建 / 拆哪种装饰 */
|
|
167
|
-
_edgeModes = resolveEdgeModes(false);
|
|
168
|
-
/** GPU 描边后处理(深度 + 法线),只在 edges 含 'gpu' 时按帧执行 */
|
|
169
|
-
_gpuEdge = null;
|
|
170
|
-
/** 给 GPU 描边传色用的临时 Color,避免每帧 new */
|
|
171
|
-
_gpuEdgeColor = new THREE.Color();
|
|
153
|
+
/** 世界高度范围增量维护(见 viewer/HeightIndex.ts) */
|
|
154
|
+
_heights;
|
|
155
|
+
/** 选中态与装饰(见 viewer/SelectionDecorator.ts) */
|
|
156
|
+
_sel;
|
|
172
157
|
/** 瓦片淡入通道的自增序号(不用 pickId:pickId 会被回收复用) */
|
|
173
158
|
_tileFadeSeq = 0;
|
|
174
159
|
/** setMode 编排的代次,用来作废被新调用顶掉的旧编排 */
|
|
@@ -185,12 +170,10 @@ export class S3MViewer {
|
|
|
185
170
|
_pathScratch = new Float32Array(3);
|
|
186
171
|
/** 求切线用的第二个采样点(包围盒换算 GPU 路径朝向时用) */
|
|
187
172
|
_pathScratch2 = new Float32Array(3);
|
|
188
|
-
/** 预制棱线缓存(Crease Cache):从外部 source-creases 载入的全局统一 LineSegments,性能最高、视觉最干净 */
|
|
189
|
-
_creaseLines = null;
|
|
190
|
-
/** 数据自带的棱线对象(LineSegments),改棱线颜色时只刷这些,不再全场景 traverse */
|
|
191
|
-
_nativeEdgeLines = new Set();
|
|
192
173
|
/** 点云对象集合:透明度动画时只刷它们,避免每帧遍历全部 pickables */
|
|
193
174
|
_pointClouds = new Set();
|
|
175
|
+
/** 泼溅排序调度(见 viewer/SplatLayer.ts) */
|
|
176
|
+
_splats = new SplatLayer();
|
|
194
177
|
/** 当前材质是否处于 transparent 状态,用来避免逐帧重复切换 program */
|
|
195
178
|
_opacityTransparent = false;
|
|
196
179
|
/**
|
|
@@ -207,27 +190,10 @@ export class S3MViewer {
|
|
|
207
190
|
_frameMvp = new THREE.Matrix4();
|
|
208
191
|
/** 颜色换算用的临时对象,避免每帧 new */
|
|
209
192
|
_colorScratch = new THREE.Color();
|
|
210
|
-
_needsRepaint = true;
|
|
211
193
|
_frame = 0;
|
|
212
194
|
_fps = { value: 0, last: 0 };
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
_keyLight;
|
|
216
|
-
_fillLight;
|
|
217
|
-
_lightOptions = {
|
|
218
|
-
ambientIntensity: 0.4,
|
|
219
|
-
ambientColor: 0xffffff,
|
|
220
|
-
hemisphereIntensity: 0.8,
|
|
221
|
-
hemisphereColor: 0xffffff,
|
|
222
|
-
hemisphereGroundColor: 0xb6c0cb,
|
|
223
|
-
keyLightIntensity: 1.0,
|
|
224
|
-
keyLightColor: 0xffffff,
|
|
225
|
-
fillLightIntensity: 0.35,
|
|
226
|
-
fillLightColor: 0xffffff,
|
|
227
|
-
mode: 'headlamp',
|
|
228
|
-
azimuth: 150,
|
|
229
|
-
altitude: 50,
|
|
230
|
-
};
|
|
195
|
+
/** 光源 / 色调映射 / 环境贴图(见 viewer/LightRig.ts) */
|
|
196
|
+
_lights;
|
|
231
197
|
_datasetQueue;
|
|
232
198
|
/** 宿主重画信号与地图朝向,构造时注入 */
|
|
233
199
|
_onRequestRepaint = () => { };
|
|
@@ -235,6 +201,8 @@ export class S3MViewer {
|
|
|
235
201
|
/** 鉴权适配(全局默认),构造时注入 */
|
|
236
202
|
_authenticate;
|
|
237
203
|
_lodRangeScale = 1.0;
|
|
204
|
+
/** 图层级降级策略,数据集可覆盖,见 `S3MViewerOptions.unsupported` */
|
|
205
|
+
_unsupported;
|
|
238
206
|
/** LOD 判定屏幕高度的 DPR 上限。Infinity = 不截断(默认,行为同旧版) */
|
|
239
207
|
_lodPixelRatioCap = Infinity;
|
|
240
208
|
/** 宿主请求重画(MapLibre 传 triggerRepaint,纯 three 宿主传自己的渲染循环) */
|
|
@@ -248,12 +216,10 @@ export class S3MViewer {
|
|
|
248
216
|
this.options = options;
|
|
249
217
|
this.pickable = options.pickable !== false;
|
|
250
218
|
this._lodRangeScale = options.lodRangeScale ?? 1.0;
|
|
219
|
+
this._unsupported = options.unsupported ?? 'warn';
|
|
251
220
|
this._lodPixelRatioCap = options.lodPixelRatioCap ?? Infinity;
|
|
252
221
|
setTextureSizeCap(options.maxTextureSize ?? Infinity);
|
|
253
222
|
setMaterialQuality(options.materialQuality ?? 'pbr');
|
|
254
|
-
if (options.light) {
|
|
255
|
-
Object.assign(this._lightOptions, options.light);
|
|
256
|
-
}
|
|
257
223
|
if (options.totalMaxTiles) {
|
|
258
224
|
this._datasetManager.totalMaxTiles = options.totalMaxTiles;
|
|
259
225
|
}
|
|
@@ -269,15 +235,61 @@ export class S3MViewer {
|
|
|
269
235
|
if (!this.scene) {
|
|
270
236
|
this.scene = new THREE.Scene();
|
|
271
237
|
this.scene.add(this.groundManager.group);
|
|
272
|
-
this._addLights();
|
|
273
238
|
}
|
|
274
239
|
this._featureStyleManager = new FeatureStyleManager(() => this.style.floorHeight, () => this._requestRepaint(), () => {
|
|
275
|
-
if (this.
|
|
276
|
-
this.
|
|
240
|
+
if (this._sel.selection)
|
|
241
|
+
this._sel.syncStyle();
|
|
277
242
|
}, () => this.getAllMeshes(), (mesh) => this._applyOverridesToMesh(mesh),
|
|
278
243
|
// 图层自身是否半透明:全局透明度改走 uOpacity 后,材质 opacity 不再代表它
|
|
279
244
|
() => this._opacityTransparent);
|
|
280
245
|
this.uniforms = createSharedUniforms();
|
|
246
|
+
// 光源状态在这里就位:LightRig 构造时会按 lightingPreset 切模块级颜色管线,
|
|
247
|
+
// 挂进场景要等 scene 建好(构造函数上面已经建了)。
|
|
248
|
+
this._lights = new LightRig({
|
|
249
|
+
scene: () => this.scene,
|
|
250
|
+
renderer: () => this.renderer,
|
|
251
|
+
uniforms: this.uniforms,
|
|
252
|
+
options: this.options,
|
|
253
|
+
getBearing: () => this._getBearing?.(),
|
|
254
|
+
// 走公共 triggerRepaint 而不是 _requestRepaint:宿主 / 单测会覆盖前者来观察重绘,
|
|
255
|
+
// 绕过它会让「有没有触发重绘」这件事失去观测点
|
|
256
|
+
repaint: () => this.triggerRepaint(),
|
|
257
|
+
});
|
|
258
|
+
this._lights.addToScene();
|
|
259
|
+
this._heights = new HeightIndex(this.uniforms);
|
|
260
|
+
const self = this;
|
|
261
|
+
this._picks = new PickService({
|
|
262
|
+
get picker() {
|
|
263
|
+
return self.picker;
|
|
264
|
+
},
|
|
265
|
+
get camera() {
|
|
266
|
+
return self.camera;
|
|
267
|
+
},
|
|
268
|
+
get scene() {
|
|
269
|
+
return self.scene;
|
|
270
|
+
},
|
|
271
|
+
get canvas() {
|
|
272
|
+
return self.canvas;
|
|
273
|
+
},
|
|
274
|
+
get style() {
|
|
275
|
+
return self.style;
|
|
276
|
+
},
|
|
277
|
+
get sceneAnchor() {
|
|
278
|
+
return self.sceneAnchor;
|
|
279
|
+
},
|
|
280
|
+
get datasets() {
|
|
281
|
+
return self.datasets;
|
|
282
|
+
},
|
|
283
|
+
get featureStyleManager() {
|
|
284
|
+
return self._featureStyleManager;
|
|
285
|
+
},
|
|
286
|
+
get smidMeshes() {
|
|
287
|
+
return self._smidMeshes;
|
|
288
|
+
},
|
|
289
|
+
lngLatFromWorld: (x, y) => self._lngLatFromWorld(x, y),
|
|
290
|
+
gpuPathPoseAt: (smid) => self._gpuPathPoseAt(smid),
|
|
291
|
+
isFeatureHidden: (smid, batchId) => self._isFeatureHidden(smid, batchId),
|
|
292
|
+
});
|
|
281
293
|
// 纹理解码优先派给共享解析 Worker(崩溃自动回主线程,见 decodeTextureWithWorker)
|
|
282
294
|
setTextureOffThreadDecode(decodeTextureWithWorker);
|
|
283
295
|
this.style = { ...DEFAULT_STYLE, ...(options.style ?? {}) };
|
|
@@ -289,29 +301,35 @@ export class S3MViewer {
|
|
|
289
301
|
// 不踢的话动画「卡住不动」,直到别的什么碰巧触发了重绘
|
|
290
302
|
this.animator.onActive = () => this._requestRepaint();
|
|
291
303
|
this.outlineMaterial = createOutlineMaterial(this.uniforms);
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
this.
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
304
|
+
// 棱线装饰:内部持有共享 uniform 与全局材质,公共字段从它取出来保持 API 不变
|
|
305
|
+
this._edges = new EdgeDecorator({
|
|
306
|
+
uniforms: this.uniforms,
|
|
307
|
+
style: this.style,
|
|
308
|
+
styleTarget: this._styleTarget,
|
|
309
|
+
animator: this.animator,
|
|
310
|
+
pickables: this._picks.pickables,
|
|
311
|
+
scene: this.scene,
|
|
312
|
+
renderer: this.renderer,
|
|
313
|
+
camera: this.camera,
|
|
314
|
+
authenticate: this._authenticate,
|
|
315
|
+
requestRepaint: () => this.triggerRepaint(),
|
|
316
|
+
});
|
|
317
|
+
this.lineShared = this._edges.lineShared;
|
|
318
|
+
this.edgeMaterial = this._edges.edgeMaterial;
|
|
319
|
+
this.selectionEdgeMaterial = this._edges.selectionEdgeMaterial;
|
|
320
|
+
this._sel = new SelectionDecorator({
|
|
321
|
+
uniforms: this.uniforms,
|
|
322
|
+
style: this.style,
|
|
323
|
+
outlineMaterial: this.outlineMaterial,
|
|
324
|
+
selectionEdgeMaterial: this.selectionEdgeMaterial,
|
|
325
|
+
featureStyleManager: this._featureStyleManager,
|
|
326
|
+
smidMeshes: this._smidMeshes,
|
|
327
|
+
pointClouds: this._pointClouds,
|
|
328
|
+
applyPointCloudStyle: (pc, selected) => this._applyPointCloudStyle(pc, selected),
|
|
329
|
+
onPick: (r) => this.onPick(r),
|
|
330
|
+
requestRepaint: () => this.triggerRepaint(),
|
|
331
|
+
createSelectionEdgeMaterial: (mesh) => this._edges.createSelectionEdgeMaterial(mesh),
|
|
332
|
+
});
|
|
315
333
|
// 关键:style.edges 是个开关,但构造函数从没据此启用过棱线,导致
|
|
316
334
|
// `style: { edges: true }` 形同虚设、_edgesEnabled 永远是 false。
|
|
317
335
|
// 这里按 style 实际值调一次 setEdges,让开关真正生效(与 setEdges 后来的手工调用一致)。
|
|
@@ -397,8 +415,7 @@ export class S3MViewer {
|
|
|
397
415
|
this.picker?.dispose();
|
|
398
416
|
this.picker = new Picker(this.renderer);
|
|
399
417
|
// GPU 描边的 RT / 材质绑定在旧上下文上,整体丢弃,下一帧按需重建
|
|
400
|
-
this.
|
|
401
|
-
this._gpuEdge = null;
|
|
418
|
+
this._edges.disposeGpuEdge();
|
|
402
419
|
// 共享 uniform / 材质引用不变,three 重新编译着色器时会自然接管;
|
|
403
420
|
// 这里只确保下一帧尽快驱动一次渲染。
|
|
404
421
|
this._requestRepaint();
|
|
@@ -411,151 +428,98 @@ export class S3MViewer {
|
|
|
411
428
|
this.groundManager.dispose();
|
|
412
429
|
this.picker?.dispose();
|
|
413
430
|
this.picker = null;
|
|
431
|
+
this._lights.dispose();
|
|
414
432
|
this.outlineMaterial.dispose();
|
|
415
|
-
this.
|
|
416
|
-
this._gpuEdge = null;
|
|
433
|
+
this._edges.disposeGpuEdge();
|
|
417
434
|
this.edgeMaterial.dispose();
|
|
418
435
|
this.selectionEdgeMaterial.dispose();
|
|
419
436
|
this.scene?.clear();
|
|
420
|
-
this.
|
|
421
|
-
this._outlineMeshes = [];
|
|
422
|
-
this._selectionEdgeMeshes = [];
|
|
423
|
-
this._outlineMesh = null;
|
|
424
|
-
this._selectionEdges = null;
|
|
437
|
+
this._sel.reset();
|
|
425
438
|
this._smidMeshes.clear();
|
|
426
|
-
this.
|
|
439
|
+
this._heights.clear();
|
|
427
440
|
// 场景已经清空,还挂着的动画通道(瓦片淡入 / 单体位移)不能再对着死对象写
|
|
428
441
|
this.animator.clear();
|
|
429
442
|
this.animator.unregisterPrefix('tile.');
|
|
430
443
|
this.animator.unregisterPrefix('feature.');
|
|
431
444
|
this._pathSpecs.clear();
|
|
432
|
-
this._heightMin = Infinity;
|
|
433
|
-
this._heightMax = -Infinity;
|
|
434
445
|
this.renderer?.dispose?.();
|
|
435
446
|
this.renderer = null;
|
|
436
447
|
this.canvas = null;
|
|
437
448
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
449
|
+
/* ---------------------------------------------------------------- 光照 */
|
|
450
|
+
/**
|
|
451
|
+
* @internal 光源实例的转发 —— `lighting.test.ts` 直接读这几个字段。
|
|
452
|
+
* 实现全在 viewer/LightRig.ts。
|
|
453
|
+
*/
|
|
454
|
+
get _ambientLight() {
|
|
455
|
+
return this._lights.ambient;
|
|
456
|
+
}
|
|
457
|
+
get _hemiLight() {
|
|
458
|
+
return this._lights.hemi;
|
|
459
|
+
}
|
|
460
|
+
get _keyLight() {
|
|
461
|
+
return this._lights.key;
|
|
462
|
+
}
|
|
463
|
+
get _fillLight() {
|
|
464
|
+
return this._lights.fill;
|
|
465
|
+
}
|
|
466
|
+
/** @internal 光照位置按当前 bearing 重算,见 viewer/LightRig.ts */
|
|
467
|
+
_updateLights() {
|
|
468
|
+
this._lights.update();
|
|
444
469
|
}
|
|
445
470
|
_addLights() {
|
|
446
|
-
|
|
447
|
-
return;
|
|
448
|
-
if (!this._ambientLight)
|
|
449
|
-
this._createLights();
|
|
450
|
-
this.scene.add(this._ambientLight, this._hemiLight, this._keyLight, this._fillLight);
|
|
471
|
+
this._lights.addToScene();
|
|
451
472
|
}
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
this._ambientLight.intensity = this._lightOptions.ambientIntensity;
|
|
455
|
-
this._ambientLight.color.set(this._lightOptions.ambientColor);
|
|
456
|
-
}
|
|
457
|
-
if (this._hemiLight) {
|
|
458
|
-
this._hemiLight.intensity = this._lightOptions.hemisphereIntensity;
|
|
459
|
-
this._hemiLight.color.set(this._lightOptions.hemisphereColor);
|
|
460
|
-
this._hemiLight.groundColor.set(this._lightOptions.hemisphereGroundColor);
|
|
461
|
-
}
|
|
462
|
-
if (this._keyLight) {
|
|
463
|
-
this._keyLight.intensity = this._lightOptions.keyLightIntensity;
|
|
464
|
-
this._keyLight.color.set(this._lightOptions.keyLightColor);
|
|
465
|
-
}
|
|
466
|
-
if (this._fillLight) {
|
|
467
|
-
this._fillLight.intensity = this._lightOptions.fillLightIntensity;
|
|
468
|
-
this._fillLight.color.set(this._lightOptions.fillLightColor);
|
|
469
|
-
}
|
|
473
|
+
setLightingPreset(preset) {
|
|
474
|
+
this._lights.setLightingPreset(preset);
|
|
470
475
|
}
|
|
471
|
-
/**
|
|
472
|
-
* 动态设置光照配置
|
|
473
|
-
*/
|
|
476
|
+
/** 动态设置光照配置 */
|
|
474
477
|
setLight(options) {
|
|
475
|
-
|
|
476
|
-
if (normalized.mainColor !== undefined && normalized.keyLightColor === undefined) {
|
|
477
|
-
normalized.keyLightColor = normalized.mainColor;
|
|
478
|
-
}
|
|
479
|
-
if (normalized.mainIntensity !== undefined && normalized.keyLightIntensity === undefined) {
|
|
480
|
-
normalized.keyLightIntensity = normalized.mainIntensity;
|
|
481
|
-
}
|
|
482
|
-
Object.assign(this._lightOptions, normalized);
|
|
483
|
-
this._applyLightOptions();
|
|
484
|
-
this._updateLights();
|
|
485
|
-
this._needsRepaint = true;
|
|
486
|
-
this.triggerRepaint();
|
|
478
|
+
this._lights.setLight(options);
|
|
487
479
|
}
|
|
488
|
-
/**
|
|
489
|
-
* 获取当前光照配置快照
|
|
490
|
-
*/
|
|
480
|
+
/** 获取当前光照配置快照 */
|
|
491
481
|
getLight() {
|
|
492
|
-
return
|
|
482
|
+
return this._lights.getLight();
|
|
493
483
|
}
|
|
494
|
-
/**
|
|
495
|
-
* 动态设置色调映射模式与曝光度
|
|
496
|
-
*/
|
|
484
|
+
/** 动态设置色调映射模式与曝光度 */
|
|
497
485
|
setToneMapping(toneMapping, exposure = 1.0) {
|
|
498
|
-
this.
|
|
499
|
-
this.options.toneMappingExposure = exposure;
|
|
500
|
-
if (this.renderer) {
|
|
501
|
-
this.renderer.toneMapping = toneMapping;
|
|
502
|
-
this.renderer.toneMappingExposure = exposure;
|
|
503
|
-
this._needsRepaint = true;
|
|
504
|
-
this.triggerRepaint();
|
|
505
|
-
}
|
|
486
|
+
this._lights.setToneMapping(toneMapping, exposure);
|
|
506
487
|
}
|
|
507
|
-
/**
|
|
508
|
-
* 获取当前色调映射模式与曝光度
|
|
509
|
-
*/
|
|
488
|
+
/** 获取当前色调映射模式与曝光度 */
|
|
510
489
|
getToneMapping() {
|
|
511
|
-
return
|
|
512
|
-
toneMapping: this.renderer ? this.renderer.toneMapping : (this.options.toneMapping ?? THREE.NoToneMapping),
|
|
513
|
-
toneMappingExposure: this.renderer ? this.renderer.toneMappingExposure : (this.options.toneMappingExposure ?? 1.0),
|
|
514
|
-
};
|
|
490
|
+
return this._lights.getToneMapping();
|
|
515
491
|
}
|
|
516
492
|
/**
|
|
517
|
-
*
|
|
518
|
-
* - 'headlamp': 随地图方位角 bearing 旋转,主光始终在镜头上方,受光面面向镜头;
|
|
519
|
-
* - 'fixed': 固定世界方位角(如模拟恒定日光)。
|
|
493
|
+
* 挂 / 摘环境贴图。见 viewer/LightRig.ts(PMREM 预滤波 + 旧 RT 释放)。
|
|
520
494
|
*/
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
return;
|
|
524
|
-
const bearing = this._lightOptions.mode === 'fixed' ? 0 : (((this._getBearing?.() ?? 0) * Math.PI) / 180);
|
|
525
|
-
const place = (light, azOffsetDeg, elevDeg) => {
|
|
526
|
-
if (!light)
|
|
527
|
-
return;
|
|
528
|
-
const az = bearing + (azOffsetDeg * Math.PI) / 180;
|
|
529
|
-
const el = (elevDeg * Math.PI) / 180;
|
|
530
|
-
const c = Math.cos(el);
|
|
531
|
-
light.position.set(Math.sin(az) * c, Math.cos(az) * c, Math.sin(el)).multiplyScalar(1000);
|
|
532
|
-
};
|
|
533
|
-
place(this._keyLight, this._lightOptions.azimuth, this._lightOptions.altitude);
|
|
534
|
-
place(this._fillLight, this._lightOptions.azimuth + 65, Math.max(10, this._lightOptions.altitude - 20));
|
|
495
|
+
setEnvironment(source, intensity = 1) {
|
|
496
|
+
return this._lights.setEnvironment(source, intensity);
|
|
535
497
|
}
|
|
536
498
|
/* ---------------------------------------------------------------- 数据集 */
|
|
537
499
|
_normalizeDatasets(options) {
|
|
538
500
|
if (Array.isArray(options.datasets))
|
|
539
|
-
return options.datasets
|
|
501
|
+
return options.datasets
|
|
502
|
+
.filter((d) => !!d?.configUrl)
|
|
503
|
+
.map((d) => ({ ...d, polygonOffset: d.polygonOffset ?? options.polygonOffset }));
|
|
540
504
|
if (options.configUrl)
|
|
541
|
-
return [{ id: 'default', name: '默认数据集', configUrl: options.configUrl, useWorker: options.useWorker }];
|
|
505
|
+
return [{ id: 'default', name: '默认数据集', configUrl: options.configUrl, useWorker: options.useWorker, polygonOffset: options.polygonOffset }];
|
|
542
506
|
return [];
|
|
543
507
|
}
|
|
544
508
|
/** 追加一个数据集。图层 addLayer 之前调用会抛错。 */
|
|
545
509
|
async addDataset(ds) {
|
|
546
510
|
if (!this.scene)
|
|
547
511
|
throw new Error('[s3m] 图层尚未 addLayer,不能添加数据集');
|
|
548
|
-
|
|
512
|
+
// 图层级默认值:数据集自己指定了就用自己的,见 `S3MViewerOptions.unsupported`
|
|
513
|
+
const merged = { ...ds, unsupported: ds.unsupported ?? this._unsupported };
|
|
514
|
+
const entry = await this._datasetManager.addDataset(merged, this.scene, this.geo, this.uniforms, this._lodRangeScale, this.options.buildTile, this.options.maxTiles ?? 64, this.options.maxConcurrent ?? 4, {
|
|
549
515
|
onTile: (node, e) => {
|
|
550
516
|
this._registerTile(node, e);
|
|
551
517
|
this.onStats(this.stats);
|
|
552
|
-
this._needsRepaint = true;
|
|
553
518
|
this._requestRepaint();
|
|
554
519
|
},
|
|
555
520
|
onTileRemoved: (node, e) => {
|
|
556
521
|
this._unregisterTile(node, e);
|
|
557
522
|
this.onStats(this.stats);
|
|
558
|
-
this._needsRepaint = true;
|
|
559
523
|
this._requestRepaint();
|
|
560
524
|
},
|
|
561
525
|
onStats: () => {
|
|
@@ -568,7 +532,7 @@ export class S3MViewer {
|
|
|
568
532
|
},
|
|
569
533
|
onReady: (e) => {
|
|
570
534
|
this.onReady(e);
|
|
571
|
-
this.
|
|
535
|
+
this._heights.refreshAll();
|
|
572
536
|
if (!this.groundManager.excavationBounds) {
|
|
573
537
|
this.groundManager.autoFitExcavation(this.datasets.values());
|
|
574
538
|
}
|
|
@@ -588,7 +552,7 @@ export class S3MViewer {
|
|
|
588
552
|
/** 当地图 CRS 投影切换或宿主 map 挂载后,重新计算场景锚点与各数据集 offset */
|
|
589
553
|
recomputeAnchors() {
|
|
590
554
|
this._datasetManager.recomputeAnchors(this.geo);
|
|
591
|
-
this.
|
|
555
|
+
this._heights.refreshAll();
|
|
592
556
|
this._requestRepaint();
|
|
593
557
|
}
|
|
594
558
|
_recomputeAnchors() {
|
|
@@ -620,7 +584,7 @@ export class S3MViewer {
|
|
|
620
584
|
}
|
|
621
585
|
this._datasetManager.setDatasetHeight(id, h, this.geo);
|
|
622
586
|
this._syncGroundHeight();
|
|
623
|
-
this.
|
|
587
|
+
this._heights.refreshAll();
|
|
624
588
|
this._requestRepaint();
|
|
625
589
|
}
|
|
626
590
|
/**
|
|
@@ -653,7 +617,7 @@ export class S3MViewer {
|
|
|
653
617
|
a = align ?? 'origin';
|
|
654
618
|
}
|
|
655
619
|
this._datasetManager.setDatasetAlign(id, a, this.geo);
|
|
656
|
-
this.
|
|
620
|
+
this._heights.refreshAll();
|
|
657
621
|
this._requestRepaint();
|
|
658
622
|
}
|
|
659
623
|
/** @internal 语义高度对齐计算代理,供内部与测试使用 */
|
|
@@ -1162,7 +1126,7 @@ export class S3MViewer {
|
|
|
1162
1126
|
return [...this._smidMeshes.keys()];
|
|
1163
1127
|
}
|
|
1164
1128
|
auditBatches() {
|
|
1165
|
-
const res = this._featureStyleManager.auditBatches(this.
|
|
1129
|
+
const res = this._featureStyleManager.auditBatches(this._picks.pickables.values());
|
|
1166
1130
|
res.smids = this._smidMeshes.size;
|
|
1167
1131
|
return res;
|
|
1168
1132
|
}
|
|
@@ -1257,7 +1221,7 @@ export class S3MViewer {
|
|
|
1257
1221
|
}
|
|
1258
1222
|
getAllMeshes() {
|
|
1259
1223
|
const list = [];
|
|
1260
|
-
for (const p of this.
|
|
1224
|
+
for (const p of this._picks.pickables.values())
|
|
1261
1225
|
list.push(p.mesh);
|
|
1262
1226
|
return list;
|
|
1263
1227
|
}
|
|
@@ -1271,14 +1235,6 @@ export class S3MViewer {
|
|
|
1271
1235
|
}
|
|
1272
1236
|
return out;
|
|
1273
1237
|
}
|
|
1274
|
-
_markOverridesDirty(scope = 'all') {
|
|
1275
|
-
if (scope === 'all') {
|
|
1276
|
-
this._featureStyleManager.markOverridesDirty(undefined, this.getAllMeshes());
|
|
1277
|
-
}
|
|
1278
|
-
else {
|
|
1279
|
-
this._featureStyleManager.markOverridesDirty(scope);
|
|
1280
|
-
}
|
|
1281
|
-
}
|
|
1282
1238
|
flushOverrides() {
|
|
1283
1239
|
this._featureStyleManager.flushOverrides();
|
|
1284
1240
|
}
|
|
@@ -1290,19 +1246,19 @@ export class S3MViewer {
|
|
|
1290
1246
|
const entry = this.datasets.get(id);
|
|
1291
1247
|
if (!entry)
|
|
1292
1248
|
return;
|
|
1293
|
-
if (this.
|
|
1249
|
+
if (this._sel.selection?.datasetId === id)
|
|
1294
1250
|
this.select(null);
|
|
1295
1251
|
this._datasetManager.removeDataset(id, this.scene, this.geo, (e) => {
|
|
1296
1252
|
e.root.traverse((o) => {
|
|
1297
1253
|
const mesh = o;
|
|
1298
1254
|
const ud = mesh.userData;
|
|
1299
1255
|
if (ud.pickId != null) {
|
|
1300
|
-
this.
|
|
1256
|
+
this._picks.pickables.delete(ud.pickId);
|
|
1301
1257
|
this._releasePickId(ud.pickId);
|
|
1302
1258
|
ud.pickId = undefined;
|
|
1303
1259
|
}
|
|
1304
1260
|
this._unindexMeshSmids(mesh);
|
|
1305
|
-
this.
|
|
1261
|
+
this._heights.remove(mesh);
|
|
1306
1262
|
mesh.geometry?.dispose?.();
|
|
1307
1263
|
const styled = styledMaterialOf(mesh);
|
|
1308
1264
|
if (styled)
|
|
@@ -1312,9 +1268,9 @@ export class S3MViewer {
|
|
|
1312
1268
|
this._featureStyleManager.disposeMesh(mesh);
|
|
1313
1269
|
});
|
|
1314
1270
|
});
|
|
1315
|
-
for (const [pickId, p] of this.
|
|
1271
|
+
for (const [pickId, p] of this._picks.pickables)
|
|
1316
1272
|
if (p.datasetId === id)
|
|
1317
|
-
this.
|
|
1273
|
+
this._picks.pickables.delete(pickId);
|
|
1318
1274
|
this.onStats(this.stats);
|
|
1319
1275
|
this._requestRepaint();
|
|
1320
1276
|
}
|
|
@@ -1325,7 +1281,7 @@ export class S3MViewer {
|
|
|
1325
1281
|
}
|
|
1326
1282
|
setDatasetVisible(id, visible) {
|
|
1327
1283
|
this._datasetManager.setDatasetVisible(id, visible);
|
|
1328
|
-
if (!visible && this.
|
|
1284
|
+
if (!visible && this._sel.selection?.datasetId === id)
|
|
1329
1285
|
this.select(null);
|
|
1330
1286
|
this._requestRepaint();
|
|
1331
1287
|
}
|
|
@@ -1337,14 +1293,12 @@ export class S3MViewer {
|
|
|
1337
1293
|
// forceCpuEdges:无视数据自带的棱线包,改由 CPU 现算。native 是 1px 的
|
|
1338
1294
|
// LineBasicMaterial,edgeWidth / 外扩 / 单体跟随全都吃不上。
|
|
1339
1295
|
// gpu 画法由 shader 自己描边,native 线段包同样不能留
|
|
1340
|
-
const useNativeEdges =
|
|
1296
|
+
const useNativeEdges = this._edges.useNativeEdges(hasNativeEdges);
|
|
1341
1297
|
for (const child of node.group.children) {
|
|
1342
1298
|
// 数据自带的棱线包:只跟着 edges 开关显示,不参与拾取、不参与着色
|
|
1343
1299
|
if (child.isLineSegments && child.userData.isEdgeLines) {
|
|
1344
|
-
//
|
|
1345
|
-
this.
|
|
1346
|
-
child.visible = useNativeEdges && this._edgesEnabled;
|
|
1347
|
-
this._styleEdgeLine(child);
|
|
1300
|
+
// 一律登记:forceCpuEdges 之后又关掉时还要能把它们放出来
|
|
1301
|
+
this._edges.registerNativeLine(child, useNativeEdges && this._edges.enabled);
|
|
1348
1302
|
continue;
|
|
1349
1303
|
}
|
|
1350
1304
|
// 点云是 THREE.Points 不是 Mesh,但同样要进拾取表、吃不透明度、跟着着色模式走
|
|
@@ -1354,30 +1308,26 @@ export class S3MViewer {
|
|
|
1354
1308
|
continue;
|
|
1355
1309
|
if (isPoints)
|
|
1356
1310
|
this._pointClouds.add(child);
|
|
1311
|
+
// 新瓦片里的泼溅实例还没按当前视角排过,下一帧要重排
|
|
1312
|
+
if (isPoints)
|
|
1313
|
+
this._splats.markDirty();
|
|
1357
1314
|
const ud = mesh.userData;
|
|
1358
1315
|
ud.pickId = this._allocPickId();
|
|
1359
1316
|
ud.datasetId = entry.id;
|
|
1360
1317
|
ud.nativeEdges = useNativeEdges;
|
|
1361
1318
|
ud.nativeEdgesRaw = hasNativeEdges;
|
|
1362
|
-
this.
|
|
1319
|
+
this._picks.pickables.set(ud.pickId, {
|
|
1363
1320
|
datasetId: entry.id,
|
|
1364
1321
|
mesh,
|
|
1365
1322
|
tile: node.tile,
|
|
1366
1323
|
skeletonName: ud.skeletonName ?? '',
|
|
1367
1324
|
});
|
|
1368
1325
|
this._indexMeshSmids(mesh);
|
|
1369
|
-
this.
|
|
1326
|
+
this._heights.add(mesh);
|
|
1370
1327
|
// 点云没有「棱」:给一堆孤立点建 EdgesGeometry 又慢又没意义
|
|
1371
1328
|
// 点云没有「棱」:给一堆孤立点建 EdgesGeometry 又慢又没意义。
|
|
1372
1329
|
// 自动描边受三道开关控制:全局 style.edges / 数据集 enableEdges / 地形绿地等关键字跳过。
|
|
1373
|
-
|
|
1374
|
-
// 数据集强制开启(enableEdges: true)时全局画法可能还是关的,退回默认折边线;
|
|
1375
|
-
// 但 gpu 画法本来就不建几何,别在这里多此一举
|
|
1376
|
-
if (this._edgeModes.line || this._edgeModes.hull)
|
|
1377
|
-
this._ensureEdgeDecoration(mesh);
|
|
1378
|
-
else if (!this._edgeModes.gpu)
|
|
1379
|
-
this._ensureEdges(mesh);
|
|
1380
|
-
}
|
|
1330
|
+
this._edges.autoDecorate(mesh, entry, useNativeEdges);
|
|
1381
1331
|
// 新瓦片要继承当前的不透明度与着色方式
|
|
1382
1332
|
if (isPoints) {
|
|
1383
1333
|
this._applyPointCloudStyle(child);
|
|
@@ -1387,9 +1337,11 @@ export class S3MViewer {
|
|
|
1387
1337
|
if (mat) {
|
|
1388
1338
|
const o = clampOpacity(this.style.opacity);
|
|
1389
1339
|
// 不写 mat.opacity:全局透明度走共享 uOpacity,材质自身 opacity 保留模型自带 alpha
|
|
1390
|
-
|
|
1340
|
+
// 混合开关不能写成 `o < 0.999` 一刀切:材质自身可能是 blend / 顶点 alpha
|
|
1341
|
+
// 透明(s3mOpts.transparent,如 SMID 4421),注册时就被这句踩掉,
|
|
1342
|
+
// 表现为「刚进去没透明度,切换材质质感重建后才恢复」。
|
|
1391
1343
|
const keepDepth = this.style.depthWrite !== false;
|
|
1392
|
-
mat
|
|
1344
|
+
restoreBaseBlendState(mat, o < 0.999, keepDepth);
|
|
1393
1345
|
// shading 走共享 uniform(uFlatShading),不写 mat.flatShading / needsUpdate——
|
|
1394
1346
|
// transparent / depthWrite 也不是 program 决定项,整套样式继承零重编译
|
|
1395
1347
|
if (mat.userData.uFlatShading) {
|
|
@@ -1401,7 +1353,7 @@ export class S3MViewer {
|
|
|
1401
1353
|
}
|
|
1402
1354
|
// 选中状态也要跟着继承:LOD 细化 / 瓦片重载会换掉 mesh,
|
|
1403
1355
|
// 不重放就出现「点中的那栋楼一拉近就没高亮了」
|
|
1404
|
-
this.
|
|
1356
|
+
this._sel.replayOnto(mesh);
|
|
1405
1357
|
}
|
|
1406
1358
|
}
|
|
1407
1359
|
/**
|
|
@@ -1443,83 +1395,24 @@ export class S3MViewer {
|
|
|
1443
1395
|
mesh.userData.tileFadeKey = undefined;
|
|
1444
1396
|
this.animator.unregister(key);
|
|
1445
1397
|
// 淡入期间强制开了 transparent + depthWrite,结束时不能自己拍板关掉——
|
|
1446
|
-
//
|
|
1398
|
+
// 先恢复「材质自身语义 || 图层透明度」的基础状态(材质可能是 blend /
|
|
1399
|
+
// 顶点 alpha 透明,如 SMID 4421),再让单体淡化流程有需要时重新打开。
|
|
1400
|
+
restoreBaseBlendState(mat, this._opacityTransparent, this.style.depthWrite !== false);
|
|
1447
1401
|
this._featureStyleManager.applyOverridesToMesh(mesh);
|
|
1448
|
-
mat.depthWrite = this.style.depthWrite !== false;
|
|
1449
1402
|
this._requestRepaint();
|
|
1450
1403
|
},
|
|
1451
1404
|
});
|
|
1452
1405
|
}
|
|
1453
|
-
/**
|
|
1454
|
-
* 把「当前选中」重放到一块新登记的 mesh 上。
|
|
1455
|
-
*
|
|
1456
|
-
* 为什么需要:选中高亮是直接写在各 mesh 材质的 uSelBatch / uSelWhole 上的,
|
|
1457
|
-
* 而 LOD 细化、瓦片重载、淘汰回补都会换掉 mesh —— 不重放的话,用户点中的那栋楼
|
|
1458
|
-
* 一细化就失去高亮(状态还在 _selection.smid 里,只是没人往新 mesh 上刷)。
|
|
1459
|
-
*/
|
|
1460
|
-
/**
|
|
1461
|
-
* 判断一块无 SMID 单体化信息的网格(如倾斜摄影或白模整块)是否空间覆盖当前选中命中点
|
|
1462
|
-
*/
|
|
1463
|
-
_isMeshCoveringSelection(mesh, sel) {
|
|
1464
|
-
if (!sel.point)
|
|
1465
|
-
return false;
|
|
1466
|
-
const geom = mesh.geometry;
|
|
1467
|
-
if (!geom)
|
|
1468
|
-
return false;
|
|
1469
|
-
if (!geom.boundingBox)
|
|
1470
|
-
geom.computeBoundingBox();
|
|
1471
|
-
if (!geom.boundingBox)
|
|
1472
|
-
return false;
|
|
1473
|
-
// 将选中的三维场景点转入网格局部坐标系进行包围盒检测
|
|
1474
|
-
const pt = new THREE.Vector3(sel.point[0], sel.point[1], sel.point[2]);
|
|
1475
|
-
mesh.updateWorldMatrix(true, false);
|
|
1476
|
-
const inv = mesh.matrixWorld.clone().invert();
|
|
1477
|
-
const localPt = pt.applyMatrix4(inv);
|
|
1478
|
-
// 留出 0.5 米容差,避免刚好处于三角网格边缘缝隙时漏判
|
|
1479
|
-
const box = geom.boundingBox.clone().expandByScalar(0.5);
|
|
1480
|
-
return box.containsPoint(localPt);
|
|
1481
|
-
}
|
|
1406
|
+
/** @internal 把当前选中重放到新登记的 mesh 上,见 viewer/SelectionDecorator.ts */
|
|
1482
1407
|
_replaySelectionOnto(mesh) {
|
|
1483
|
-
|
|
1484
|
-
if (!sel)
|
|
1485
|
-
return;
|
|
1486
|
-
const mat = styledMaterialOf(mesh);
|
|
1487
|
-
if (!mat)
|
|
1488
|
-
return;
|
|
1489
|
-
const ud = mesh.userData;
|
|
1490
|
-
let batchId = -1;
|
|
1491
|
-
if (sel.smid != null) {
|
|
1492
|
-
const pickInfo = ud.geoRef?.pickInfo;
|
|
1493
|
-
const entry = pickInfo?.[sel.smid] ?? pickInfo?.[String(sel.smid)];
|
|
1494
|
-
batchId = entry ? pickEntryId(entry) : -1;
|
|
1495
|
-
}
|
|
1496
|
-
// 拾取命中的那块 mesh 用原始 batchId;同一 smid 散在别的几何里时按各自的区间算
|
|
1497
|
-
if (mesh === sel.mesh)
|
|
1498
|
-
batchId = sel.batchId;
|
|
1499
|
-
// 这块几何里根本没有这个单体 → 不动它(否则会把无关瓦片一起点亮)
|
|
1500
|
-
if (batchId < 0 && mesh !== sel.mesh) {
|
|
1501
|
-
if (sel.smid == null && ud.datasetId === sel.datasetId && this._isMeshCoveringSelection(mesh, sel)) {
|
|
1502
|
-
batchId = -1;
|
|
1503
|
-
}
|
|
1504
|
-
else {
|
|
1505
|
-
return;
|
|
1506
|
-
}
|
|
1507
|
-
}
|
|
1508
|
-
mat.userData.uSelBatch.value = batchId;
|
|
1509
|
-
mat.userData.uSelWhole.value = batchId < 0 ? 1 : 0;
|
|
1510
|
-
if (!this._selectedMeshes.includes(mesh))
|
|
1511
|
-
this._selectedMeshes.push(mesh);
|
|
1512
|
-
if (isPointCloudObject(mesh))
|
|
1513
|
-
this._applyPointCloudStyle(mesh, true);
|
|
1514
|
-
this.uniforms.uSelMix.value = 1;
|
|
1515
|
-
this.uniforms.uDim.value = this.style.dimOthers ? 1 : 0;
|
|
1408
|
+
this._sel.replayOnto(mesh);
|
|
1516
1409
|
}
|
|
1517
1410
|
/** 瓦片被淘汰:从拾取表和场景里摘掉,并深度彻底释放所有几何体与材质显存 */
|
|
1518
1411
|
_unregisterTile(node, entry) {
|
|
1519
1412
|
node.group.traverse((child) => {
|
|
1520
1413
|
const obj = child;
|
|
1521
1414
|
if (child.isLineSegments && obj.userData?.isEdgeLines) {
|
|
1522
|
-
this.
|
|
1415
|
+
this._edges.unregisterNativeLine(child);
|
|
1523
1416
|
}
|
|
1524
1417
|
if (isPointCloudObject(child))
|
|
1525
1418
|
this._pointClouds.delete(child);
|
|
@@ -1529,33 +1422,33 @@ export class S3MViewer {
|
|
|
1529
1422
|
obj.userData.tileFadeKey = undefined;
|
|
1530
1423
|
}
|
|
1531
1424
|
if (obj.userData?.pickId != null) {
|
|
1532
|
-
this.
|
|
1425
|
+
this._picks.pickables.delete(obj.userData.pickId);
|
|
1533
1426
|
this._releasePickId(obj.userData.pickId);
|
|
1534
1427
|
obj.userData.pickId = undefined;
|
|
1535
1428
|
}
|
|
1536
1429
|
if (obj.isMesh) {
|
|
1537
|
-
const wasSelected = this.
|
|
1430
|
+
const wasSelected = this._sel.selectedMeshes.includes(obj);
|
|
1538
1431
|
if (wasSelected) {
|
|
1539
|
-
this.
|
|
1432
|
+
this._sel.selectedMeshes = this._sel.selectedMeshes.filter((m) => m !== obj);
|
|
1540
1433
|
}
|
|
1541
|
-
const currentSel = this.
|
|
1434
|
+
const currentSel = this._sel.selection;
|
|
1542
1435
|
if (currentSel && currentSel.mesh === obj) {
|
|
1543
1436
|
const smid = currentSel.smid;
|
|
1544
1437
|
const carrier = (smid != null
|
|
1545
1438
|
? [...(this._smidMeshes.get(smid) ?? [])].find((m) => m !== obj && m.parent)
|
|
1546
|
-
: this.
|
|
1547
|
-
this.
|
|
1439
|
+
: this._sel.selectedMeshes.find((m) => m !== obj && m.parent && m.parent.visible) ??
|
|
1440
|
+
this._sel.selectedMeshes.find((m) => m !== obj && m.parent));
|
|
1548
1441
|
if (carrier) {
|
|
1549
1442
|
// 选中身份交给承载同一 smid 的另一块 mesh(通常是接替显示的子瓦)
|
|
1550
|
-
this.
|
|
1551
|
-
this.
|
|
1443
|
+
this._sel.selection = { ...currentSel, mesh: carrier };
|
|
1444
|
+
this._sel.replayOnto(carrier);
|
|
1552
1445
|
}
|
|
1553
1446
|
else {
|
|
1554
1447
|
this.select(null);
|
|
1555
1448
|
}
|
|
1556
1449
|
}
|
|
1557
1450
|
this._unindexMeshSmids(obj);
|
|
1558
|
-
this.
|
|
1451
|
+
this._heights.remove(obj);
|
|
1559
1452
|
this._featureStyleManager.disposeMesh(obj);
|
|
1560
1453
|
const styled = styledMaterialOf(obj);
|
|
1561
1454
|
styled?.dispose?.();
|
|
@@ -1581,21 +1474,6 @@ export class S3MViewer {
|
|
|
1581
1474
|
* 拾取 ID 只有 2 个字节(RG 两个通道),65535 个网格就到头了,
|
|
1582
1475
|
* 所以必须回收:瓦片淘汰 / 数据集移除时把 id 还回池子。
|
|
1583
1476
|
*/
|
|
1584
|
-
_allocPickId() {
|
|
1585
|
-
const reused = this._freePickIds.pop();
|
|
1586
|
-
if (reused !== undefined)
|
|
1587
|
-
return reused;
|
|
1588
|
-
if (this._pickSeq > MAX_PICK_ID) {
|
|
1589
|
-
for (let id = 0; id <= MAX_PICK_ID; id++)
|
|
1590
|
-
if (!this._pickables.has(id))
|
|
1591
|
-
return id;
|
|
1592
|
-
throw new Error(`[s3m] 可拾取网格超过 ${MAX_PICK_ID + 1} 个,2 字节 ID 编码不够用`);
|
|
1593
|
-
}
|
|
1594
|
-
return this._pickSeq++;
|
|
1595
|
-
}
|
|
1596
|
-
_releasePickId(id) {
|
|
1597
|
-
this._freePickIds.push(id);
|
|
1598
|
-
}
|
|
1599
1477
|
/* ---------------------------------------------------------------- 样式 */
|
|
1600
1478
|
/** 动态设置图层整体透明度 (0.0 ~ 1.0) */
|
|
1601
1479
|
setOpacity(opacity, transition) {
|
|
@@ -1705,7 +1583,7 @@ export class S3MViewer {
|
|
|
1705
1583
|
write: (v) => {
|
|
1706
1584
|
if (this.uniforms.uEdgeFade)
|
|
1707
1585
|
this.uniforms.uEdgeFade.value = v[0];
|
|
1708
|
-
this.
|
|
1586
|
+
this._edges.syncStyle();
|
|
1709
1587
|
},
|
|
1710
1588
|
});
|
|
1711
1589
|
// 整体压暗乘子:setMode 的 fade-through 用它,也不是 S3MStyle 字段
|
|
@@ -1733,7 +1611,7 @@ export class S3MViewer {
|
|
|
1733
1611
|
key === 'edgeOpacity' ||
|
|
1734
1612
|
key === 'edgeOffset' ||
|
|
1735
1613
|
key === 'edgeHullWidth') {
|
|
1736
|
-
this.
|
|
1614
|
+
this._edges.refreshLines();
|
|
1737
1615
|
}
|
|
1738
1616
|
}
|
|
1739
1617
|
/** 取某个颜色键对应的共享 uniform Color 对象 */
|
|
@@ -1767,8 +1645,8 @@ export class S3MViewer {
|
|
|
1767
1645
|
// 临界角变了要重建 CPU 棱线(EdgesGeometry 的参数),开 / 关走 setEdges 整体刷新。
|
|
1768
1646
|
// 必须先 _disposeEdges():setEdges 只挑「还没有棱线」的网格来建,
|
|
1769
1647
|
// 不先拆掉的话已经建好的会被全部跳过,临界角改了等于没改。
|
|
1770
|
-
if (('edgeThreshold' in patch || 'forceCpuEdges' in patch) && this.
|
|
1771
|
-
this.
|
|
1648
|
+
if (('edgeThreshold' in patch || 'forceCpuEdges' in patch) && this._edges.enabled) {
|
|
1649
|
+
this._edges.disposeEdgeGeometry();
|
|
1772
1650
|
this.setEdges(this.style.edges);
|
|
1773
1651
|
}
|
|
1774
1652
|
else if ('edgeColor' in patch ||
|
|
@@ -1778,14 +1656,14 @@ export class S3MViewer {
|
|
|
1778
1656
|
'edgeOffset' in patch ||
|
|
1779
1657
|
'edgeHullWidth' in patch) {
|
|
1780
1658
|
// 颜色 / 粗细 / 外推距离 / 外扩厚度都只是 uniform,拖着滑块逐帧改也不重建几何
|
|
1781
|
-
this.
|
|
1659
|
+
this._edges.refreshLines();
|
|
1782
1660
|
}
|
|
1783
|
-
if (this.
|
|
1661
|
+
if (this._sel.selection) {
|
|
1784
1662
|
// 换描边画法要重建几何;颜色 / 粗细 / 拔高只是 uniform,拖着滑块也不卡
|
|
1785
1663
|
if ('outline' in patch || 'outlineStyle' in patch)
|
|
1786
|
-
this.select(this.
|
|
1664
|
+
this.select(this._sel.selection, { silent: true });
|
|
1787
1665
|
else
|
|
1788
|
-
this.
|
|
1666
|
+
this._sel.syncStyle();
|
|
1789
1667
|
}
|
|
1790
1668
|
}
|
|
1791
1669
|
_applyStyleUniforms() {
|
|
@@ -1825,6 +1703,11 @@ export class S3MViewer {
|
|
|
1825
1703
|
if (u.uAmbientGain && s.ambientGain != null) {
|
|
1826
1704
|
u.uAmbientGain.value = s.ambientGain;
|
|
1827
1705
|
}
|
|
1706
|
+
// 金属度 / 粗糙度:负值 = 不覆盖,保持材质(数据)自身的值
|
|
1707
|
+
if (u.uMetalness)
|
|
1708
|
+
u.uMetalness.value = s.metalness ?? -1;
|
|
1709
|
+
if (u.uRoughness)
|
|
1710
|
+
u.uRoughness.value = s.roughness ?? -1;
|
|
1828
1711
|
if (u.uOpacity && s.opacity != null) {
|
|
1829
1712
|
u.uOpacity.value = clampOpacity(s.opacity);
|
|
1830
1713
|
}
|
|
@@ -1860,7 +1743,7 @@ export class S3MViewer {
|
|
|
1860
1743
|
this._opacityTransparent = transparent;
|
|
1861
1744
|
// 关键:若 depthWrite 开启(默认开启),即使半透明也保持深度写入,完美遮挡背面棱线,防止背面线条透出
|
|
1862
1745
|
const keepDepth = this.style.depthWrite !== false;
|
|
1863
|
-
for (const p of this.
|
|
1746
|
+
for (const p of this._picks.pickables.values()) {
|
|
1864
1747
|
if (isPointCloudObject(p.mesh))
|
|
1865
1748
|
continue;
|
|
1866
1749
|
const m = styledMaterialOf(p.mesh);
|
|
@@ -1871,18 +1754,19 @@ export class S3MViewer {
|
|
|
1871
1754
|
continue;
|
|
1872
1755
|
if ((m.userData.uTileFade?.value ?? 1) < 1)
|
|
1873
1756
|
continue;
|
|
1874
|
-
|
|
1875
|
-
|
|
1757
|
+
// 材质自身可能是 blend / 顶点 alpha 透明(s3mOpts.transparent),
|
|
1758
|
+
// 关全局混合时不能把它一刀切成不透明
|
|
1759
|
+
restoreBaseBlendState(m, transparent, keepDepth);
|
|
1876
1760
|
}
|
|
1877
1761
|
}
|
|
1878
1762
|
// 点云不走 StyleMaterial,自己有一套 uOpacity,逐帧刷的只有这几个对象
|
|
1879
1763
|
for (const pc of this._pointClouds)
|
|
1880
1764
|
this._applyPointCloudStyle(pc);
|
|
1881
|
-
this.
|
|
1765
|
+
this._edges.syncStyle();
|
|
1882
1766
|
this._requestRepaint();
|
|
1883
1767
|
}
|
|
1884
1768
|
/** 单个点云对象同步当前样式(着色模式 + 不透明度 + 选中高亮) */
|
|
1885
|
-
_applyPointCloudStyle(points, selected = this.
|
|
1769
|
+
_applyPointCloudStyle(points, selected = this._sel.selectedMeshes.includes(points)) {
|
|
1886
1770
|
applyPointCloudStyle(points, this.style, this.uniforms, selected);
|
|
1887
1771
|
}
|
|
1888
1772
|
/** 全部点云刷新:着色模式切换时用(uniform 是共享引用,改色不用走这里) */
|
|
@@ -1896,7 +1780,7 @@ export class S3MViewer {
|
|
|
1896
1780
|
// shading 现在是共享 uniform(uFlatShading)驱动的运行期分支,不再是
|
|
1897
1781
|
// flatShading define:翻一个 float 就完成切换,零重编译、零卡顿。
|
|
1898
1782
|
const target = this.style.shading !== 'smooth' ? 1 : 0;
|
|
1899
|
-
for (const p of this.
|
|
1783
|
+
for (const p of this._picks.pickables.values()) {
|
|
1900
1784
|
const m = styledMaterialOf(p.mesh);
|
|
1901
1785
|
const u = m?.userData.uFlatShading;
|
|
1902
1786
|
if (!u || u.value === target)
|
|
@@ -1934,7 +1818,7 @@ export class S3MViewer {
|
|
|
1934
1818
|
'uBatchTexActive',
|
|
1935
1819
|
'uTileFade',
|
|
1936
1820
|
];
|
|
1937
|
-
for (const p of this.
|
|
1821
|
+
for (const p of this._picks.pickables.values()) {
|
|
1938
1822
|
const mesh = p.mesh;
|
|
1939
1823
|
const raw = mesh.material;
|
|
1940
1824
|
if (Array.isArray(raw))
|
|
@@ -1972,7 +1856,7 @@ export class S3MViewer {
|
|
|
1972
1856
|
return;
|
|
1973
1857
|
// 场上还没有材质就没必要预热:program 会在首个材质出现时编译
|
|
1974
1858
|
let hasMaterial = false;
|
|
1975
|
-
for (const p of this.
|
|
1859
|
+
for (const p of this._picks.pickables.values()) {
|
|
1976
1860
|
if (styledMaterialOf(p.mesh)) {
|
|
1977
1861
|
hasMaterial = true;
|
|
1978
1862
|
break;
|
|
@@ -2000,67 +1884,13 @@ export class S3MViewer {
|
|
|
2000
1884
|
* 加载 n 块瓦片就是 O(n²) 次矩阵乘 + n² 个垃圾对象。现在只在网格进出的记一次,
|
|
2001
1885
|
* 全局值从缓存里滚:只有被移除的那块正好卡在边界上时,才回退到一次 O(n) 重算。
|
|
2002
1886
|
*/
|
|
1887
|
+
/** @internal 见 viewer/HeightIndex.ts(regressions.test.ts 直接调) */
|
|
2003
1888
|
_addMeshHeight(mesh) {
|
|
2004
|
-
|
|
2005
|
-
if (!bb)
|
|
2006
|
-
return;
|
|
2007
|
-
this._boxScratch.copy(bb).applyMatrix4(mesh.matrixWorld);
|
|
2008
|
-
const min = this._boxScratch.min.z;
|
|
2009
|
-
const max = this._boxScratch.max.z;
|
|
2010
|
-
if (!Number.isFinite(min) || !Number.isFinite(max))
|
|
2011
|
-
return;
|
|
2012
|
-
this._meshHeight.set(mesh, { min, max });
|
|
2013
|
-
if (min < this._heightMin)
|
|
2014
|
-
this._heightMin = min;
|
|
2015
|
-
if (max > this._heightMax)
|
|
2016
|
-
this._heightMax = max;
|
|
2017
|
-
this._pushHeightRange();
|
|
1889
|
+
this._heights.add(mesh);
|
|
2018
1890
|
}
|
|
1891
|
+
/** @internal 见 viewer/HeightIndex.ts */
|
|
2019
1892
|
_removeMeshHeight(mesh) {
|
|
2020
|
-
|
|
2021
|
-
if (!rec)
|
|
2022
|
-
return;
|
|
2023
|
-
this._meshHeight.delete(mesh);
|
|
2024
|
-
// 只有正好卡在边界上才需要重算,否则滚动的 min/max 依然成立
|
|
2025
|
-
if (rec.min === this._heightMin || rec.max === this._heightMax)
|
|
2026
|
-
this._recomputeHeightRange();
|
|
2027
|
-
else
|
|
2028
|
-
this._pushHeightRange();
|
|
2029
|
-
}
|
|
2030
|
-
/** 数据集整体位移(换高程偏移 / 重新布局)后,缓存的世界高度全部失效 */
|
|
2031
|
-
_refreshAllHeights() {
|
|
2032
|
-
if (this._meshHeight.size === 0)
|
|
2033
|
-
return;
|
|
2034
|
-
const meshes = [...this._meshHeight.keys()];
|
|
2035
|
-
this._meshHeight.clear();
|
|
2036
|
-
this._heightMin = Infinity;
|
|
2037
|
-
this._heightMax = -Infinity;
|
|
2038
|
-
for (const mesh of meshes)
|
|
2039
|
-
this._addMeshHeight(mesh);
|
|
2040
|
-
this._pushHeightRange();
|
|
2041
|
-
}
|
|
2042
|
-
_recomputeHeightRange() {
|
|
2043
|
-
let min = Infinity;
|
|
2044
|
-
let max = -Infinity;
|
|
2045
|
-
for (const h of this._meshHeight.values()) {
|
|
2046
|
-
if (h.min < min)
|
|
2047
|
-
min = h.min;
|
|
2048
|
-
if (h.max > max)
|
|
2049
|
-
max = h.max;
|
|
2050
|
-
}
|
|
2051
|
-
this._heightMin = min;
|
|
2052
|
-
this._heightMax = max;
|
|
2053
|
-
this._pushHeightRange();
|
|
2054
|
-
}
|
|
2055
|
-
_pushHeightRange() {
|
|
2056
|
-
let min = this._heightMin;
|
|
2057
|
-
let max = this._heightMax;
|
|
2058
|
-
if (!Number.isFinite(min) || !Number.isFinite(max) || max <= min) {
|
|
2059
|
-
min = 0;
|
|
2060
|
-
max = 100;
|
|
2061
|
-
}
|
|
2062
|
-
this.uniforms.uHeightMin.value = min;
|
|
2063
|
-
this.uniforms.uHeightMax.value = max;
|
|
1893
|
+
this._heights.remove(mesh);
|
|
2064
1894
|
}
|
|
2065
1895
|
/** 建 SMID → 网格 反查表,逐单体改样式时才知道该重算哪几块 */
|
|
2066
1896
|
_indexMeshSmids(mesh) {
|
|
@@ -2091,1117 +1921,118 @@ export class S3MViewer {
|
|
|
2091
1921
|
this._smidMeshes.delete(smid);
|
|
2092
1922
|
}
|
|
2093
1923
|
}
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
* - `true` / `'line'` 折边线(旧口径)
|
|
2099
|
-
* - `'offset'` 折边线 + 沿边法线外推 `edgeOffset` 米
|
|
2100
|
-
* - `'hull'` 背面外扩轮廓(选中侧 outlineStyle:'hull' 的整场景版)
|
|
2101
|
-
* - `'gpu'` 纯片元描边:零几何、零额外 pass,线宽约 1 像素
|
|
2102
|
-
* - 数组 叠加,例如 `['line', 'hull']`
|
|
2103
|
-
*
|
|
2104
|
-
* 数据自带棱线包(primitiveType 13)时直接用,成本为零;`style.forceCpuEdges`
|
|
2105
|
-
* 可以无视它,改由 CPU 现算 —— 那是 CPU 端的活,几十万面一次性建会卡住主线程
|
|
2106
|
-
* (实测 20 个网格 1.9s),所以按 8ms 一帧切片,边建边出。
|
|
2107
|
-
*
|
|
2108
|
-
* 棱线本身用「屏幕空间等宽线」画(见 EdgeLines.ts):WebGL 的 lineWidth 基本无效,
|
|
2109
|
-
* 只有摊成面向屏幕的四边形,`edgeWidth` 才能真正调粗。
|
|
2110
|
-
*
|
|
2111
|
-
* 带 `transition` 时做「淡入 / 淡出」,**不对几何做插值**(几何没法插值):
|
|
2112
|
-
* 开:立刻置位 + 开始分片建线,同时 `uEdgeFade` 0→1 —— 建好一块亮一块,视觉上是渐显;
|
|
2113
|
-
* 关:`uEdgeFade` 1→0,淡完才 dispose,避免棱线「啪」一下消失。
|
|
2114
|
-
*/
|
|
2115
|
-
setEdges(edges, transition) {
|
|
2116
|
-
const wantAnim = !!transition && ((transition.duration ?? 0) > 0 || !!transition.spring);
|
|
2117
|
-
const modes = resolveEdgeModes(edges);
|
|
2118
|
-
// 换画法(line ↔ hull ↔ offset)是结构变更:旧装饰必须拆掉再建,
|
|
2119
|
-
// 否则会出现「切到 hull 了折边线还留着」这种叠影。
|
|
2120
|
-
// 同一画法重复调用(滑块逐帧 setEdges(true))不重建,几何是复用的。
|
|
2121
|
-
const modesChanged = !sameEdgeModes(this._edgeModes, modes);
|
|
2122
|
-
this._edgeModes = modes;
|
|
2123
|
-
this._edgesEnabled = modes.enabled;
|
|
2124
|
-
this.style.edges = edges;
|
|
2125
|
-
this._styleTarget.edges = edges;
|
|
2126
|
-
// 数据自带的棱线:直接切可见性(forceCpuEdges 时永远收起,改由 CPU 现算)。
|
|
2127
|
-
// 只遍历注册时收集好的那批 LineSegments —— 全数据集 traverse 在几千节点下太贵。
|
|
2128
|
-
// gpu 模式下 shader 自己描边,native 线段包必须收起,否则叠成双线
|
|
2129
|
-
const nativeVisible = modes.enabled && !modes.gpu && this.style.forceCpuEdges !== true;
|
|
2130
|
-
for (const lines of this._nativeEdgeLines) {
|
|
2131
|
-
lines.visible = nativeVisible;
|
|
2132
|
-
this._styleEdgeLine(lines);
|
|
2133
|
-
}
|
|
2134
|
-
this._syncEdgeStyle();
|
|
2135
|
-
// 关掉 gpu 画法就把后处理资源还回去(整屏 RT 白占显存没意义)。
|
|
2136
|
-
// 必须放在「整体关闭」的早退之前,否则关棱线时永远轮不到它。
|
|
2137
|
-
if (!modes.gpu && this._gpuEdge) {
|
|
2138
|
-
this._gpuEdge.dispose();
|
|
2139
|
-
this._gpuEdge = null;
|
|
2140
|
-
}
|
|
2141
|
-
if (!modes.enabled) {
|
|
2142
|
-
if (wantAnim) {
|
|
2143
|
-
this.animator.to('style.edgeFade', 0, {
|
|
2144
|
-
...transition,
|
|
2145
|
-
onComplete: () => {
|
|
2146
|
-
this._disposeEdges();
|
|
2147
|
-
this._requestRepaint();
|
|
2148
|
-
transition?.onComplete?.();
|
|
2149
|
-
},
|
|
2150
|
-
});
|
|
2151
|
-
}
|
|
2152
|
-
else {
|
|
2153
|
-
this._setEdgeFade(1);
|
|
2154
|
-
this._disposeEdges();
|
|
2155
|
-
}
|
|
2156
|
-
this._requestRepaint();
|
|
2157
|
-
return;
|
|
2158
|
-
}
|
|
2159
|
-
if (modesChanged)
|
|
2160
|
-
this._disposeEdges();
|
|
2161
|
-
const pending = [...this._pickables.values()]
|
|
2162
|
-
.map((p) => p.mesh)
|
|
2163
|
-
.filter((m) => {
|
|
2164
|
-
const ud = m.userData;
|
|
2165
|
-
// 点云没有棱可描
|
|
2166
|
-
if (isPointCloudObject(m))
|
|
2167
|
-
return false;
|
|
2168
|
-
// native 棱线还算不算数,取决于**当前**的 forceCpuEdges(它可以在运行中切换),
|
|
2169
|
-
// 所以这里按注册时记下的原始标记重算,而不是沿用注册那一刻的结论。
|
|
2170
|
-
ud.nativeEdges = ud.nativeEdgesRaw === true && this.style.forceCpuEdges !== true;
|
|
2171
|
-
return !ud.nativeEdges && this._needsEdgeDecoration(ud);
|
|
2172
|
-
});
|
|
2173
|
-
let i = 0;
|
|
2174
|
-
const step = () => {
|
|
2175
|
-
const t0 = performance.now();
|
|
2176
|
-
while (i < pending.length && performance.now() - t0 < 8)
|
|
2177
|
-
this._ensureEdgeDecoration(pending[i++]);
|
|
2178
|
-
if (i < pending.length)
|
|
2179
|
-
setTimeout(step, 0);
|
|
2180
|
-
else
|
|
2181
|
-
this._requestRepaint();
|
|
2182
|
-
};
|
|
2183
|
-
step();
|
|
2184
|
-
if (wantAnim) {
|
|
2185
|
-
// 从 0 起淡入:新建出来的棱线自动继承当前进度,不会半路冒出一条满亮度的线
|
|
2186
|
-
this._setEdgeFade(0);
|
|
2187
|
-
this.animator.to('style.edgeFade', 1, transition);
|
|
2188
|
-
}
|
|
2189
|
-
else {
|
|
2190
|
-
this._setEdgeFade(1);
|
|
2191
|
-
}
|
|
2192
|
-
}
|
|
2193
|
-
/** 直接写棱线淡入乘子(不走动画通道) */
|
|
2194
|
-
_setEdgeFade(v) {
|
|
2195
|
-
if (this.uniforms.uEdgeFade)
|
|
2196
|
-
this.uniforms.uEdgeFade.value = v;
|
|
2197
|
-
this._syncEdgeStyle();
|
|
2198
|
-
}
|
|
2199
|
-
/** 释放所有现算棱线几何 */
|
|
2200
|
-
_disposeEdges() {
|
|
2201
|
-
for (const p of this._pickables.values())
|
|
2202
|
-
this._removeEdges(p.mesh);
|
|
2203
|
-
}
|
|
2204
|
-
/**
|
|
2205
|
-
* 挂载预制棱线缓存(Crease Cache)。
|
|
2206
|
-
*
|
|
2207
|
-
* 直接把 `source-creases.f32` 这类 Float32Array 缓冲载入为一组全局统一的
|
|
2208
|
-
* LineSegments —— 相比逐 Mesh 现算 EdgesGeometry,性能高一个量级,视觉也最干净。
|
|
2209
|
-
* 坐标按「场景本地坐标」(相对场景锚点的米)处理,与瓦片几何同一坐标系。
|
|
2210
|
-
*
|
|
2211
|
-
* @param url 棱线缓存地址(二进制 Float32 序列:x,y,z,x,y,z,… 成对成段)
|
|
2212
|
-
*/
|
|
2213
|
-
async loadCreases(url) {
|
|
2214
|
-
const resp = await authFetch(url, this._authenticate, 'creases');
|
|
2215
|
-
if (!resp.ok)
|
|
2216
|
-
throw new Error(`[s3m] 棱线缓存加载失败: ${resp.status} ${url}`);
|
|
2217
|
-
const buf = await resp.arrayBuffer();
|
|
2218
|
-
const arr = new Float32Array(buf);
|
|
2219
|
-
if (arr.length < 6)
|
|
2220
|
-
return;
|
|
2221
|
-
const geo = new THREE.BufferGeometry();
|
|
2222
|
-
geo.setAttribute('position', new THREE.BufferAttribute(arr, 3));
|
|
2223
|
-
const s = this.style;
|
|
2224
|
-
const mat = new THREE.LineBasicMaterial({
|
|
2225
|
-
color: new THREE.Color(s.edgeColor),
|
|
2226
|
-
transparent: true,
|
|
2227
|
-
opacity: Math.min(1, Math.max(0, s.edgeOpacity ?? 0.6)) * clampOpacity(s.opacity),
|
|
2228
|
-
depthWrite: false,
|
|
2229
|
-
});
|
|
2230
|
-
const lines = new THREE.LineSegments(geo, mat);
|
|
2231
|
-
lines.renderOrder = 2;
|
|
2232
|
-
lines.frustumCulled = false;
|
|
2233
|
-
lines.raycast = () => { };
|
|
2234
|
-
this.clearCreases();
|
|
2235
|
-
this._creaseLines = lines;
|
|
2236
|
-
this.scene?.add(lines);
|
|
2237
|
-
this._requestRepaint();
|
|
2238
|
-
}
|
|
2239
|
-
/** 移除并释放预制棱线缓存 */
|
|
2240
|
-
clearCreases() {
|
|
2241
|
-
if (!this._creaseLines)
|
|
2242
|
-
return;
|
|
2243
|
-
this.scene?.remove(this._creaseLines);
|
|
2244
|
-
this._creaseLines.geometry.dispose();
|
|
2245
|
-
this._creaseLines.material?.dispose?.();
|
|
2246
|
-
this._creaseLines = null;
|
|
2247
|
-
this._requestRepaint();
|
|
2248
|
-
}
|
|
2249
|
-
/**
|
|
2250
|
-
* 让全局棱线材质跟上 style.edgeColor / edgeWidth / edgeOpacity / edgeOffset。
|
|
2251
|
-
*
|
|
2252
|
-
* 全程 O(1):只有一份共享 edgeMaterial(所有逐 mesh 棱线都用它)+ 一份预制缓存
|
|
2253
|
-
* + 一批共享外观对象的轮廓壳,所以棱线淡入淡出只要改一个 uniform,
|
|
2254
|
-
* 新建瓦片自动继承当前进度。
|
|
2255
|
-
*/
|
|
2256
|
-
_syncEdgeStyle() {
|
|
2257
|
-
const s = this.style;
|
|
2258
|
-
const fade = this.uniforms.uEdgeFade?.value ?? 1;
|
|
2259
|
-
const opacity = Math.min(1, Math.max(0, s.edgeOpacity ?? 0.6)) *
|
|
2260
|
-
clampOpacity(s.opacity) *
|
|
2261
|
-
fade;
|
|
2262
|
-
const u = this.edgeMaterial.uniforms;
|
|
2263
|
-
u.uColor.value.set(s.edgeColor);
|
|
2264
|
-
u.uWidth.value = Math.max(0.1, s.edgeWidth ?? 1.5);
|
|
2265
|
-
u.uOpacity.value = opacity;
|
|
2266
|
-
// 外推距离只在 offset 画法下非 0;其它画法把线钉在面上(与改动前一致)
|
|
2267
|
-
u.uOffset.value = this._edgeModes.offset ? Math.max(0, s.edgeOffset ?? 0.15) : 0;
|
|
2268
|
-
// 外扩轮廓壳与折边线共用颜色 / 透明度,厚度是米制的单独一项
|
|
2269
|
-
const h = this._hullStyleUniforms;
|
|
2270
|
-
h.uColor.value.set(s.edgeColor);
|
|
2271
|
-
h.uWidth.value = Math.max(0, s.edgeHullWidth ?? 0.9);
|
|
2272
|
-
h.uOpacity.value = opacity;
|
|
2273
|
-
// GPU 描边(edges: 'gpu')是后处理,参数每帧从 style 现取,这里不用同步 uniform
|
|
2274
|
-
// 预制棱线缓存(LineBasicMaterial)同步颜色 / 不透明度
|
|
2275
|
-
if (this._creaseLines) {
|
|
2276
|
-
const m = this._creaseLines.material;
|
|
2277
|
-
m.color.set(s.edgeColor);
|
|
2278
|
-
m.opacity = opacity;
|
|
2279
|
-
}
|
|
2280
|
-
}
|
|
2281
|
-
/** 让数据自带的棱线跟着 style.edgeColor / opacity 走(这些是 LineBasicMaterial,改不了粗细) */
|
|
2282
|
-
_styleEdgeLine(lines) {
|
|
2283
|
-
const mat = lines.material;
|
|
2284
|
-
if (!mat || !mat.color)
|
|
2285
|
-
return;
|
|
2286
|
-
mat.color.set(this.style.edgeColor);
|
|
2287
|
-
const baseOpacity = this.style.edgeOpacity ?? 0.6;
|
|
2288
|
-
mat.opacity =
|
|
2289
|
-
baseOpacity *
|
|
2290
|
-
clampOpacity(this.style.opacity) *
|
|
2291
|
-
(this.uniforms.uEdgeFade?.value ?? 1);
|
|
2292
|
-
mat.transparent = true;
|
|
2293
|
-
mat.polygonOffset = true;
|
|
2294
|
-
mat.polygonOffsetFactor = -1;
|
|
2295
|
-
mat.polygonOffsetUnits = -1;
|
|
2296
|
-
mat.needsUpdate = true;
|
|
2297
|
-
}
|
|
2298
|
-
/**
|
|
2299
|
-
* 刷新所有棱线外观。
|
|
2300
|
-
*
|
|
2301
|
-
* 数据自带的棱线是逐对象的 LineBasicMaterial,改颜色只能一个个写;但原来每次都要
|
|
2302
|
-
* 遍历整个数据集树(几千个节点),颜色动画逐帧跑就是灾难。改成只刷注册时收集好的
|
|
2303
|
-
* 那批 LineSegments,复杂度降到 O(棱线条数)。
|
|
2304
|
-
*/
|
|
2305
|
-
_refreshEdgeLines() {
|
|
2306
|
-
this._syncEdgeStyle();
|
|
2307
|
-
for (const lines of this._nativeEdgeLines) {
|
|
2308
|
-
if (lines.parent)
|
|
2309
|
-
this._styleEdgeLine(lines);
|
|
2310
|
-
}
|
|
2311
|
-
}
|
|
2312
|
-
/** 地形 / 绿地 / 道路 / 地面铺装等不参与自动描边(数据自带棱线包另说) */
|
|
2313
|
-
static _EDGE_SKIP_KEYWORDS = [
|
|
2314
|
-
'terrain', 'ground', 'vegetation', 'green', 'road', 'dem',
|
|
2315
|
-
'绿地', '道路', '地面', '地形', '植被', '铺装',
|
|
2316
|
-
];
|
|
2317
|
-
/** 数据集级别的棱线开关:enableEdges 优先,否则跟随全局 style.edges */
|
|
2318
|
-
_datasetEdgesEnabled(entry) {
|
|
2319
|
-
const opt = entry.options?.enableEdges;
|
|
2320
|
-
if (opt === true)
|
|
2321
|
-
return true;
|
|
2322
|
-
if (opt === false)
|
|
2323
|
-
return false;
|
|
2324
|
-
return this._edgesEnabled;
|
|
1924
|
+
/* ---------------------------------------------------------------- 棱线 */
|
|
1925
|
+
/** @internal 测试用:棱线是否开启(实现在 EdgeDecorator) */
|
|
1926
|
+
get _edgesEnabled() {
|
|
1927
|
+
return this._edges.enabled;
|
|
2325
1928
|
}
|
|
2326
|
-
/**
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
return S3MViewer._EDGE_SKIP_KEYWORDS.some((k) => hay.includes(k));
|
|
1929
|
+
/** @internal 测试用:当前画法 */
|
|
1930
|
+
get _edgeModes() {
|
|
1931
|
+
return this._edges.modes;
|
|
2330
1932
|
}
|
|
2331
|
-
/**
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
* setEdges 的分片建线靠它筛「还没建完的」,注册新瓦片也靠它决定要不要动手。
|
|
2335
|
-
*/
|
|
2336
|
-
_needsEdgeDecoration(ud) {
|
|
2337
|
-
if (this._edgeModes.line && !ud.edges)
|
|
2338
|
-
return true;
|
|
2339
|
-
if (this._edgeModes.hull && !ud.edgeHull)
|
|
2340
|
-
return true;
|
|
2341
|
-
return false;
|
|
1933
|
+
/** @internal 测试用:GPU 描边实例(可替换) */
|
|
1934
|
+
get _gpuEdge() {
|
|
1935
|
+
return this._edges.gpuEdge;
|
|
2342
1936
|
}
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
if (this._edgeModes.line)
|
|
2346
|
-
this._ensureEdges(mesh);
|
|
2347
|
-
if (this._edgeModes.hull)
|
|
2348
|
-
this._ensureHull(mesh);
|
|
1937
|
+
set _gpuEdge(v) {
|
|
1938
|
+
this._edges.gpuEdge = v;
|
|
2349
1939
|
}
|
|
1940
|
+
/** @internal 测试用:给一块网格补折边线 */
|
|
2350
1941
|
_ensureEdges(mesh) {
|
|
2351
|
-
|
|
2352
|
-
if (ud.edges || ud.nativeEdges || !mesh.geometry)
|
|
2353
|
-
return;
|
|
2354
|
-
// 地形 / 绿地 / 道路等不参与自动描边(即使全局开棱线也要跳过)
|
|
2355
|
-
if (this._shouldSkipAutoEdges(ud, ''))
|
|
2356
|
-
return;
|
|
2357
|
-
const posAttr = mesh.geometry.getAttribute('position');
|
|
2358
|
-
const indexCount = mesh.geometry.index?.count ?? (posAttr ? posAttr.count : 0);
|
|
2359
|
-
// 棱线是 CPU 端建的,超大几何直接跳过,避免拖慢加载
|
|
2360
|
-
if (indexCount > 120000)
|
|
2361
|
-
return;
|
|
2362
|
-
// 1. 先做顶点容差焊合,让相邻三角形正确共享边索引。
|
|
2363
|
-
// S3M 几何常为「每面独立顶点」,不焊合的话 EdgesGeometry 会把每条三角形边都当成边界,棱线爆炸。
|
|
2364
|
-
// 注意用本地实现而不是 three/examples/jsm —— jsm 子路径导入会在宿主里
|
|
2365
|
-
// 制造第二份 three 实例("Multiple instances of Three.js" 警告的元凶)。
|
|
2366
|
-
// tolerance 取米级模型下的 5mm,足以合并重合顶点又不会误并真实相邻面。
|
|
2367
|
-
const welded = mergeVerticesByPosition(mesh.geometry, 0.005);
|
|
2368
|
-
// 2. 调大临界角(默认 55°,可在 style.edgeThreshold 调),过滤浅坡与曲面细分,只保留真正的棱。
|
|
2369
|
-
const thresholdAngle = this.style.edgeThreshold ?? 55;
|
|
2370
|
-
// 'offset' 画法要额外带一条「边法线」才知道往哪推,所以走本地实现(它本来就算好了
|
|
2371
|
-
// 相邻两面法线);其余画法保持 EdgesGeometry 这条老路,几何输出与改动前逐字一致。
|
|
2372
|
-
let segments;
|
|
2373
|
-
let normals;
|
|
2374
|
-
if (this._edgeModes.offset) {
|
|
2375
|
-
const raw = [];
|
|
2376
|
-
segments = buildFeatureEdgeSegments(welded, null, thresholdAngle, raw);
|
|
2377
|
-
if (raw.length > 0)
|
|
2378
|
-
normals = new Float32Array(raw);
|
|
2379
|
-
}
|
|
2380
|
-
else {
|
|
2381
|
-
const source = new THREE.EdgesGeometry(welded, thresholdAngle);
|
|
2382
|
-
const attr = source.getAttribute('position');
|
|
2383
|
-
segments = attr ? attr.array : null;
|
|
2384
|
-
source.dispose();
|
|
2385
|
-
}
|
|
2386
|
-
welded.dispose();
|
|
2387
|
-
if (!segments || segments.length < 6)
|
|
2388
|
-
return;
|
|
2389
|
-
// 3. 回填每段棱线所属的单体 batchId。
|
|
2390
|
-
// EdgesGeometry 只输出坐标不输出顶点号,只能拿端点坐标去查「位置 → batchId」表。
|
|
2391
|
-
// 没有这张表,单体一动棱线就留在原地——这正是「棱线跟随」的关键一环。
|
|
2392
|
-
const TOL = 0.005;
|
|
2393
|
-
const batchMap = buildPositionBatchMap(mesh.geometry, TOL);
|
|
2394
|
-
let batchIds;
|
|
2395
|
-
if (batchMap) {
|
|
2396
|
-
const segCount = segments.length / 6;
|
|
2397
|
-
batchIds = new Float32Array(segCount);
|
|
2398
|
-
for (let i = 0; i < segCount; i++) {
|
|
2399
|
-
const s = i * 6;
|
|
2400
|
-
batchIds[i] =
|
|
2401
|
-
batchMap.get(positionQuantKey(segments[s], segments[s + 1], segments[s + 2], 1 / TOL)) ?? -1;
|
|
2402
|
-
}
|
|
2403
|
-
}
|
|
2404
|
-
const lines = new THREE.Mesh(buildThickLineGeometry(segments, batchIds, normals), this._createEdgeMaterial(mesh));
|
|
2405
|
-
lines.renderOrder = 2;
|
|
2406
|
-
// 屏幕空间外扩后可能超出原包围球,关掉剔除更稳
|
|
2407
|
-
lines.frustumCulled = false;
|
|
2408
|
-
lines.raycast = () => { };
|
|
2409
|
-
lines.userData.isThickEdge = true;
|
|
2410
|
-
ud.edges = lines;
|
|
2411
|
-
mesh.add(lines); // 作为子节点,自动继承 mesh 的世界变换
|
|
1942
|
+
this._edges.ensureEdges(mesh);
|
|
2412
1943
|
}
|
|
2413
|
-
/**
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
* 复用选中侧那套 `createOutlineMaterial`:顶点沿法线鼓 `edgeHullWidth` 米、只画背面,
|
|
2417
|
-
* 于是被本体挡住的部分不显示,露出来的那圈就是描边。与折边线的差别在于——
|
|
2418
|
-
* 这里**不建任何新几何**,成本只是把同一块网格多画一遍,折边判定那套 CPU 活全省了。
|
|
2419
|
-
*
|
|
2420
|
-
* 实例化几何拿不到逐实例变换(轮廓壳是 mesh 的子节点,不吃 instanceMatrix),
|
|
2421
|
-
* 直接跳过:宁可这栋没描边,也不要所有实例的壳叠在原点。
|
|
2422
|
-
*/
|
|
2423
|
-
_ensureHull(mesh) {
|
|
2424
|
-
const ud = mesh.userData;
|
|
2425
|
-
if (ud.edgeHull || !mesh.geometry)
|
|
2426
|
-
return;
|
|
2427
|
-
if (mesh.isInstancedMesh)
|
|
2428
|
-
return;
|
|
2429
|
-
// 地形 / 绿地 / 道路等不参与自动描边(即使全局开棱线也要跳过)
|
|
2430
|
-
if (this._shouldSkipAutoEdges(ud, ''))
|
|
2431
|
-
return;
|
|
2432
|
-
// 逐网格一份材质(各绑各的单体状态纹理),外观 uniform 复用共享对象
|
|
2433
|
-
const mat = createOutlineMaterial(this.uniforms, this.style.edgeHullWidth ?? 0.9, this._hullStyleUniforms, true);
|
|
2434
|
-
// 整块几何都要外扩:选中侧靠 uSelBatch 只扩一栋,全局必须置 whole
|
|
2435
|
-
mat.uniforms.uSelWhole.value = 1;
|
|
2436
|
-
mat.uniforms.uSelBatch.value = -1;
|
|
2437
|
-
const styled = styledMaterialOf(mesh);
|
|
2438
|
-
if (styled?.userData?.uBatchTex) {
|
|
2439
|
-
mat.uniforms.uBatchTex.value = styled.userData.uBatchTex.value;
|
|
2440
|
-
mat.uniforms.uBatchTexWidth.value = styled.userData.uBatchTexWidth.value;
|
|
2441
|
-
mat.uniforms.uBatchTexActive.value = styled.userData.uBatchTexActive.value;
|
|
2442
|
-
}
|
|
2443
|
-
const hull = new THREE.Mesh(mesh.geometry, mat);
|
|
2444
|
-
hull.renderOrder = 2;
|
|
2445
|
-
hull.frustumCulled = false;
|
|
2446
|
-
hull.raycast = () => { };
|
|
2447
|
-
hull.userData.isEdgeHull = true;
|
|
2448
|
-
ud.edgeHull = hull;
|
|
2449
|
-
mesh.add(hull); // 作为子节点,自动继承 mesh 的世界变换
|
|
1944
|
+
/** @internal 测试用:拆掉一块网格的棱线装饰 */
|
|
1945
|
+
_removeEdges(mesh) {
|
|
1946
|
+
this._edges.removeEdges(mesh);
|
|
2450
1947
|
}
|
|
2451
|
-
/**
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
* 棱线材质必须逐网格一份,因为它要绑定**这个网格**的单体状态纹理;但外观
|
|
2455
|
-
* (颜色 / 粗细 / 透明度)必须全局统一——所以外观 uniform 直接复用
|
|
2456
|
-
* `_edgeStyleUniforms` 里的对象,`_syncEdgeStyle()` 改一次全部生效,
|
|
2457
|
-
* program 也因为着色器相同而只编译一次。
|
|
2458
|
-
*/
|
|
2459
|
-
_createEdgeMaterial(mesh) {
|
|
2460
|
-
const styled = styledMaterialOf(mesh);
|
|
2461
|
-
const batch = styled?.userData?.uBatchTex
|
|
2462
|
-
? {
|
|
2463
|
-
uBatchTex: styled.userData.uBatchTex,
|
|
2464
|
-
uBatchTex2: styled.userData.uBatchTex2,
|
|
2465
|
-
uBatchMix: this.uniforms.uBatchMix,
|
|
2466
|
-
uBatchTexWidth: styled.userData.uBatchTexWidth,
|
|
2467
|
-
uBatchTexActive: styled.userData.uBatchTexActive,
|
|
2468
|
-
uPathTex: this.uniforms.uPathTex,
|
|
2469
|
-
uPathCount: this.uniforms.uPathCount,
|
|
2470
|
-
uPathTexActive: this.uniforms.uPathTexActive,
|
|
2471
|
-
uPathTime: this.uniforms.uPathTime,
|
|
2472
|
-
}
|
|
2473
|
-
: undefined;
|
|
2474
|
-
return createThickLineMaterial(this.lineShared, {}, this._edgeStyleUniforms, batch);
|
|
1948
|
+
/** 全局棱线开关,实现在 viewer/EdgeDecorator.ts */
|
|
1949
|
+
setEdges(edges, transition) {
|
|
1950
|
+
this._edges.setEdges(edges, transition);
|
|
2475
1951
|
}
|
|
2476
|
-
/**
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
const batch = styled?.userData?.uBatchTex
|
|
2480
|
-
? {
|
|
2481
|
-
uBatchTex: styled.userData.uBatchTex,
|
|
2482
|
-
uBatchTex2: styled.userData.uBatchTex2,
|
|
2483
|
-
uBatchMix: this.uniforms.uBatchMix,
|
|
2484
|
-
uBatchTexWidth: styled.userData.uBatchTexWidth,
|
|
2485
|
-
uBatchTexActive: styled.userData.uBatchTexActive,
|
|
2486
|
-
uPathTex: this.uniforms.uPathTex,
|
|
2487
|
-
uPathCount: this.uniforms.uPathCount,
|
|
2488
|
-
uPathTexActive: this.uniforms.uPathTexActive,
|
|
2489
|
-
uPathTime: this.uniforms.uPathTime,
|
|
2490
|
-
}
|
|
2491
|
-
: undefined;
|
|
2492
|
-
return createThickLineMaterial(this.lineShared, {}, this._selEdgeStyleUniforms, batch);
|
|
1952
|
+
/** 挂载预制棱线缓存(Crease Cache),见 viewer/EdgeDecorator.ts */
|
|
1953
|
+
loadCreases(url) {
|
|
1954
|
+
return this._edges.loadCreases(url);
|
|
2493
1955
|
}
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
if (lines) {
|
|
2498
|
-
mesh.remove(lines);
|
|
2499
|
-
lines.geometry?.dispose();
|
|
2500
|
-
// 棱线材质现在是逐网格一份(要绑各自的 batch 纹理),得跟着释放
|
|
2501
|
-
const mat = lines.material;
|
|
2502
|
-
if (Array.isArray(mat)) {
|
|
2503
|
-
mat.forEach((m) => m?.dispose?.());
|
|
2504
|
-
}
|
|
2505
|
-
else {
|
|
2506
|
-
mat?.dispose?.();
|
|
2507
|
-
}
|
|
2508
|
-
ud.edges = null;
|
|
2509
|
-
}
|
|
2510
|
-
// 外扩壳的 geometry 是**复用主网格的**,只能释放材质,不能 dispose 几何
|
|
2511
|
-
const hull = ud.edgeHull;
|
|
2512
|
-
if (hull) {
|
|
2513
|
-
mesh.remove(hull);
|
|
2514
|
-
const hmat = hull.material;
|
|
2515
|
-
if (Array.isArray(hmat))
|
|
2516
|
-
hmat.forEach((m) => m?.dispose?.());
|
|
2517
|
-
else
|
|
2518
|
-
hmat?.dispose?.();
|
|
2519
|
-
ud.edgeHull = null;
|
|
2520
|
-
}
|
|
1956
|
+
/** 移除并释放预制棱线缓存 */
|
|
1957
|
+
clearCreases() {
|
|
1958
|
+
this._edges.clearCreases();
|
|
2521
1959
|
}
|
|
2522
1960
|
/* ---------------------------------------------------------------- 拾取 */
|
|
2523
|
-
/**
|
|
1961
|
+
/** 屏幕像素 → 场景射线,见 viewer/PickService.ts */
|
|
2524
1962
|
getRayAt(x, y) {
|
|
2525
|
-
|
|
2526
|
-
return null;
|
|
2527
|
-
const canvas = this.canvas;
|
|
2528
|
-
const w = canvas.clientWidth || canvas.width;
|
|
2529
|
-
const h = canvas.clientHeight || canvas.height;
|
|
2530
|
-
if (!w || !h)
|
|
2531
|
-
return null;
|
|
2532
|
-
const ndcX = (x / w) * 2 - 1;
|
|
2533
|
-
const ndcY = -(y / h) * 2 + 1;
|
|
2534
|
-
const near = new THREE.Vector3(ndcX, ndcY, -1).unproject(this.camera);
|
|
2535
|
-
const far = new THREE.Vector3(ndcX, ndcY, 1).unproject(this.camera);
|
|
2536
|
-
const dir = far.clone().sub(near).normalize();
|
|
2537
|
-
return new THREE.Ray(near, dir);
|
|
2538
|
-
}
|
|
2539
|
-
_intersectMeshAtRay(mesh, ray, instanceIndex) {
|
|
2540
|
-
const raycaster = new THREE.Raycaster();
|
|
2541
|
-
raycaster.ray.copy(ray);
|
|
2542
|
-
const hits = raycaster.intersectObject(mesh, false);
|
|
2543
|
-
if (!hits.length)
|
|
2544
|
-
return null;
|
|
2545
|
-
if (instanceIndex != null) {
|
|
2546
|
-
const match = hits.find((h) => h.instanceId === instanceIndex);
|
|
2547
|
-
return match ? match.point : hits[0].point;
|
|
2548
|
-
}
|
|
2549
|
-
return hits[0].point;
|
|
1963
|
+
return this._picks.getRayAt(x, y);
|
|
2550
1964
|
}
|
|
2551
|
-
|
|
2552
|
-
const ud = ref.mesh.userData;
|
|
2553
|
-
let smid = null;
|
|
2554
|
-
let batchId = hit.batchId;
|
|
2555
|
-
let instanceIndex = hit.instanceIndex;
|
|
2556
|
-
let geoRef = ud.geoRef;
|
|
2557
|
-
let segments = [];
|
|
2558
|
-
if (instanceIndex != null) {
|
|
2559
|
-
const feature = this._instanceFeature(ref.mesh, geoRef, instanceIndex);
|
|
2560
|
-
if (!feature)
|
|
2561
|
-
return null;
|
|
2562
|
-
smid = feature.smid;
|
|
2563
|
-
batchId = feature.batchId;
|
|
2564
|
-
}
|
|
2565
|
-
else {
|
|
2566
|
-
geoRef = ud.geoByBatchId?.get(hit.batchId) ?? ud.geoRef;
|
|
2567
|
-
const resolved = resolveFeature(geoRef, hit.batchId);
|
|
2568
|
-
smid = resolved.smid;
|
|
2569
|
-
segments = resolved.segments;
|
|
2570
|
-
}
|
|
2571
|
-
if (this._isFeatureHidden(smid, batchId))
|
|
2572
|
-
return null;
|
|
2573
|
-
let box = null;
|
|
2574
|
-
let totalTriangles = 0;
|
|
2575
|
-
if (instanceIndex != null) {
|
|
2576
|
-
box = this._instanceBox(ref.mesh, instanceIndex);
|
|
2577
|
-
totalTriangles = ref.mesh.geometry.index ? Math.round(ref.mesh.geometry.index.count / 3) : 0;
|
|
2578
|
-
}
|
|
2579
|
-
else if (smid != null && this._smidMeshes.has(smid)) {
|
|
2580
|
-
const meshes = this._smidMeshes.get(smid);
|
|
2581
|
-
let minX = Infinity;
|
|
2582
|
-
let minY = Infinity;
|
|
2583
|
-
let minZ = Infinity;
|
|
2584
|
-
let maxX = -Infinity;
|
|
2585
|
-
let maxY = -Infinity;
|
|
2586
|
-
let maxZ = -Infinity;
|
|
2587
|
-
let count = 0;
|
|
2588
|
-
for (const m of meshes) {
|
|
2589
|
-
const mUd = m.userData;
|
|
2590
|
-
const mPick = mUd.geoRef?.pickInfo;
|
|
2591
|
-
const entry = mPick?.[smid] ??
|
|
2592
|
-
mPick?.[String(smid)];
|
|
2593
|
-
const bId = entry ? pickEntryId(entry) : -1;
|
|
2594
|
-
const mSegs = entry?.segments ?? (bId >= 0 ? resolveFeature(mUd.geoRef, bId).segments : []);
|
|
2595
|
-
const mBox = measureSegments(m.geometry, mSegs);
|
|
2596
|
-
if (mBox) {
|
|
2597
|
-
if (mBox.minX < minX)
|
|
2598
|
-
minX = mBox.minX;
|
|
2599
|
-
if (mBox.minY < minY)
|
|
2600
|
-
minY = mBox.minY;
|
|
2601
|
-
if (mBox.minZ < minZ)
|
|
2602
|
-
minZ = mBox.minZ;
|
|
2603
|
-
if (mBox.maxX > maxX)
|
|
2604
|
-
maxX = mBox.maxX;
|
|
2605
|
-
if (mBox.maxY > maxY)
|
|
2606
|
-
maxY = mBox.maxY;
|
|
2607
|
-
if (mBox.maxZ > maxZ)
|
|
2608
|
-
maxZ = mBox.maxZ;
|
|
2609
|
-
count += mBox.count;
|
|
2610
|
-
totalTriangles += mBox.triangles;
|
|
2611
|
-
}
|
|
2612
|
-
}
|
|
2613
|
-
if (count > 0) {
|
|
2614
|
-
box = { minX, minY, minZ, maxX, maxY, maxZ, count, triangles: totalTriangles };
|
|
2615
|
-
}
|
|
2616
|
-
}
|
|
2617
|
-
if (!box) {
|
|
2618
|
-
box = measureSegments(ref.mesh.geometry, segments);
|
|
2619
|
-
if (!box && ref.mesh.geometry) {
|
|
2620
|
-
if (!ref.mesh.geometry.boundingBox)
|
|
2621
|
-
ref.mesh.geometry.computeBoundingBox();
|
|
2622
|
-
const bb = ref.mesh.geometry.boundingBox;
|
|
2623
|
-
if (bb) {
|
|
2624
|
-
box = {
|
|
2625
|
-
minX: bb.min.x,
|
|
2626
|
-
maxX: bb.max.x,
|
|
2627
|
-
minY: bb.min.y,
|
|
2628
|
-
maxY: bb.max.y,
|
|
2629
|
-
minZ: bb.min.z,
|
|
2630
|
-
maxZ: bb.max.z,
|
|
2631
|
-
count: 0,
|
|
2632
|
-
triangles: 0,
|
|
2633
|
-
};
|
|
2634
|
-
}
|
|
2635
|
-
}
|
|
2636
|
-
}
|
|
2637
|
-
const geometry = ref.mesh.geometry;
|
|
2638
|
-
const triangles = totalTriangles > 0 ? totalTriangles : (geometry.index ? Math.round(geometry.index.count / 3) : 0);
|
|
2639
|
-
const attributes = buildAttributes({ smid, box, triangles, floorHeight: this.style.floorHeight });
|
|
2640
|
-
let centerLngLat = null;
|
|
2641
|
-
if (box && this.sceneAnchor) {
|
|
2642
|
-
const v = new THREE.Vector3((box.minX + box.maxX) / 2, (box.minY + box.maxY) / 2, (box.minZ + box.maxZ) / 2);
|
|
2643
|
-
v.applyMatrix4(ref.mesh.matrixWorld);
|
|
2644
|
-
const s = this.sceneAnchor.meterInMercator;
|
|
2645
|
-
centerLngLat = this._lngLatFromWorld(v.x * s + this.sceneAnchor.x, -v.y * s + this.sceneAnchor.y);
|
|
2646
|
-
}
|
|
2647
|
-
// 精确命中点求交计算
|
|
2648
|
-
let hitPoint = exactPoint ?? null;
|
|
2649
|
-
if (!hitPoint && ray) {
|
|
2650
|
-
hitPoint = this._intersectMeshAtRay(ref.mesh, ray, instanceIndex);
|
|
2651
|
-
}
|
|
2652
|
-
let point = null;
|
|
2653
|
-
let lngLat = null;
|
|
2654
|
-
let altitude = null;
|
|
2655
|
-
if (hitPoint && this.sceneAnchor) {
|
|
2656
|
-
const off = smid != null ? this._featureStyleManager.getFeatureEffectiveOffset(smid) : {};
|
|
2657
|
-
const trans = off.translation;
|
|
2658
|
-
const elev = off.elevation;
|
|
2659
|
-
const finalPos = hitPoint.clone();
|
|
2660
|
-
if (off.rotation) {
|
|
2661
|
-
const rotMat = resolveRotationMatrix(off.rotation);
|
|
2662
|
-
if (rotMat) {
|
|
2663
|
-
const m3 = new THREE.Matrix3().set(rotMat[0], rotMat[1], rotMat[2], rotMat[3], rotMat[4], rotMat[5], rotMat[6], rotMat[7], rotMat[8]);
|
|
2664
|
-
const piv = off.pivot ? new THREE.Vector3(off.pivot[0], off.pivot[1], off.pivot[2]) : new THREE.Vector3(0, 0, 0);
|
|
2665
|
-
finalPos.sub(piv).applyMatrix3(m3).add(piv);
|
|
2666
|
-
}
|
|
2667
|
-
}
|
|
2668
|
-
if (trans)
|
|
2669
|
-
finalPos.add(new THREE.Vector3(trans[0], trans[1], trans[2]));
|
|
2670
|
-
else if (elev)
|
|
2671
|
-
finalPos.z += elev;
|
|
2672
|
-
point = [finalPos.x, finalPos.y, finalPos.z];
|
|
2673
|
-
const s = this.sceneAnchor.meterInMercator;
|
|
2674
|
-
lngLat = this._lngLatFromWorld(finalPos.x * s + this.sceneAnchor.x, -finalPos.y * s + this.sceneAnchor.y);
|
|
2675
|
-
altitude = finalPos.z + (this.sceneAnchor.altitude ?? 0);
|
|
2676
|
-
}
|
|
2677
|
-
else if (centerLngLat && box && this.sceneAnchor) {
|
|
2678
|
-
lngLat = centerLngLat;
|
|
2679
|
-
altitude = (box.minZ + box.maxZ) / 2 + (this.sceneAnchor.altitude ?? 0);
|
|
2680
|
-
}
|
|
2681
|
-
return {
|
|
2682
|
-
datasetId: ref.datasetId,
|
|
2683
|
-
datasetName: this.datasets.get(ref.datasetId)?.name ?? ref.datasetId,
|
|
2684
|
-
objectId: hit.objectId,
|
|
2685
|
-
batchId,
|
|
2686
|
-
instanceIndex,
|
|
2687
|
-
skeletonName: ref.skeletonName,
|
|
2688
|
-
centerLngLat,
|
|
2689
|
-
point,
|
|
2690
|
-
lngLat,
|
|
2691
|
-
altitude,
|
|
2692
|
-
...attributes,
|
|
2693
|
-
mesh: ref.mesh,
|
|
2694
|
-
};
|
|
2695
|
-
}
|
|
2696
|
-
/** 按 canvas 左上角的 CSS 像素坐标拾取。 */
|
|
1965
|
+
/** 按 canvas 左上角的 CSS 像素坐标拾取 */
|
|
2697
1966
|
pick(x, y) {
|
|
2698
|
-
|
|
2699
|
-
return null;
|
|
2700
|
-
const hit = this.picker.pick(this.scene, this.camera, x, y);
|
|
2701
|
-
if (!hit)
|
|
2702
|
-
return null;
|
|
2703
|
-
const ref = this._pickables.get(hit.objectId);
|
|
2704
|
-
if (!ref)
|
|
2705
|
-
return null;
|
|
2706
|
-
const ray = this.getRayAt(x, y);
|
|
2707
|
-
return this._buildPickResult(hit, ref, undefined, ray ?? undefined);
|
|
1967
|
+
return this._picks.pick(x, y);
|
|
2708
1968
|
}
|
|
2709
|
-
/**
|
|
2710
|
-
* 拾取指定屏幕像素对应的世界坐标与场景位置。
|
|
2711
|
-
* 优先与命中的建筑网格精确求交;未命中建筑时若视线与地表相交则返回地面交点。
|
|
2712
|
-
*/
|
|
1969
|
+
/** 拾取并返回经纬度 + 高程 */
|
|
2713
1970
|
pickPosition(x, y) {
|
|
2714
|
-
|
|
2715
|
-
return null;
|
|
2716
|
-
const hit = this.pick(x, y);
|
|
2717
|
-
if (hit?.point && hit?.lngLat && hit.altitude != null) {
|
|
2718
|
-
return {
|
|
2719
|
-
lngLat: hit.lngLat,
|
|
2720
|
-
altitude: hit.altitude,
|
|
2721
|
-
point: hit.point,
|
|
2722
|
-
};
|
|
2723
|
-
}
|
|
2724
|
-
const ray = this.getRayAt(x, y);
|
|
2725
|
-
if (!ray)
|
|
2726
|
-
return null;
|
|
2727
|
-
const groundPlane = new THREE.Plane(new THREE.Vector3(0, 0, 1), 0);
|
|
2728
|
-
const target = new THREE.Vector3();
|
|
2729
|
-
const intersect = ray.intersectPlane(groundPlane, target);
|
|
2730
|
-
if (intersect) {
|
|
2731
|
-
const s = this.sceneAnchor.meterInMercator;
|
|
2732
|
-
const lngLat = this._lngLatFromWorld(intersect.x * s + this.sceneAnchor.x, -intersect.y * s + this.sceneAnchor.y);
|
|
2733
|
-
const altitude = intersect.z + (this.sceneAnchor.altitude ?? 0);
|
|
2734
|
-
return {
|
|
2735
|
-
lngLat,
|
|
2736
|
-
altitude,
|
|
2737
|
-
point: [intersect.x, intersect.y, intersect.z],
|
|
2738
|
-
};
|
|
2739
|
-
}
|
|
2740
|
-
return null;
|
|
1971
|
+
return this._picks.pickPosition(x, y);
|
|
2741
1972
|
}
|
|
2742
|
-
/**
|
|
2743
|
-
* 沿视线方向穿透拾取多个单体,按由近至远排序返回。
|
|
2744
|
-
*/
|
|
1973
|
+
/** 穿透拾取:沿射线返回前 `limit` 个命中 */
|
|
2745
1974
|
drillPick(x, y, limit = 10) {
|
|
2746
|
-
|
|
2747
|
-
return [];
|
|
2748
|
-
const ray = this.getRayAt(x, y);
|
|
2749
|
-
if (!ray)
|
|
2750
|
-
return [];
|
|
2751
|
-
const raycaster = new THREE.Raycaster();
|
|
2752
|
-
raycaster.ray.copy(ray);
|
|
2753
|
-
const meshes = [...this._pickables.values()]
|
|
2754
|
-
.map((p) => p.mesh)
|
|
2755
|
-
.filter((m) => {
|
|
2756
|
-
let curr = m;
|
|
2757
|
-
while (curr) {
|
|
2758
|
-
if (!curr.visible)
|
|
2759
|
-
return false;
|
|
2760
|
-
curr = curr.parent;
|
|
2761
|
-
}
|
|
2762
|
-
return true;
|
|
2763
|
-
});
|
|
2764
|
-
const hits = raycaster.intersectObjects(meshes, false);
|
|
2765
|
-
if (!hits.length)
|
|
2766
|
-
return [];
|
|
2767
|
-
const results = [];
|
|
2768
|
-
const seenSmids = new Set();
|
|
2769
|
-
const seenBatches = new Set();
|
|
2770
|
-
for (const hit of hits) {
|
|
2771
|
-
if (results.length >= limit)
|
|
2772
|
-
break;
|
|
2773
|
-
const mesh = hit.object;
|
|
2774
|
-
const ud = mesh.userData;
|
|
2775
|
-
if (ud.pickId == null)
|
|
2776
|
-
continue;
|
|
2777
|
-
const ref = this._pickables.get(ud.pickId);
|
|
2778
|
-
if (!ref)
|
|
2779
|
-
continue;
|
|
2780
|
-
let smid = null;
|
|
2781
|
-
let batchId = -1;
|
|
2782
|
-
let instanceIndex = hit.instanceId;
|
|
2783
|
-
if (instanceIndex != null) {
|
|
2784
|
-
const feature = this._instanceFeature(mesh, ud.geoRef, instanceIndex);
|
|
2785
|
-
if (!feature)
|
|
2786
|
-
continue;
|
|
2787
|
-
smid = feature.smid;
|
|
2788
|
-
batchId = feature.batchId;
|
|
2789
|
-
}
|
|
2790
|
-
else if (hit.faceIndex != null) {
|
|
2791
|
-
const batchAttr = mesh.geometry.attributes.aBatchId;
|
|
2792
|
-
if (batchAttr) {
|
|
2793
|
-
const idx = mesh.geometry.index ? mesh.geometry.index.getX(hit.faceIndex * 3) : hit.faceIndex * 3;
|
|
2794
|
-
batchId = Math.round(batchAttr.getX(idx));
|
|
2795
|
-
const geoRef = ud.geoByBatchId?.get(batchId) ?? ud.geoRef;
|
|
2796
|
-
const resolved = resolveFeature(geoRef, batchId);
|
|
2797
|
-
smid = resolved.smid;
|
|
2798
|
-
}
|
|
2799
|
-
}
|
|
2800
|
-
if (smid != null) {
|
|
2801
|
-
if (seenSmids.has(smid))
|
|
2802
|
-
continue;
|
|
2803
|
-
seenSmids.add(smid);
|
|
2804
|
-
}
|
|
2805
|
-
else {
|
|
2806
|
-
const key = `${ud.pickId}:${batchId}:${instanceIndex ?? -1}`;
|
|
2807
|
-
if (seenBatches.has(key))
|
|
2808
|
-
continue;
|
|
2809
|
-
seenBatches.add(key);
|
|
2810
|
-
}
|
|
2811
|
-
if (this._isFeatureHidden(smid, batchId))
|
|
2812
|
-
continue;
|
|
2813
|
-
const pickHit = {
|
|
2814
|
-
objectId: ud.pickId,
|
|
2815
|
-
batchId,
|
|
2816
|
-
instanceIndex,
|
|
2817
|
-
};
|
|
2818
|
-
const res = this._buildPickResult(pickHit, ref, hit.point);
|
|
2819
|
-
if (res)
|
|
2820
|
-
results.push(res);
|
|
2821
|
-
}
|
|
2822
|
-
return results;
|
|
1975
|
+
return this._picks.drillPick(x, y, limit);
|
|
2823
1976
|
}
|
|
2824
|
-
/**
|
|
2825
|
-
* 获取指定 SMID 单体的经纬度包围盒 [[minLng, minLat], [maxLng, maxLat]]。
|
|
2826
|
-
* 若模型尚未加载到场景中或无此单体则返回 null。
|
|
2827
|
-
*/
|
|
1977
|
+
/** 单体的经纬度包围盒 */
|
|
2828
1978
|
getFeatureBounds(smid) {
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
const ud = mesh.userData;
|
|
2839
|
-
const geoRef = ud.geoRef;
|
|
2840
|
-
const pickInfo = geoRef?.pickInfo;
|
|
2841
|
-
if (!pickInfo)
|
|
2842
|
-
continue;
|
|
2843
|
-
let box = null;
|
|
2844
|
-
if (mesh.isInstancedMesh) {
|
|
2845
|
-
let lookup = ud.instanceLookup;
|
|
2846
|
-
if (!lookup) {
|
|
2847
|
-
lookup = buildInstanceLookup(pickInfo);
|
|
2848
|
-
ud.instanceLookup = lookup;
|
|
2849
|
-
}
|
|
2850
|
-
for (const [instIdx, feat] of lookup) {
|
|
2851
|
-
if (feat.smid === smid) {
|
|
2852
|
-
box = this._instanceBox(mesh, instIdx);
|
|
2853
|
-
break;
|
|
2854
|
-
}
|
|
2855
|
-
}
|
|
2856
|
-
}
|
|
2857
|
-
else {
|
|
2858
|
-
const infoMap = pickInfo;
|
|
2859
|
-
const entry = infoMap[smid] ?? infoMap[String(smid)];
|
|
2860
|
-
if (entry) {
|
|
2861
|
-
const bId = pickEntryId(entry);
|
|
2862
|
-
const resolved = resolveFeature(geoRef, bId);
|
|
2863
|
-
box = measureSegments(mesh.geometry, resolved.segments);
|
|
2864
|
-
if (!box && mesh.geometry) {
|
|
2865
|
-
if (!mesh.geometry.boundingBox)
|
|
2866
|
-
mesh.geometry.computeBoundingBox();
|
|
2867
|
-
const bb = mesh.geometry.boundingBox;
|
|
2868
|
-
if (bb) {
|
|
2869
|
-
box = {
|
|
2870
|
-
minX: bb.min.x,
|
|
2871
|
-
maxX: bb.max.x,
|
|
2872
|
-
minY: bb.min.y,
|
|
2873
|
-
maxY: bb.max.y,
|
|
2874
|
-
minZ: bb.min.z,
|
|
2875
|
-
maxZ: bb.max.z,
|
|
2876
|
-
count: 0,
|
|
2877
|
-
triangles: 0,
|
|
2878
|
-
};
|
|
2879
|
-
}
|
|
2880
|
-
}
|
|
2881
|
-
}
|
|
2882
|
-
}
|
|
2883
|
-
if (box) {
|
|
2884
|
-
found = true;
|
|
2885
|
-
const corners = [
|
|
2886
|
-
new THREE.Vector3(box.minX, box.minY, box.minZ),
|
|
2887
|
-
new THREE.Vector3(box.maxX, box.minY, box.minZ),
|
|
2888
|
-
new THREE.Vector3(box.minX, box.maxY, box.minZ),
|
|
2889
|
-
new THREE.Vector3(box.maxX, box.maxY, box.minZ),
|
|
2890
|
-
new THREE.Vector3(box.minX, box.minY, box.maxZ),
|
|
2891
|
-
new THREE.Vector3(box.maxX, box.minY, box.maxZ),
|
|
2892
|
-
new THREE.Vector3(box.minX, box.maxY, box.maxZ),
|
|
2893
|
-
new THREE.Vector3(box.maxX, box.maxY, box.maxZ),
|
|
2894
|
-
];
|
|
2895
|
-
const off = smid != null ? this._featureStyleManager.getFeatureEffectiveOffset(smid) : {};
|
|
2896
|
-
const trans = off.translation;
|
|
2897
|
-
const elev = off.elevation;
|
|
2898
|
-
const rotMat = off.rotation ? resolveRotationMatrix(off.rotation) : null;
|
|
2899
|
-
let rotM3 = null;
|
|
2900
|
-
let rotPiv = null;
|
|
2901
|
-
if (rotMat) {
|
|
2902
|
-
rotM3 = new THREE.Matrix3().set(rotMat[0], rotMat[1], rotMat[2], rotMat[3], rotMat[4], rotMat[5], rotMat[6], rotMat[7], rotMat[8]);
|
|
2903
|
-
rotPiv = off.pivot ? new THREE.Vector3(off.pivot[0], off.pivot[1], off.pivot[2]) : new THREE.Vector3(0, 0, 0);
|
|
2904
|
-
}
|
|
2905
|
-
// GPU 路径:位移发生在顶点着色器里,包围盒要按同一套公式在 CPU 复算,
|
|
2906
|
-
// 否则「运动中的车」的包围盒会停在路径原点(相机 fitBounds / 属性过滤都会错)。
|
|
2907
|
-
const gpuPose = smid != null ? this._gpuPathPoseAt(smid) : null;
|
|
2908
|
-
let headM3 = null;
|
|
2909
|
-
if (gpuPose && gpuPose.yaw !== 0) {
|
|
2910
|
-
const c = Math.cos(gpuPose.yaw);
|
|
2911
|
-
const s = Math.sin(gpuPose.yaw);
|
|
2912
|
-
// 与着色器的 mat3( c, s, 0, -s, c, 0, 0, 0, 1 ) 等价:GLSL 是列优先,
|
|
2913
|
-
// three 的 Matrix3.set 是行优先,所以这里写成 (c, -s; s, c) 才是同一个矩阵。
|
|
2914
|
-
headM3 = new THREE.Matrix3().set(c, -s, 0, s, c, 0, 0, 0, 1);
|
|
2915
|
-
}
|
|
2916
|
-
for (const c of corners) {
|
|
2917
|
-
if (rotM3 && rotPiv)
|
|
2918
|
-
c.sub(rotPiv).applyMatrix3(rotM3).add(rotPiv);
|
|
2919
|
-
if (headM3) {
|
|
2920
|
-
const piv = rotPiv ?? new THREE.Vector3(0, 0, 0);
|
|
2921
|
-
c.sub(piv).applyMatrix3(headM3).add(piv);
|
|
2922
|
-
}
|
|
2923
|
-
if (trans)
|
|
2924
|
-
c.add(new THREE.Vector3(trans[0], trans[1], trans[2]));
|
|
2925
|
-
else if (elev)
|
|
2926
|
-
c.z += elev;
|
|
2927
|
-
if (gpuPose)
|
|
2928
|
-
c.add(new THREE.Vector3(gpuPose.pos[0], gpuPose.pos[1], gpuPose.pos[2]));
|
|
2929
|
-
c.applyMatrix4(mesh.matrixWorld);
|
|
2930
|
-
const s = this.sceneAnchor.meterInMercator;
|
|
2931
|
-
const [lng, lat] = this._lngLatFromWorld(c.x * s + this.sceneAnchor.x, -c.y * s + this.sceneAnchor.y);
|
|
2932
|
-
if (lng < minLng)
|
|
2933
|
-
minLng = lng;
|
|
2934
|
-
if (lng > maxLng)
|
|
2935
|
-
maxLng = lng;
|
|
2936
|
-
if (lat < minLat)
|
|
2937
|
-
minLat = lat;
|
|
2938
|
-
if (lat > maxLat)
|
|
2939
|
-
maxLat = lat;
|
|
2940
|
-
}
|
|
2941
|
-
}
|
|
2942
|
-
}
|
|
2943
|
-
if (!found)
|
|
2944
|
-
return null;
|
|
2945
|
-
return [
|
|
2946
|
-
[minLng, minLat],
|
|
2947
|
-
[maxLng, maxLat],
|
|
2948
|
-
];
|
|
1979
|
+
return this._picks.getFeatureBounds(smid);
|
|
1980
|
+
}
|
|
1981
|
+
/** @internal 见 viewer/PickService.ts */
|
|
1982
|
+
_intersectMeshAtRay(mesh, ray, instanceIndex) {
|
|
1983
|
+
return this._picks.intersectMeshAtRay(mesh, ray, instanceIndex);
|
|
1984
|
+
}
|
|
1985
|
+
/** @internal 见 viewer/PickService.ts */
|
|
1986
|
+
_buildPickResult(hit, ref, exactPoint, ray) {
|
|
1987
|
+
return this._picks.buildPickResult(hit, ref, exactPoint, ray);
|
|
2949
1988
|
}
|
|
2950
|
-
/**
|
|
1989
|
+
/** @internal 见 viewer/PickService.ts */
|
|
1990
|
+
_buildSplatPickResult(hit, ref, exactPoint, ray) {
|
|
1991
|
+
return this._picks.buildSplatPickResult(hit, ref, exactPoint, ray);
|
|
1992
|
+
}
|
|
1993
|
+
/** @internal 见 viewer/PickService.ts */
|
|
2951
1994
|
_instanceFeature(mesh, geoRef, instanceIndex) {
|
|
2952
|
-
|
|
2953
|
-
if (!ud.instanceLookup)
|
|
2954
|
-
ud.instanceLookup = buildInstanceLookup(geoRef?.pickInfo);
|
|
2955
|
-
return ud.instanceLookup.get(instanceIndex) ?? null;
|
|
1995
|
+
return this._picks.instanceFeature(mesh, geoRef, instanceIndex);
|
|
2956
1996
|
}
|
|
2957
|
-
/**
|
|
1997
|
+
/** @internal 见 viewer/PickService.ts */
|
|
2958
1998
|
_instanceBox(mesh, instanceIndex) {
|
|
2959
|
-
|
|
2960
|
-
if (!im.isInstancedMesh)
|
|
2961
|
-
return null;
|
|
2962
|
-
const geometry = mesh.geometry;
|
|
2963
|
-
if (!geometry.boundingBox)
|
|
2964
|
-
geometry.computeBoundingBox();
|
|
2965
|
-
const bb = geometry.boundingBox;
|
|
2966
|
-
if (!bb)
|
|
2967
|
-
return null;
|
|
2968
|
-
const m = new THREE.Matrix4();
|
|
2969
|
-
im.getMatrixAt(instanceIndex, m);
|
|
2970
|
-
const box = bb.clone().applyMatrix4(m);
|
|
2971
|
-
return {
|
|
2972
|
-
minX: box.min.x,
|
|
2973
|
-
minY: box.min.y,
|
|
2974
|
-
minZ: box.min.z,
|
|
2975
|
-
maxX: box.max.x,
|
|
2976
|
-
maxY: box.max.y,
|
|
2977
|
-
maxZ: box.max.z,
|
|
2978
|
-
count: geometry.getAttribute('position')?.count ?? 0,
|
|
2979
|
-
triangles: geometry.index ? Math.round(geometry.index.count / 3) : 0,
|
|
2980
|
-
};
|
|
1999
|
+
return this._picks.instanceBox(mesh, instanceIndex);
|
|
2981
2000
|
}
|
|
2982
|
-
/**
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
this._applyPointCloudStyle(obj, false);
|
|
2990
|
-
continue;
|
|
2991
|
-
}
|
|
2992
|
-
const mat = styledMaterialOf(obj);
|
|
2993
|
-
if (mat) {
|
|
2994
|
-
mat.userData.uSelBatch.value = -1;
|
|
2995
|
-
mat.userData.uSelWhole.value = 0;
|
|
2996
|
-
}
|
|
2997
|
-
}
|
|
2998
|
-
this._selectedMeshes = [];
|
|
2999
|
-
this._clearSelectionDecoration();
|
|
3000
|
-
this._selection = null;
|
|
3001
|
-
}
|
|
3002
|
-
if (!result) {
|
|
3003
|
-
this.uniforms.uSelMix.value = 0;
|
|
3004
|
-
this.uniforms.uDim.value = 0;
|
|
3005
|
-
this.outlineMaterial.uniforms.uSelBatch.value = -1;
|
|
3006
|
-
this.outlineMaterial.uniforms.uSelWhole.value = 0;
|
|
3007
|
-
this.outlineMaterial.uniforms.uElevation.value = 0;
|
|
3008
|
-
this._requestRepaint();
|
|
3009
|
-
if (!silent)
|
|
3010
|
-
this.onPick(null);
|
|
3011
|
-
return;
|
|
3012
|
-
}
|
|
3013
|
-
this._selection = result;
|
|
3014
|
-
const targetMeshes = new Set();
|
|
3015
|
-
if (result.smid != null) {
|
|
3016
|
-
const smidSet = this._smidMeshes.get(result.smid);
|
|
3017
|
-
if (smidSet) {
|
|
3018
|
-
for (const m of smidSet)
|
|
3019
|
-
targetMeshes.add(m);
|
|
3020
|
-
}
|
|
3021
|
-
}
|
|
3022
|
-
targetMeshes.add(result.mesh);
|
|
3023
|
-
for (const mesh of targetMeshes) {
|
|
3024
|
-
const mat = styledMaterialOf(mesh);
|
|
3025
|
-
if (!mat)
|
|
3026
|
-
continue;
|
|
3027
|
-
let batchId = result.batchId;
|
|
3028
|
-
if (result.instanceIndex != null && mesh === result.mesh) {
|
|
3029
|
-
batchId = result.batchId;
|
|
3030
|
-
}
|
|
3031
|
-
else if (result.smid != null) {
|
|
3032
|
-
const ud = mesh.userData;
|
|
3033
|
-
const pickInfo = ud.geoRef?.pickInfo;
|
|
3034
|
-
const entry = pickInfo?.[result.smid] ??
|
|
3035
|
-
pickInfo?.[String(result.smid)];
|
|
3036
|
-
const bId = entry ? pickEntryId(entry) : -1;
|
|
3037
|
-
batchId = bId >= 0 ? bId : (mesh === result.mesh ? result.batchId : -1);
|
|
3038
|
-
}
|
|
3039
|
-
mat.userData.uSelBatch.value = batchId;
|
|
3040
|
-
// 没有单体化信息时整块高亮,否则会出现「点中了但什么都没亮、其他全暗了」
|
|
3041
|
-
mat.userData.uSelWhole.value = batchId < 0 ? 1 : 0;
|
|
3042
|
-
this._selectedMeshes.push(mesh);
|
|
3043
|
-
}
|
|
3044
|
-
// 点云没有单体粒度:整块混成 selectColor,让选中至少看得见
|
|
3045
|
-
for (const mesh of this._selectedMeshes) {
|
|
3046
|
-
if (isPointCloudObject(mesh))
|
|
3047
|
-
this._applyPointCloudStyle(mesh, true);
|
|
3048
|
-
}
|
|
3049
|
-
this.uniforms.uSelMix.value = 1;
|
|
3050
|
-
this.uniforms.uDim.value = this.style.dimOthers ? 1 : 0;
|
|
3051
|
-
if (this.style.outline) {
|
|
3052
|
-
// 棱线 / 外扩轮廓都是按「面」设计的,点云画不出来也不该画
|
|
3053
|
-
const decoMeshes = [...targetMeshes].filter((m) => !isPointCloudObject(m));
|
|
3054
|
-
if (decoMeshes.length) {
|
|
3055
|
-
if ((this.style.outlineStyle ?? 'edges') === 'edges') {
|
|
3056
|
-
this._addSelectionEdges(result, decoMeshes);
|
|
3057
|
-
}
|
|
3058
|
-
else {
|
|
3059
|
-
this._addSelectionHull(result, decoMeshes);
|
|
3060
|
-
}
|
|
3061
|
-
}
|
|
3062
|
-
}
|
|
3063
|
-
this._requestRepaint();
|
|
2001
|
+
/** @internal 拾取 id 池,见 viewer/PickService.ts */
|
|
2002
|
+
_allocPickId() {
|
|
2003
|
+
return this._picks.allocPickId();
|
|
2004
|
+
}
|
|
2005
|
+
/** @internal */
|
|
2006
|
+
_releasePickId(id) {
|
|
2007
|
+
this._picks.releasePickId(id);
|
|
3064
2008
|
}
|
|
2009
|
+
/** @internal 拾取表(测试直接读写),见 viewer/PickService.ts */
|
|
2010
|
+
get _pickables() {
|
|
2011
|
+
return this._picks.pickables;
|
|
2012
|
+
}
|
|
2013
|
+
/* ---------------------------------------------------------------- 选中 */
|
|
3065
2014
|
/**
|
|
3066
|
-
*
|
|
3067
|
-
*
|
|
2015
|
+
* 选中一个单体(或整块网格),高亮与轮廓装饰见 viewer/SelectionDecorator.ts。
|
|
2016
|
+
* 传 `null` 清空选中;`silent` 为真时不触发 `onPick`。
|
|
3068
2017
|
*/
|
|
3069
|
-
|
|
3070
|
-
this.
|
|
3071
|
-
for (const mesh of meshes) {
|
|
3072
|
-
const ud = mesh.userData;
|
|
3073
|
-
const geoRef = ud.geoRef;
|
|
3074
|
-
let segments = [];
|
|
3075
|
-
// 这块网格上的棱线全部属于选中的那个单体,整批填同一个 batchId,
|
|
3076
|
-
// 顶点着色器才能从状态纹理里取到平移 / 拔高——选中跟随移动靠它。
|
|
3077
|
-
let batchId = -1;
|
|
3078
|
-
if (mesh === result.mesh && result.instanceIndex != null) {
|
|
3079
|
-
// 实例化几何没有逐单体的顶点区间,整个 base mesh 就是这一栋的外形
|
|
3080
|
-
segments = [{ vertexOffset: 0, vertexCount: mesh.geometry.getAttribute('position')?.count ?? 0 }];
|
|
3081
|
-
const pickInfo = geoRef?.pickInfo;
|
|
3082
|
-
const entry = pickInfo?.[result.smid ?? ''] ??
|
|
3083
|
-
pickInfo?.[String(result.smid)];
|
|
3084
|
-
batchId = entry ? pickEntryId(entry) : -1;
|
|
3085
|
-
}
|
|
3086
|
-
else if (result.smid != null) {
|
|
3087
|
-
const pickInfo = geoRef?.pickInfo;
|
|
3088
|
-
const entry = pickInfo?.[result.smid] ??
|
|
3089
|
-
pickInfo?.[String(result.smid)];
|
|
3090
|
-
const bId = entry ? pickEntryId(entry) : -1;
|
|
3091
|
-
batchId = bId;
|
|
3092
|
-
segments = entry?.segments ?? (bId >= 0 ? resolveFeature(geoRef, bId).segments : []);
|
|
3093
|
-
}
|
|
3094
|
-
else if (mesh === result.mesh) {
|
|
3095
|
-
const localGeo = ud.geoByBatchId?.get(result.batchId) ?? geoRef;
|
|
3096
|
-
segments = resolveFeature(localGeo, result.batchId).segments;
|
|
3097
|
-
batchId = result.batchId;
|
|
3098
|
-
}
|
|
3099
|
-
const pairs = buildFeatureEdgeSegments(mesh.geometry, segments);
|
|
3100
|
-
if (pairs && pairs.length > 0) {
|
|
3101
|
-
const batchIds = batchId >= 0 ? new Float32Array(pairs.length / 6).fill(batchId) : undefined;
|
|
3102
|
-
const lines = new THREE.Mesh(buildThickLineGeometry(pairs, batchIds), this._createSelectionEdgeMaterial(mesh));
|
|
3103
|
-
lines.renderOrder = 4;
|
|
3104
|
-
// 屏幕空间外扩后可能超出原包围球,关掉剔除更稳
|
|
3105
|
-
lines.frustumCulled = false;
|
|
3106
|
-
lines.raycast = () => { };
|
|
3107
|
-
lines.userData.isSelectionEdge = true;
|
|
3108
|
-
if (mesh === result.mesh && result.instanceIndex != null) {
|
|
3109
|
-
this._applyInstanceTransform(lines, result);
|
|
3110
|
-
}
|
|
3111
|
-
mesh.add(lines); // 作为子节点,直接继承世界变换
|
|
3112
|
-
this._selectionEdgeMeshes.push(lines);
|
|
3113
|
-
}
|
|
3114
|
-
}
|
|
3115
|
-
if (this._selectionEdgeMeshes.length === 0) {
|
|
3116
|
-
// 抽不出折边(退化几何 / 没有任何夹角)时退回外扩轮廓,至少保证选中可见
|
|
3117
|
-
this._addSelectionHull(result, meshes);
|
|
3118
|
-
return;
|
|
3119
|
-
}
|
|
3120
|
-
this._selectionEdges = this._selectionEdgeMeshes[0] ?? null;
|
|
3121
|
-
}
|
|
3122
|
-
/** 实例化几何:把某个实例的矩阵套到一个普通 Mesh/子节点上(子节点不参与父级的 instanceMatrix) */
|
|
3123
|
-
_applyInstanceTransform(obj, result) {
|
|
3124
|
-
const im = result.mesh;
|
|
3125
|
-
if (result.instanceIndex == null || !im.isInstancedMesh)
|
|
3126
|
-
return;
|
|
3127
|
-
const m = new THREE.Matrix4();
|
|
3128
|
-
im.getMatrixAt(result.instanceIndex, m);
|
|
3129
|
-
m.decompose(obj.position, obj.quaternion, obj.scale);
|
|
3130
|
-
}
|
|
3131
|
-
/** 旧的描边画法:背面外扩法,整块几何一起上,靠 uSelBatch 只显示选中那栋 */
|
|
3132
|
-
_addSelectionHull(result, meshes) {
|
|
3133
|
-
const instanced = result.instanceIndex != null;
|
|
3134
|
-
// 实例化:轮廓是这个实例的单独一份副本(普通 Mesh),所以整份都画;
|
|
3135
|
-
// 顶点区间型:整块几何一起画,靠 uSelBatch 只显示选中那栋。
|
|
3136
|
-
this.outlineMaterial.uniforms.uSelBatch.value = instanced ? -1 : result.batchId;
|
|
3137
|
-
this.outlineMaterial.uniforms.uSelWhole.value = instanced || result.batchId < 0 ? 1 : 0;
|
|
3138
|
-
this.outlineMaterial.uniforms.uElevation.value = instanced
|
|
3139
|
-
? this._selectionElevation(result)
|
|
3140
|
-
: this.style.selectElevation ?? 0;
|
|
3141
|
-
for (const mesh of meshes) {
|
|
3142
|
-
const mat = styledMaterialOf(mesh);
|
|
3143
|
-
if (mat?.userData?.uBatchTex) {
|
|
3144
|
-
this.outlineMaterial.uniforms.uBatchTex.value = mat.userData.uBatchTex.value;
|
|
3145
|
-
this.outlineMaterial.uniforms.uBatchTexWidth.value = mat.userData.uBatchTexWidth.value;
|
|
3146
|
-
this.outlineMaterial.uniforms.uBatchTexActive.value = mat.userData.uBatchTexActive.value;
|
|
3147
|
-
}
|
|
3148
|
-
const outline = new THREE.Mesh(mesh.geometry, this.outlineMaterial);
|
|
3149
|
-
outline.renderOrder = 3;
|
|
3150
|
-
outline.frustumCulled = false;
|
|
3151
|
-
outline.raycast = () => { };
|
|
3152
|
-
if (mesh === result.mesh && result.instanceIndex != null) {
|
|
3153
|
-
this._applyInstanceTransform(outline, result);
|
|
3154
|
-
}
|
|
3155
|
-
mesh.add(outline); // 作为子节点,直接继承世界变换
|
|
3156
|
-
this._outlineMeshes.push(outline);
|
|
3157
|
-
}
|
|
3158
|
-
this._outlineMesh = this._outlineMeshes[0] ?? null;
|
|
2018
|
+
select(result, opts = {}) {
|
|
2019
|
+
this._sel.select(result, opts);
|
|
3159
2020
|
}
|
|
3160
|
-
/**
|
|
2021
|
+
/** @internal 选中外观跟随样式变化,见 viewer/SelectionDecorator.ts */
|
|
3161
2022
|
_syncSelectionStyle() {
|
|
3162
|
-
|
|
3163
|
-
const u = this.selectionEdgeMaterial.uniforms;
|
|
3164
|
-
u.uColor.value.set(s.outlineColor ?? s.selectColor);
|
|
3165
|
-
u.uWidth.value = Math.max(0.1, s.outlineWidthPx ?? 3);
|
|
3166
|
-
u.uOpacity.value = clampOpacity(s.opacity);
|
|
3167
|
-
// 只加全局 selectElevation:单体自己的拔高已经写在状态纹理第 2 行 z 里,
|
|
3168
|
-
// 顶点着色器采样时带出来了,这里再加一次就会双重拔高
|
|
3169
|
-
if (u.uElevation)
|
|
3170
|
-
u.uElevation.value = s.selectElevation ?? 0;
|
|
3171
|
-
// 外扩描边是另一个材质实例,单体状态纹理的开关是建的时候抄过去的。
|
|
3172
|
-
// 先选中、再抬高(或反过来)时模板会停留在旧值,这里补一次同步。
|
|
3173
|
-
for (const outline of this._outlineMeshes) {
|
|
3174
|
-
const parent = outline.parent;
|
|
3175
|
-
const ud = parent ? styledMaterialOf(parent)?.userData : null;
|
|
3176
|
-
if (ud?.uBatchTex) {
|
|
3177
|
-
this.outlineMaterial.uniforms.uBatchTex.value = ud.uBatchTex.value;
|
|
3178
|
-
this.outlineMaterial.uniforms.uBatchTexWidth.value = ud.uBatchTexWidth.value;
|
|
3179
|
-
this.outlineMaterial.uniforms.uBatchTexActive.value = ud.uBatchTexActive.value;
|
|
3180
|
-
}
|
|
3181
|
-
}
|
|
2023
|
+
this._sel.syncStyle();
|
|
3182
2024
|
}
|
|
3183
|
-
/**
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
if (result.smid != null) {
|
|
3187
|
-
const off = this._featureStyleManager.getFeatureEffectiveOffset(result.smid);
|
|
3188
|
-
if (off.elevation)
|
|
3189
|
-
elev += off.elevation;
|
|
3190
|
-
}
|
|
3191
|
-
return elev;
|
|
2025
|
+
/** @internal 测试用:当前选中 */
|
|
2026
|
+
get _selection() {
|
|
2027
|
+
return this._sel.selection;
|
|
3192
2028
|
}
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
edges.parent?.remove(edges);
|
|
3201
|
-
edges.geometry.dispose();
|
|
3202
|
-
}
|
|
3203
|
-
this._selectionEdgeMeshes = [];
|
|
3204
|
-
this._selectionEdges = null;
|
|
2029
|
+
/** @internal 测试用:被高亮的网格 */
|
|
2030
|
+
get _selectedMeshes() {
|
|
2031
|
+
return this._sel.selectedMeshes;
|
|
2032
|
+
}
|
|
2033
|
+
/** @internal 测试用:选中棱线的网格列表 */
|
|
2034
|
+
get _selectionEdgeMeshes() {
|
|
2035
|
+
return this._sel.selectionEdgeMeshes;
|
|
3205
2036
|
}
|
|
3206
2037
|
/* ---------------------------------------------------------------- 渲染 */
|
|
3207
2038
|
/**
|
|
@@ -3210,30 +2041,6 @@ export class S3MViewer {
|
|
|
3210
2041
|
* 参数全部来自 style,不缓存:`edgeWidth` 在这里是「采样偏移」,线宽 ≈ 2×它;
|
|
3211
2042
|
* `edgeThreshold` 是折边敏感度(夹角小于它不描),和 CPU 折边线同一口径。
|
|
3212
2043
|
*/
|
|
3213
|
-
_renderGpuEdges(canvas) {
|
|
3214
|
-
if (!this.renderer || !this.scene || !this.camera)
|
|
3215
|
-
return;
|
|
3216
|
-
const s = this.style;
|
|
3217
|
-
const fade = this.uniforms.uEdgeFade?.value ?? 1;
|
|
3218
|
-
const opacity = Math.min(1, Math.max(0, s.edgeOpacity ?? 0.6)) * clampOpacity(s.opacity) * fade;
|
|
3219
|
-
if (opacity <= 0.004)
|
|
3220
|
-
return;
|
|
3221
|
-
if (!this._gpuEdge)
|
|
3222
|
-
this._gpuEdge = new GpuEdgePass();
|
|
3223
|
-
if (!this._gpuEdge.supported(this.renderer))
|
|
3224
|
-
return;
|
|
3225
|
-
// 度数 → 归一化法线差(|Δn| = 2·sin(θ/2)),乘 0.6 让阈值落在「刚好能描出」的位置
|
|
3226
|
-
const deg = Math.min(179, Math.max(1, s.edgeThreshold ?? 55));
|
|
3227
|
-
const normalThreshold = 2 * Math.sin((deg * Math.PI) / 360) * 0.6;
|
|
3228
|
-
this._gpuEdge.render(this.renderer, this.scene, this.camera, canvas.width, canvas.height, {
|
|
3229
|
-
color: this._gpuEdgeColor.set(s.edgeColor),
|
|
3230
|
-
opacity,
|
|
3231
|
-
radius: Math.max(1, s.edgeWidth ?? 1.5),
|
|
3232
|
-
normalThreshold,
|
|
3233
|
-
depthThreshold: 0.08,
|
|
3234
|
-
});
|
|
3235
|
-
}
|
|
3236
|
-
/** 等宽线要按设备像素换算线宽:uResolution 用绘图缓冲尺寸,uDpr 是缓冲 / CSS 的比值 */
|
|
3237
2044
|
_updateLineResolution(canvas) {
|
|
3238
2045
|
const w = canvas.width || 1;
|
|
3239
2046
|
const h = canvas.height || 1;
|
|
@@ -3378,6 +2185,15 @@ export class S3MViewer {
|
|
|
3378
2185
|
}
|
|
3379
2186
|
this._requestRepaint();
|
|
3380
2187
|
}
|
|
2188
|
+
/**
|
|
2189
|
+
* 泼溅实例按视深 back-to-front 重排:alpha 混合是顺序相关的,
|
|
2190
|
+
* 乱序混合就是满屏彩色噪点。相机没动且没有新瓦片时整个跳过;
|
|
2191
|
+
* 单个瓦片顺序本来就对时 sortSplatMesh 内部也会跳过重写。
|
|
2192
|
+
*/
|
|
2193
|
+
/** @internal 泼溅按视深重排,见 viewer/SplatLayer.ts */
|
|
2194
|
+
_sortSplats() {
|
|
2195
|
+
this._splats.sort(this.camera, this._pointClouds);
|
|
2196
|
+
}
|
|
3381
2197
|
/**
|
|
3382
2198
|
* 渲染一帧。所有宿主相关量都由调用方喂进来,viewer 不碰任何地图 API:
|
|
3383
2199
|
*
|
|
@@ -3449,16 +2265,18 @@ export class S3MViewer {
|
|
|
3449
2265
|
this._updateLights();
|
|
3450
2266
|
// 等宽棱线按设备像素撑开,分辨率每帧同步一次
|
|
3451
2267
|
this._updateLineResolution(canvas);
|
|
2268
|
+
// 泼溅的最小像素半径 / 抗锯齿低通同样要把 ndc 换算成像素
|
|
2269
|
+
setSplatViewport(canvas.width, canvas.height);
|
|
3452
2270
|
this.renderer.setViewport(0, 0, canvas.width, canvas.height);
|
|
3453
2271
|
this.renderer.resetState();
|
|
3454
2272
|
// 完全透明时什么都不画:透明物体照样写深度,画了会在底图上挖出一个洞。
|
|
3455
2273
|
// 这里跳过的是「本层的绘制」,宿主自己的清屏 / 底图照旧。
|
|
3456
2274
|
if ((this.uniforms.uOpacity?.value ?? 1) > OPACITY_EPS) {
|
|
2275
|
+
this._sortSplats();
|
|
3457
2276
|
this.renderer.render(this.scene, this.camera);
|
|
3458
2277
|
// GPU 描边必须画在主场景之后:它的全屏 quad 参与宿主深度测试,
|
|
3459
2278
|
// 依赖本帧已经写好的深度(被地形挡住的地方不该描边)
|
|
3460
|
-
|
|
3461
|
-
this._renderGpuEdges(canvas);
|
|
2279
|
+
this._edges.renderGpuEdges(canvas);
|
|
3462
2280
|
}
|
|
3463
2281
|
this._frame++;
|
|
3464
2282
|
const now = performance.now();
|
|
@@ -3475,7 +2293,7 @@ export class S3MViewer {
|
|
|
3475
2293
|
this.stats.loading > 0 ||
|
|
3476
2294
|
tilesetChanged;
|
|
3477
2295
|
// 首帧画完、场上有材质了,再把另一种 shading 变体编出来(异步,不挡当前帧)
|
|
3478
|
-
if (this._prewarmShadingEnabled && !this._shadingPrewarmed && this.
|
|
2296
|
+
if (this._prewarmShadingEnabled && !this._shadingPrewarmed && this._picks.pickables.size > 0) {
|
|
3479
2297
|
void this.prewarmShading();
|
|
3480
2298
|
}
|
|
3481
2299
|
return needsNextFrame;
|
|
@@ -3497,7 +2315,9 @@ export class S3MViewer {
|
|
|
3497
2315
|
programs: 0,
|
|
3498
2316
|
textures: 0,
|
|
3499
2317
|
geometries: 0,
|
|
2318
|
+
degradations: [],
|
|
3500
2319
|
};
|
|
2320
|
+
let degradations = null;
|
|
3501
2321
|
for (const entry of this.datasets.values()) {
|
|
3502
2322
|
const s = entry.tileset?.stats;
|
|
3503
2323
|
if (!s)
|
|
@@ -3508,7 +2328,17 @@ export class S3MViewer {
|
|
|
3508
2328
|
total.meshes += s.meshes;
|
|
3509
2329
|
total.vertices += s.vertices;
|
|
3510
2330
|
total.triangles += s.triangles;
|
|
2331
|
+
// 降级项跨数据集去重:同一类只报一次给 UI
|
|
2332
|
+
if (s.degradations?.length) {
|
|
2333
|
+
degradations ??= [];
|
|
2334
|
+
for (const d of s.degradations) {
|
|
2335
|
+
if (!degradations.includes(d))
|
|
2336
|
+
degradations.push(d);
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
3511
2339
|
}
|
|
2340
|
+
if (degradations)
|
|
2341
|
+
total.degradations = degradations;
|
|
3512
2342
|
// GPU 侧预算观测:draw call / program / 显存驻留。没有渲染器(未 mount /
|
|
3513
2343
|
// 纯解析场景)时保持 0,不抛错。
|
|
3514
2344
|
const info = this.renderer?.info;
|