s3m-three 0.4.19 → 0.4.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser/assets/{s3mWorker-4iTAmWIu.js → s3mWorker-DvAUA8NJ.js} +1 -1
- package/dist/browser/s3m-three.iife.ext-three.js +243 -243
- package/dist/browser/s3m-three.iife.js +227 -227
- 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/S3MBlockParser.d.ts.map +1 -1
- package/dist/core/S3MBlockParser.js +27 -14
- package/dist/core/S3MBlockParser.js.map +1 -1
- package/dist/core/SplatSort.d.ts +14 -1
- package/dist/core/SplatSort.d.ts.map +1 -1
- package/dist/core/SplatSort.js +51 -4
- package/dist/core/SplatSort.js.map +1 -1
- package/dist/core/TileGeometry.d.ts +10 -0
- package/dist/core/TileGeometry.d.ts.map +1 -1
- package/dist/core/TileGeometry.js +17 -14
- package/dist/core/TileGeometry.js.map +1 -1
- package/dist/render/DatasetManager.d.ts +3 -1
- package/dist/render/DatasetManager.d.ts.map +1 -1
- package/dist/render/DatasetManager.js +2 -1
- package/dist/render/DatasetManager.js.map +1 -1
- package/dist/render/FeatureStyleManager.d.ts.map +1 -1
- package/dist/render/GeometryBuilder.d.ts +8 -0
- package/dist/render/GeometryBuilder.d.ts.map +1 -1
- package/dist/render/GeometryBuilder.js +62 -7
- package/dist/render/GeometryBuilder.js.map +1 -1
- package/dist/render/S3MViewer.d.ts +106 -440
- package/dist/render/S3MViewer.d.ts.map +1 -1
- package/dist/render/S3MViewer.js +297 -1653
- package/dist/render/S3MViewer.js.map +1 -1
- package/dist/render/SplatSorter.d.ts.map +1 -1
- package/dist/render/SplatSorter.js +8 -1
- package/dist/render/SplatSorter.js.map +1 -1
- 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 +256 -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 +30 -3
- package/dist/tileset/S3MTileset.d.ts.map +1 -1
- package/dist/tileset/S3MTileset.js +99 -5
- package/dist/tileset/S3MTileset.js.map +1 -1
- package/dist/worker/s3mWorker.js +1 -1
- package/dist/worker/s3mWorker.js.map +1 -1
- package/docs/API.md +9 -28
- package/docs/FORMAT-NOTES.md +38 -14
- package/package.json +1 -1
- package/src/core/Draco.ts +1 -2
- package/src/core/S3MBlockParser.ts +27 -16
- package/src/core/SplatSort.ts +67 -5
- package/src/core/TileGeometry.ts +24 -11
- package/src/render/DatasetManager.ts +5 -1
- package/src/render/FeatureStyleManager.ts +1 -1
- package/src/render/GeometryBuilder.ts +59 -7
- package/src/render/S3MViewer.ts +327 -1916
- package/src/render/SplatSorter.ts +8 -1
- 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 +264 -0
- package/src/tileset/S3MTileset.ts +106 -6
package/src/render/S3MViewer.ts
CHANGED
|
@@ -23,13 +23,25 @@
|
|
|
23
23
|
import * as THREE from 'three';
|
|
24
24
|
import { S3MTileset, usableGeoBounds } from '../tileset/S3MTileset.js';
|
|
25
25
|
import type { BuildTileFn, TileNode } from '../tileset/S3MTileset.js';
|
|
26
|
-
import {
|
|
27
|
-
import { sortSplatMesh } from './SplatSorter.js';
|
|
28
|
-
import { splatCenterBounds } from './SplatMaterial.js';
|
|
26
|
+
import type { AuthenticateFn } from '../core/AuthFetch.js';
|
|
29
27
|
import { setSplatViewport } from './GeometryBuilder.js';
|
|
30
|
-
import { setMaxAnisotropy,
|
|
31
|
-
import { getMaterialQuality, setMaterialQuality
|
|
32
|
-
import {
|
|
28
|
+
import { setMaxAnisotropy, setCompressedTexturesSupported, setTextureOffThreadDecode, setMaxTextureSize as setTextureSizeCap } from './TextureFactory.js';
|
|
29
|
+
import { getMaterialQuality, setMaterialQuality } from './StyleMaterial.js';
|
|
30
|
+
import type { LightingPreset } from './lightingPreset.js';
|
|
31
|
+
import { LightRig } from './viewer/LightRig.js';
|
|
32
|
+
import { HeightIndex } from './viewer/HeightIndex.js';
|
|
33
|
+
import { SplatLayer } from './viewer/SplatLayer.js';
|
|
34
|
+
import { EdgeDecorator } from './viewer/EdgeDecorator.js';
|
|
35
|
+
import { PickService } from './viewer/PickService.js';
|
|
36
|
+
import { SelectionDecorator } from './viewer/SelectionDecorator.js';
|
|
37
|
+
import type {
|
|
38
|
+
LightOptions,
|
|
39
|
+
S3MViewerOptions,
|
|
40
|
+
PickResult,
|
|
41
|
+
PickableRef,
|
|
42
|
+
PathFollowOptions,
|
|
43
|
+
MeshUserData,
|
|
44
|
+
} from './viewer/types.js';
|
|
33
45
|
import { decodeTextureWithWorker } from '../worker/S3MWorkerClient.js';
|
|
34
46
|
import {
|
|
35
47
|
STYLE,
|
|
@@ -39,17 +51,13 @@ import {
|
|
|
39
51
|
createSharedUniforms,
|
|
40
52
|
createStyledMaterial,
|
|
41
53
|
createOutlineMaterial,
|
|
42
|
-
resolveEdgeModes,
|
|
43
|
-
sameEdgeModes,
|
|
44
54
|
restoreBaseBlendState,
|
|
45
55
|
} from './StyleMaterial.js';
|
|
46
56
|
import type {
|
|
47
57
|
OutlineMaterial,
|
|
48
|
-
OutlineStyleUniforms,
|
|
49
58
|
StyledMaterial,
|
|
50
59
|
StyledMaterialOptions,
|
|
51
60
|
StyleMode,
|
|
52
|
-
SelectMode,
|
|
53
61
|
S3MStyle,
|
|
54
62
|
EdgeModes,
|
|
55
63
|
EdgesOption,
|
|
@@ -63,35 +71,16 @@ import { Animator } from '../anim/Animator.js';
|
|
|
63
71
|
import type { Channel, TransitionOptions } from '../anim/Animator.js';
|
|
64
72
|
import { Path } from '../anim/Path.js';
|
|
65
73
|
import type { EasingFn, EasingName } from '../anim/easing.js';
|
|
66
|
-
import type { LineSharedUniforms, ThickLineMaterial
|
|
74
|
+
import type { LineSharedUniforms, ThickLineMaterial } from './EdgeLines.js';
|
|
67
75
|
import { GPUPathManager } from './GPUPathManager.js';
|
|
68
76
|
import type { GPUPathOptions } from './FeatureStyleManager.js';
|
|
69
|
-
import {
|
|
70
|
-
buildFeatureEdgeSegments,
|
|
71
|
-
buildPositionBatchMap,
|
|
72
|
-
buildThickLineGeometry,
|
|
73
|
-
createLineSharedUniforms,
|
|
74
|
-
createThickLineMaterial,
|
|
75
|
-
positionQuantKey,
|
|
76
|
-
} from './EdgeLines.js';
|
|
77
|
-
import { mergeVerticesByPosition } from './MergeVertices.js';
|
|
78
|
-
import {
|
|
79
|
-
resolveFeature,
|
|
80
|
-
measureSegments,
|
|
81
|
-
buildAttributes,
|
|
82
|
-
buildInstanceLookup,
|
|
83
|
-
pickEntryId,
|
|
84
|
-
DEFAULT_FLOOR_HEIGHT,
|
|
85
|
-
} from '../core/FeatureInfo.js';
|
|
86
77
|
import type { InstanceFeature } from '../core/FeatureInfo.js';
|
|
87
78
|
import { AttributeQuery } from '../core/AttributeQuery.js';
|
|
88
|
-
import type { AttributeQueryOptions
|
|
79
|
+
import type { AttributeQueryOptions } from '../core/AttributeQuery.js';
|
|
89
80
|
import type {
|
|
90
|
-
Anchor,
|
|
91
81
|
FeatureAttributes,
|
|
92
82
|
GeoPackageEntry,
|
|
93
83
|
ParsedTile,
|
|
94
|
-
PickSegment,
|
|
95
84
|
SegmentBox,
|
|
96
85
|
Stats,
|
|
97
86
|
StyleUniforms,
|
|
@@ -110,16 +99,13 @@ import {
|
|
|
110
99
|
FeatureStyleManager,
|
|
111
100
|
type FeatureStyle,
|
|
112
101
|
type ColorByFieldOptions,
|
|
113
|
-
type ThematicLegendEntry,
|
|
114
102
|
type ThematicResult,
|
|
115
|
-
type BatchConflict,
|
|
116
103
|
type BatchAudit,
|
|
117
104
|
type FieldFilterOptions,
|
|
118
105
|
type FieldFilterResult,
|
|
119
106
|
type FieldElevationOptions,
|
|
120
107
|
type FieldElevationResult,
|
|
121
108
|
type S3MFilter,
|
|
122
|
-
resolveRotationMatrix,
|
|
123
109
|
extractEulerAngles,
|
|
124
110
|
} from './FeatureStyleManager.js';
|
|
125
111
|
|
|
@@ -156,224 +142,14 @@ export type {
|
|
|
156
142
|
|
|
157
143
|
export { GroundManager, DatasetManager, FeatureStyleManager };
|
|
158
144
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
batchId: number;
|
|
168
|
-
/** 实例化几何命中的实例序号;非实例化为 undefined */
|
|
169
|
-
instanceIndex?: number;
|
|
170
|
-
skeletonName: string;
|
|
171
|
-
/** 单体中心经纬度,拿不到时为 null */
|
|
172
|
-
centerLngLat: [number, number] | null;
|
|
173
|
-
/** 命中点三维场景坐标 [x, y, z](米,场景 ENU) */
|
|
174
|
-
point?: [number, number, number] | null;
|
|
175
|
-
/** 命中点经纬度 [lng, lat] */
|
|
176
|
-
lngLat?: [number, number] | null;
|
|
177
|
-
/** 命中点高程(米) */
|
|
178
|
-
altitude?: number | null;
|
|
179
|
-
mesh: THREE.Mesh;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
export interface LightOptions {
|
|
183
|
-
/** 环境光强度,默认 0.4 */
|
|
184
|
-
ambientIntensity?: number;
|
|
185
|
-
/** 环境光颜色,十六进制或 CSS 颜色字符串,默认 0xffffff */
|
|
186
|
-
ambientColor?: number | string;
|
|
187
|
-
/** 半球光天空强度,默认 0.8 */
|
|
188
|
-
hemisphereIntensity?: number;
|
|
189
|
-
/** 半球光天空颜色,默认 0xffffff */
|
|
190
|
-
hemisphereColor?: number | string;
|
|
191
|
-
/** 半球光地面颜色,默认 0xb6c0cb */
|
|
192
|
-
hemisphereGroundColor?: number | string;
|
|
193
|
-
/** 主平行光强度,默认 1.0 */
|
|
194
|
-
keyLightIntensity?: number;
|
|
195
|
-
/** 主平行光强度别名(兼容写法) */
|
|
196
|
-
mainIntensity?: number;
|
|
197
|
-
/** 主平行光颜色,默认 0xffffff */
|
|
198
|
-
keyLightColor?: number | string;
|
|
199
|
-
/** 主平行光颜色别名(兼容写法) */
|
|
200
|
-
mainColor?: number | string;
|
|
201
|
-
/** 补光强度,默认 0.35 */
|
|
202
|
-
fillLightIntensity?: number;
|
|
203
|
-
/** 补光颜色,默认 0xffffff */
|
|
204
|
-
fillLightColor?: number | string;
|
|
205
|
-
/**
|
|
206
|
-
* 光照朝向模式:
|
|
207
|
-
* - 'headlamp': 默认头灯模式(随相机 bearing 旋转,受光面始终面向镜头)
|
|
208
|
-
* - 'fixed': 固定世界坐标方位角(如太阳光模拟)
|
|
209
|
-
*/
|
|
210
|
-
mode?: 'headlamp' | 'fixed';
|
|
211
|
-
/** 方位角(度,0~360),固定模式下以正北为 0 顺时针,头灯模式下为相机相对偏角,默认 150 */
|
|
212
|
-
azimuth?: number;
|
|
213
|
-
/** 太阳/光源仰角(度,0~90),默认 50 */
|
|
214
|
-
altitude?: number;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
export interface S3MViewerOptions {
|
|
218
|
-
/**
|
|
219
|
-
* 光照预设:
|
|
220
|
-
* - `'default'`(默认):现有 PBR 打光,亮部线性总量 ≈ 2.55;
|
|
221
|
-
* - `'supermap'`:对齐 iClient3D 观感(暗部 0.46 / 亮部 2.44),远处不发灰、饱和度更高。
|
|
222
|
-
* 只改光源,不动材质与色调映射;`light` 里的显式字段仍然优先生效。
|
|
223
|
-
*/
|
|
224
|
-
lightingPreset?: LightingPreset;
|
|
225
|
-
/** 光照参数配置(强度、颜色、朝向模式、太阳方位角/仰角等) */
|
|
226
|
-
light?: LightOptions;
|
|
227
|
-
/**
|
|
228
|
-
* 三角面材质深度偏移(共面摩尔纹 / 重合面归属的人工定序)。
|
|
229
|
-
* 例:factor -1 / units -2 把配置材质的片元深度略微拉近。
|
|
230
|
-
* 这是 mesh 粒度开关,见 BuildTileOptions.polygonOffset 的说明。
|
|
231
|
-
*/
|
|
232
|
-
polygonOffset?: { factor: number; units: number };
|
|
233
|
-
/**
|
|
234
|
-
* 色调映射模式(默认 THREE.NoToneMapping,避免 ACESFilmic 压暗中暗部及导致底图与建筑色彩失真;
|
|
235
|
-
* 可传入 THREE.ACESFilmicToneMapping、THREE.LinearToneMapping、THREE.ReinhardToneMapping 等)
|
|
236
|
-
*/
|
|
237
|
-
toneMapping?: THREE.ToneMapping;
|
|
238
|
-
/** 色调映射曝光度,默认 1.0 */
|
|
239
|
-
toneMappingExposure?: number;
|
|
240
|
-
/** 全局总驻留瓦片上限(多数据集多图层共享预算),在所有可见数据集之间动态均衡分配,彻底防止爆显存 */
|
|
241
|
-
totalMaxTiles?: number;
|
|
242
|
-
/** 单数据集简写 */
|
|
243
|
-
configUrl?: string;
|
|
244
|
-
/** 多数据集 */
|
|
245
|
-
datasets?: S3MDatasetOptions[];
|
|
246
|
-
style?: Partial<S3MStyle>;
|
|
247
|
-
/** 3D 物理地表面 / 基坑开挖配置 */
|
|
248
|
-
ground?: GroundOptions;
|
|
249
|
-
pickable?: boolean;
|
|
250
|
-
/** 每个数据集最多驻留瓦片 */
|
|
251
|
-
maxTiles?: number;
|
|
252
|
-
/** 每个数据集并发请求数 */
|
|
253
|
-
maxConcurrent?: number;
|
|
254
|
-
/**
|
|
255
|
-
* 图层级默认的降级处置策略(默认 `'warn'`),数据集可用 `unsupported` 单独覆盖。
|
|
256
|
-
* 见 `S3MTilesetOptions.unsupported`。
|
|
257
|
-
*/
|
|
258
|
-
unsupported?: UnsupportedPolicy;
|
|
259
|
-
/** 是否用 Web Worker 解析经典 .s3mb 瓦片(默认开,崩溃自动降级主线程),见 S3MTilesetOptions.useWorker */
|
|
260
|
-
useWorker?: boolean;
|
|
261
|
-
/** LOD 切换范围缩放系数(默认 1.0),> 1 更倾向保留粗模,< 1 提前换细模 */
|
|
262
|
-
lodRangeScale?: number;
|
|
263
|
-
/**
|
|
264
|
-
* LOD 判定用屏幕高度时的「设备像素比上限」(默认不限制,行为与旧版一致)。
|
|
265
|
-
*
|
|
266
|
-
* LOD 的屏幕空间误差按 `canvasHeight` 物理像素算(与官方 `calcPixFromCam` 同口径),
|
|
267
|
-
* 于是 3x DPR 的手机上判定高度是 2556 而不是 852,细化层数与瓦片数都会远多于桌面。
|
|
268
|
-
* 传 2 表示「按 CSS 高度 × 2 截断」,砍掉 3x 屏的超采样余量;传 `Infinity` = 不截断。
|
|
269
|
-
*
|
|
270
|
-
* 代价:截断会把每块瓦片的屏幕投影放大同样倍数,几何误差与纹理密度同步变差,
|
|
271
|
-
* 所以默认不启用,由接入方按设备档位自行决定。运行时可用 `setLodPixelRatioCap()` 调整。
|
|
272
|
-
*/
|
|
273
|
-
lodPixelRatioCap?: number;
|
|
274
|
-
/**
|
|
275
|
-
* 每个数据集驻留三角形总数的软上限(默认不限制)。
|
|
276
|
-
* 见 `S3MTilesetOptions.maxTriangles`:瓦片数管不住的场景(精模、大面片)用这条刹车。
|
|
277
|
-
*/
|
|
278
|
-
maxTriangles?: number;
|
|
279
|
-
/**
|
|
280
|
-
* 纹理缓存条数上限(默认 128)。一条对应一张已解码的瓦片纹理,
|
|
281
|
-
* 1024² 的 RGBA 一张约 4MB,128 条就是 512MB 显存 —— 低端设备建议 32~48。
|
|
282
|
-
* 与 `maxTextureSize` 配合使用:后者降单张尺寸,这条降张数。
|
|
283
|
-
*/
|
|
284
|
-
maxImageCache?: number;
|
|
285
|
-
/**
|
|
286
|
-
* 各向异性过滤上限(默认不封顶,取 GPU 报的最大值,通常 16)。
|
|
287
|
-
* 移动端封到 4 或 8 肉眼通常无差,采样成本明显下降。只影响**之后创建**的纹理。
|
|
288
|
-
*/
|
|
289
|
-
anisotropyCap?: number;
|
|
290
|
-
/**
|
|
291
|
-
* 纹理长边像素上限(默认不限制)。
|
|
292
|
-
* 非图集的 RGBA / WebP 纹理会按 2× 盒式滤波逐次减半到上限内;DXT 压缩直传与
|
|
293
|
-
* 图集(UV 已重映射)不受影响。手机上 1024 与 2048 肉眼几乎无差,显存差 4 倍。
|
|
294
|
-
*/
|
|
295
|
-
maxTextureSize?: number;
|
|
296
|
-
/**
|
|
297
|
-
* 光照模型(默认 `'pbr'`)。
|
|
298
|
-
*
|
|
299
|
-
* - `'pbr'`:`MeshStandardMaterial`,GGX + 环境球谐,质感最好也最贵
|
|
300
|
-
* - `'lambert'`:`MeshLambertMaterial`,只保留漫反射 + 平行光,片元开销约省一半;
|
|
301
|
-
* 白模 / 纯色 / 按高度着色这类不需要高光的模式肉眼差别很小
|
|
302
|
-
*
|
|
303
|
-
* 模块级全局状态:同页多个图层会互相覆盖,后创建的那个决定全局。
|
|
304
|
-
* 只影响**之后创建**的材质 —— 已加载的瓦片要重新加载才会换。
|
|
305
|
-
*/
|
|
306
|
-
materialQuality?: 'pbr' | 'lambert';
|
|
307
|
-
/** 自定义瓦片构建器(测试 / 服务端渲染时注入) */
|
|
308
|
-
buildTile?: BuildTileFn | null;
|
|
309
|
-
/**
|
|
310
|
-
* 可选:业务属性查询配置(iServer 数据服务)。
|
|
311
|
-
* 配上之后才能用 `colorByField()` 按业务字段着色 —— 属性值不在 S3M 瓦片里,只能去数据服务查。
|
|
312
|
-
*/
|
|
313
|
-
attributes?: AttributeQueryOptions;
|
|
314
|
-
/** 宿主请求重画的信号。MapLibre 传 `() => map.triggerRepaint()`;纯 three 宿主传自己的渲染循环 */
|
|
315
|
-
onRequestRepaint?: () => void;
|
|
316
|
-
/** 地图朝向(弧度),用于让灯光跟着视角走。不传则灯光方向固定 */
|
|
317
|
-
getBearing?: () => number;
|
|
318
|
-
/**
|
|
319
|
-
* 鉴权适配函数(全局默认,数据集可用自己的 `authenticate` 覆盖)。
|
|
320
|
-
* 内核主线程发出的全部请求(配置 scp / 瓦片 / 子图集 / 棱线缓存 / 属性查询)
|
|
321
|
-
* 发出前都过一遍,可改写 URL(加 `?token=`)或附加请求头。
|
|
322
|
-
* 不传 = 行为与旧版完全一致。见 `AuthenticateFn`。
|
|
323
|
-
*/
|
|
324
|
-
authenticate?: AuthenticateFn;
|
|
325
|
-
onStats?: (stats: Stats) => void;
|
|
326
|
-
onReady?: (entry: S3MDatasetEntry) => void;
|
|
327
|
-
onPick?: (result: PickResult | null) => void;
|
|
328
|
-
onError?: (error: unknown) => void;
|
|
329
|
-
/**
|
|
330
|
-
* 首次出现瓦片后自动预热另一种 `shading` 变体(默认开)。
|
|
331
|
-
*
|
|
332
|
-
* `flat` / `smooth` 是编译期变体,不预热的话第一次 `setStyle({ shading })` 会同步
|
|
333
|
-
* 编译着色器、卡一帧。预热在首帧之后异步做,期间跳一两帧渲染,肉眼不可见。
|
|
334
|
-
*/
|
|
335
|
-
prewarmShading?: boolean;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
/* ------------------------------------------------------------------ 内部类型 */
|
|
339
|
-
|
|
340
|
-
interface PickableRef {
|
|
341
|
-
datasetId: string;
|
|
342
|
-
mesh: THREE.Mesh;
|
|
343
|
-
tile: ParsedTile;
|
|
344
|
-
skeletonName: string;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
interface MeshUserData {
|
|
348
|
-
pickId?: number;
|
|
349
|
-
datasetId?: string;
|
|
350
|
-
skeletonName?: string;
|
|
351
|
-
geoRef?: GeoPackageEntry;
|
|
352
|
-
tileRef?: ParsedTile;
|
|
353
|
-
primitiveType?: number;
|
|
354
|
-
/** 点云对象(THREE.Points):不吃面着色,也没有棱可描 */
|
|
355
|
-
isPointCloud?: boolean;
|
|
356
|
-
/** CPU 端现算的棱线对象(等宽线 Mesh) */
|
|
357
|
-
edges?: THREE.Object3D | null;
|
|
358
|
-
/** 背面外扩轮廓壳(edges 含 'hull' 时挂上) */
|
|
359
|
-
edgeHull?: THREE.Mesh | null;
|
|
360
|
-
/** 数据自带棱线包(此时不再用 EdgesGeometry 现算) */
|
|
361
|
-
nativeEdges?: boolean;
|
|
362
|
-
/** 数据本身有没有棱线包(不受 forceCpuEdges 影响,用来运行中切换时重算 nativeEdges) */
|
|
363
|
-
nativeEdgesRaw?: boolean;
|
|
364
|
-
/** 数据自带的棱线对象本身 */
|
|
365
|
-
isEdgeLines?: boolean;
|
|
366
|
-
/** CPU 现算的等宽棱线(与 isEdgeLines 区分,便于刷新样式) */
|
|
367
|
-
isThickEdge?: boolean;
|
|
368
|
-
/** 背面外扩轮廓壳(edges 含 'hull' 时挂上) */
|
|
369
|
-
isEdgeHull?: boolean;
|
|
370
|
-
/** 选中单体的棱线对象 */
|
|
371
|
-
isSelectionEdge?: boolean;
|
|
372
|
-
/** 该网格正在淡入时占用的动画通道 key,瓦片淘汰时要回收 */
|
|
373
|
-
tileFadeKey?: string;
|
|
374
|
-
pickMaterial?: THREE.Material;
|
|
375
|
-
[key: string]: unknown;
|
|
376
|
-
}
|
|
145
|
+
/* 公共类型定义在 viewer/types.ts;这里 re-export 保持既有 import 路径不变 */
|
|
146
|
+
export type {
|
|
147
|
+
PickResult,
|
|
148
|
+
LightOptions,
|
|
149
|
+
S3MViewerOptions,
|
|
150
|
+
PickableRef,
|
|
151
|
+
PathFollowOptions,
|
|
152
|
+
} from './viewer/types.js';
|
|
377
153
|
|
|
378
154
|
function styledMaterialOf(mesh: THREE.Mesh): StyledMaterial | null {
|
|
379
155
|
const m = Array.isArray(mesh.material) ? mesh.material[0] : mesh.material;
|
|
@@ -436,25 +212,6 @@ const FEATURE_PROPS = ['translation', 'elevation', 'opacity', 'color', 'rotation
|
|
|
436
212
|
type FeatureProp = (typeof FEATURE_PROPS)[number];
|
|
437
213
|
|
|
438
214
|
/** 单体沿折线运动的参数 */
|
|
439
|
-
export interface PathFollowOptions {
|
|
440
|
-
/** 走完全程的时长(毫秒)。不传则按 speed 或默认 2000ms 推算 */
|
|
441
|
-
duration?: number;
|
|
442
|
-
/** 线速度(米/秒)。给了 duration 就以 duration 为准 */
|
|
443
|
-
speed?: number;
|
|
444
|
-
/** 是否循环 */
|
|
445
|
-
loop?: boolean;
|
|
446
|
-
/** 是否把折线首尾相连 */
|
|
447
|
-
closed?: boolean;
|
|
448
|
-
/** 缓动曲线:作用在**已走距离**上,默认 linear(恒速) */
|
|
449
|
-
easing?: EasingName | EasingFn;
|
|
450
|
-
/** 路径整体偏移(米),相当于路径原点 */
|
|
451
|
-
offset?: [number, number, number];
|
|
452
|
-
/** 单次走完(非循环)时回调 */
|
|
453
|
-
onComplete?: () => void;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
/* ------------------------------------------------------------------ 图层 */
|
|
457
|
-
|
|
458
215
|
export class S3MViewer {
|
|
459
216
|
readonly options: S3MViewerOptions;
|
|
460
217
|
readonly pickable: boolean;
|
|
@@ -485,12 +242,8 @@ export class S3MViewer {
|
|
|
485
242
|
readonly edgeMaterial: ThickLineMaterial;
|
|
486
243
|
/** 选中棱线材质(同上,选中棱线的逐网格材质共享它的对象) */
|
|
487
244
|
readonly selectionEdgeMaterial: ThickLineMaterial;
|
|
488
|
-
/**
|
|
489
|
-
private readonly
|
|
490
|
-
/** 选中棱线的外观 uniform */
|
|
491
|
-
private readonly _selEdgeStyleUniforms: ThickLineStyleUniforms;
|
|
492
|
-
/** 全局外扩轮廓的外观 uniform:逐网格轮廓壳共享这些对象 */
|
|
493
|
-
private readonly _hullStyleUniforms: OutlineStyleUniforms;
|
|
245
|
+
/** 棱线装饰(见 viewer/EdgeDecorator.ts) */
|
|
246
|
+
private readonly _edges: EdgeDecorator;
|
|
494
247
|
|
|
495
248
|
/** 3D 物理地表面 / 基坑开挖管理器 */
|
|
496
249
|
readonly groundManager: GroundManager;
|
|
@@ -515,29 +268,16 @@ export class S3MViewer {
|
|
|
515
268
|
this._datasetManager.sceneAnchor = val;
|
|
516
269
|
}
|
|
517
270
|
|
|
518
|
-
|
|
519
|
-
private
|
|
520
|
-
private readonly _freePickIds: number[] = [];
|
|
271
|
+
/** 拾取表 / pickId 池 / 射线求交(见 viewer/PickService.ts) */
|
|
272
|
+
private readonly _picks: PickService;
|
|
521
273
|
/** SMID → 包含它的网格。改单个单体样式时只重算这几块,不用全量扫 */
|
|
522
274
|
private readonly _smidMeshes = new Map<number, Set<THREE.Mesh>>();
|
|
523
275
|
/** 每块网格的世界高度范围,用来增量维护 uHeightMin / uHeightMax */
|
|
524
|
-
|
|
525
|
-
private
|
|
526
|
-
|
|
527
|
-
private readonly
|
|
528
|
-
|
|
529
|
-
private _selectedMeshes: THREE.Object3D[] = [];
|
|
530
|
-
private _outlineMesh: THREE.Mesh | null = null;
|
|
531
|
-
private _outlineMeshes: THREE.Mesh[] = [];
|
|
532
|
-
private _selectionEdges: THREE.Mesh | null = null;
|
|
533
|
-
private _selectionEdgeMeshes: THREE.Mesh[] = [];
|
|
534
|
-
private _edgesEnabled = false;
|
|
535
|
-
/** 当前生效的棱线画法(line / offset / hull / gpu),决定要不要建 / 拆哪种装饰 */
|
|
536
|
-
private _edgeModes: EdgeModes = resolveEdgeModes(false);
|
|
537
|
-
/** GPU 描边后处理(深度 + 法线),只在 edges 含 'gpu' 时按帧执行 */
|
|
538
|
-
private _gpuEdge: GpuEdgePass | null = null;
|
|
539
|
-
/** 给 GPU 描边传色用的临时 Color,避免每帧 new */
|
|
540
|
-
private readonly _gpuEdgeColor = new THREE.Color();
|
|
276
|
+
/** 世界高度范围增量维护(见 viewer/HeightIndex.ts) */
|
|
277
|
+
private readonly _heights: HeightIndex;
|
|
278
|
+
/** 选中态与装饰(见 viewer/SelectionDecorator.ts) */
|
|
279
|
+
private readonly _sel: SelectionDecorator;
|
|
280
|
+
|
|
541
281
|
/** 瓦片淡入通道的自增序号(不用 pickId:pickId 会被回收复用) */
|
|
542
282
|
private _tileFadeSeq = 0;
|
|
543
283
|
/** setMode 编排的代次,用来作废被新调用顶掉的旧编排 */
|
|
@@ -554,15 +294,11 @@ export class S3MViewer {
|
|
|
554
294
|
private readonly _pathScratch = new Float32Array(3);
|
|
555
295
|
/** 求切线用的第二个采样点(包围盒换算 GPU 路径朝向时用) */
|
|
556
296
|
private readonly _pathScratch2 = new Float32Array(3);
|
|
557
|
-
|
|
558
|
-
private _creaseLines: THREE.LineSegments | null = null;
|
|
559
|
-
/** 数据自带的棱线对象(LineSegments),改棱线颜色时只刷这些,不再全场景 traverse */
|
|
560
|
-
private readonly _nativeEdgeLines = new Set<THREE.LineSegments>();
|
|
297
|
+
|
|
561
298
|
/** 点云对象集合:透明度动画时只刷它们,避免每帧遍历全部 pickables */
|
|
562
299
|
private readonly _pointClouds = new Set<THREE.Points>();
|
|
563
|
-
/**
|
|
564
|
-
private readonly
|
|
565
|
-
private _splatsSorted = false;
|
|
300
|
+
/** 泼溅排序调度(见 viewer/SplatLayer.ts) */
|
|
301
|
+
private readonly _splats = new SplatLayer();
|
|
566
302
|
/** 当前材质是否处于 transparent 状态,用来避免逐帧重复切换 program */
|
|
567
303
|
private _opacityTransparent = false;
|
|
568
304
|
|
|
@@ -582,29 +318,10 @@ export class S3MViewer {
|
|
|
582
318
|
private readonly _frameMvp = new THREE.Matrix4();
|
|
583
319
|
/** 颜色换算用的临时对象,避免每帧 new */
|
|
584
320
|
private readonly _colorScratch = new THREE.Color();
|
|
585
|
-
private _needsRepaint = true;
|
|
586
321
|
private _frame = 0;
|
|
587
322
|
private readonly _fps = { value: 0, last: 0 };
|
|
588
|
-
|
|
589
|
-
private
|
|
590
|
-
private _keyLight!: THREE.DirectionalLight;
|
|
591
|
-
private _fillLight!: THREE.DirectionalLight;
|
|
592
|
-
/** 环境贴图的 PMREM 渲染目标(setEnvironment 建的,unmount 时释放) */
|
|
593
|
-
private _envRT: THREE.WebGLRenderTarget | null = null;
|
|
594
|
-
private readonly _lightOptions: Required<Omit<LightOptions, 'mainIntensity' | 'mainColor'>> = {
|
|
595
|
-
ambientIntensity: 0.4,
|
|
596
|
-
ambientColor: 0xffffff,
|
|
597
|
-
hemisphereIntensity: 0.8,
|
|
598
|
-
hemisphereColor: 0xffffff,
|
|
599
|
-
hemisphereGroundColor: 0xb6c0cb,
|
|
600
|
-
keyLightIntensity: 1.0,
|
|
601
|
-
keyLightColor: 0xffffff,
|
|
602
|
-
fillLightIntensity: 0.35,
|
|
603
|
-
fillLightColor: 0xffffff,
|
|
604
|
-
mode: 'headlamp',
|
|
605
|
-
azimuth: 150,
|
|
606
|
-
altitude: 50,
|
|
607
|
-
};
|
|
323
|
+
/** 光源 / 色调映射 / 环境贴图(见 viewer/LightRig.ts) */
|
|
324
|
+
private readonly _lights: LightRig;
|
|
608
325
|
|
|
609
326
|
private readonly _datasetQueue: S3MDatasetOptions[];
|
|
610
327
|
|
|
@@ -636,13 +353,6 @@ export class S3MViewer {
|
|
|
636
353
|
this._lodPixelRatioCap = options.lodPixelRatioCap ?? Infinity;
|
|
637
354
|
setTextureSizeCap(options.maxTextureSize ?? Infinity);
|
|
638
355
|
setMaterialQuality(options.materialQuality ?? 'pbr');
|
|
639
|
-
if (options.lightingPreset && options.lightingPreset !== 'default') {
|
|
640
|
-
Object.assign(this._lightOptions, resolveLightingPreset(options.lightingPreset));
|
|
641
|
-
this._applyColorPipeline(options.lightingPreset);
|
|
642
|
-
}
|
|
643
|
-
if (options.light) {
|
|
644
|
-
Object.assign(this._lightOptions, options.light);
|
|
645
|
-
}
|
|
646
356
|
if (options.totalMaxTiles) {
|
|
647
357
|
this._datasetManager.totalMaxTiles = options.totalMaxTiles;
|
|
648
358
|
}
|
|
@@ -659,13 +369,12 @@ export class S3MViewer {
|
|
|
659
369
|
if (!this.scene) {
|
|
660
370
|
this.scene = new THREE.Scene();
|
|
661
371
|
this.scene.add(this.groundManager.group);
|
|
662
|
-
this._addLights();
|
|
663
372
|
}
|
|
664
373
|
this._featureStyleManager = new FeatureStyleManager(
|
|
665
374
|
() => this.style.floorHeight,
|
|
666
375
|
() => this._requestRepaint(),
|
|
667
376
|
() => {
|
|
668
|
-
if (this.
|
|
377
|
+
if (this._sel.selection) this._sel.syncStyle();
|
|
669
378
|
},
|
|
670
379
|
() => this.getAllMeshes(),
|
|
671
380
|
(mesh) => this._applyOverridesToMesh(mesh),
|
|
@@ -674,8 +383,56 @@ export class S3MViewer {
|
|
|
674
383
|
);
|
|
675
384
|
|
|
676
385
|
this.uniforms = createSharedUniforms();
|
|
386
|
+
// 光源状态在这里就位:LightRig 构造时会按 lightingPreset 切模块级颜色管线,
|
|
387
|
+
// 挂进场景要等 scene 建好(构造函数上面已经建了)。
|
|
388
|
+
this._lights = new LightRig({
|
|
389
|
+
scene: () => this.scene,
|
|
390
|
+
renderer: () => this.renderer,
|
|
391
|
+
uniforms: this.uniforms,
|
|
392
|
+
options: this.options,
|
|
393
|
+
getBearing: () => this._getBearing?.(),
|
|
394
|
+
// 走公共 triggerRepaint 而不是 _requestRepaint:宿主 / 单测会覆盖前者来观察重绘,
|
|
395
|
+
// 绕过它会让「有没有触发重绘」这件事失去观测点
|
|
396
|
+
repaint: () => this.triggerRepaint(),
|
|
397
|
+
});
|
|
398
|
+
this._lights.addToScene();
|
|
399
|
+
this._heights = new HeightIndex(this.uniforms);
|
|
400
|
+
const self = this;
|
|
401
|
+
this._picks = new PickService({
|
|
402
|
+
get picker() {
|
|
403
|
+
return self.picker;
|
|
404
|
+
},
|
|
405
|
+
get camera() {
|
|
406
|
+
return self.camera;
|
|
407
|
+
},
|
|
408
|
+
get scene() {
|
|
409
|
+
return self.scene;
|
|
410
|
+
},
|
|
411
|
+
get canvas() {
|
|
412
|
+
return self.canvas;
|
|
413
|
+
},
|
|
414
|
+
get style() {
|
|
415
|
+
return self.style;
|
|
416
|
+
},
|
|
417
|
+
get sceneAnchor() {
|
|
418
|
+
return self.sceneAnchor;
|
|
419
|
+
},
|
|
420
|
+
get datasets() {
|
|
421
|
+
return self.datasets;
|
|
422
|
+
},
|
|
423
|
+
get featureStyleManager() {
|
|
424
|
+
return self._featureStyleManager;
|
|
425
|
+
},
|
|
426
|
+
get smidMeshes() {
|
|
427
|
+
return self._smidMeshes;
|
|
428
|
+
},
|
|
429
|
+
lngLatFromWorld: (x, y) => self._lngLatFromWorld(x, y),
|
|
430
|
+
gpuPathPoseAt: (smid) => self._gpuPathPoseAt(smid),
|
|
431
|
+
isFeatureHidden: (smid, batchId) => self._isFeatureHidden(smid, batchId),
|
|
432
|
+
});
|
|
677
433
|
// 纹理解码优先派给共享解析 Worker(崩溃自动回主线程,见 decodeTextureWithWorker)
|
|
678
|
-
setTextureOffThreadDecode(decodeTextureWithWorker);
|
|
434
|
+
setTextureOffThreadDecode(decodeTextureWithWorker);
|
|
435
|
+
this.style = { ...DEFAULT_STYLE, ...(options.style ?? {}) };
|
|
679
436
|
this._styleTarget = { ...this.style };
|
|
680
437
|
this._opacityTransparent = (this.style.opacity ?? 1) < 0.999;
|
|
681
438
|
this._applyStyleUniforms();
|
|
@@ -685,29 +442,35 @@ export class S3MViewer {
|
|
|
685
442
|
this.animator.onActive = () => this._requestRepaint();
|
|
686
443
|
this.outlineMaterial = createOutlineMaterial(this.uniforms);
|
|
687
444
|
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
this.
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
445
|
+
// 棱线装饰:内部持有共享 uniform 与全局材质,公共字段从它取出来保持 API 不变
|
|
446
|
+
this._edges = new EdgeDecorator({
|
|
447
|
+
uniforms: this.uniforms,
|
|
448
|
+
style: this.style,
|
|
449
|
+
styleTarget: this._styleTarget,
|
|
450
|
+
animator: this.animator,
|
|
451
|
+
pickables: this._picks.pickables,
|
|
452
|
+
scene: this.scene,
|
|
453
|
+
renderer: this.renderer,
|
|
454
|
+
camera: this.camera,
|
|
455
|
+
authenticate: this._authenticate,
|
|
456
|
+
requestRepaint: () => this.triggerRepaint(),
|
|
457
|
+
});
|
|
458
|
+
this.lineShared = this._edges.lineShared;
|
|
459
|
+
this.edgeMaterial = this._edges.edgeMaterial;
|
|
460
|
+
this.selectionEdgeMaterial = this._edges.selectionEdgeMaterial;
|
|
461
|
+
this._sel = new SelectionDecorator({
|
|
462
|
+
uniforms: this.uniforms,
|
|
463
|
+
style: this.style,
|
|
464
|
+
outlineMaterial: this.outlineMaterial,
|
|
465
|
+
selectionEdgeMaterial: this.selectionEdgeMaterial,
|
|
466
|
+
featureStyleManager: this._featureStyleManager,
|
|
467
|
+
smidMeshes: this._smidMeshes,
|
|
468
|
+
pointClouds: this._pointClouds,
|
|
469
|
+
applyPointCloudStyle: (pc, selected) => this._applyPointCloudStyle(pc, selected),
|
|
470
|
+
onPick: (r) => this.onPick(r),
|
|
471
|
+
requestRepaint: () => this.triggerRepaint(),
|
|
472
|
+
createSelectionEdgeMaterial: (mesh) => this._edges.createSelectionEdgeMaterial(mesh),
|
|
473
|
+
});
|
|
711
474
|
|
|
712
475
|
// 关键:style.edges 是个开关,但构造函数从没据此启用过棱线,导致
|
|
713
476
|
// `style: { edges: true }` 形同虚设、_edgesEnabled 永远是 false。
|
|
@@ -804,8 +567,7 @@ export class S3MViewer {
|
|
|
804
567
|
this.picker?.dispose();
|
|
805
568
|
this.picker = new Picker(this.renderer);
|
|
806
569
|
// GPU 描边的 RT / 材质绑定在旧上下文上,整体丢弃,下一帧按需重建
|
|
807
|
-
this.
|
|
808
|
-
this._gpuEdge = null;
|
|
570
|
+
this._edges.disposeGpuEdge();
|
|
809
571
|
// 共享 uniform / 材质引用不变,three 重新编译着色器时会自然接管;
|
|
810
572
|
// 这里只确保下一帧尽快驱动一次渲染。
|
|
811
573
|
this._requestRepaint();
|
|
@@ -818,169 +580,83 @@ export class S3MViewer {
|
|
|
818
580
|
this.groundManager.dispose();
|
|
819
581
|
this.picker?.dispose();
|
|
820
582
|
this.picker = null;
|
|
821
|
-
this.
|
|
822
|
-
this._envRT = null;
|
|
583
|
+
this._lights.dispose();
|
|
823
584
|
this.outlineMaterial.dispose();
|
|
824
|
-
this.
|
|
825
|
-
this._gpuEdge = null;
|
|
585
|
+
this._edges.disposeGpuEdge();
|
|
826
586
|
this.edgeMaterial.dispose();
|
|
827
587
|
this.selectionEdgeMaterial.dispose();
|
|
828
588
|
this.scene?.clear();
|
|
829
|
-
this.
|
|
830
|
-
this._outlineMeshes = [];
|
|
831
|
-
this._selectionEdgeMeshes = [];
|
|
832
|
-
this._outlineMesh = null;
|
|
833
|
-
this._selectionEdges = null;
|
|
589
|
+
this._sel.reset();
|
|
834
590
|
this._smidMeshes.clear();
|
|
835
|
-
this.
|
|
591
|
+
this._heights.clear();
|
|
836
592
|
// 场景已经清空,还挂着的动画通道(瓦片淡入 / 单体位移)不能再对着死对象写
|
|
837
593
|
this.animator.clear();
|
|
838
594
|
this.animator.unregisterPrefix('tile.');
|
|
839
595
|
this.animator.unregisterPrefix('feature.');
|
|
840
596
|
this._pathSpecs.clear();
|
|
841
|
-
|
|
842
|
-
this._heightMax = -Infinity;
|
|
597
|
+
|
|
843
598
|
this.renderer?.dispose?.();
|
|
844
599
|
this.renderer = null;
|
|
845
600
|
this.canvas = null;
|
|
846
601
|
}
|
|
847
602
|
|
|
848
|
-
|
|
849
|
-
this._ambientLight = new THREE.AmbientLight(this._lightOptions.ambientColor as any, this._lightOptions.ambientIntensity);
|
|
850
|
-
this._hemiLight = new THREE.HemisphereLight(
|
|
851
|
-
this._lightOptions.hemisphereColor as any,
|
|
852
|
-
this._lightOptions.hemisphereGroundColor as any,
|
|
853
|
-
this._lightOptions.hemisphereIntensity
|
|
854
|
-
);
|
|
855
|
-
this._keyLight = new THREE.DirectionalLight(this._lightOptions.keyLightColor as any, this._lightOptions.keyLightIntensity);
|
|
856
|
-
this._fillLight = new THREE.DirectionalLight(this._lightOptions.fillLightColor as any, this._lightOptions.fillLightIntensity);
|
|
857
|
-
this._updateLights();
|
|
858
|
-
}
|
|
603
|
+
/* ---------------------------------------------------------------- 光照 */
|
|
859
604
|
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
605
|
+
/**
|
|
606
|
+
* @internal 光源实例的转发 —— `lighting.test.ts` 直接读这几个字段。
|
|
607
|
+
* 实现全在 viewer/LightRig.ts。
|
|
608
|
+
*/
|
|
609
|
+
get _ambientLight(): THREE.AmbientLight | null {
|
|
610
|
+
return this._lights.ambient;
|
|
611
|
+
}
|
|
612
|
+
get _hemiLight(): THREE.HemisphereLight | null {
|
|
613
|
+
return this._lights.hemi;
|
|
614
|
+
}
|
|
615
|
+
get _keyLight(): THREE.DirectionalLight | null {
|
|
616
|
+
return this._lights.key;
|
|
617
|
+
}
|
|
618
|
+
get _fillLight(): THREE.DirectionalLight | null {
|
|
619
|
+
return this._lights.fill;
|
|
864
620
|
}
|
|
865
621
|
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
this._ambientLight.color.set(this._lightOptions.ambientColor as any);
|
|
870
|
-
}
|
|
871
|
-
if (this._hemiLight) {
|
|
872
|
-
this._hemiLight.intensity = this._lightOptions.hemisphereIntensity;
|
|
873
|
-
this._hemiLight.color.set(this._lightOptions.hemisphereColor as any);
|
|
874
|
-
this._hemiLight.groundColor.set(this._lightOptions.hemisphereGroundColor as any);
|
|
875
|
-
}
|
|
876
|
-
if (this._keyLight) {
|
|
877
|
-
this._keyLight.intensity = this._lightOptions.keyLightIntensity;
|
|
878
|
-
this._keyLight.color.set(this._lightOptions.keyLightColor as any);
|
|
879
|
-
}
|
|
880
|
-
if (this._fillLight) {
|
|
881
|
-
this._fillLight.intensity = this._lightOptions.fillLightIntensity;
|
|
882
|
-
this._fillLight.color.set(this._lightOptions.fillLightColor as any);
|
|
883
|
-
}
|
|
622
|
+
/** @internal 光照位置按当前 bearing 重算,见 viewer/LightRig.ts */
|
|
623
|
+
_updateLights(): void {
|
|
624
|
+
this._lights.update();
|
|
884
625
|
}
|
|
885
626
|
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
* 之后再调 `setLight({...})` 仍然可以逐项微调。
|
|
889
|
-
*/
|
|
890
|
-
/**
|
|
891
|
-
* supermap 完整版 = 光照强度 + 颜色管线一起切:
|
|
892
|
-
* direct 管线下纹理不做 sRGB 解码、片元按官方常数 Lambert 直乘(见 lightingPreset.ts)。
|
|
893
|
-
* 两个都是模块级全局态,只影响之后创建的材质 / 纹理 —— 已加载瓦片要重载才换。
|
|
894
|
-
*/
|
|
895
|
-
private _applyColorPipeline(preset: LightingPreset): void {
|
|
896
|
-
const direct = preset === 'supermap';
|
|
897
|
-
setColorPipeline(direct ? 'direct' : 'linear');
|
|
898
|
-
setSrgbTextureDecode(!direct);
|
|
899
|
-
// 官方不开各向异性:远处直接采高 mip 平均成纯色("灰色滤镜"的一部分)。
|
|
900
|
-
// 我们默认 Infinity(倾斜摄影立面清晰度优先),supermap 模式必须收回。
|
|
901
|
-
setAnisotropyCap(direct ? 1 : Infinity);
|
|
627
|
+
private _addLights(): void {
|
|
628
|
+
this._lights.addToScene();
|
|
902
629
|
}
|
|
903
630
|
|
|
904
631
|
setLightingPreset(preset: LightingPreset): void {
|
|
905
|
-
|
|
906
|
-
this._applyColorPipeline(preset);
|
|
907
|
-
this.options.lightingPreset = preset;
|
|
908
|
-
this._applyLightOptions();
|
|
909
|
-
this._updateLights();
|
|
910
|
-
this._needsRepaint = true;
|
|
911
|
-
this.triggerRepaint();
|
|
632
|
+
this._lights.setLightingPreset(preset);
|
|
912
633
|
}
|
|
913
634
|
|
|
914
|
-
/**
|
|
915
|
-
* 动态设置光照配置
|
|
916
|
-
*/
|
|
635
|
+
/** 动态设置光照配置 */
|
|
917
636
|
setLight(options: Partial<LightOptions>): void {
|
|
918
|
-
|
|
919
|
-
if (normalized.mainColor !== undefined && normalized.keyLightColor === undefined) {
|
|
920
|
-
normalized.keyLightColor = normalized.mainColor;
|
|
921
|
-
}
|
|
922
|
-
if (normalized.mainIntensity !== undefined && normalized.keyLightIntensity === undefined) {
|
|
923
|
-
normalized.keyLightIntensity = normalized.mainIntensity;
|
|
924
|
-
}
|
|
925
|
-
Object.assign(this._lightOptions, normalized);
|
|
926
|
-
this._applyLightOptions();
|
|
927
|
-
this._updateLights();
|
|
928
|
-
this._needsRepaint = true;
|
|
929
|
-
this.triggerRepaint();
|
|
637
|
+
this._lights.setLight(options);
|
|
930
638
|
}
|
|
931
639
|
|
|
932
|
-
/**
|
|
933
|
-
* 获取当前光照配置快照
|
|
934
|
-
*/
|
|
640
|
+
/** 获取当前光照配置快照 */
|
|
935
641
|
getLight(): LightOptions {
|
|
936
|
-
return
|
|
642
|
+
return this._lights.getLight();
|
|
937
643
|
}
|
|
938
644
|
|
|
939
|
-
/**
|
|
940
|
-
* 动态设置色调映射模式与曝光度
|
|
941
|
-
*/
|
|
645
|
+
/** 动态设置色调映射模式与曝光度 */
|
|
942
646
|
setToneMapping(toneMapping: THREE.ToneMapping, exposure = 1.0): void {
|
|
943
|
-
this.
|
|
944
|
-
this.options.toneMappingExposure = exposure;
|
|
945
|
-
if (this.renderer) {
|
|
946
|
-
this.renderer.toneMapping = toneMapping;
|
|
947
|
-
this.renderer.toneMappingExposure = exposure;
|
|
948
|
-
this._needsRepaint = true;
|
|
949
|
-
this.triggerRepaint();
|
|
950
|
-
}
|
|
647
|
+
this._lights.setToneMapping(toneMapping, exposure);
|
|
951
648
|
}
|
|
952
649
|
|
|
953
|
-
/**
|
|
954
|
-
* 获取当前色调映射模式与曝光度
|
|
955
|
-
*/
|
|
650
|
+
/** 获取当前色调映射模式与曝光度 */
|
|
956
651
|
getToneMapping(): { toneMapping: THREE.ToneMapping; toneMappingExposure: number } {
|
|
957
|
-
return
|
|
958
|
-
toneMapping: this.renderer ? this.renderer.toneMapping : (this.options.toneMapping ?? THREE.NoToneMapping),
|
|
959
|
-
toneMappingExposure: this.renderer ? this.renderer.toneMappingExposure : (this.options.toneMappingExposure ?? 1.0),
|
|
960
|
-
};
|
|
652
|
+
return this._lights.getToneMapping();
|
|
961
653
|
}
|
|
962
654
|
|
|
963
655
|
/**
|
|
964
|
-
*
|
|
965
|
-
* - 'headlamp': 随地图方位角 bearing 旋转,主光始终在镜头上方,受光面面向镜头;
|
|
966
|
-
* - 'fixed': 固定世界方位角(如模拟恒定日光)。
|
|
656
|
+
* 挂 / 摘环境贴图。见 viewer/LightRig.ts(PMREM 预滤波 + 旧 RT 释放)。
|
|
967
657
|
*/
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
const bearing = this._lightOptions.mode === 'fixed' ? 0 : (((this._getBearing?.() ?? 0) * Math.PI) / 180);
|
|
971
|
-
const place = (light: THREE.DirectionalLight | null, azOffsetDeg: number, elevDeg: number): void => {
|
|
972
|
-
if (!light) return;
|
|
973
|
-
const az = bearing + (azOffsetDeg * Math.PI) / 180;
|
|
974
|
-
const el = (elevDeg * Math.PI) / 180;
|
|
975
|
-
const c = Math.cos(el);
|
|
976
|
-
light.position.set(Math.sin(az) * c, Math.cos(az) * c, Math.sin(el)).multiplyScalar(1000);
|
|
977
|
-
};
|
|
978
|
-
place(this._keyLight, this._lightOptions.azimuth, this._lightOptions.altitude);
|
|
979
|
-
place(this._fillLight, this._lightOptions.azimuth + 65, Math.max(10, this._lightOptions.altitude - 20));
|
|
980
|
-
// direct 管线的官方 Lambert 用主光方向(共享 uniform,所有材质同步生效)
|
|
981
|
-
if (this.uniforms?.uSunDirection && this._keyLight) {
|
|
982
|
-
(this.uniforms.uSunDirection.value as THREE.Vector3).copy(this._keyLight.position).normalize();
|
|
983
|
-
}
|
|
658
|
+
setEnvironment(source: string | THREE.Texture | null, intensity = 1): Promise<void> {
|
|
659
|
+
return this._lights.setEnvironment(source, intensity);
|
|
984
660
|
}
|
|
985
661
|
|
|
986
662
|
/* ---------------------------------------------------------------- 数据集 */
|
|
@@ -1013,13 +689,11 @@ export class S3MViewer {
|
|
|
1013
689
|
onTile: (node, e) => {
|
|
1014
690
|
this._registerTile(node, e);
|
|
1015
691
|
this.onStats(this.stats);
|
|
1016
|
-
this._needsRepaint = true;
|
|
1017
692
|
this._requestRepaint();
|
|
1018
693
|
},
|
|
1019
694
|
onTileRemoved: (node, e) => {
|
|
1020
695
|
this._unregisterTile(node, e);
|
|
1021
696
|
this.onStats(this.stats);
|
|
1022
|
-
this._needsRepaint = true;
|
|
1023
697
|
this._requestRepaint();
|
|
1024
698
|
},
|
|
1025
699
|
onStats: () => {
|
|
@@ -1032,7 +706,7 @@ export class S3MViewer {
|
|
|
1032
706
|
},
|
|
1033
707
|
onReady: (e) => {
|
|
1034
708
|
this.onReady(e);
|
|
1035
|
-
this.
|
|
709
|
+
this._heights.refreshAll();
|
|
1036
710
|
if (!this.groundManager.excavationBounds) {
|
|
1037
711
|
this.groundManager.autoFitExcavation(this.datasets.values());
|
|
1038
712
|
}
|
|
@@ -1043,7 +717,8 @@ export class S3MViewer {
|
|
|
1043
717
|
},
|
|
1044
718
|
this.options.maxTriangles ?? Infinity,
|
|
1045
719
|
this.options.maxImageCache ?? 128,
|
|
1046
|
-
this._authenticate
|
|
720
|
+
this._authenticate,
|
|
721
|
+
this.options.maxMemory ?? Infinity
|
|
1047
722
|
);
|
|
1048
723
|
return entry;
|
|
1049
724
|
}
|
|
@@ -1058,7 +733,7 @@ export class S3MViewer {
|
|
|
1058
733
|
/** 当地图 CRS 投影切换或宿主 map 挂载后,重新计算场景锚点与各数据集 offset */
|
|
1059
734
|
recomputeAnchors(): void {
|
|
1060
735
|
this._datasetManager.recomputeAnchors(this.geo);
|
|
1061
|
-
this.
|
|
736
|
+
this._heights.refreshAll();
|
|
1062
737
|
this._requestRepaint();
|
|
1063
738
|
}
|
|
1064
739
|
|
|
@@ -1092,7 +767,7 @@ export class S3MViewer {
|
|
|
1092
767
|
}
|
|
1093
768
|
this._datasetManager.setDatasetHeight(id, h, this.geo);
|
|
1094
769
|
this._syncGroundHeight();
|
|
1095
|
-
this.
|
|
770
|
+
this._heights.refreshAll();
|
|
1096
771
|
this._requestRepaint();
|
|
1097
772
|
}
|
|
1098
773
|
|
|
@@ -1124,7 +799,7 @@ export class S3MViewer {
|
|
|
1124
799
|
a = align ?? 'origin';
|
|
1125
800
|
}
|
|
1126
801
|
this._datasetManager.setDatasetAlign(id, a, this.geo);
|
|
1127
|
-
this.
|
|
802
|
+
this._heights.refreshAll();
|
|
1128
803
|
this._requestRepaint();
|
|
1129
804
|
}
|
|
1130
805
|
|
|
@@ -1719,7 +1394,7 @@ export class S3MViewer {
|
|
|
1719
1394
|
}
|
|
1720
1395
|
|
|
1721
1396
|
auditBatches(): BatchAudit {
|
|
1722
|
-
const res = this._featureStyleManager.auditBatches(this.
|
|
1397
|
+
const res = this._featureStyleManager.auditBatches(this._picks.pickables.values());
|
|
1723
1398
|
res.smids = this._smidMeshes.size;
|
|
1724
1399
|
return res;
|
|
1725
1400
|
}
|
|
@@ -1832,7 +1507,7 @@ export class S3MViewer {
|
|
|
1832
1507
|
|
|
1833
1508
|
getAllMeshes(): THREE.Mesh[] {
|
|
1834
1509
|
const list: THREE.Mesh[] = [];
|
|
1835
|
-
for (const p of this.
|
|
1510
|
+
for (const p of this._picks.pickables.values()) list.push(p.mesh);
|
|
1836
1511
|
return list;
|
|
1837
1512
|
}
|
|
1838
1513
|
|
|
@@ -1845,14 +1520,6 @@ export class S3MViewer {
|
|
|
1845
1520
|
return out;
|
|
1846
1521
|
}
|
|
1847
1522
|
|
|
1848
|
-
private _markOverridesDirty(scope: Iterable<THREE.Mesh> | 'all' = 'all'): void {
|
|
1849
|
-
if (scope === 'all') {
|
|
1850
|
-
this._featureStyleManager.markOverridesDirty(undefined, this.getAllMeshes());
|
|
1851
|
-
} else {
|
|
1852
|
-
this._featureStyleManager.markOverridesDirty(scope);
|
|
1853
|
-
}
|
|
1854
|
-
}
|
|
1855
|
-
|
|
1856
1523
|
flushOverrides(): void {
|
|
1857
1524
|
this._featureStyleManager.flushOverrides();
|
|
1858
1525
|
}
|
|
@@ -1865,18 +1532,18 @@ export class S3MViewer {
|
|
|
1865
1532
|
removeDataset(id: string): void {
|
|
1866
1533
|
const entry = this.datasets.get(id);
|
|
1867
1534
|
if (!entry) return;
|
|
1868
|
-
if (this.
|
|
1535
|
+
if (this._sel.selection?.datasetId === id) this.select(null);
|
|
1869
1536
|
this._datasetManager.removeDataset(id, this.scene!, this.geo, (e) => {
|
|
1870
1537
|
e.root.traverse((o) => {
|
|
1871
1538
|
const mesh = o as THREE.Mesh;
|
|
1872
1539
|
const ud = mesh.userData as MeshUserData;
|
|
1873
1540
|
if (ud.pickId != null) {
|
|
1874
|
-
this.
|
|
1541
|
+
this._picks.pickables.delete(ud.pickId);
|
|
1875
1542
|
this._releasePickId(ud.pickId);
|
|
1876
1543
|
ud.pickId = undefined;
|
|
1877
1544
|
}
|
|
1878
1545
|
this._unindexMeshSmids(mesh);
|
|
1879
|
-
this.
|
|
1546
|
+
this._heights.remove(mesh);
|
|
1880
1547
|
mesh.geometry?.dispose?.();
|
|
1881
1548
|
const styled = styledMaterialOf(mesh);
|
|
1882
1549
|
if (styled) styled.dispose();
|
|
@@ -1884,7 +1551,7 @@ export class S3MViewer {
|
|
|
1884
1551
|
this._featureStyleManager.disposeMesh(mesh);
|
|
1885
1552
|
});
|
|
1886
1553
|
});
|
|
1887
|
-
for (const [pickId, p] of this.
|
|
1554
|
+
for (const [pickId, p] of this._picks.pickables) if (p.datasetId === id) this._picks.pickables.delete(pickId);
|
|
1888
1555
|
this.onStats(this.stats);
|
|
1889
1556
|
this._requestRepaint();
|
|
1890
1557
|
}
|
|
@@ -1896,7 +1563,7 @@ export class S3MViewer {
|
|
|
1896
1563
|
|
|
1897
1564
|
setDatasetVisible(id: string, visible: boolean): void {
|
|
1898
1565
|
this._datasetManager.setDatasetVisible(id, visible);
|
|
1899
|
-
if (!visible && this.
|
|
1566
|
+
if (!visible && this._sel.selection?.datasetId === id) this.select(null);
|
|
1900
1567
|
this._requestRepaint();
|
|
1901
1568
|
}
|
|
1902
1569
|
|
|
@@ -1910,16 +1577,13 @@ export class S3MViewer {
|
|
|
1910
1577
|
// forceCpuEdges:无视数据自带的棱线包,改由 CPU 现算。native 是 1px 的
|
|
1911
1578
|
// LineBasicMaterial,edgeWidth / 外扩 / 单体跟随全都吃不上。
|
|
1912
1579
|
// gpu 画法由 shader 自己描边,native 线段包同样不能留
|
|
1913
|
-
const useNativeEdges =
|
|
1914
|
-
hasNativeEdges && this.style.forceCpuEdges !== true && !this._edgeModes.gpu;
|
|
1580
|
+
const useNativeEdges = this._edges.useNativeEdges(hasNativeEdges);
|
|
1915
1581
|
|
|
1916
1582
|
for (const child of node.group.children) {
|
|
1917
1583
|
// 数据自带的棱线包:只跟着 edges 开关显示,不参与拾取、不参与着色
|
|
1918
1584
|
if ((child as THREE.LineSegments).isLineSegments && (child.userData as MeshUserData).isEdgeLines) {
|
|
1919
|
-
//
|
|
1920
|
-
this.
|
|
1921
|
-
child.visible = useNativeEdges && this._edgesEnabled;
|
|
1922
|
-
this._styleEdgeLine(child as THREE.LineSegments);
|
|
1585
|
+
// 一律登记:forceCpuEdges 之后又关掉时还要能把它们放出来
|
|
1586
|
+
this._edges.registerNativeLine(child as THREE.LineSegments, useNativeEdges && this._edges.enabled);
|
|
1923
1587
|
continue;
|
|
1924
1588
|
}
|
|
1925
1589
|
|
|
@@ -1929,29 +1593,24 @@ export class S3MViewer {
|
|
|
1929
1593
|
if (!mesh.isMesh && !isPoints) continue;
|
|
1930
1594
|
if (isPoints) this._pointClouds.add(child as THREE.Points);
|
|
1931
1595
|
// 新瓦片里的泼溅实例还没按当前视角排过,下一帧要重排
|
|
1932
|
-
if (isPoints) this.
|
|
1596
|
+
if (isPoints) this._splats.markDirty();
|
|
1933
1597
|
const ud = mesh.userData as MeshUserData;
|
|
1934
1598
|
ud.pickId = this._allocPickId();
|
|
1935
1599
|
ud.datasetId = entry.id;
|
|
1936
1600
|
ud.nativeEdges = useNativeEdges;
|
|
1937
1601
|
ud.nativeEdgesRaw = hasNativeEdges;
|
|
1938
|
-
this.
|
|
1602
|
+
this._picks.pickables.set(ud.pickId, {
|
|
1939
1603
|
datasetId: entry.id,
|
|
1940
1604
|
mesh,
|
|
1941
1605
|
tile: node.tile,
|
|
1942
1606
|
skeletonName: ud.skeletonName ?? '',
|
|
1943
1607
|
});
|
|
1944
1608
|
this._indexMeshSmids(mesh);
|
|
1945
|
-
this.
|
|
1609
|
+
this._heights.add(mesh);
|
|
1946
1610
|
// 点云没有「棱」:给一堆孤立点建 EdgesGeometry 又慢又没意义
|
|
1947
1611
|
// 点云没有「棱」:给一堆孤立点建 EdgesGeometry 又慢又没意义。
|
|
1948
1612
|
// 自动描边受三道开关控制:全局 style.edges / 数据集 enableEdges / 地形绿地等关键字跳过。
|
|
1949
|
-
|
|
1950
|
-
// 数据集强制开启(enableEdges: true)时全局画法可能还是关的,退回默认折边线;
|
|
1951
|
-
// 但 gpu 画法本来就不建几何,别在这里多此一举
|
|
1952
|
-
if (this._edgeModes.line || this._edgeModes.hull) this._ensureEdgeDecoration(mesh);
|
|
1953
|
-
else if (!this._edgeModes.gpu) this._ensureEdges(mesh);
|
|
1954
|
-
}
|
|
1613
|
+
this._edges.autoDecorate(mesh, entry, useNativeEdges);
|
|
1955
1614
|
|
|
1956
1615
|
// 新瓦片要继承当前的不透明度与着色方式
|
|
1957
1616
|
if (isPoints) {
|
|
@@ -1978,7 +1637,7 @@ export class S3MViewer {
|
|
|
1978
1637
|
|
|
1979
1638
|
// 选中状态也要跟着继承:LOD 细化 / 瓦片重载会换掉 mesh,
|
|
1980
1639
|
// 不重放就出现「点中的那栋楼一拉近就没高亮了」
|
|
1981
|
-
this.
|
|
1640
|
+
this._sel.replayOnto(mesh);
|
|
1982
1641
|
}
|
|
1983
1642
|
}
|
|
1984
1643
|
|
|
@@ -2030,64 +1689,9 @@ export class S3MViewer {
|
|
|
2030
1689
|
});
|
|
2031
1690
|
}
|
|
2032
1691
|
|
|
2033
|
-
/**
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
* 为什么需要:选中高亮是直接写在各 mesh 材质的 uSelBatch / uSelWhole 上的,
|
|
2037
|
-
* 而 LOD 细化、瓦片重载、淘汰回补都会换掉 mesh —— 不重放的话,用户点中的那栋楼
|
|
2038
|
-
* 一细化就失去高亮(状态还在 _selection.smid 里,只是没人往新 mesh 上刷)。
|
|
2039
|
-
*/
|
|
2040
|
-
/**
|
|
2041
|
-
* 判断一块无 SMID 单体化信息的网格(如倾斜摄影或白模整块)是否空间覆盖当前选中命中点
|
|
2042
|
-
*/
|
|
2043
|
-
private _isMeshCoveringSelection(mesh: THREE.Mesh, sel: PickResult): boolean {
|
|
2044
|
-
if (!sel.point) return false;
|
|
2045
|
-
const geom = mesh.geometry;
|
|
2046
|
-
if (!geom) return false;
|
|
2047
|
-
if (!geom.boundingBox) geom.computeBoundingBox();
|
|
2048
|
-
if (!geom.boundingBox) return false;
|
|
2049
|
-
|
|
2050
|
-
// 将选中的三维场景点转入网格局部坐标系进行包围盒检测
|
|
2051
|
-
const pt = new THREE.Vector3(sel.point[0], sel.point[1], sel.point[2]);
|
|
2052
|
-
mesh.updateWorldMatrix(true, false);
|
|
2053
|
-
const inv = mesh.matrixWorld.clone().invert();
|
|
2054
|
-
const localPt = pt.applyMatrix4(inv);
|
|
2055
|
-
|
|
2056
|
-
// 留出 0.5 米容差,避免刚好处于三角网格边缘缝隙时漏判
|
|
2057
|
-
const box = geom.boundingBox.clone().expandByScalar(0.5);
|
|
2058
|
-
return box.containsPoint(localPt);
|
|
2059
|
-
}
|
|
2060
|
-
|
|
2061
|
-
private _replaySelectionOnto(mesh: THREE.Mesh): void {
|
|
2062
|
-
const sel = this._selection;
|
|
2063
|
-
if (!sel) return;
|
|
2064
|
-
const mat = styledMaterialOf(mesh);
|
|
2065
|
-
if (!mat) return;
|
|
2066
|
-
|
|
2067
|
-
const ud = mesh.userData as MeshUserData;
|
|
2068
|
-
let batchId = -1;
|
|
2069
|
-
if (sel.smid != null) {
|
|
2070
|
-
const pickInfo = ud.geoRef?.pickInfo as Record<string | number, unknown> | undefined;
|
|
2071
|
-
const entry = pickInfo?.[sel.smid] ?? pickInfo?.[String(sel.smid)];
|
|
2072
|
-
batchId = entry ? pickEntryId(entry as any) : -1;
|
|
2073
|
-
}
|
|
2074
|
-
// 拾取命中的那块 mesh 用原始 batchId;同一 smid 散在别的几何里时按各自的区间算
|
|
2075
|
-
if (mesh === sel.mesh) batchId = sel.batchId;
|
|
2076
|
-
// 这块几何里根本没有这个单体 → 不动它(否则会把无关瓦片一起点亮)
|
|
2077
|
-
if (batchId < 0 && mesh !== sel.mesh) {
|
|
2078
|
-
if (sel.smid == null && ud.datasetId === sel.datasetId && this._isMeshCoveringSelection(mesh, sel)) {
|
|
2079
|
-
batchId = -1;
|
|
2080
|
-
} else {
|
|
2081
|
-
return;
|
|
2082
|
-
}
|
|
2083
|
-
}
|
|
2084
|
-
|
|
2085
|
-
mat.userData.uSelBatch.value = batchId;
|
|
2086
|
-
mat.userData.uSelWhole.value = batchId < 0 ? 1 : 0;
|
|
2087
|
-
if (!this._selectedMeshes.includes(mesh)) this._selectedMeshes.push(mesh);
|
|
2088
|
-
if (isPointCloudObject(mesh)) this._applyPointCloudStyle(mesh, true);
|
|
2089
|
-
this.uniforms.uSelMix.value = 1;
|
|
2090
|
-
this.uniforms.uDim.value = this.style.dimOthers ? 1 : 0;
|
|
1692
|
+
/** @internal 把当前选中重放到新登记的 mesh 上,见 viewer/SelectionDecorator.ts */
|
|
1693
|
+
_replaySelectionOnto(mesh: THREE.Mesh): void {
|
|
1694
|
+
this._sel.replayOnto(mesh);
|
|
2091
1695
|
}
|
|
2092
1696
|
|
|
2093
1697
|
/** 瓦片被淘汰:从拾取表和场景里摘掉,并深度彻底释放所有几何体与材质显存 */
|
|
@@ -2095,7 +1699,7 @@ export class S3MViewer {
|
|
|
2095
1699
|
node.group.traverse((child) => {
|
|
2096
1700
|
const obj = child as any;
|
|
2097
1701
|
if ((child as THREE.LineSegments).isLineSegments && obj.userData?.isEdgeLines) {
|
|
2098
|
-
this.
|
|
1702
|
+
this._edges.unregisterNativeLine(child as THREE.LineSegments);
|
|
2099
1703
|
}
|
|
2100
1704
|
if (isPointCloudObject(child)) this._pointClouds.delete(child);
|
|
2101
1705
|
// 淡入还没结束就被淘汰:把通道收掉,否则会对着已释放的材质继续写
|
|
@@ -2104,34 +1708,34 @@ export class S3MViewer {
|
|
|
2104
1708
|
obj.userData.tileFadeKey = undefined;
|
|
2105
1709
|
}
|
|
2106
1710
|
if (obj.userData?.pickId != null) {
|
|
2107
|
-
this.
|
|
1711
|
+
this._picks.pickables.delete(obj.userData.pickId);
|
|
2108
1712
|
this._releasePickId(obj.userData.pickId);
|
|
2109
1713
|
obj.userData.pickId = undefined;
|
|
2110
1714
|
}
|
|
2111
1715
|
if (obj.isMesh) {
|
|
2112
|
-
const wasSelected = this.
|
|
1716
|
+
const wasSelected = this._sel.selectedMeshes.includes(obj);
|
|
2113
1717
|
if (wasSelected) {
|
|
2114
|
-
this.
|
|
1718
|
+
this._sel.selectedMeshes = this._sel.selectedMeshes.filter((m) => m !== obj);
|
|
2115
1719
|
}
|
|
2116
|
-
const currentSel = this.
|
|
1720
|
+
const currentSel = this._sel.selection;
|
|
2117
1721
|
if (currentSel && currentSel.mesh === obj) {
|
|
2118
1722
|
const smid = currentSel.smid;
|
|
2119
1723
|
const carrier = (
|
|
2120
1724
|
smid != null
|
|
2121
1725
|
? [...(this._smidMeshes.get(smid) ?? [])].find((m) => m !== obj && m.parent)
|
|
2122
|
-
: this.
|
|
2123
|
-
this.
|
|
1726
|
+
: this._sel.selectedMeshes.find((m) => m !== obj && m.parent && (m.parent as any).visible) ??
|
|
1727
|
+
this._sel.selectedMeshes.find((m) => m !== obj && m.parent)
|
|
2124
1728
|
) as THREE.Mesh | undefined;
|
|
2125
1729
|
if (carrier) {
|
|
2126
1730
|
// 选中身份交给承载同一 smid 的另一块 mesh(通常是接替显示的子瓦)
|
|
2127
|
-
this.
|
|
2128
|
-
this.
|
|
1731
|
+
this._sel.selection = { ...currentSel, mesh: carrier };
|
|
1732
|
+
this._sel.replayOnto(carrier);
|
|
2129
1733
|
} else {
|
|
2130
1734
|
this.select(null);
|
|
2131
1735
|
}
|
|
2132
1736
|
}
|
|
2133
1737
|
this._unindexMeshSmids(obj);
|
|
2134
|
-
this.
|
|
1738
|
+
this._heights.remove(obj);
|
|
2135
1739
|
this._featureStyleManager.disposeMesh(obj);
|
|
2136
1740
|
const styled = styledMaterialOf(obj);
|
|
2137
1741
|
styled?.dispose?.();
|
|
@@ -2156,20 +1760,6 @@ export class S3MViewer {
|
|
|
2156
1760
|
* 拾取 ID 只有 2 个字节(RG 两个通道),65535 个网格就到头了,
|
|
2157
1761
|
* 所以必须回收:瓦片淘汰 / 数据集移除时把 id 还回池子。
|
|
2158
1762
|
*/
|
|
2159
|
-
private _allocPickId(): number {
|
|
2160
|
-
const reused = this._freePickIds.pop();
|
|
2161
|
-
if (reused !== undefined) return reused;
|
|
2162
|
-
if (this._pickSeq > MAX_PICK_ID) {
|
|
2163
|
-
for (let id = 0; id <= MAX_PICK_ID; id++) if (!this._pickables.has(id)) return id;
|
|
2164
|
-
throw new Error(`[s3m] 可拾取网格超过 ${MAX_PICK_ID + 1} 个,2 字节 ID 编码不够用`);
|
|
2165
|
-
}
|
|
2166
|
-
return this._pickSeq++;
|
|
2167
|
-
}
|
|
2168
|
-
|
|
2169
|
-
private _releasePickId(id: number): void {
|
|
2170
|
-
this._freePickIds.push(id);
|
|
2171
|
-
}
|
|
2172
|
-
|
|
2173
1763
|
/* ---------------------------------------------------------------- 样式 */
|
|
2174
1764
|
|
|
2175
1765
|
/** 动态设置图层整体透明度 (0.0 ~ 1.0) */
|
|
@@ -2228,52 +1818,6 @@ export class S3MViewer {
|
|
|
2228
1818
|
return { ...this._styleTarget, modeLabel: STYLE_LABELS[this._styleTarget.mode] };
|
|
2229
1819
|
}
|
|
2230
1820
|
|
|
2231
|
-
/**
|
|
2232
|
-
* 挂 / 摘环境贴图。
|
|
2233
|
-
*
|
|
2234
|
-
* 金属度(`setStyle({ metalness })`)**必须**有环境图才看得出质感:
|
|
2235
|
-
* PBR 的金属几乎没有漫反射,画面全靠 `scene.environment` 里的环境反射,
|
|
2236
|
-
* 没有它时 metalness 一高就整片发黑,只剩两盏平行灯打成的小高光点。
|
|
2237
|
-
*
|
|
2238
|
-
* @param source 等距柱状图(.hdr / .jpg / .png,2:1)地址,或已加载的 `THREE.Texture`;
|
|
2239
|
-
* `null` = 关掉环境图
|
|
2240
|
-
* @param intensity 环境光强度(`scene.environmentIntensity`,默认 1)
|
|
2241
|
-
*
|
|
2242
|
-
* 传入 URL 时纹理由内部加载并转成 PMREM(预滤波 mipmap);传 Texture 时不会接管它的生命周期。
|
|
2243
|
-
*/
|
|
2244
|
-
async setEnvironment(
|
|
2245
|
-
source: string | THREE.Texture | null,
|
|
2246
|
-
intensity = 1
|
|
2247
|
-
): Promise<void> {
|
|
2248
|
-
if (!this.renderer || !this.scene) return;
|
|
2249
|
-
this.scene.environmentIntensity = Math.max(0, intensity);
|
|
2250
|
-
|
|
2251
|
-
if (source == null) {
|
|
2252
|
-
this.scene.environment = null;
|
|
2253
|
-
this._envRT?.dispose();
|
|
2254
|
-
this._envRT = null;
|
|
2255
|
-
this._requestRepaint();
|
|
2256
|
-
return;
|
|
2257
|
-
}
|
|
2258
|
-
|
|
2259
|
-
const fromUrl = typeof source === 'string';
|
|
2260
|
-
const tex = fromUrl ? await new THREE.TextureLoader().loadAsync(source) : source;
|
|
2261
|
-
// PMREM 只认等距柱状图;普通贴图也按 2:1 全景处理
|
|
2262
|
-
tex.mapping = THREE.EquirectangularReflectionMapping;
|
|
2263
|
-
|
|
2264
|
-
const pmrem = new THREE.PMREMGenerator(this.renderer);
|
|
2265
|
-
pmrem.compileEquirectangularShader();
|
|
2266
|
-
const rt = pmrem.fromEquirectangular(tex);
|
|
2267
|
-
pmrem.dispose();
|
|
2268
|
-
if (fromUrl) tex.dispose();
|
|
2269
|
-
|
|
2270
|
-
this.scene.environment = rt.texture;
|
|
2271
|
-
// 上一张(同一个 viewer 反复换)要释放,否则显存里会一直堆
|
|
2272
|
-
this._envRT?.dispose();
|
|
2273
|
-
this._envRT = rt;
|
|
2274
|
-
this._requestRepaint();
|
|
2275
|
-
}
|
|
2276
|
-
|
|
2277
1821
|
/** 把某个样式键的取值转成动画通道能吃的形状(标量 or 线性 RGB 三元组) */
|
|
2278
1822
|
private _animTargetOf(key: string, patch: Partial<S3MStyle>): number | number[] {
|
|
2279
1823
|
const value = (patch as Record<string, unknown>)[key];
|
|
@@ -2335,7 +1879,7 @@ export class S3MViewer {
|
|
|
2335
1879
|
},
|
|
2336
1880
|
write: (v) => {
|
|
2337
1881
|
if (this.uniforms.uEdgeFade) this.uniforms.uEdgeFade.value = v[0];
|
|
2338
|
-
this.
|
|
1882
|
+
this._edges.syncStyle();
|
|
2339
1883
|
},
|
|
2340
1884
|
});
|
|
2341
1885
|
|
|
@@ -2366,7 +1910,7 @@ export class S3MViewer {
|
|
|
2366
1910
|
key === 'edgeOffset' ||
|
|
2367
1911
|
key === 'edgeHullWidth'
|
|
2368
1912
|
) {
|
|
2369
|
-
this.
|
|
1913
|
+
this._edges.refreshLines();
|
|
2370
1914
|
}
|
|
2371
1915
|
}
|
|
2372
1916
|
|
|
@@ -2399,8 +1943,8 @@ export class S3MViewer {
|
|
|
2399
1943
|
// 临界角变了要重建 CPU 棱线(EdgesGeometry 的参数),开 / 关走 setEdges 整体刷新。
|
|
2400
1944
|
// 必须先 _disposeEdges():setEdges 只挑「还没有棱线」的网格来建,
|
|
2401
1945
|
// 不先拆掉的话已经建好的会被全部跳过,临界角改了等于没改。
|
|
2402
|
-
if (('edgeThreshold' in patch || 'forceCpuEdges' in patch) && this.
|
|
2403
|
-
this.
|
|
1946
|
+
if (('edgeThreshold' in patch || 'forceCpuEdges' in patch) && this._edges.enabled) {
|
|
1947
|
+
this._edges.disposeEdgeGeometry();
|
|
2404
1948
|
this.setEdges(this.style.edges);
|
|
2405
1949
|
} else if (
|
|
2406
1950
|
'edgeColor' in patch ||
|
|
@@ -2411,12 +1955,12 @@ export class S3MViewer {
|
|
|
2411
1955
|
'edgeHullWidth' in patch
|
|
2412
1956
|
) {
|
|
2413
1957
|
// 颜色 / 粗细 / 外推距离 / 外扩厚度都只是 uniform,拖着滑块逐帧改也不重建几何
|
|
2414
|
-
this.
|
|
1958
|
+
this._edges.refreshLines();
|
|
2415
1959
|
}
|
|
2416
|
-
if (this.
|
|
1960
|
+
if (this._sel.selection) {
|
|
2417
1961
|
// 换描边画法要重建几何;颜色 / 粗细 / 拔高只是 uniform,拖着滑块也不卡
|
|
2418
|
-
if ('outline' in patch || 'outlineStyle' in patch) this.select(this.
|
|
2419
|
-
else this.
|
|
1962
|
+
if ('outline' in patch || 'outlineStyle' in patch) this.select(this._sel.selection, { silent: true });
|
|
1963
|
+
else this._sel.syncStyle();
|
|
2420
1964
|
}
|
|
2421
1965
|
}
|
|
2422
1966
|
|
|
@@ -2492,7 +2036,7 @@ export class S3MViewer {
|
|
|
2492
2036
|
this._opacityTransparent = transparent;
|
|
2493
2037
|
// 关键:若 depthWrite 开启(默认开启),即使半透明也保持深度写入,完美遮挡背面棱线,防止背面线条透出
|
|
2494
2038
|
const keepDepth = this.style.depthWrite !== false;
|
|
2495
|
-
for (const p of this.
|
|
2039
|
+
for (const p of this._picks.pickables.values()) {
|
|
2496
2040
|
if (isPointCloudObject(p.mesh)) continue;
|
|
2497
2041
|
const m = styledMaterialOf(p.mesh);
|
|
2498
2042
|
if (!m) continue;
|
|
@@ -2508,12 +2052,12 @@ export class S3MViewer {
|
|
|
2508
2052
|
// 点云不走 StyleMaterial,自己有一套 uOpacity,逐帧刷的只有这几个对象
|
|
2509
2053
|
for (const pc of this._pointClouds) this._applyPointCloudStyle(pc);
|
|
2510
2054
|
|
|
2511
|
-
this.
|
|
2055
|
+
this._edges.syncStyle();
|
|
2512
2056
|
this._requestRepaint();
|
|
2513
2057
|
}
|
|
2514
2058
|
|
|
2515
2059
|
/** 单个点云对象同步当前样式(着色模式 + 不透明度 + 选中高亮) */
|
|
2516
|
-
private _applyPointCloudStyle(points: THREE.Points, selected = this.
|
|
2060
|
+
private _applyPointCloudStyle(points: THREE.Points, selected = this._sel.selectedMeshes.includes(points)): void {
|
|
2517
2061
|
applyPointCloudStyle(points, this.style, this.uniforms, selected);
|
|
2518
2062
|
}
|
|
2519
2063
|
|
|
@@ -2528,7 +2072,7 @@ export class S3MViewer {
|
|
|
2528
2072
|
// shading 现在是共享 uniform(uFlatShading)驱动的运行期分支,不再是
|
|
2529
2073
|
// flatShading define:翻一个 float 就完成切换,零重编译、零卡顿。
|
|
2530
2074
|
const target = this.style.shading !== 'smooth' ? 1 : 0;
|
|
2531
|
-
for (const p of this.
|
|
2075
|
+
for (const p of this._picks.pickables.values()) {
|
|
2532
2076
|
const m = styledMaterialOf(p.mesh);
|
|
2533
2077
|
const u = m?.userData.uFlatShading;
|
|
2534
2078
|
if (!u || u.value === target) continue;
|
|
@@ -2568,7 +2112,7 @@ export class S3MViewer {
|
|
|
2568
2112
|
'uBatchTexActive',
|
|
2569
2113
|
'uTileFade',
|
|
2570
2114
|
] as const;
|
|
2571
|
-
for (const p of this.
|
|
2115
|
+
for (const p of this._picks.pickables.values()) {
|
|
2572
2116
|
const mesh = p.mesh;
|
|
2573
2117
|
const raw = mesh.material;
|
|
2574
2118
|
if (Array.isArray(raw)) continue; // styled 材质恒为单材质;数组材质(点云等)不归这里管
|
|
@@ -2603,7 +2147,7 @@ export class S3MViewer {
|
|
|
2603
2147
|
|
|
2604
2148
|
// 场上还没有材质就没必要预热:program 会在首个材质出现时编译
|
|
2605
2149
|
let hasMaterial = false;
|
|
2606
|
-
for (const p of this.
|
|
2150
|
+
for (const p of this._picks.pickables.values()) {
|
|
2607
2151
|
if (styledMaterialOf(p.mesh)) {
|
|
2608
2152
|
hasMaterial = true;
|
|
2609
2153
|
break;
|
|
@@ -2631,60 +2175,14 @@ export class S3MViewer {
|
|
|
2631
2175
|
* 加载 n 块瓦片就是 O(n²) 次矩阵乘 + n² 个垃圾对象。现在只在网格进出的记一次,
|
|
2632
2176
|
* 全局值从缓存里滚:只有被移除的那块正好卡在边界上时,才回退到一次 O(n) 重算。
|
|
2633
2177
|
*/
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
this._boxScratch.copy(bb).applyMatrix4(mesh.matrixWorld);
|
|
2638
|
-
const min = this._boxScratch.min.z;
|
|
2639
|
-
const max = this._boxScratch.max.z;
|
|
2640
|
-
if (!Number.isFinite(min) || !Number.isFinite(max)) return;
|
|
2641
|
-
this._meshHeight.set(mesh, { min, max });
|
|
2642
|
-
if (min < this._heightMin) this._heightMin = min;
|
|
2643
|
-
if (max > this._heightMax) this._heightMax = max;
|
|
2644
|
-
this._pushHeightRange();
|
|
2645
|
-
}
|
|
2646
|
-
|
|
2647
|
-
private _removeMeshHeight(mesh: THREE.Mesh): void {
|
|
2648
|
-
const rec = this._meshHeight.get(mesh);
|
|
2649
|
-
if (!rec) return;
|
|
2650
|
-
this._meshHeight.delete(mesh);
|
|
2651
|
-
// 只有正好卡在边界上才需要重算,否则滚动的 min/max 依然成立
|
|
2652
|
-
if (rec.min === this._heightMin || rec.max === this._heightMax) this._recomputeHeightRange();
|
|
2653
|
-
else this._pushHeightRange();
|
|
2654
|
-
}
|
|
2655
|
-
|
|
2656
|
-
/** 数据集整体位移(换高程偏移 / 重新布局)后,缓存的世界高度全部失效 */
|
|
2657
|
-
private _refreshAllHeights(): void {
|
|
2658
|
-
if (this._meshHeight.size === 0) return;
|
|
2659
|
-
const meshes = [...this._meshHeight.keys()];
|
|
2660
|
-
this._meshHeight.clear();
|
|
2661
|
-
this._heightMin = Infinity;
|
|
2662
|
-
this._heightMax = -Infinity;
|
|
2663
|
-
for (const mesh of meshes) this._addMeshHeight(mesh);
|
|
2664
|
-
this._pushHeightRange();
|
|
2665
|
-
}
|
|
2666
|
-
|
|
2667
|
-
private _recomputeHeightRange(): void {
|
|
2668
|
-
let min = Infinity;
|
|
2669
|
-
let max = -Infinity;
|
|
2670
|
-
for (const h of this._meshHeight.values()) {
|
|
2671
|
-
if (h.min < min) min = h.min;
|
|
2672
|
-
if (h.max > max) max = h.max;
|
|
2673
|
-
}
|
|
2674
|
-
this._heightMin = min;
|
|
2675
|
-
this._heightMax = max;
|
|
2676
|
-
this._pushHeightRange();
|
|
2178
|
+
/** @internal 见 viewer/HeightIndex.ts(regressions.test.ts 直接调) */
|
|
2179
|
+
_addMeshHeight(mesh: THREE.Mesh): void {
|
|
2180
|
+
this._heights.add(mesh);
|
|
2677
2181
|
}
|
|
2678
2182
|
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
if (!Number.isFinite(min) || !Number.isFinite(max) || max <= min) {
|
|
2683
|
-
min = 0;
|
|
2684
|
-
max = 100;
|
|
2685
|
-
}
|
|
2686
|
-
this.uniforms.uHeightMin.value = min;
|
|
2687
|
-
this.uniforms.uHeightMax.value = max;
|
|
2183
|
+
/** @internal 见 viewer/HeightIndex.ts */
|
|
2184
|
+
_removeMeshHeight(mesh: THREE.Mesh): void {
|
|
2185
|
+
this._heights.remove(mesh);
|
|
2688
2186
|
}
|
|
2689
2187
|
|
|
2690
2188
|
/** 建 SMID → 网格 反查表,逐单体改样式时才知道该重算哪几块 */
|
|
@@ -2712,1232 +2210,154 @@ export class S3MViewer {
|
|
|
2712
2210
|
}
|
|
2713
2211
|
}
|
|
2714
2212
|
|
|
2715
|
-
|
|
2716
|
-
* 全局棱线开关。
|
|
2717
|
-
*
|
|
2718
|
-
* 取值是 `boolean | EdgeStyle | EdgeStyle[]`(见 resolveEdgeModes):
|
|
2719
|
-
* - `true` / `'line'` 折边线(旧口径)
|
|
2720
|
-
* - `'offset'` 折边线 + 沿边法线外推 `edgeOffset` 米
|
|
2721
|
-
* - `'hull'` 背面外扩轮廓(选中侧 outlineStyle:'hull' 的整场景版)
|
|
2722
|
-
* - `'gpu'` 纯片元描边:零几何、零额外 pass,线宽约 1 像素
|
|
2723
|
-
* - 数组 叠加,例如 `['line', 'hull']`
|
|
2724
|
-
*
|
|
2725
|
-
* 数据自带棱线包(primitiveType 13)时直接用,成本为零;`style.forceCpuEdges`
|
|
2726
|
-
* 可以无视它,改由 CPU 现算 —— 那是 CPU 端的活,几十万面一次性建会卡住主线程
|
|
2727
|
-
* (实测 20 个网格 1.9s),所以按 8ms 一帧切片,边建边出。
|
|
2728
|
-
*
|
|
2729
|
-
* 棱线本身用「屏幕空间等宽线」画(见 EdgeLines.ts):WebGL 的 lineWidth 基本无效,
|
|
2730
|
-
* 只有摊成面向屏幕的四边形,`edgeWidth` 才能真正调粗。
|
|
2731
|
-
*
|
|
2732
|
-
* 带 `transition` 时做「淡入 / 淡出」,**不对几何做插值**(几何没法插值):
|
|
2733
|
-
* 开:立刻置位 + 开始分片建线,同时 `uEdgeFade` 0→1 —— 建好一块亮一块,视觉上是渐显;
|
|
2734
|
-
* 关:`uEdgeFade` 1→0,淡完才 dispose,避免棱线「啪」一下消失。
|
|
2735
|
-
*/
|
|
2736
|
-
setEdges(edges: EdgesOption, transition?: TransitionOptions): void {
|
|
2737
|
-
const wantAnim = !!transition && ((transition.duration ?? 0) > 0 || !!transition.spring);
|
|
2738
|
-
const modes = resolveEdgeModes(edges);
|
|
2739
|
-
// 换画法(line ↔ hull ↔ offset)是结构变更:旧装饰必须拆掉再建,
|
|
2740
|
-
// 否则会出现「切到 hull 了折边线还留着」这种叠影。
|
|
2741
|
-
// 同一画法重复调用(滑块逐帧 setEdges(true))不重建,几何是复用的。
|
|
2742
|
-
const modesChanged = !sameEdgeModes(this._edgeModes, modes);
|
|
2743
|
-
|
|
2744
|
-
this._edgeModes = modes;
|
|
2745
|
-
this._edgesEnabled = modes.enabled;
|
|
2746
|
-
this.style.edges = edges;
|
|
2747
|
-
this._styleTarget.edges = edges;
|
|
2748
|
-
|
|
2749
|
-
// 数据自带的棱线:直接切可见性(forceCpuEdges 时永远收起,改由 CPU 现算)。
|
|
2750
|
-
// 只遍历注册时收集好的那批 LineSegments —— 全数据集 traverse 在几千节点下太贵。
|
|
2751
|
-
// gpu 模式下 shader 自己描边,native 线段包必须收起,否则叠成双线
|
|
2752
|
-
const nativeVisible = modes.enabled && !modes.gpu && this.style.forceCpuEdges !== true;
|
|
2753
|
-
for (const lines of this._nativeEdgeLines) {
|
|
2754
|
-
lines.visible = nativeVisible;
|
|
2755
|
-
this._styleEdgeLine(lines);
|
|
2756
|
-
}
|
|
2757
|
-
this._syncEdgeStyle();
|
|
2758
|
-
|
|
2759
|
-
// 关掉 gpu 画法就把后处理资源还回去(整屏 RT 白占显存没意义)。
|
|
2760
|
-
// 必须放在「整体关闭」的早退之前,否则关棱线时永远轮不到它。
|
|
2761
|
-
if (!modes.gpu && this._gpuEdge) {
|
|
2762
|
-
this._gpuEdge.dispose();
|
|
2763
|
-
this._gpuEdge = null;
|
|
2764
|
-
}
|
|
2765
|
-
|
|
2766
|
-
if (!modes.enabled) {
|
|
2767
|
-
if (wantAnim) {
|
|
2768
|
-
this.animator.to('style.edgeFade', 0, {
|
|
2769
|
-
...transition,
|
|
2770
|
-
onComplete: () => {
|
|
2771
|
-
this._disposeEdges();
|
|
2772
|
-
this._requestRepaint();
|
|
2773
|
-
transition?.onComplete?.();
|
|
2774
|
-
},
|
|
2775
|
-
});
|
|
2776
|
-
} else {
|
|
2777
|
-
this._setEdgeFade(1);
|
|
2778
|
-
this._disposeEdges();
|
|
2779
|
-
}
|
|
2780
|
-
this._requestRepaint();
|
|
2781
|
-
return;
|
|
2782
|
-
}
|
|
2783
|
-
|
|
2784
|
-
if (modesChanged) this._disposeEdges();
|
|
2785
|
-
|
|
2786
|
-
const pending = [...this._pickables.values()]
|
|
2787
|
-
.map((p) => p.mesh)
|
|
2788
|
-
.filter((m) => {
|
|
2789
|
-
const ud = m.userData as MeshUserData;
|
|
2790
|
-
// 点云没有棱可描
|
|
2791
|
-
if (isPointCloudObject(m)) return false;
|
|
2792
|
-
// native 棱线还算不算数,取决于**当前**的 forceCpuEdges(它可以在运行中切换),
|
|
2793
|
-
// 所以这里按注册时记下的原始标记重算,而不是沿用注册那一刻的结论。
|
|
2794
|
-
ud.nativeEdges = ud.nativeEdgesRaw === true && this.style.forceCpuEdges !== true;
|
|
2795
|
-
return !ud.nativeEdges && this._needsEdgeDecoration(ud);
|
|
2796
|
-
});
|
|
2797
|
-
let i = 0;
|
|
2798
|
-
const step = (): void => {
|
|
2799
|
-
const t0 = performance.now();
|
|
2800
|
-
while (i < pending.length && performance.now() - t0 < 8) this._ensureEdgeDecoration(pending[i++]);
|
|
2801
|
-
if (i < pending.length) setTimeout(step, 0);
|
|
2802
|
-
else this._requestRepaint();
|
|
2803
|
-
};
|
|
2804
|
-
step();
|
|
2805
|
-
|
|
2806
|
-
if (wantAnim) {
|
|
2807
|
-
// 从 0 起淡入:新建出来的棱线自动继承当前进度,不会半路冒出一条满亮度的线
|
|
2808
|
-
this._setEdgeFade(0);
|
|
2809
|
-
this.animator.to('style.edgeFade', 1, transition);
|
|
2810
|
-
} else {
|
|
2811
|
-
this._setEdgeFade(1);
|
|
2812
|
-
}
|
|
2813
|
-
}
|
|
2814
|
-
|
|
2815
|
-
/** 直接写棱线淡入乘子(不走动画通道) */
|
|
2816
|
-
private _setEdgeFade(v: number): void {
|
|
2817
|
-
if (this.uniforms.uEdgeFade) this.uniforms.uEdgeFade.value = v;
|
|
2818
|
-
this._syncEdgeStyle();
|
|
2819
|
-
}
|
|
2213
|
+
/* ---------------------------------------------------------------- 棱线 */
|
|
2820
2214
|
|
|
2821
|
-
/**
|
|
2822
|
-
|
|
2823
|
-
|
|
2215
|
+
/** @internal 测试用:棱线是否开启(实现在 EdgeDecorator) */
|
|
2216
|
+
get _edgesEnabled(): boolean {
|
|
2217
|
+
return this._edges.enabled;
|
|
2824
2218
|
}
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
*
|
|
2829
|
-
* 直接把 `source-creases.f32` 这类 Float32Array 缓冲载入为一组全局统一的
|
|
2830
|
-
* LineSegments —— 相比逐 Mesh 现算 EdgesGeometry,性能高一个量级,视觉也最干净。
|
|
2831
|
-
* 坐标按「场景本地坐标」(相对场景锚点的米)处理,与瓦片几何同一坐标系。
|
|
2832
|
-
*
|
|
2833
|
-
* @param url 棱线缓存地址(二进制 Float32 序列:x,y,z,x,y,z,… 成对成段)
|
|
2834
|
-
*/
|
|
2835
|
-
async loadCreases(url: string): Promise<void> {
|
|
2836
|
-
const resp = await authFetch(url, this._authenticate, 'creases');
|
|
2837
|
-
if (!resp.ok) throw new Error(`[s3m] 棱线缓存加载失败: ${resp.status} ${url}`);
|
|
2838
|
-
const buf = await resp.arrayBuffer();
|
|
2839
|
-
const arr = new Float32Array(buf);
|
|
2840
|
-
if (arr.length < 6) return;
|
|
2841
|
-
|
|
2842
|
-
const geo = new THREE.BufferGeometry();
|
|
2843
|
-
geo.setAttribute('position', new THREE.BufferAttribute(arr, 3));
|
|
2844
|
-
const s = this.style;
|
|
2845
|
-
const mat = new THREE.LineBasicMaterial({
|
|
2846
|
-
color: new THREE.Color(s.edgeColor),
|
|
2847
|
-
transparent: true,
|
|
2848
|
-
opacity: Math.min(1, Math.max(0, s.edgeOpacity ?? 0.6)) * clampOpacity(s.opacity),
|
|
2849
|
-
depthWrite: false,
|
|
2850
|
-
});
|
|
2851
|
-
const lines = new THREE.LineSegments(geo, mat);
|
|
2852
|
-
lines.renderOrder = 2;
|
|
2853
|
-
lines.frustumCulled = false;
|
|
2854
|
-
lines.raycast = () => {};
|
|
2855
|
-
|
|
2856
|
-
this.clearCreases();
|
|
2857
|
-
this._creaseLines = lines;
|
|
2858
|
-
this.scene?.add(lines);
|
|
2859
|
-
this._requestRepaint();
|
|
2219
|
+
/** @internal 测试用:当前画法 */
|
|
2220
|
+
get _edgeModes(): EdgeModes {
|
|
2221
|
+
return this._edges.modes;
|
|
2860
2222
|
}
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
if (!this._creaseLines) return;
|
|
2865
|
-
this.scene?.remove(this._creaseLines);
|
|
2866
|
-
this._creaseLines.geometry.dispose();
|
|
2867
|
-
(this._creaseLines.material as THREE.Material)?.dispose?.();
|
|
2868
|
-
this._creaseLines = null;
|
|
2869
|
-
this._requestRepaint();
|
|
2223
|
+
/** @internal 测试用:GPU 描边实例(可替换) */
|
|
2224
|
+
get _gpuEdge(): GpuEdgePass | null {
|
|
2225
|
+
return this._edges.gpuEdge;
|
|
2870
2226
|
}
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
* 让全局棱线材质跟上 style.edgeColor / edgeWidth / edgeOpacity / edgeOffset。
|
|
2874
|
-
*
|
|
2875
|
-
* 全程 O(1):只有一份共享 edgeMaterial(所有逐 mesh 棱线都用它)+ 一份预制缓存
|
|
2876
|
-
* + 一批共享外观对象的轮廓壳,所以棱线淡入淡出只要改一个 uniform,
|
|
2877
|
-
* 新建瓦片自动继承当前进度。
|
|
2878
|
-
*/
|
|
2879
|
-
private _syncEdgeStyle(): void {
|
|
2880
|
-
const s = this.style;
|
|
2881
|
-
const fade = this.uniforms.uEdgeFade?.value ?? 1;
|
|
2882
|
-
const opacity =
|
|
2883
|
-
Math.min(1, Math.max(0, s.edgeOpacity ?? 0.6)) *
|
|
2884
|
-
clampOpacity(s.opacity) *
|
|
2885
|
-
fade;
|
|
2886
|
-
const u = this.edgeMaterial.uniforms;
|
|
2887
|
-
u.uColor.value.set(s.edgeColor);
|
|
2888
|
-
u.uWidth.value = Math.max(0.1, s.edgeWidth ?? 1.5);
|
|
2889
|
-
u.uOpacity.value = opacity;
|
|
2890
|
-
// 外推距离只在 offset 画法下非 0;其它画法把线钉在面上(与改动前一致)
|
|
2891
|
-
u.uOffset.value = this._edgeModes.offset ? Math.max(0, s.edgeOffset ?? 0.15) : 0;
|
|
2892
|
-
// 外扩轮廓壳与折边线共用颜色 / 透明度,厚度是米制的单独一项
|
|
2893
|
-
const h = this._hullStyleUniforms;
|
|
2894
|
-
h.uColor.value.set(s.edgeColor);
|
|
2895
|
-
h.uWidth.value = Math.max(0, s.edgeHullWidth ?? 0.9);
|
|
2896
|
-
h.uOpacity.value = opacity;
|
|
2897
|
-
// GPU 描边(edges: 'gpu')是后处理,参数每帧从 style 现取,这里不用同步 uniform
|
|
2898
|
-
// 预制棱线缓存(LineBasicMaterial)同步颜色 / 不透明度
|
|
2899
|
-
if (this._creaseLines) {
|
|
2900
|
-
const m = this._creaseLines.material as THREE.LineBasicMaterial;
|
|
2901
|
-
m.color.set(s.edgeColor);
|
|
2902
|
-
m.opacity = opacity;
|
|
2903
|
-
}
|
|
2227
|
+
set _gpuEdge(v: GpuEdgePass | null) {
|
|
2228
|
+
this._edges.gpuEdge = v;
|
|
2904
2229
|
}
|
|
2905
2230
|
|
|
2906
|
-
/**
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
if (!mat || !mat.color) return;
|
|
2910
|
-
mat.color.set(this.style.edgeColor);
|
|
2911
|
-
const baseOpacity = this.style.edgeOpacity ?? 0.6;
|
|
2912
|
-
mat.opacity =
|
|
2913
|
-
baseOpacity *
|
|
2914
|
-
clampOpacity(this.style.opacity) *
|
|
2915
|
-
(this.uniforms.uEdgeFade?.value ?? 1);
|
|
2916
|
-
mat.transparent = true;
|
|
2917
|
-
mat.polygonOffset = true;
|
|
2918
|
-
mat.polygonOffsetFactor = -1;
|
|
2919
|
-
mat.polygonOffsetUnits = -1;
|
|
2920
|
-
mat.needsUpdate = true;
|
|
2231
|
+
/** @internal 测试用:给一块网格补折边线 */
|
|
2232
|
+
_ensureEdges(mesh: THREE.Mesh): void {
|
|
2233
|
+
this._edges.ensureEdges(mesh);
|
|
2921
2234
|
}
|
|
2922
2235
|
|
|
2923
|
-
/**
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
* 数据自带的棱线是逐对象的 LineBasicMaterial,改颜色只能一个个写;但原来每次都要
|
|
2927
|
-
* 遍历整个数据集树(几千个节点),颜色动画逐帧跑就是灾难。改成只刷注册时收集好的
|
|
2928
|
-
* 那批 LineSegments,复杂度降到 O(棱线条数)。
|
|
2929
|
-
*/
|
|
2930
|
-
private _refreshEdgeLines(): void {
|
|
2931
|
-
this._syncEdgeStyle();
|
|
2932
|
-
for (const lines of this._nativeEdgeLines) {
|
|
2933
|
-
if (lines.parent) this._styleEdgeLine(lines);
|
|
2934
|
-
}
|
|
2236
|
+
/** @internal 测试用:拆掉一块网格的棱线装饰 */
|
|
2237
|
+
_removeEdges(mesh: THREE.Mesh): void {
|
|
2238
|
+
this._edges.removeEdges(mesh);
|
|
2935
2239
|
}
|
|
2936
2240
|
|
|
2937
|
-
/**
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
'绿地', '道路', '地面', '地形', '植被', '铺装',
|
|
2941
|
-
];
|
|
2942
|
-
|
|
2943
|
-
/** 数据集级别的棱线开关:enableEdges 优先,否则跟随全局 style.edges */
|
|
2944
|
-
private _datasetEdgesEnabled(entry: S3MDatasetEntry): boolean {
|
|
2945
|
-
const opt = entry.options?.enableEdges;
|
|
2946
|
-
if (opt === true) return true;
|
|
2947
|
-
if (opt === false) return false;
|
|
2948
|
-
return this._edgesEnabled;
|
|
2241
|
+
/** 全局棱线开关,实现在 viewer/EdgeDecorator.ts */
|
|
2242
|
+
setEdges(edges: EdgesOption, transition?: TransitionOptions): void {
|
|
2243
|
+
this._edges.setEdges(edges, transition);
|
|
2949
2244
|
}
|
|
2950
2245
|
|
|
2951
|
-
/**
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
return S3MViewer._EDGE_SKIP_KEYWORDS.some((k) => hay.includes(k));
|
|
2246
|
+
/** 挂载预制棱线缓存(Crease Cache),见 viewer/EdgeDecorator.ts */
|
|
2247
|
+
loadCreases(url: string): Promise<void> {
|
|
2248
|
+
return this._edges.loadCreases(url);
|
|
2955
2249
|
}
|
|
2956
2250
|
|
|
2957
|
-
/**
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
* setEdges 的分片建线靠它筛「还没建完的」,注册新瓦片也靠它决定要不要动手。
|
|
2961
|
-
*/
|
|
2962
|
-
private _needsEdgeDecoration(ud: MeshUserData): boolean {
|
|
2963
|
-
if (this._edgeModes.line && !ud.edges) return true;
|
|
2964
|
-
if (this._edgeModes.hull && !ud.edgeHull) return true;
|
|
2965
|
-
return false;
|
|
2251
|
+
/** 移除并释放预制棱线缓存 */
|
|
2252
|
+
clearCreases(): void {
|
|
2253
|
+
this._edges.clearCreases();
|
|
2966
2254
|
}
|
|
2967
2255
|
|
|
2968
|
-
|
|
2969
|
-
private _ensureEdgeDecoration(mesh: THREE.Mesh): void {
|
|
2970
|
-
if (this._edgeModes.line) this._ensureEdges(mesh);
|
|
2971
|
-
if (this._edgeModes.hull) this._ensureHull(mesh);
|
|
2972
|
-
}
|
|
2973
|
-
|
|
2974
|
-
private _ensureEdges(mesh: THREE.Mesh): void {
|
|
2975
|
-
const ud = mesh.userData as MeshUserData;
|
|
2976
|
-
if (ud.edges || ud.nativeEdges || !mesh.geometry) return;
|
|
2977
|
-
// 地形 / 绿地 / 道路等不参与自动描边(即使全局开棱线也要跳过)
|
|
2978
|
-
if (this._shouldSkipAutoEdges(ud, '')) return;
|
|
2979
|
-
|
|
2980
|
-
const posAttr = mesh.geometry.getAttribute('position');
|
|
2981
|
-
const indexCount = mesh.geometry.index?.count ?? (posAttr ? posAttr.count : 0);
|
|
2982
|
-
// 棱线是 CPU 端建的,超大几何直接跳过,避免拖慢加载
|
|
2983
|
-
if (indexCount > 120000) return;
|
|
2984
|
-
|
|
2985
|
-
// 1. 先做顶点容差焊合,让相邻三角形正确共享边索引。
|
|
2986
|
-
// S3M 几何常为「每面独立顶点」,不焊合的话 EdgesGeometry 会把每条三角形边都当成边界,棱线爆炸。
|
|
2987
|
-
// 注意用本地实现而不是 three/examples/jsm —— jsm 子路径导入会在宿主里
|
|
2988
|
-
// 制造第二份 three 实例("Multiple instances of Three.js" 警告的元凶)。
|
|
2989
|
-
// tolerance 取米级模型下的 5mm,足以合并重合顶点又不会误并真实相邻面。
|
|
2990
|
-
const welded = mergeVerticesByPosition(mesh.geometry, 0.005);
|
|
2991
|
-
// 2. 调大临界角(默认 55°,可在 style.edgeThreshold 调),过滤浅坡与曲面细分,只保留真正的棱。
|
|
2992
|
-
const thresholdAngle = this.style.edgeThreshold ?? 55;
|
|
2993
|
-
|
|
2994
|
-
// 'offset' 画法要额外带一条「边法线」才知道往哪推,所以走本地实现(它本来就算好了
|
|
2995
|
-
// 相邻两面法线);其余画法保持 EdgesGeometry 这条老路,几何输出与改动前逐字一致。
|
|
2996
|
-
let segments: Float32Array | null;
|
|
2997
|
-
let normals: Float32Array | undefined;
|
|
2998
|
-
if (this._edgeModes.offset) {
|
|
2999
|
-
const raw: number[] = [];
|
|
3000
|
-
segments = buildFeatureEdgeSegments(welded, null, thresholdAngle, raw);
|
|
3001
|
-
if (raw.length > 0) normals = new Float32Array(raw);
|
|
3002
|
-
} else {
|
|
3003
|
-
const source = new THREE.EdgesGeometry(welded, thresholdAngle);
|
|
3004
|
-
const attr = source.getAttribute('position') as THREE.BufferAttribute | undefined;
|
|
3005
|
-
segments = attr ? (attr.array as Float32Array) : null;
|
|
3006
|
-
source.dispose();
|
|
3007
|
-
}
|
|
3008
|
-
welded.dispose();
|
|
3009
|
-
if (!segments || segments.length < 6) return;
|
|
3010
|
-
|
|
3011
|
-
// 3. 回填每段棱线所属的单体 batchId。
|
|
3012
|
-
// EdgesGeometry 只输出坐标不输出顶点号,只能拿端点坐标去查「位置 → batchId」表。
|
|
3013
|
-
// 没有这张表,单体一动棱线就留在原地——这正是「棱线跟随」的关键一环。
|
|
3014
|
-
const TOL = 0.005;
|
|
3015
|
-
const batchMap = buildPositionBatchMap(mesh.geometry, TOL);
|
|
3016
|
-
let batchIds: Float32Array | undefined;
|
|
3017
|
-
if (batchMap) {
|
|
3018
|
-
const segCount = segments.length / 6;
|
|
3019
|
-
batchIds = new Float32Array(segCount);
|
|
3020
|
-
for (let i = 0; i < segCount; i++) {
|
|
3021
|
-
const s = i * 6;
|
|
3022
|
-
batchIds[i] =
|
|
3023
|
-
batchMap.get(positionQuantKey(segments[s], segments[s + 1], segments[s + 2], 1 / TOL)) ?? -1;
|
|
3024
|
-
}
|
|
3025
|
-
}
|
|
2256
|
+
/* ---------------------------------------------------------------- 拾取 */
|
|
3026
2257
|
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
);
|
|
3031
|
-
lines.renderOrder = 2;
|
|
3032
|
-
// 屏幕空间外扩后可能超出原包围球,关掉剔除更稳
|
|
3033
|
-
lines.frustumCulled = false;
|
|
3034
|
-
lines.raycast = () => {};
|
|
3035
|
-
(lines.userData as MeshUserData).isThickEdge = true;
|
|
3036
|
-
ud.edges = lines;
|
|
3037
|
-
mesh.add(lines); // 作为子节点,自动继承 mesh 的世界变换
|
|
2258
|
+
/** 屏幕像素 → 场景射线,见 viewer/PickService.ts */
|
|
2259
|
+
getRayAt(x: number, y: number): THREE.Ray | null {
|
|
2260
|
+
return this._picks.getRayAt(x, y);
|
|
3038
2261
|
}
|
|
3039
2262
|
|
|
3040
|
-
/**
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
* 复用选中侧那套 `createOutlineMaterial`:顶点沿法线鼓 `edgeHullWidth` 米、只画背面,
|
|
3044
|
-
* 于是被本体挡住的部分不显示,露出来的那圈就是描边。与折边线的差别在于——
|
|
3045
|
-
* 这里**不建任何新几何**,成本只是把同一块网格多画一遍,折边判定那套 CPU 活全省了。
|
|
3046
|
-
*
|
|
3047
|
-
* 实例化几何拿不到逐实例变换(轮廓壳是 mesh 的子节点,不吃 instanceMatrix),
|
|
3048
|
-
* 直接跳过:宁可这栋没描边,也不要所有实例的壳叠在原点。
|
|
3049
|
-
*/
|
|
3050
|
-
private _ensureHull(mesh: THREE.Mesh): void {
|
|
3051
|
-
const ud = mesh.userData as MeshUserData;
|
|
3052
|
-
if (ud.edgeHull || !mesh.geometry) return;
|
|
3053
|
-
if ((mesh as unknown as { isInstancedMesh?: boolean }).isInstancedMesh) return;
|
|
3054
|
-
// 地形 / 绿地 / 道路等不参与自动描边(即使全局开棱线也要跳过)
|
|
3055
|
-
if (this._shouldSkipAutoEdges(ud, '')) return;
|
|
3056
|
-
|
|
3057
|
-
// 逐网格一份材质(各绑各的单体状态纹理),外观 uniform 复用共享对象
|
|
3058
|
-
const mat = createOutlineMaterial(
|
|
3059
|
-
this.uniforms,
|
|
3060
|
-
this.style.edgeHullWidth ?? 0.9,
|
|
3061
|
-
this._hullStyleUniforms,
|
|
3062
|
-
true
|
|
3063
|
-
);
|
|
3064
|
-
// 整块几何都要外扩:选中侧靠 uSelBatch 只扩一栋,全局必须置 whole
|
|
3065
|
-
mat.uniforms.uSelWhole.value = 1;
|
|
3066
|
-
mat.uniforms.uSelBatch.value = -1;
|
|
3067
|
-
const styled = styledMaterialOf(mesh);
|
|
3068
|
-
if (styled?.userData?.uBatchTex) {
|
|
3069
|
-
mat.uniforms.uBatchTex.value = styled.userData.uBatchTex.value as THREE.Texture;
|
|
3070
|
-
mat.uniforms.uBatchTexWidth.value = styled.userData.uBatchTexWidth.value as number;
|
|
3071
|
-
mat.uniforms.uBatchTexActive.value = styled.userData.uBatchTexActive.value as number;
|
|
3072
|
-
}
|
|
3073
|
-
|
|
3074
|
-
const hull = new THREE.Mesh(mesh.geometry, mat);
|
|
3075
|
-
hull.renderOrder = 2;
|
|
3076
|
-
hull.frustumCulled = false;
|
|
3077
|
-
hull.raycast = () => {};
|
|
3078
|
-
(hull.userData as MeshUserData).isEdgeHull = true;
|
|
3079
|
-
ud.edgeHull = hull;
|
|
3080
|
-
mesh.add(hull); // 作为子节点,自动继承 mesh 的世界变换
|
|
2263
|
+
/** 按 canvas 左上角的 CSS 像素坐标拾取 */
|
|
2264
|
+
pick(x: number, y: number): PickResult | null {
|
|
2265
|
+
return this._picks.pick(x, y);
|
|
3081
2266
|
}
|
|
3082
2267
|
|
|
3083
|
-
/**
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
* 棱线材质必须逐网格一份,因为它要绑定**这个网格**的单体状态纹理;但外观
|
|
3087
|
-
* (颜色 / 粗细 / 透明度)必须全局统一——所以外观 uniform 直接复用
|
|
3088
|
-
* `_edgeStyleUniforms` 里的对象,`_syncEdgeStyle()` 改一次全部生效,
|
|
3089
|
-
* program 也因为着色器相同而只编译一次。
|
|
3090
|
-
*/
|
|
3091
|
-
private _createEdgeMaterial(mesh: THREE.Mesh): ThickLineMaterial {
|
|
3092
|
-
const styled = styledMaterialOf(mesh);
|
|
3093
|
-
const batch = styled?.userData?.uBatchTex
|
|
3094
|
-
? {
|
|
3095
|
-
uBatchTex: styled.userData.uBatchTex,
|
|
3096
|
-
uBatchTex2: styled.userData.uBatchTex2,
|
|
3097
|
-
uBatchMix: this.uniforms.uBatchMix,
|
|
3098
|
-
uBatchTexWidth: styled.userData.uBatchTexWidth,
|
|
3099
|
-
uBatchTexActive: styled.userData.uBatchTexActive,
|
|
3100
|
-
uPathTex: this.uniforms.uPathTex,
|
|
3101
|
-
uPathCount: this.uniforms.uPathCount,
|
|
3102
|
-
uPathTexActive: this.uniforms.uPathTexActive,
|
|
3103
|
-
uPathTime: this.uniforms.uPathTime,
|
|
3104
|
-
}
|
|
3105
|
-
: undefined;
|
|
3106
|
-
return createThickLineMaterial(this.lineShared, {}, this._edgeStyleUniforms, batch);
|
|
2268
|
+
/** 拾取并返回经纬度 + 高程 */
|
|
2269
|
+
pickPosition(x: number, y: number): { lngLat: [number, number]; altitude: number; point: [number, number, number] } | null {
|
|
2270
|
+
return this._picks.pickPosition(x, y);
|
|
3107
2271
|
}
|
|
3108
2272
|
|
|
3109
|
-
/**
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
const batch = styled?.userData?.uBatchTex
|
|
3113
|
-
? {
|
|
3114
|
-
uBatchTex: styled.userData.uBatchTex,
|
|
3115
|
-
uBatchTex2: styled.userData.uBatchTex2,
|
|
3116
|
-
uBatchMix: this.uniforms.uBatchMix,
|
|
3117
|
-
uBatchTexWidth: styled.userData.uBatchTexWidth,
|
|
3118
|
-
uBatchTexActive: styled.userData.uBatchTexActive,
|
|
3119
|
-
uPathTex: this.uniforms.uPathTex,
|
|
3120
|
-
uPathCount: this.uniforms.uPathCount,
|
|
3121
|
-
uPathTexActive: this.uniforms.uPathTexActive,
|
|
3122
|
-
uPathTime: this.uniforms.uPathTime,
|
|
3123
|
-
}
|
|
3124
|
-
: undefined;
|
|
3125
|
-
return createThickLineMaterial(this.lineShared, {}, this._selEdgeStyleUniforms, batch);
|
|
2273
|
+
/** 穿透拾取:沿射线返回前 `limit` 个命中 */
|
|
2274
|
+
drillPick(x: number, y: number, limit = 10): PickResult[] {
|
|
2275
|
+
return this._picks.drillPick(x, y, limit);
|
|
3126
2276
|
}
|
|
3127
2277
|
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
const lines = ud.edges;
|
|
3132
|
-
if (lines) {
|
|
3133
|
-
mesh.remove(lines);
|
|
3134
|
-
(lines as THREE.Mesh).geometry?.dispose();
|
|
3135
|
-
// 棱线材质现在是逐网格一份(要绑各自的 batch 纹理),得跟着释放
|
|
3136
|
-
const mat = (lines as THREE.Mesh).material;
|
|
3137
|
-
if (Array.isArray(mat)) {
|
|
3138
|
-
mat.forEach((m) => (m as any)?.dispose?.());
|
|
3139
|
-
} else {
|
|
3140
|
-
(mat as any)?.dispose?.();
|
|
3141
|
-
}
|
|
3142
|
-
ud.edges = null;
|
|
3143
|
-
}
|
|
3144
|
-
|
|
3145
|
-
// 外扩壳的 geometry 是**复用主网格的**,只能释放材质,不能 dispose 几何
|
|
3146
|
-
const hull = ud.edgeHull;
|
|
3147
|
-
if (hull) {
|
|
3148
|
-
mesh.remove(hull);
|
|
3149
|
-
const hmat = hull.material;
|
|
3150
|
-
if (Array.isArray(hmat)) hmat.forEach((m) => (m as any)?.dispose?.());
|
|
3151
|
-
else (hmat as any)?.dispose?.();
|
|
3152
|
-
ud.edgeHull = null;
|
|
3153
|
-
}
|
|
2278
|
+
/** 单体的经纬度包围盒 */
|
|
2279
|
+
getFeatureBounds(smid: number): [[number, number], [number, number]] | null {
|
|
2280
|
+
return this._picks.getFeatureBounds(smid);
|
|
3154
2281
|
}
|
|
3155
2282
|
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
getRayAt(x: number, y: number): THREE.Ray | null {
|
|
3160
|
-
if (!this.camera || !this.canvas) return null;
|
|
3161
|
-
const canvas = this.canvas;
|
|
3162
|
-
const w = canvas.clientWidth || canvas.width;
|
|
3163
|
-
const h = canvas.clientHeight || canvas.height;
|
|
3164
|
-
if (!w || !h) return null;
|
|
3165
|
-
|
|
3166
|
-
const ndcX = (x / w) * 2 - 1;
|
|
3167
|
-
const ndcY = -(y / h) * 2 + 1;
|
|
3168
|
-
|
|
3169
|
-
const near = new THREE.Vector3(ndcX, ndcY, -1).unproject(this.camera);
|
|
3170
|
-
const far = new THREE.Vector3(ndcX, ndcY, 1).unproject(this.camera);
|
|
3171
|
-
const dir = far.clone().sub(near).normalize();
|
|
3172
|
-
return new THREE.Ray(near, dir);
|
|
3173
|
-
}
|
|
3174
|
-
|
|
3175
|
-
private _intersectMeshAtRay(mesh: THREE.Mesh, ray: THREE.Ray, instanceIndex?: number): THREE.Vector3 | null {
|
|
3176
|
-
const raycaster = new THREE.Raycaster();
|
|
3177
|
-
raycaster.ray.copy(ray);
|
|
3178
|
-
const hits = raycaster.intersectObject(mesh, false);
|
|
3179
|
-
if (!hits.length) return null;
|
|
3180
|
-
if (instanceIndex != null) {
|
|
3181
|
-
const match = hits.find((h) => h.instanceId === instanceIndex);
|
|
3182
|
-
return match ? match.point : hits[0].point;
|
|
3183
|
-
}
|
|
3184
|
-
return hits[0].point;
|
|
2283
|
+
/** @internal 见 viewer/PickService.ts */
|
|
2284
|
+
_intersectMeshAtRay(mesh: THREE.Mesh, ray: THREE.Ray, instanceIndex?: number): THREE.Vector3 | null {
|
|
2285
|
+
return this._picks.intersectMeshAtRay(mesh, ray, instanceIndex);
|
|
3185
2286
|
}
|
|
3186
2287
|
|
|
3187
|
-
|
|
2288
|
+
/** @internal 见 viewer/PickService.ts */
|
|
2289
|
+
_buildPickResult(
|
|
3188
2290
|
hit: { objectId: number; batchId: number; instanceIndex?: number },
|
|
3189
2291
|
ref: PickableRef,
|
|
3190
2292
|
exactPoint?: THREE.Vector3,
|
|
3191
2293
|
ray?: THREE.Ray
|
|
3192
2294
|
): PickResult | null {
|
|
3193
|
-
|
|
3194
|
-
// 泼溅走独立分支:它既没有 instanceMatrix,也没有 geoRef.pickInfo 里的单体表,
|
|
3195
|
-
// 走下面那段会在 `_instanceFeature()` 拿到 null 直接返回 —— 表现是「点得中但拿不到结果」。
|
|
3196
|
-
if (ud.isGaussianSplat) return this._buildSplatPickResult(hit, ref, exactPoint, ray);
|
|
3197
|
-
let smid: number | null = null;
|
|
3198
|
-
let batchId = hit.batchId;
|
|
3199
|
-
let instanceIndex = hit.instanceIndex;
|
|
3200
|
-
let geoRef = ud.geoRef;
|
|
3201
|
-
let segments: Array<{ vertexOffset: number; vertexCount: number }> = [];
|
|
3202
|
-
|
|
3203
|
-
if (instanceIndex != null) {
|
|
3204
|
-
const feature = this._instanceFeature(ref.mesh, geoRef, instanceIndex);
|
|
3205
|
-
if (!feature) return null;
|
|
3206
|
-
smid = feature.smid;
|
|
3207
|
-
batchId = feature.batchId;
|
|
3208
|
-
} else {
|
|
3209
|
-
geoRef = (ud.geoByBatchId as Map<number, GeoPackageEntry> | undefined)?.get(hit.batchId) ?? ud.geoRef;
|
|
3210
|
-
const resolved = resolveFeature(geoRef, hit.batchId);
|
|
3211
|
-
smid = resolved.smid;
|
|
3212
|
-
segments = resolved.segments;
|
|
3213
|
-
}
|
|
3214
|
-
|
|
3215
|
-
if (this._isFeatureHidden(smid, batchId)) return null;
|
|
3216
|
-
|
|
3217
|
-
let box: SegmentBox | null = null;
|
|
3218
|
-
let totalTriangles = 0;
|
|
3219
|
-
|
|
3220
|
-
if (instanceIndex != null) {
|
|
3221
|
-
box = this._instanceBox(ref.mesh, instanceIndex);
|
|
3222
|
-
totalTriangles = ref.mesh.geometry.index ? Math.round(ref.mesh.geometry.index.count / 3) : 0;
|
|
3223
|
-
} else if (smid != null && this._smidMeshes.has(smid)) {
|
|
3224
|
-
const meshes = this._smidMeshes.get(smid)!;
|
|
3225
|
-
let minX = Infinity;
|
|
3226
|
-
let minY = Infinity;
|
|
3227
|
-
let minZ = Infinity;
|
|
3228
|
-
let maxX = -Infinity;
|
|
3229
|
-
let maxY = -Infinity;
|
|
3230
|
-
let maxZ = -Infinity;
|
|
3231
|
-
let count = 0;
|
|
3232
|
-
for (const m of meshes) {
|
|
3233
|
-
const mUd = m.userData as MeshUserData;
|
|
3234
|
-
const mPick = mUd.geoRef?.pickInfo;
|
|
3235
|
-
const entry = (mPick as Record<string | number, unknown> | undefined)?.[smid] ??
|
|
3236
|
-
(mPick as Record<string | number, unknown> | undefined)?.[String(smid)];
|
|
3237
|
-
const bId = entry ? pickEntryId(entry as any) : -1;
|
|
3238
|
-
const mSegs = (entry as any)?.segments ?? (bId >= 0 ? resolveFeature(mUd.geoRef, bId).segments : []);
|
|
3239
|
-
const mBox = measureSegments(m.geometry, mSegs);
|
|
3240
|
-
if (mBox) {
|
|
3241
|
-
if (mBox.minX < minX) minX = mBox.minX;
|
|
3242
|
-
if (mBox.minY < minY) minY = mBox.minY;
|
|
3243
|
-
if (mBox.minZ < minZ) minZ = mBox.minZ;
|
|
3244
|
-
if (mBox.maxX > maxX) maxX = mBox.maxX;
|
|
3245
|
-
if (mBox.maxY > maxY) maxY = mBox.maxY;
|
|
3246
|
-
if (mBox.maxZ > maxZ) maxZ = mBox.maxZ;
|
|
3247
|
-
count += mBox.count;
|
|
3248
|
-
totalTriangles += mBox.triangles;
|
|
3249
|
-
}
|
|
3250
|
-
}
|
|
3251
|
-
if (count > 0) {
|
|
3252
|
-
box = { minX, minY, minZ, maxX, maxY, maxZ, count, triangles: totalTriangles };
|
|
3253
|
-
}
|
|
3254
|
-
}
|
|
3255
|
-
|
|
3256
|
-
if (!box) {
|
|
3257
|
-
box = measureSegments(ref.mesh.geometry, segments);
|
|
3258
|
-
if (!box && ref.mesh.geometry) {
|
|
3259
|
-
if (!ref.mesh.geometry.boundingBox) ref.mesh.geometry.computeBoundingBox();
|
|
3260
|
-
const bb = ref.mesh.geometry.boundingBox;
|
|
3261
|
-
if (bb) {
|
|
3262
|
-
box = {
|
|
3263
|
-
minX: bb.min.x,
|
|
3264
|
-
maxX: bb.max.x,
|
|
3265
|
-
minY: bb.min.y,
|
|
3266
|
-
maxY: bb.max.y,
|
|
3267
|
-
minZ: bb.min.z,
|
|
3268
|
-
maxZ: bb.max.z,
|
|
3269
|
-
count: 0,
|
|
3270
|
-
triangles: 0,
|
|
3271
|
-
};
|
|
3272
|
-
}
|
|
3273
|
-
}
|
|
3274
|
-
}
|
|
3275
|
-
const geometry = ref.mesh.geometry;
|
|
3276
|
-
const triangles = totalTriangles > 0 ? totalTriangles : (geometry.index ? Math.round(geometry.index.count / 3) : 0);
|
|
3277
|
-
const attributes = buildAttributes({ smid, box, triangles, floorHeight: this.style.floorHeight });
|
|
3278
|
-
|
|
3279
|
-
let centerLngLat: [number, number] | null = null;
|
|
3280
|
-
if (box && this.sceneAnchor) {
|
|
3281
|
-
const v = new THREE.Vector3((box.minX + box.maxX) / 2, (box.minY + box.maxY) / 2, (box.minZ + box.maxZ) / 2);
|
|
3282
|
-
v.applyMatrix4(ref.mesh.matrixWorld);
|
|
3283
|
-
const s = this.sceneAnchor.meterInMercator;
|
|
3284
|
-
centerLngLat = this._lngLatFromWorld(v.x * s + this.sceneAnchor.x, -v.y * s + this.sceneAnchor.y);
|
|
3285
|
-
}
|
|
3286
|
-
|
|
3287
|
-
// 精确命中点求交计算
|
|
3288
|
-
let hitPoint: THREE.Vector3 | null = exactPoint ?? null;
|
|
3289
|
-
if (!hitPoint && ray) {
|
|
3290
|
-
hitPoint = this._intersectMeshAtRay(ref.mesh, ray, instanceIndex);
|
|
3291
|
-
}
|
|
3292
|
-
|
|
3293
|
-
let point: [number, number, number] | null = null;
|
|
3294
|
-
let lngLat: [number, number] | null = null;
|
|
3295
|
-
let altitude: number | null = null;
|
|
3296
|
-
|
|
3297
|
-
if (hitPoint && this.sceneAnchor) {
|
|
3298
|
-
const off = smid != null ? this._featureStyleManager.getFeatureEffectiveOffset(smid) : {};
|
|
3299
|
-
const trans = off.translation;
|
|
3300
|
-
const elev = off.elevation;
|
|
3301
|
-
const finalPos = hitPoint.clone();
|
|
3302
|
-
|
|
3303
|
-
if (off.rotation) {
|
|
3304
|
-
const rotMat = resolveRotationMatrix(off.rotation);
|
|
3305
|
-
if (rotMat) {
|
|
3306
|
-
const m3 = new THREE.Matrix3().set(
|
|
3307
|
-
rotMat[0], rotMat[1], rotMat[2],
|
|
3308
|
-
rotMat[3], rotMat[4], rotMat[5],
|
|
3309
|
-
rotMat[6], rotMat[7], rotMat[8]
|
|
3310
|
-
);
|
|
3311
|
-
const piv = off.pivot ? new THREE.Vector3(off.pivot[0], off.pivot[1], off.pivot[2]) : new THREE.Vector3(0, 0, 0);
|
|
3312
|
-
finalPos.sub(piv).applyMatrix3(m3).add(piv);
|
|
3313
|
-
}
|
|
3314
|
-
}
|
|
3315
|
-
|
|
3316
|
-
if (trans) finalPos.add(new THREE.Vector3(trans[0], trans[1], trans[2]));
|
|
3317
|
-
else if (elev) finalPos.z += elev;
|
|
3318
|
-
|
|
3319
|
-
point = [finalPos.x, finalPos.y, finalPos.z];
|
|
3320
|
-
const s = this.sceneAnchor.meterInMercator;
|
|
3321
|
-
lngLat = this._lngLatFromWorld(finalPos.x * s + this.sceneAnchor.x, -finalPos.y * s + this.sceneAnchor.y);
|
|
3322
|
-
altitude = finalPos.z + (this.sceneAnchor.altitude ?? 0);
|
|
3323
|
-
} else if (centerLngLat && box && this.sceneAnchor) {
|
|
3324
|
-
lngLat = centerLngLat;
|
|
3325
|
-
altitude = (box.minZ + box.maxZ) / 2 + (this.sceneAnchor.altitude ?? 0);
|
|
3326
|
-
}
|
|
3327
|
-
|
|
3328
|
-
return {
|
|
3329
|
-
datasetId: ref.datasetId,
|
|
3330
|
-
datasetName: this.datasets.get(ref.datasetId)?.name ?? ref.datasetId,
|
|
3331
|
-
objectId: hit.objectId,
|
|
3332
|
-
batchId,
|
|
3333
|
-
instanceIndex,
|
|
3334
|
-
skeletonName: ref.skeletonName,
|
|
3335
|
-
centerLngLat,
|
|
3336
|
-
point,
|
|
3337
|
-
lngLat,
|
|
3338
|
-
altitude,
|
|
3339
|
-
...attributes,
|
|
3340
|
-
mesh: ref.mesh,
|
|
3341
|
-
};
|
|
2295
|
+
return this._picks.buildPickResult(hit, ref, exactPoint, ray);
|
|
3342
2296
|
}
|
|
3343
2297
|
|
|
3344
|
-
/**
|
|
3345
|
-
|
|
3346
|
-
*
|
|
3347
|
-
* 整块泼溅瓦片当一个单体(没有单体级 smid 可报),命中点由 `splatRaycast` 给出 ——
|
|
3348
|
-
* 它按每个高斯的 3σ 球求交,比拾取那一趟为了命中率摊开的 4px 方块准得多。
|
|
3349
|
-
* 包围盒必须由 `iCenter` 算:geometry 的 position 只是 [-1,1]² 的四边形角点。
|
|
3350
|
-
*/
|
|
3351
|
-
private _buildSplatPickResult(
|
|
2298
|
+
/** @internal 见 viewer/PickService.ts */
|
|
2299
|
+
_buildSplatPickResult(
|
|
3352
2300
|
hit: { objectId: number; batchId: number; instanceIndex?: number },
|
|
3353
2301
|
ref: PickableRef,
|
|
3354
2302
|
exactPoint?: THREE.Vector3,
|
|
3355
2303
|
ray?: THREE.Ray
|
|
3356
2304
|
): PickResult | null {
|
|
3357
|
-
|
|
3358
|
-
if (!anchor) return null;
|
|
3359
|
-
const mesh = ref.mesh;
|
|
3360
|
-
|
|
3361
|
-
let hitPoint = exactPoint ?? null;
|
|
3362
|
-
if (!hitPoint && ray) hitPoint = this._intersectMeshAtRay(mesh, ray);
|
|
3363
|
-
|
|
3364
|
-
const local = splatCenterBounds(mesh.geometry);
|
|
3365
|
-
const box: SegmentBox | null = local
|
|
3366
|
-
? {
|
|
3367
|
-
minX: local.minX,
|
|
3368
|
-
minY: local.minY,
|
|
3369
|
-
minZ: local.minZ,
|
|
3370
|
-
maxX: local.maxX,
|
|
3371
|
-
maxY: local.maxY,
|
|
3372
|
-
maxZ: local.maxZ,
|
|
3373
|
-
count: local.count,
|
|
3374
|
-
triangles: 0,
|
|
3375
|
-
}
|
|
3376
|
-
: null;
|
|
3377
|
-
|
|
3378
|
-
const attributes = buildAttributes({
|
|
3379
|
-
smid: null,
|
|
3380
|
-
box,
|
|
3381
|
-
triangles: 0,
|
|
3382
|
-
floorHeight: this.style.floorHeight,
|
|
3383
|
-
});
|
|
3384
|
-
|
|
3385
|
-
const s = anchor.meterInMercator;
|
|
3386
|
-
let point: [number, number, number] | null = null;
|
|
3387
|
-
let lngLat: [number, number] | null = null;
|
|
3388
|
-
let altitude: number | null = null;
|
|
3389
|
-
let centerLngLat: [number, number] | null = null;
|
|
3390
|
-
|
|
3391
|
-
if (hitPoint) {
|
|
3392
|
-
point = [hitPoint.x, hitPoint.y, hitPoint.z];
|
|
3393
|
-
lngLat = this._lngLatFromWorld(hitPoint.x * s + anchor.x, -hitPoint.y * s + anchor.y);
|
|
3394
|
-
altitude = hitPoint.z + (anchor.altitude ?? 0);
|
|
3395
|
-
// 泼溅命中的就是一个高斯,没有「单体中心」可言,用命中点当中心
|
|
3396
|
-
centerLngLat = lngLat;
|
|
3397
|
-
} else if (box) {
|
|
3398
|
-
const v = new THREE.Vector3(
|
|
3399
|
-
(box.minX + box.maxX) / 2,
|
|
3400
|
-
(box.minY + box.maxY) / 2,
|
|
3401
|
-
(box.minZ + box.maxZ) / 2
|
|
3402
|
-
).applyMatrix4(mesh.matrixWorld);
|
|
3403
|
-
centerLngLat = this._lngLatFromWorld(v.x * s + anchor.x, -v.y * s + anchor.y);
|
|
3404
|
-
altitude = v.z + (anchor.altitude ?? 0);
|
|
3405
|
-
}
|
|
3406
|
-
|
|
3407
|
-
return {
|
|
3408
|
-
datasetId: ref.datasetId,
|
|
3409
|
-
datasetName: this.datasets.get(ref.datasetId)?.name ?? ref.datasetId,
|
|
3410
|
-
objectId: hit.objectId,
|
|
3411
|
-
batchId: -1,
|
|
3412
|
-
instanceIndex: undefined,
|
|
3413
|
-
skeletonName: ref.skeletonName,
|
|
3414
|
-
centerLngLat,
|
|
3415
|
-
point,
|
|
3416
|
-
lngLat,
|
|
3417
|
-
altitude,
|
|
3418
|
-
...attributes,
|
|
3419
|
-
mesh,
|
|
3420
|
-
};
|
|
2305
|
+
return this._picks.buildSplatPickResult(hit, ref, exactPoint, ray);
|
|
3421
2306
|
}
|
|
3422
2307
|
|
|
3423
|
-
/**
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
const hit = this.picker.pick(this.scene, this.camera, x, y);
|
|
3427
|
-
if (!hit) return null;
|
|
3428
|
-
|
|
3429
|
-
const ref = this._pickables.get(hit.objectId);
|
|
3430
|
-
if (!ref) return null;
|
|
3431
|
-
|
|
3432
|
-
const ray = this.getRayAt(x, y);
|
|
3433
|
-
return this._buildPickResult(hit, ref, undefined, ray ?? undefined);
|
|
2308
|
+
/** @internal 见 viewer/PickService.ts */
|
|
2309
|
+
_instanceFeature(mesh: THREE.Mesh, geoRef: GeoPackageEntry | undefined, instanceIndex: number): InstanceFeature | null {
|
|
2310
|
+
return this._picks.instanceFeature(mesh, geoRef, instanceIndex);
|
|
3434
2311
|
}
|
|
3435
2312
|
|
|
3436
|
-
/**
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
*/
|
|
3440
|
-
pickPosition(x: number, y: number): { lngLat: [number, number]; altitude: number; point: [number, number, number] } | null {
|
|
3441
|
-
if (!this.camera || !this.sceneAnchor) return null;
|
|
3442
|
-
|
|
3443
|
-
const hit = this.pick(x, y);
|
|
3444
|
-
if (hit?.point && hit?.lngLat && hit.altitude != null) {
|
|
3445
|
-
return {
|
|
3446
|
-
lngLat: hit.lngLat,
|
|
3447
|
-
altitude: hit.altitude,
|
|
3448
|
-
point: hit.point,
|
|
3449
|
-
};
|
|
3450
|
-
}
|
|
3451
|
-
|
|
3452
|
-
const ray = this.getRayAt(x, y);
|
|
3453
|
-
if (!ray) return null;
|
|
3454
|
-
|
|
3455
|
-
const groundPlane = new THREE.Plane(new THREE.Vector3(0, 0, 1), 0);
|
|
3456
|
-
const target = new THREE.Vector3();
|
|
3457
|
-
const intersect = ray.intersectPlane(groundPlane, target);
|
|
3458
|
-
if (intersect) {
|
|
3459
|
-
const s = this.sceneAnchor.meterInMercator;
|
|
3460
|
-
const lngLat = this._lngLatFromWorld(intersect.x * s + this.sceneAnchor.x, -intersect.y * s + this.sceneAnchor.y);
|
|
3461
|
-
const altitude = intersect.z + (this.sceneAnchor.altitude ?? 0);
|
|
3462
|
-
return {
|
|
3463
|
-
lngLat,
|
|
3464
|
-
altitude,
|
|
3465
|
-
point: [intersect.x, intersect.y, intersect.z],
|
|
3466
|
-
};
|
|
3467
|
-
}
|
|
3468
|
-
return null;
|
|
2313
|
+
/** @internal 见 viewer/PickService.ts */
|
|
2314
|
+
_instanceBox(mesh: THREE.Mesh, instanceIndex: number): SegmentBox | null {
|
|
2315
|
+
return this._picks.instanceBox(mesh, instanceIndex);
|
|
3469
2316
|
}
|
|
3470
2317
|
|
|
3471
|
-
/**
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
drillPick(x: number, y: number, limit = 10): PickResult[] {
|
|
3475
|
-
if (!this.sceneAnchor || !this.camera) return [];
|
|
3476
|
-
const ray = this.getRayAt(x, y);
|
|
3477
|
-
if (!ray) return [];
|
|
3478
|
-
|
|
3479
|
-
const raycaster = new THREE.Raycaster();
|
|
3480
|
-
raycaster.ray.copy(ray);
|
|
3481
|
-
|
|
3482
|
-
const meshes = [...this._pickables.values()]
|
|
3483
|
-
.map((p) => p.mesh)
|
|
3484
|
-
.filter((m) => {
|
|
3485
|
-
let curr: THREE.Object3D | null = m;
|
|
3486
|
-
while (curr) {
|
|
3487
|
-
if (!curr.visible) return false;
|
|
3488
|
-
curr = curr.parent;
|
|
3489
|
-
}
|
|
3490
|
-
return true;
|
|
3491
|
-
});
|
|
3492
|
-
const hits = raycaster.intersectObjects(meshes, false);
|
|
3493
|
-
if (!hits.length) return [];
|
|
3494
|
-
|
|
3495
|
-
const results: PickResult[] = [];
|
|
3496
|
-
const seenSmids = new Set<number>();
|
|
3497
|
-
const seenBatches = new Set<string>();
|
|
3498
|
-
|
|
3499
|
-
for (const hit of hits) {
|
|
3500
|
-
if (results.length >= limit) break;
|
|
3501
|
-
const mesh = hit.object as THREE.Mesh;
|
|
3502
|
-
const ud = mesh.userData as MeshUserData & { pickId?: number };
|
|
3503
|
-
if (ud.pickId == null) continue;
|
|
3504
|
-
const ref = this._pickables.get(ud.pickId);
|
|
3505
|
-
if (!ref) continue;
|
|
3506
|
-
|
|
3507
|
-
let smid: number | null = null;
|
|
3508
|
-
let batchId = -1;
|
|
3509
|
-
let instanceIndex = hit.instanceId;
|
|
3510
|
-
|
|
3511
|
-
if (instanceIndex != null) {
|
|
3512
|
-
const feature = this._instanceFeature(mesh, ud.geoRef, instanceIndex);
|
|
3513
|
-
if (!feature) continue;
|
|
3514
|
-
smid = feature.smid;
|
|
3515
|
-
batchId = feature.batchId;
|
|
3516
|
-
} else if (hit.faceIndex != null) {
|
|
3517
|
-
const batchAttr = mesh.geometry.attributes.aBatchId;
|
|
3518
|
-
if (batchAttr) {
|
|
3519
|
-
const idx = mesh.geometry.index ? mesh.geometry.index.getX(hit.faceIndex * 3) : hit.faceIndex * 3;
|
|
3520
|
-
batchId = Math.round(batchAttr.getX(idx));
|
|
3521
|
-
const geoRef = (ud.geoByBatchId as Map<number, GeoPackageEntry> | undefined)?.get(batchId) ?? ud.geoRef;
|
|
3522
|
-
const resolved = resolveFeature(geoRef, batchId);
|
|
3523
|
-
smid = resolved.smid;
|
|
3524
|
-
}
|
|
3525
|
-
}
|
|
3526
|
-
|
|
3527
|
-
if (smid != null) {
|
|
3528
|
-
if (seenSmids.has(smid)) continue;
|
|
3529
|
-
seenSmids.add(smid);
|
|
3530
|
-
} else {
|
|
3531
|
-
const key = `${ud.pickId}:${batchId}:${instanceIndex ?? -1}`;
|
|
3532
|
-
if (seenBatches.has(key)) continue;
|
|
3533
|
-
seenBatches.add(key);
|
|
3534
|
-
}
|
|
3535
|
-
|
|
3536
|
-
if (this._isFeatureHidden(smid, batchId)) continue;
|
|
3537
|
-
|
|
3538
|
-
const pickHit = {
|
|
3539
|
-
objectId: ud.pickId,
|
|
3540
|
-
batchId,
|
|
3541
|
-
instanceIndex,
|
|
3542
|
-
};
|
|
3543
|
-
const res = this._buildPickResult(pickHit, ref, hit.point);
|
|
3544
|
-
if (res) results.push(res);
|
|
3545
|
-
}
|
|
3546
|
-
|
|
3547
|
-
return results;
|
|
2318
|
+
/** @internal 拾取 id 池,见 viewer/PickService.ts */
|
|
2319
|
+
_allocPickId(): number {
|
|
2320
|
+
return this._picks.allocPickId();
|
|
3548
2321
|
}
|
|
3549
2322
|
|
|
3550
|
-
/**
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
*/
|
|
3554
|
-
getFeatureBounds(smid: number): [[number, number], [number, number]] | null {
|
|
3555
|
-
if (!this.sceneAnchor) return null;
|
|
3556
|
-
let minLng = Infinity;
|
|
3557
|
-
let maxLng = -Infinity;
|
|
3558
|
-
let minLat = Infinity;
|
|
3559
|
-
let maxLat = -Infinity;
|
|
3560
|
-
let found = false;
|
|
3561
|
-
|
|
3562
|
-
for (const ref of this._pickables.values()) {
|
|
3563
|
-
const mesh = ref.mesh;
|
|
3564
|
-
const ud = mesh.userData as MeshUserData;
|
|
3565
|
-
const geoRef = ud.geoRef;
|
|
3566
|
-
const pickInfo = geoRef?.pickInfo;
|
|
3567
|
-
if (!pickInfo) continue;
|
|
3568
|
-
|
|
3569
|
-
let box: SegmentBox | null = null;
|
|
3570
|
-
if ((mesh as THREE.InstancedMesh).isInstancedMesh) {
|
|
3571
|
-
let lookup = ud.instanceLookup as Map<number, InstanceFeature> | undefined;
|
|
3572
|
-
if (!lookup) {
|
|
3573
|
-
lookup = buildInstanceLookup(pickInfo);
|
|
3574
|
-
ud.instanceLookup = lookup;
|
|
3575
|
-
}
|
|
3576
|
-
for (const [instIdx, feat] of lookup) {
|
|
3577
|
-
if (feat.smid === smid) {
|
|
3578
|
-
box = this._instanceBox(mesh, instIdx);
|
|
3579
|
-
break;
|
|
3580
|
-
}
|
|
3581
|
-
}
|
|
3582
|
-
} else {
|
|
3583
|
-
const infoMap = pickInfo as Record<string | number, unknown>;
|
|
3584
|
-
const entry = infoMap[smid] ?? infoMap[String(smid)];
|
|
3585
|
-
if (entry) {
|
|
3586
|
-
const bId = pickEntryId(entry);
|
|
3587
|
-
const resolved = resolveFeature(geoRef, bId);
|
|
3588
|
-
box = measureSegments(mesh.geometry, resolved.segments);
|
|
3589
|
-
if (!box && mesh.geometry) {
|
|
3590
|
-
if (!mesh.geometry.boundingBox) mesh.geometry.computeBoundingBox();
|
|
3591
|
-
const bb = mesh.geometry.boundingBox;
|
|
3592
|
-
if (bb) {
|
|
3593
|
-
box = {
|
|
3594
|
-
minX: bb.min.x,
|
|
3595
|
-
maxX: bb.max.x,
|
|
3596
|
-
minY: bb.min.y,
|
|
3597
|
-
maxY: bb.max.y,
|
|
3598
|
-
minZ: bb.min.z,
|
|
3599
|
-
maxZ: bb.max.z,
|
|
3600
|
-
count: 0,
|
|
3601
|
-
triangles: 0,
|
|
3602
|
-
};
|
|
3603
|
-
}
|
|
3604
|
-
}
|
|
3605
|
-
}
|
|
3606
|
-
}
|
|
3607
|
-
|
|
3608
|
-
if (box) {
|
|
3609
|
-
found = true;
|
|
3610
|
-
const corners = [
|
|
3611
|
-
new THREE.Vector3(box.minX, box.minY, box.minZ),
|
|
3612
|
-
new THREE.Vector3(box.maxX, box.minY, box.minZ),
|
|
3613
|
-
new THREE.Vector3(box.minX, box.maxY, box.minZ),
|
|
3614
|
-
new THREE.Vector3(box.maxX, box.maxY, box.minZ),
|
|
3615
|
-
new THREE.Vector3(box.minX, box.minY, box.maxZ),
|
|
3616
|
-
new THREE.Vector3(box.maxX, box.minY, box.maxZ),
|
|
3617
|
-
new THREE.Vector3(box.minX, box.maxY, box.maxZ),
|
|
3618
|
-
new THREE.Vector3(box.maxX, box.maxY, box.maxZ),
|
|
3619
|
-
];
|
|
3620
|
-
|
|
3621
|
-
const off = smid != null ? this._featureStyleManager.getFeatureEffectiveOffset(smid) : {};
|
|
3622
|
-
const trans = off.translation;
|
|
3623
|
-
const elev = off.elevation;
|
|
3624
|
-
|
|
3625
|
-
const rotMat = off.rotation ? resolveRotationMatrix(off.rotation) : null;
|
|
3626
|
-
let rotM3: THREE.Matrix3 | null = null;
|
|
3627
|
-
let rotPiv: THREE.Vector3 | null = null;
|
|
3628
|
-
if (rotMat) {
|
|
3629
|
-
rotM3 = new THREE.Matrix3().set(
|
|
3630
|
-
rotMat[0], rotMat[1], rotMat[2],
|
|
3631
|
-
rotMat[3], rotMat[4], rotMat[5],
|
|
3632
|
-
rotMat[6], rotMat[7], rotMat[8]
|
|
3633
|
-
);
|
|
3634
|
-
rotPiv = off.pivot ? new THREE.Vector3(off.pivot[0], off.pivot[1], off.pivot[2]) : new THREE.Vector3(0, 0, 0);
|
|
3635
|
-
}
|
|
3636
|
-
|
|
3637
|
-
// GPU 路径:位移发生在顶点着色器里,包围盒要按同一套公式在 CPU 复算,
|
|
3638
|
-
// 否则「运动中的车」的包围盒会停在路径原点(相机 fitBounds / 属性过滤都会错)。
|
|
3639
|
-
const gpuPose = smid != null ? this._gpuPathPoseAt(smid) : null;
|
|
3640
|
-
let headM3: THREE.Matrix3 | null = null;
|
|
3641
|
-
if (gpuPose && gpuPose.yaw !== 0) {
|
|
3642
|
-
const c = Math.cos(gpuPose.yaw);
|
|
3643
|
-
const s = Math.sin(gpuPose.yaw);
|
|
3644
|
-
// 与着色器的 mat3( c, s, 0, -s, c, 0, 0, 0, 1 ) 等价:GLSL 是列优先,
|
|
3645
|
-
// three 的 Matrix3.set 是行优先,所以这里写成 (c, -s; s, c) 才是同一个矩阵。
|
|
3646
|
-
headM3 = new THREE.Matrix3().set(c, -s, 0, s, c, 0, 0, 0, 1);
|
|
3647
|
-
}
|
|
3648
|
-
|
|
3649
|
-
for (const c of corners) {
|
|
3650
|
-
if (rotM3 && rotPiv) c.sub(rotPiv).applyMatrix3(rotM3).add(rotPiv);
|
|
3651
|
-
if (headM3) {
|
|
3652
|
-
const piv = rotPiv ?? new THREE.Vector3(0, 0, 0);
|
|
3653
|
-
c.sub(piv).applyMatrix3(headM3).add(piv);
|
|
3654
|
-
}
|
|
3655
|
-
if (trans) c.add(new THREE.Vector3(trans[0], trans[1], trans[2]));
|
|
3656
|
-
else if (elev) c.z += elev;
|
|
3657
|
-
if (gpuPose) c.add(new THREE.Vector3(gpuPose.pos[0], gpuPose.pos[1], gpuPose.pos[2]));
|
|
3658
|
-
|
|
3659
|
-
c.applyMatrix4(mesh.matrixWorld);
|
|
3660
|
-
const s = this.sceneAnchor.meterInMercator;
|
|
3661
|
-
const [lng, lat] = this._lngLatFromWorld(c.x * s + this.sceneAnchor.x, -c.y * s + this.sceneAnchor.y);
|
|
3662
|
-
if (lng < minLng) minLng = lng;
|
|
3663
|
-
if (lng > maxLng) maxLng = lng;
|
|
3664
|
-
if (lat < minLat) minLat = lat;
|
|
3665
|
-
if (lat > maxLat) maxLat = lat;
|
|
3666
|
-
}
|
|
3667
|
-
}
|
|
3668
|
-
}
|
|
3669
|
-
|
|
3670
|
-
if (!found) return null;
|
|
3671
|
-
return [
|
|
3672
|
-
[minLng, minLat],
|
|
3673
|
-
[maxLng, maxLat],
|
|
3674
|
-
];
|
|
2323
|
+
/** @internal */
|
|
2324
|
+
_releasePickId(id: number): void {
|
|
2325
|
+
this._picks.releasePickId(id);
|
|
3675
2326
|
}
|
|
3676
2327
|
|
|
3677
|
-
/**
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
geoRef: GeoPackageEntry | undefined,
|
|
3681
|
-
instanceIndex: number
|
|
3682
|
-
): InstanceFeature | null {
|
|
3683
|
-
const ud = mesh.userData as MeshUserData & { instanceLookup?: Map<number, InstanceFeature> };
|
|
3684
|
-
if (!ud.instanceLookup) ud.instanceLookup = buildInstanceLookup(geoRef?.pickInfo);
|
|
3685
|
-
return ud.instanceLookup.get(instanceIndex) ?? null;
|
|
3686
|
-
}
|
|
3687
|
-
|
|
3688
|
-
/** 实例化几何的单体包围盒:base mesh 的包围盒套上该实例的矩阵 */
|
|
3689
|
-
private _instanceBox(mesh: THREE.Mesh, instanceIndex: number): SegmentBox | null {
|
|
3690
|
-
const im = mesh as THREE.InstancedMesh;
|
|
3691
|
-
if (!im.isInstancedMesh) return null;
|
|
3692
|
-
const geometry = mesh.geometry;
|
|
3693
|
-
if (!geometry.boundingBox) geometry.computeBoundingBox();
|
|
3694
|
-
const bb = geometry.boundingBox;
|
|
3695
|
-
if (!bb) return null;
|
|
3696
|
-
const m = new THREE.Matrix4();
|
|
3697
|
-
im.getMatrixAt(instanceIndex, m);
|
|
3698
|
-
const box = bb.clone().applyMatrix4(m);
|
|
3699
|
-
return {
|
|
3700
|
-
minX: box.min.x,
|
|
3701
|
-
minY: box.min.y,
|
|
3702
|
-
minZ: box.min.z,
|
|
3703
|
-
maxX: box.max.x,
|
|
3704
|
-
maxY: box.max.y,
|
|
3705
|
-
maxZ: box.max.z,
|
|
3706
|
-
count: geometry.getAttribute('position')?.count ?? 0,
|
|
3707
|
-
triangles: geometry.index ? Math.round(geometry.index.count / 3) : 0,
|
|
3708
|
-
};
|
|
2328
|
+
/** @internal 拾取表(测试直接读写),见 viewer/PickService.ts */
|
|
2329
|
+
get _pickables(): Map<number, PickableRef> {
|
|
2330
|
+
return this._picks.pickables;
|
|
3709
2331
|
}
|
|
3710
2332
|
|
|
3711
|
-
|
|
3712
|
-
select(result: PickResult | null, { silent = false }: { silent?: boolean } = {}): void {
|
|
3713
|
-
// 清掉上一次
|
|
3714
|
-
if (this._selection) {
|
|
3715
|
-
for (const obj of this._selectedMeshes) {
|
|
3716
|
-
// 点云:恢复成当前着色模式(去掉选中混色)
|
|
3717
|
-
if (isPointCloudObject(obj)) {
|
|
3718
|
-
this._applyPointCloudStyle(obj, false);
|
|
3719
|
-
continue;
|
|
3720
|
-
}
|
|
3721
|
-
const mat = styledMaterialOf(obj as THREE.Mesh);
|
|
3722
|
-
if (mat) {
|
|
3723
|
-
mat.userData.uSelBatch.value = -1;
|
|
3724
|
-
mat.userData.uSelWhole.value = 0;
|
|
3725
|
-
}
|
|
3726
|
-
}
|
|
3727
|
-
this._selectedMeshes = [];
|
|
3728
|
-
this._clearSelectionDecoration();
|
|
3729
|
-
this._selection = null;
|
|
3730
|
-
}
|
|
3731
|
-
|
|
3732
|
-
if (!result) {
|
|
3733
|
-
this.uniforms.uSelMix.value = 0;
|
|
3734
|
-
this.uniforms.uDim.value = 0;
|
|
3735
|
-
this.outlineMaterial.uniforms.uSelBatch.value = -1;
|
|
3736
|
-
this.outlineMaterial.uniforms.uSelWhole.value = 0;
|
|
3737
|
-
this.outlineMaterial.uniforms.uElevation.value = 0;
|
|
3738
|
-
this._requestRepaint();
|
|
3739
|
-
if (!silent) this.onPick(null);
|
|
3740
|
-
return;
|
|
3741
|
-
}
|
|
3742
|
-
|
|
3743
|
-
this._selection = result;
|
|
3744
|
-
const targetMeshes = new Set<THREE.Mesh>();
|
|
3745
|
-
if (result.smid != null) {
|
|
3746
|
-
const smidSet = this._smidMeshes.get(result.smid);
|
|
3747
|
-
if (smidSet) {
|
|
3748
|
-
for (const m of smidSet) targetMeshes.add(m);
|
|
3749
|
-
}
|
|
3750
|
-
}
|
|
3751
|
-
targetMeshes.add(result.mesh);
|
|
3752
|
-
|
|
3753
|
-
for (const mesh of targetMeshes) {
|
|
3754
|
-
const mat = styledMaterialOf(mesh);
|
|
3755
|
-
if (!mat) continue;
|
|
3756
|
-
let batchId = result.batchId;
|
|
3757
|
-
if (result.instanceIndex != null && mesh === result.mesh) {
|
|
3758
|
-
batchId = result.batchId;
|
|
3759
|
-
} else if (result.smid != null) {
|
|
3760
|
-
const ud = mesh.userData as MeshUserData;
|
|
3761
|
-
const pickInfo = ud.geoRef?.pickInfo;
|
|
3762
|
-
const entry = (pickInfo as Record<string | number, unknown> | undefined)?.[result.smid] ??
|
|
3763
|
-
(pickInfo as Record<string | number, unknown> | undefined)?.[String(result.smid)];
|
|
3764
|
-
const bId = entry ? pickEntryId(entry as any) : -1;
|
|
3765
|
-
batchId = bId >= 0 ? bId : (mesh === result.mesh ? result.batchId : -1);
|
|
3766
|
-
}
|
|
3767
|
-
mat.userData.uSelBatch.value = batchId;
|
|
3768
|
-
// 没有单体化信息时整块高亮,否则会出现「点中了但什么都没亮、其他全暗了」
|
|
3769
|
-
mat.userData.uSelWhole.value = batchId < 0 ? 1 : 0;
|
|
3770
|
-
this._selectedMeshes.push(mesh);
|
|
3771
|
-
}
|
|
3772
|
-
// 点云没有单体粒度:整块混成 selectColor,让选中至少看得见
|
|
3773
|
-
for (const mesh of this._selectedMeshes) {
|
|
3774
|
-
if (isPointCloudObject(mesh)) this._applyPointCloudStyle(mesh, true);
|
|
3775
|
-
}
|
|
3776
|
-
this.uniforms.uSelMix.value = 1;
|
|
3777
|
-
this.uniforms.uDim.value = this.style.dimOthers ? 1 : 0;
|
|
3778
|
-
|
|
3779
|
-
if (this.style.outline) {
|
|
3780
|
-
// 棱线 / 外扩轮廓都是按「面」设计的,点云画不出来也不该画
|
|
3781
|
-
const decoMeshes = [...targetMeshes].filter((m) => !isPointCloudObject(m));
|
|
3782
|
-
if (decoMeshes.length) {
|
|
3783
|
-
if ((this.style.outlineStyle ?? 'edges') === 'edges') {
|
|
3784
|
-
this._addSelectionEdges(result, decoMeshes);
|
|
3785
|
-
} else {
|
|
3786
|
-
this._addSelectionHull(result, decoMeshes);
|
|
3787
|
-
}
|
|
3788
|
-
}
|
|
3789
|
-
}
|
|
3790
|
-
this._requestRepaint();
|
|
3791
|
-
}
|
|
2333
|
+
/* ---------------------------------------------------------------- 选中 */
|
|
3792
2334
|
|
|
3793
2335
|
/**
|
|
3794
|
-
*
|
|
3795
|
-
*
|
|
2336
|
+
* 选中一个单体(或整块网格),高亮与轮廓装饰见 viewer/SelectionDecorator.ts。
|
|
2337
|
+
* 传 `null` 清空选中;`silent` 为真时不触发 `onPick`。
|
|
3796
2338
|
*/
|
|
3797
|
-
|
|
3798
|
-
this.
|
|
3799
|
-
for (const mesh of meshes) {
|
|
3800
|
-
const ud = mesh.userData as MeshUserData;
|
|
3801
|
-
const geoRef = ud.geoRef;
|
|
3802
|
-
let segments: PickSegment[] = [];
|
|
3803
|
-
// 这块网格上的棱线全部属于选中的那个单体,整批填同一个 batchId,
|
|
3804
|
-
// 顶点着色器才能从状态纹理里取到平移 / 拔高——选中跟随移动靠它。
|
|
3805
|
-
let batchId = -1;
|
|
3806
|
-
if (mesh === result.mesh && result.instanceIndex != null) {
|
|
3807
|
-
// 实例化几何没有逐单体的顶点区间,整个 base mesh 就是这一栋的外形
|
|
3808
|
-
segments = [{ vertexOffset: 0, vertexCount: mesh.geometry.getAttribute('position')?.count ?? 0 }];
|
|
3809
|
-
const pickInfo = geoRef?.pickInfo;
|
|
3810
|
-
const entry = (pickInfo as Record<string | number, unknown> | undefined)?.[result.smid ?? ''] ??
|
|
3811
|
-
(pickInfo as Record<string | number, unknown> | undefined)?.[String(result.smid)];
|
|
3812
|
-
batchId = entry ? pickEntryId(entry as any) : -1;
|
|
3813
|
-
} else if (result.smid != null) {
|
|
3814
|
-
const pickInfo = geoRef?.pickInfo;
|
|
3815
|
-
const entry = (pickInfo as Record<string | number, unknown> | undefined)?.[result.smid] ??
|
|
3816
|
-
(pickInfo as Record<string | number, unknown> | undefined)?.[String(result.smid)];
|
|
3817
|
-
const bId = entry ? pickEntryId(entry as any) : -1;
|
|
3818
|
-
batchId = bId;
|
|
3819
|
-
segments = (entry as any)?.segments ?? (bId >= 0 ? resolveFeature(geoRef, bId).segments : []);
|
|
3820
|
-
} else if (mesh === result.mesh) {
|
|
3821
|
-
const localGeo = (ud.geoByBatchId as Map<number, GeoPackageEntry> | undefined)?.get(result.batchId) ?? geoRef;
|
|
3822
|
-
segments = resolveFeature(localGeo, result.batchId).segments;
|
|
3823
|
-
batchId = result.batchId;
|
|
3824
|
-
}
|
|
3825
|
-
|
|
3826
|
-
const pairs = buildFeatureEdgeSegments(mesh.geometry, segments);
|
|
3827
|
-
if (pairs && pairs.length > 0) {
|
|
3828
|
-
const batchIds = batchId >= 0 ? new Float32Array(pairs.length / 6).fill(batchId) : undefined;
|
|
3829
|
-
const lines = new THREE.Mesh(
|
|
3830
|
-
buildThickLineGeometry(pairs, batchIds),
|
|
3831
|
-
this._createSelectionEdgeMaterial(mesh)
|
|
3832
|
-
);
|
|
3833
|
-
lines.renderOrder = 4;
|
|
3834
|
-
// 屏幕空间外扩后可能超出原包围球,关掉剔除更稳
|
|
3835
|
-
lines.frustumCulled = false;
|
|
3836
|
-
lines.raycast = () => {};
|
|
3837
|
-
(lines.userData as MeshUserData).isSelectionEdge = true;
|
|
3838
|
-
if (mesh === result.mesh && result.instanceIndex != null) {
|
|
3839
|
-
this._applyInstanceTransform(lines, result);
|
|
3840
|
-
}
|
|
3841
|
-
mesh.add(lines); // 作为子节点,直接继承世界变换
|
|
3842
|
-
this._selectionEdgeMeshes.push(lines);
|
|
3843
|
-
}
|
|
3844
|
-
}
|
|
3845
|
-
|
|
3846
|
-
if (this._selectionEdgeMeshes.length === 0) {
|
|
3847
|
-
// 抽不出折边(退化几何 / 没有任何夹角)时退回外扩轮廓,至少保证选中可见
|
|
3848
|
-
this._addSelectionHull(result, meshes);
|
|
3849
|
-
return;
|
|
3850
|
-
}
|
|
3851
|
-
this._selectionEdges = this._selectionEdgeMeshes[0] ?? null;
|
|
3852
|
-
}
|
|
3853
|
-
|
|
3854
|
-
/** 实例化几何:把某个实例的矩阵套到一个普通 Mesh/子节点上(子节点不参与父级的 instanceMatrix) */
|
|
3855
|
-
private _applyInstanceTransform(obj: THREE.Object3D, result: PickResult): void {
|
|
3856
|
-
const im = result.mesh as THREE.InstancedMesh;
|
|
3857
|
-
if (result.instanceIndex == null || !im.isInstancedMesh) return;
|
|
3858
|
-
const m = new THREE.Matrix4();
|
|
3859
|
-
im.getMatrixAt(result.instanceIndex, m);
|
|
3860
|
-
m.decompose(obj.position, obj.quaternion, obj.scale);
|
|
3861
|
-
}
|
|
3862
|
-
|
|
3863
|
-
/** 旧的描边画法:背面外扩法,整块几何一起上,靠 uSelBatch 只显示选中那栋 */
|
|
3864
|
-
private _addSelectionHull(result: PickResult, meshes: Iterable<THREE.Mesh>): void {
|
|
3865
|
-
const instanced = result.instanceIndex != null;
|
|
3866
|
-
// 实例化:轮廓是这个实例的单独一份副本(普通 Mesh),所以整份都画;
|
|
3867
|
-
// 顶点区间型:整块几何一起画,靠 uSelBatch 只显示选中那栋。
|
|
3868
|
-
this.outlineMaterial.uniforms.uSelBatch.value = instanced ? -1 : result.batchId;
|
|
3869
|
-
this.outlineMaterial.uniforms.uSelWhole.value = instanced || result.batchId < 0 ? 1 : 0;
|
|
3870
|
-
this.outlineMaterial.uniforms.uElevation.value = instanced
|
|
3871
|
-
? this._selectionElevation(result)
|
|
3872
|
-
: this.style.selectElevation ?? 0;
|
|
3873
|
-
|
|
3874
|
-
for (const mesh of meshes) {
|
|
3875
|
-
const mat = styledMaterialOf(mesh);
|
|
3876
|
-
if (mat?.userData?.uBatchTex) {
|
|
3877
|
-
this.outlineMaterial.uniforms.uBatchTex.value = mat.userData.uBatchTex.value as THREE.Texture;
|
|
3878
|
-
this.outlineMaterial.uniforms.uBatchTexWidth.value = mat.userData.uBatchTexWidth.value as number;
|
|
3879
|
-
this.outlineMaterial.uniforms.uBatchTexActive.value = mat.userData.uBatchTexActive.value as number;
|
|
3880
|
-
}
|
|
3881
|
-
const outline = new THREE.Mesh(mesh.geometry, this.outlineMaterial);
|
|
3882
|
-
outline.renderOrder = 3;
|
|
3883
|
-
outline.frustumCulled = false;
|
|
3884
|
-
outline.raycast = () => {};
|
|
3885
|
-
if (mesh === result.mesh && result.instanceIndex != null) {
|
|
3886
|
-
this._applyInstanceTransform(outline, result);
|
|
3887
|
-
}
|
|
3888
|
-
mesh.add(outline); // 作为子节点,直接继承世界变换
|
|
3889
|
-
this._outlineMeshes.push(outline);
|
|
3890
|
-
}
|
|
3891
|
-
this._outlineMesh = this._outlineMeshes[0] ?? null;
|
|
2339
|
+
select(result: PickResult | null, opts: { silent?: boolean } = {}): void {
|
|
2340
|
+
this._sel.select(result, opts);
|
|
3892
2341
|
}
|
|
3893
2342
|
|
|
3894
|
-
/**
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
const u = this.selectionEdgeMaterial.uniforms;
|
|
3898
|
-
u.uColor.value.set(s.outlineColor ?? s.selectColor);
|
|
3899
|
-
u.uWidth.value = Math.max(0.1, s.outlineWidthPx ?? 3);
|
|
3900
|
-
u.uOpacity.value = clampOpacity(s.opacity);
|
|
3901
|
-
// 只加全局 selectElevation:单体自己的拔高已经写在状态纹理第 2 行 z 里,
|
|
3902
|
-
// 顶点着色器采样时带出来了,这里再加一次就会双重拔高
|
|
3903
|
-
if (u.uElevation) u.uElevation.value = s.selectElevation ?? 0;
|
|
3904
|
-
|
|
3905
|
-
// 外扩描边是另一个材质实例,单体状态纹理的开关是建的时候抄过去的。
|
|
3906
|
-
// 先选中、再抬高(或反过来)时模板会停留在旧值,这里补一次同步。
|
|
3907
|
-
for (const outline of this._outlineMeshes) {
|
|
3908
|
-
const parent = outline.parent as THREE.Mesh | null;
|
|
3909
|
-
const ud = parent ? styledMaterialOf(parent)?.userData : null;
|
|
3910
|
-
if (ud?.uBatchTex) {
|
|
3911
|
-
this.outlineMaterial.uniforms.uBatchTex.value = ud.uBatchTex.value;
|
|
3912
|
-
this.outlineMaterial.uniforms.uBatchTexWidth.value = ud.uBatchTexWidth.value;
|
|
3913
|
-
this.outlineMaterial.uniforms.uBatchTexActive.value = ud.uBatchTexActive.value;
|
|
3914
|
-
}
|
|
3915
|
-
}
|
|
2343
|
+
/** @internal 选中外观跟随样式变化,见 viewer/SelectionDecorator.ts */
|
|
2344
|
+
_syncSelectionStyle(): void {
|
|
2345
|
+
this._sel.syncStyle();
|
|
3916
2346
|
}
|
|
3917
2347
|
|
|
3918
|
-
/**
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
if (result.smid != null) {
|
|
3922
|
-
const off = this._featureStyleManager.getFeatureEffectiveOffset(result.smid);
|
|
3923
|
-
if (off.elevation) elev += off.elevation;
|
|
3924
|
-
}
|
|
3925
|
-
return elev;
|
|
2348
|
+
/** @internal 测试用:当前选中 */
|
|
2349
|
+
get _selection(): PickResult | null {
|
|
2350
|
+
return this._sel.selection;
|
|
3926
2351
|
}
|
|
3927
2352
|
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
this._outlineMeshes = [];
|
|
3933
|
-
this._outlineMesh = null;
|
|
2353
|
+
/** @internal 测试用:被高亮的网格 */
|
|
2354
|
+
get _selectedMeshes(): THREE.Object3D[] {
|
|
2355
|
+
return this._sel.selectedMeshes;
|
|
2356
|
+
}
|
|
3934
2357
|
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
}
|
|
3939
|
-
this._selectionEdgeMeshes = [];
|
|
3940
|
-
this._selectionEdges = null;
|
|
2358
|
+
/** @internal 测试用:选中棱线的网格列表 */
|
|
2359
|
+
get _selectionEdgeMeshes(): THREE.Mesh[] {
|
|
2360
|
+
return this._sel.selectionEdgeMeshes;
|
|
3941
2361
|
}
|
|
3942
2362
|
|
|
3943
2363
|
/* ---------------------------------------------------------------- 渲染 */
|
|
@@ -3948,31 +2368,6 @@ export class S3MViewer {
|
|
|
3948
2368
|
* 参数全部来自 style,不缓存:`edgeWidth` 在这里是「采样偏移」,线宽 ≈ 2×它;
|
|
3949
2369
|
* `edgeThreshold` 是折边敏感度(夹角小于它不描),和 CPU 折边线同一口径。
|
|
3950
2370
|
*/
|
|
3951
|
-
private _renderGpuEdges(canvas: HTMLCanvasElement): void {
|
|
3952
|
-
if (!this.renderer || !this.scene || !this.camera) return;
|
|
3953
|
-
const s = this.style;
|
|
3954
|
-
const fade = this.uniforms.uEdgeFade?.value ?? 1;
|
|
3955
|
-
const opacity =
|
|
3956
|
-
Math.min(1, Math.max(0, s.edgeOpacity ?? 0.6)) * clampOpacity(s.opacity) * fade;
|
|
3957
|
-
if (opacity <= 0.004) return;
|
|
3958
|
-
|
|
3959
|
-
if (!this._gpuEdge) this._gpuEdge = new GpuEdgePass();
|
|
3960
|
-
if (!this._gpuEdge.supported(this.renderer)) return;
|
|
3961
|
-
|
|
3962
|
-
// 度数 → 归一化法线差(|Δn| = 2·sin(θ/2)),乘 0.6 让阈值落在「刚好能描出」的位置
|
|
3963
|
-
const deg = Math.min(179, Math.max(1, s.edgeThreshold ?? 55));
|
|
3964
|
-
const normalThreshold = 2 * Math.sin((deg * Math.PI) / 360) * 0.6;
|
|
3965
|
-
|
|
3966
|
-
this._gpuEdge.render(this.renderer, this.scene, this.camera, canvas.width, canvas.height, {
|
|
3967
|
-
color: this._gpuEdgeColor.set(s.edgeColor),
|
|
3968
|
-
opacity,
|
|
3969
|
-
radius: Math.max(1, s.edgeWidth ?? 1.5),
|
|
3970
|
-
normalThreshold,
|
|
3971
|
-
depthThreshold: 0.08,
|
|
3972
|
-
});
|
|
3973
|
-
}
|
|
3974
|
-
|
|
3975
|
-
/** 等宽线要按设备像素换算线宽:uResolution 用绘图缓冲尺寸,uDpr 是缓冲 / CSS 的比值 */
|
|
3976
2371
|
private _updateLineResolution(canvas: HTMLCanvasElement): void {
|
|
3977
2372
|
const w = canvas.width || 1;
|
|
3978
2373
|
const h = canvas.height || 1;
|
|
@@ -4067,6 +2462,29 @@ export class S3MViewer {
|
|
|
4067
2462
|
}
|
|
4068
2463
|
}
|
|
4069
2464
|
|
|
2465
|
+
/** 当前每个数据集的驻留显存/内存字节预算。Infinity = 不限制 */
|
|
2466
|
+
get maxMemory(): number {
|
|
2467
|
+
return this.options.maxMemory ?? Infinity;
|
|
2468
|
+
}
|
|
2469
|
+
|
|
2470
|
+
/**
|
|
2471
|
+
* 调整每个数据集驻留显存字节数的软上限(默认 Infinity = 不限制)。
|
|
2472
|
+
* 不传 `datasetId` 时对所有已存在数据集立即生效,并作为之后新增数据集的默认值。
|
|
2473
|
+
* 实际裁剪发生在每帧调度末尾(make-before-break),不会当场开洞。
|
|
2474
|
+
*/
|
|
2475
|
+
setMaxMemory(bytes: number, datasetId?: string): void {
|
|
2476
|
+
const next = Number.isFinite(bytes) && bytes > 0 ? bytes : Infinity;
|
|
2477
|
+
if (datasetId) {
|
|
2478
|
+
const entry = this.datasets.get(datasetId);
|
|
2479
|
+
if (entry?.tileset) (entry.tileset as any).setMaxMemory(next);
|
|
2480
|
+
} else {
|
|
2481
|
+
this.options.maxMemory = next;
|
|
2482
|
+
for (const entry of this.datasets.values()) {
|
|
2483
|
+
if (entry.tileset) (entry.tileset as any).setMaxMemory(next);
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
|
|
4070
2488
|
/** 当前各向异性过滤上限。Infinity = 不封顶 */
|
|
4071
2489
|
get anisotropyCap(): number {
|
|
4072
2490
|
return this.options.anisotropyCap ?? Infinity;
|
|
@@ -4125,16 +2543,9 @@ export class S3MViewer {
|
|
|
4125
2543
|
* 乱序混合就是满屏彩色噪点。相机没动且没有新瓦片时整个跳过;
|
|
4126
2544
|
* 单个瓦片顺序本来就对时 sortSplatMesh 内部也会跳过重写。
|
|
4127
2545
|
*/
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
if (this._splatsSorted && this._lastSplatSortView.equals(cam.matrixWorldInverse)) return;
|
|
4132
|
-
for (const pc of this._pointClouds) {
|
|
4133
|
-
if (!(pc.userData as { isGaussianSplat?: boolean }).isGaussianSplat) continue;
|
|
4134
|
-
sortSplatMesh(pc as unknown as THREE.Mesh, cam.matrixWorldInverse);
|
|
4135
|
-
}
|
|
4136
|
-
this._lastSplatSortView.copy(cam.matrixWorldInverse);
|
|
4137
|
-
this._splatsSorted = true;
|
|
2546
|
+
/** @internal 泼溅按视深重排,见 viewer/SplatLayer.ts */
|
|
2547
|
+
_sortSplats(): void {
|
|
2548
|
+
this._splats.sort(this.camera, this._pointClouds);
|
|
4138
2549
|
}
|
|
4139
2550
|
|
|
4140
2551
|
/**
|
|
@@ -4240,7 +2651,7 @@ export class S3MViewer {
|
|
|
4240
2651
|
this.renderer.render(this.scene, this.camera);
|
|
4241
2652
|
// GPU 描边必须画在主场景之后:它的全屏 quad 参与宿主深度测试,
|
|
4242
2653
|
// 依赖本帧已经写好的深度(被地形挡住的地方不该描边)
|
|
4243
|
-
|
|
2654
|
+
this._edges.renderGpuEdges(canvas);
|
|
4244
2655
|
}
|
|
4245
2656
|
|
|
4246
2657
|
this._frame++;
|
|
@@ -4261,7 +2672,7 @@ export class S3MViewer {
|
|
|
4261
2672
|
tilesetChanged;
|
|
4262
2673
|
|
|
4263
2674
|
// 首帧画完、场上有材质了,再把另一种 shading 变体编出来(异步,不挡当前帧)
|
|
4264
|
-
if (this._prewarmShadingEnabled && !this._shadingPrewarmed && this.
|
|
2675
|
+
if (this._prewarmShadingEnabled && !this._shadingPrewarmed && this._picks.pickables.size > 0) {
|
|
4265
2676
|
void this.prewarmShading();
|
|
4266
2677
|
}
|
|
4267
2678
|
|