s3m-three 0.4.22 → 0.4.24
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/analysis/clipping.d.ts +63 -0
- package/dist/analysis/clipping.d.ts.map +1 -0
- package/dist/analysis/clipping.js +69 -0
- package/dist/analysis/clipping.js.map +1 -0
- package/dist/analysis/depth.d.ts +55 -0
- package/dist/analysis/depth.d.ts.map +1 -0
- package/dist/analysis/depth.js +99 -0
- package/dist/analysis/depth.js.map +1 -0
- package/dist/analysis/index.d.ts +21 -0
- package/dist/analysis/index.d.ts.map +1 -0
- package/dist/analysis/index.js +17 -0
- package/dist/analysis/index.js.map +1 -0
- package/dist/analysis/lineOfSight.d.ts +60 -0
- package/dist/analysis/lineOfSight.d.ts.map +1 -0
- package/dist/analysis/lineOfSight.js +56 -0
- package/dist/analysis/lineOfSight.js.map +1 -0
- package/dist/analysis/measure.d.ts +56 -0
- package/dist/analysis/measure.d.ts.map +1 -0
- package/dist/analysis/measure.js +95 -0
- package/dist/analysis/measure.js.map +1 -0
- package/dist/analysis/skyline.d.ts +55 -0
- package/dist/analysis/skyline.d.ts.map +1 -0
- package/dist/analysis/skyline.js +78 -0
- package/dist/analysis/skyline.js.map +1 -0
- package/dist/analysis/viewshed.d.ts +48 -0
- package/dist/analysis/viewshed.d.ts.map +1 -0
- package/dist/analysis/viewshed.js +115 -0
- package/dist/analysis/viewshed.js.map +1 -0
- package/dist/browser/s3m-three.iife.ext-three.js +52 -35
- package/dist/browser/s3m-three.iife.js +47 -30
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/render/DatasetManager.d.ts +13 -0
- package/dist/render/DatasetManager.d.ts.map +1 -1
- package/dist/render/DatasetManager.js +20 -0
- package/dist/render/DatasetManager.js.map +1 -1
- package/dist/render/GeometryBuilder.d.ts +11 -0
- package/dist/render/GeometryBuilder.d.ts.map +1 -1
- package/dist/render/GeometryBuilder.js +2 -1
- package/dist/render/GeometryBuilder.js.map +1 -1
- package/dist/render/S3MViewer.d.ts +159 -5
- package/dist/render/S3MViewer.d.ts.map +1 -1
- package/dist/render/S3MViewer.js +331 -13
- package/dist/render/S3MViewer.js.map +1 -1
- package/dist/render/StyleMaterial.d.ts +10 -1
- package/dist/render/StyleMaterial.d.ts.map +1 -1
- package/dist/render/StyleMaterial.js +9 -1
- package/dist/render/StyleMaterial.js.map +1 -1
- package/dist/render/TextureFactory.d.ts +23 -1
- package/dist/render/TextureFactory.d.ts.map +1 -1
- package/dist/render/TextureFactory.js +32 -8
- package/dist/render/TextureFactory.js.map +1 -1
- package/dist/render/analysis/DepthPass.d.ts +78 -0
- package/dist/render/analysis/DepthPass.d.ts.map +1 -0
- package/dist/render/analysis/DepthPass.js +209 -0
- package/dist/render/analysis/DepthPass.js.map +1 -0
- package/dist/render/viewer/LightRig.d.ts +21 -7
- package/dist/render/viewer/LightRig.d.ts.map +1 -1
- package/dist/render/viewer/LightRig.js +11 -13
- package/dist/render/viewer/LightRig.js.map +1 -1
- package/dist/render/viewer/PickService.d.ts +15 -0
- package/dist/render/viewer/PickService.d.ts.map +1 -1
- package/dist/render/viewer/PickService.js +41 -0
- package/dist/render/viewer/PickService.js.map +1 -1
- package/dist/render/viewer/types.d.ts +12 -0
- package/dist/render/viewer/types.d.ts.map +1 -1
- package/dist/render/viewer/types.js.map +1 -1
- package/dist/tileset/S3MTileset.d.ts +19 -0
- package/dist/tileset/S3MTileset.d.ts.map +1 -1
- package/dist/tileset/S3MTileset.js +16 -0
- package/dist/tileset/S3MTileset.js.map +1 -1
- package/docs/API.md +124 -3
- package/package.json +1 -1
- package/src/analysis/clipping.ts +135 -0
- package/src/analysis/depth.ts +126 -0
- package/src/analysis/index.ts +47 -0
- package/src/analysis/lineOfSight.ts +110 -0
- package/src/analysis/measure.ts +119 -0
- package/src/analysis/skyline.ts +125 -0
- package/src/analysis/viewshed.ts +160 -0
- package/src/index.ts +5 -0
- package/src/render/DatasetManager.ts +21 -0
- package/src/render/GeometryBuilder.ts +13 -1
- package/src/render/S3MViewer.ts +401 -14
- package/src/render/StyleMaterial.ts +16 -2
- package/src/render/TextureFactory.ts +50 -8
- package/src/render/analysis/DepthPass.ts +278 -0
- package/src/render/viewer/LightRig.ts +23 -13
- package/src/render/viewer/PickService.ts +47 -0
- package/src/render/viewer/types.ts +12 -0
- package/src/tileset/S3MTileset.ts +24 -0
package/src/render/S3MViewer.ts
CHANGED
|
@@ -25,8 +25,9 @@ import { S3MTileset, usableGeoBounds } from '../tileset/S3MTileset.js';
|
|
|
25
25
|
import type { BuildTileFn, TileNode } from '../tileset/S3MTileset.js';
|
|
26
26
|
import type { AuthenticateFn } from '../core/AuthFetch.js';
|
|
27
27
|
import { setSplatViewport } from './GeometryBuilder.js';
|
|
28
|
-
import { setMaxAnisotropy, setCompressedTexturesSupported, setTextureOffThreadDecode, setMaxTextureSize as setTextureSizeCap } from './TextureFactory.js';
|
|
29
|
-
import {
|
|
28
|
+
import { setMaxAnisotropy, setCompressedTexturesSupported, setTextureOffThreadDecode, setMaxTextureSize as setTextureSizeCap, applyAnisotropyCap } from './TextureFactory.js';
|
|
29
|
+
import type { TextureCreateOptions } from './TextureFactory.js';
|
|
30
|
+
import { getMaterialQuality, normalizeMaterialQuality, getColorPipeline, normalizeColorPipeline } from './StyleMaterial.js';
|
|
30
31
|
import type { LightingPreset } from './lightingPreset.js';
|
|
31
32
|
import { LightRig } from './viewer/LightRig.js';
|
|
32
33
|
import { HeightIndex } from './viewer/HeightIndex.js';
|
|
@@ -62,13 +63,37 @@ import type {
|
|
|
62
63
|
EdgeModes,
|
|
63
64
|
EdgesOption,
|
|
64
65
|
MaterialQuality,
|
|
66
|
+
ColorPipeline,
|
|
65
67
|
} from './StyleMaterial.js';
|
|
66
68
|
import { applyPointCloudStyle, isOwnedPointCloudMaterial } from './PointCloudMaterial.js';
|
|
67
69
|
import type { DegradationKind, UnsupportedPolicy } from '../core/degradations.js';
|
|
68
70
|
import { Picker } from './Picker.js';
|
|
69
71
|
import { GpuEdgePass } from './GpuEdgePass.js';
|
|
70
72
|
import { Animator } from '../anim/Animator.js';
|
|
71
|
-
import
|
|
73
|
+
import {
|
|
74
|
+
buildClipPlanes,
|
|
75
|
+
lineOfSight as computeLineOfSight,
|
|
76
|
+
measureSegment,
|
|
77
|
+
polygonArea,
|
|
78
|
+
polygonAreaHorizontal,
|
|
79
|
+
polygonPerimeter,
|
|
80
|
+
polylineLength,
|
|
81
|
+
polylineLengthHorizontal,
|
|
82
|
+
} from '../analysis/index.js';
|
|
83
|
+
import type {
|
|
84
|
+
ClipOptions,
|
|
85
|
+
LineOfSightOptions,
|
|
86
|
+
LineOfSightResult,
|
|
87
|
+
LngLatAlt,
|
|
88
|
+
MeasureResult,
|
|
89
|
+
SkylineResult,
|
|
90
|
+
Vec3,
|
|
91
|
+
ViewshedResult,
|
|
92
|
+
} from '../analysis/index.js';
|
|
93
|
+
import { skylineFromDepth } from '../analysis/skyline.js';
|
|
94
|
+
import { computeViewshed, makeCubeDistanceSampler } from '../analysis/viewshed.js';
|
|
95
|
+
import { captureRadialCube, captureSkylineDepth } from './analysis/DepthPass.js';
|
|
96
|
+
import type { DepthCaptureContext } from './analysis/DepthPass.js';import type { Channel, TransitionOptions } from '../anim/Animator.js';
|
|
72
97
|
import { Path } from '../anim/Path.js';
|
|
73
98
|
import type { EasingFn, EasingName } from '../anim/easing.js';
|
|
74
99
|
import type { LineSharedUniforms, ThickLineMaterial } from './EdgeLines.js';
|
|
@@ -86,7 +111,7 @@ import type {
|
|
|
86
111
|
StyleUniforms,
|
|
87
112
|
} from '../core/types.js';
|
|
88
113
|
|
|
89
|
-
import { lngLatFromWorld, type GeoAdapter } from './GeoAdapter.js';
|
|
114
|
+
import { lngLatFromWorld, worldFromLngLat, type GeoAdapter } from './GeoAdapter.js';
|
|
90
115
|
|
|
91
116
|
import {
|
|
92
117
|
DatasetManager,
|
|
@@ -204,6 +229,18 @@ const STYLE_SCALAR_KEYS = [
|
|
|
204
229
|
'outlineWidth',
|
|
205
230
|
] as const;
|
|
206
231
|
|
|
232
|
+
/**
|
|
233
|
+
* 标量样式键在「用户还没设过」时的动画起点。
|
|
234
|
+
*
|
|
235
|
+
* 只对 `metalness` / `roughness` 有意义:它们的 uniform 默认是 -1(不覆盖,保持数据
|
|
236
|
+
* 自身的值),从 -1 插值没意义 —— 取数据侧常见值当起点,过渡才不会先跳到 0。
|
|
237
|
+
* 其余键缺省仍是 0(与旧行为一致)。
|
|
238
|
+
*/
|
|
239
|
+
const SCALAR_ANIM_START: Partial<Record<(typeof STYLE_SCALAR_KEYS)[number], number>> = {
|
|
240
|
+
metalness: 0, // S3M 数据的 metalness 恒为 0
|
|
241
|
+
roughness: 0.95, // shininess 映射后的常见值,见 GeometryBuilder 的 roughness
|
|
242
|
+
};
|
|
243
|
+
|
|
207
244
|
/** 能逐帧插值的颜色键(在线性空间插值,与 shader 消费端一致) */
|
|
208
245
|
const STYLE_COLOR_KEYS = ['colorA', 'colorB', 'colorC', 'selectColor', 'edgeColor'] as const;
|
|
209
246
|
|
|
@@ -257,6 +294,19 @@ export class S3MViewer {
|
|
|
257
294
|
* 传了值就只认自己,多图层同页各用各的,互不覆盖。
|
|
258
295
|
*/
|
|
259
296
|
private _materialQuality: MaterialQuality | undefined;
|
|
297
|
+
/**
|
|
298
|
+
* 本图层的颜色管线。
|
|
299
|
+
*
|
|
300
|
+
* `undefined` = 没指定,跟随模块级默认(`getColorPipeline()`)。
|
|
301
|
+
* 与 materialQuality 同理:传了值就只认自己,同页多图层互不污染 —— 这条尤其重要,
|
|
302
|
+
* 因为 `lightingPreset: 'supermap'` 会同时改管线、纹理解码方式与各向异性,
|
|
303
|
+
* 以前是三个模块级全局一起被改写。
|
|
304
|
+
*/
|
|
305
|
+
private _colorPipeline: ColorPipeline | undefined;
|
|
306
|
+
/** fade-through 编排的代次:中途再切一次,旧编排作废(与 setMode 同一套) */
|
|
307
|
+
private _pipelineGen = 0;
|
|
308
|
+
/** 当前裁剪配置;renderer 就绪前后都要能设,所以单独存一份(见 `_applyClipping`) */
|
|
309
|
+
private _clip: ClipOptions | null = null;
|
|
260
310
|
/** 单体样式、专题着色与状态纹理管理器 */
|
|
261
311
|
private readonly _featureStyleManager: FeatureStyleManager;
|
|
262
312
|
/** GPU 路径动画管理器 */
|
|
@@ -395,7 +445,12 @@ export class S3MViewer {
|
|
|
395
445
|
);
|
|
396
446
|
|
|
397
447
|
this.uniforms = createSharedUniforms();
|
|
398
|
-
//
|
|
448
|
+
// 图层 profile 先落位(显式传了就只认自己),再把 LightRig 挂上来 ——
|
|
449
|
+
// 它构造时会按 lightingPreset 覆盖一次管线,这一步走的是图层级通道,
|
|
450
|
+
// 不再去改模块级全局(以前三个全局一起被改写,同页多图层互相污染)。
|
|
451
|
+
this._colorPipeline = normalizeColorPipeline(options.colorPipeline);
|
|
452
|
+
this._applyRenderProfile({}, true);
|
|
453
|
+
// 光源状态在这里就位:LightRig 构造时会按 lightingPreset 切图层颜色管线,
|
|
399
454
|
// 挂进场景要等 scene 建好(构造函数上面已经建了)。
|
|
400
455
|
this._lights = new LightRig({
|
|
401
456
|
scene: () => this.scene,
|
|
@@ -406,6 +461,8 @@ export class S3MViewer {
|
|
|
406
461
|
// 走公共 triggerRepaint 而不是 _requestRepaint:宿主 / 单测会覆盖前者来观察重绘,
|
|
407
462
|
// 绕过它会让「有没有触发重绘」这件事失去观测点
|
|
408
463
|
repaint: () => this.triggerRepaint(),
|
|
464
|
+
// 构造期(initial)只记状态:那时还没有材质可重建、没有数据集可同步
|
|
465
|
+
applyRenderProfile: (p, initial) => this._applyRenderProfile(p, initial),
|
|
409
466
|
});
|
|
410
467
|
this._lights.addToScene();
|
|
411
468
|
this._heights = new HeightIndex(this.uniforms);
|
|
@@ -534,6 +591,8 @@ export class S3MViewer {
|
|
|
534
591
|
this.renderer = this._createRenderer(canvas, gl);
|
|
535
592
|
setMaxAnisotropy(this.renderer.capabilities.getMaxAnisotropy(), this.options.anisotropyCap ?? Infinity);
|
|
536
593
|
this._syncTextureCapabilities(gl);
|
|
594
|
+
// 裁剪是构造期就能设的(还没 mount 时 `_applyClipping` 会先跳过),这里补挂一次
|
|
595
|
+
this._applyClipping();
|
|
537
596
|
|
|
538
597
|
this.scene = new THREE.Scene();
|
|
539
598
|
this.scene.add(this.groundManager.group);
|
|
@@ -576,6 +635,8 @@ export class S3MViewer {
|
|
|
576
635
|
this.renderer = this._createRenderer(this.canvas, gl);
|
|
577
636
|
setMaxAnisotropy(this.renderer.capabilities.getMaxAnisotropy(), this.options.anisotropyCap ?? Infinity);
|
|
578
637
|
this._syncTextureCapabilities(gl);
|
|
638
|
+
// 新 renderer 的 clippingPlanes 是空的,裁剪状态要重新挂上去
|
|
639
|
+
this._applyClipping();
|
|
579
640
|
this.picker?.dispose();
|
|
580
641
|
this.picker = new Picker(this.renderer);
|
|
581
642
|
// GPU 描边的 RT / 材质绑定在旧上下文上,整体丢弃,下一帧按需重建
|
|
@@ -1855,7 +1916,13 @@ export class S3MViewer {
|
|
|
1855
1916
|
this.animator.register(`style.${key}`, {
|
|
1856
1917
|
dim: 1,
|
|
1857
1918
|
read: (out) => {
|
|
1858
|
-
|
|
1919
|
+
// 金属度 / 粗糙度没设过时 uniform 是 -1(= 不覆盖,保持数据自身的值),
|
|
1920
|
+
// 动画起点不能取 0:那会让 roughness 从 0.95 先跳到 0 再爬回去。
|
|
1921
|
+
// 这里给的是数据侧的常见值(S3M 的 shininess 映射后 ≈0.95、metalness 恒 0),
|
|
1922
|
+
// 过渡起点因此是连续的。
|
|
1923
|
+
out[0] = Number(
|
|
1924
|
+
(this.style as unknown as Record<string, number>)[key] ?? SCALAR_ANIM_START[key] ?? 0
|
|
1925
|
+
);
|
|
1859
1926
|
},
|
|
1860
1927
|
write: (v) => {
|
|
1861
1928
|
(this.style as unknown as Record<string, number>)[key] = v[0];
|
|
@@ -2115,6 +2182,112 @@ export class S3MViewer {
|
|
|
2115
2182
|
return this._materialQuality ?? getMaterialQuality();
|
|
2116
2183
|
}
|
|
2117
2184
|
|
|
2185
|
+
/**
|
|
2186
|
+
* 运行时切换**本图层**的颜色管线(`'linear'` | `'direct'`)。
|
|
2187
|
+
*
|
|
2188
|
+
* `'direct'` = 官方 iClient3D 的 sRGB 直乘管线(配合纹理解码关 sRGB),
|
|
2189
|
+
* 见 `lightingPreset: 'supermap'`。切换牵扯三样东西:材质宏(换 program)、
|
|
2190
|
+
* 纹理 colorSpace(重传,**不重新下载瓦片**)、以及各向异性档位。
|
|
2191
|
+
*
|
|
2192
|
+
* 给 `duration > 0` 时走 fade-through:压暗 → 在同一帧回调里换管线 → 亮回来。
|
|
2193
|
+
* 逐像素混合做不到干净 —— 两条管线的差异有一部分在采样端(硬件 sRGB 解码 vs
|
|
2194
|
+
* 原始字节),要混合就得让贴图恒为原始字节并在片元里手动解码,那会改掉默认路径。
|
|
2195
|
+
*/
|
|
2196
|
+
setColorPipeline(q: ColorPipeline, opts: { duration?: number; dim?: number } = {}): Promise<void> {
|
|
2197
|
+
const next = normalizeColorPipeline(q) ?? 'linear';
|
|
2198
|
+
if (next === this.colorPipeline) return Promise.resolve();
|
|
2199
|
+
|
|
2200
|
+
const d = Math.max(0, opts.duration ?? 0);
|
|
2201
|
+
if (d <= 0) {
|
|
2202
|
+
this._applyRenderProfile({ colorPipeline: next });
|
|
2203
|
+
return Promise.resolve();
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
const dim = Math.min(1, Math.max(0.05, opts.dim ?? 0.35));
|
|
2207
|
+
const gen = ++this._pipelineGen;
|
|
2208
|
+
return new Promise<void>((resolve) => {
|
|
2209
|
+
this.animator.to('style.fade', dim, {
|
|
2210
|
+
duration: d,
|
|
2211
|
+
easing: 'easeInOutQuad',
|
|
2212
|
+
onComplete: () => {
|
|
2213
|
+
// 中途又切了一次:这次编排作废,收尾交给新的那次
|
|
2214
|
+
if (gen !== this._pipelineGen) {
|
|
2215
|
+
resolve();
|
|
2216
|
+
return;
|
|
2217
|
+
}
|
|
2218
|
+
this._applyRenderProfile({ colorPipeline: next });
|
|
2219
|
+
this.animator.to('style.fade', 1, {
|
|
2220
|
+
duration: d,
|
|
2221
|
+
easing: 'easeInOutQuad',
|
|
2222
|
+
onComplete: resolve,
|
|
2223
|
+
});
|
|
2224
|
+
},
|
|
2225
|
+
});
|
|
2226
|
+
});
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
/** 当前颜色管线(本图层指定了就用本图层的,否则是模块级默认) */
|
|
2230
|
+
get colorPipeline(): ColorPipeline {
|
|
2231
|
+
return this._colorPipeline ?? getColorPipeline();
|
|
2232
|
+
}
|
|
2233
|
+
|
|
2234
|
+
/** 本图层此刻该用的纹理创建参数(图层的渲染 profile 就落在这里) */
|
|
2235
|
+
private _textureOptions(): TextureCreateOptions {
|
|
2236
|
+
return {
|
|
2237
|
+
// direct 管线的前提是贴图字节值不解码,两者必须一起切
|
|
2238
|
+
srgbDecode: this.colorPipeline !== 'direct',
|
|
2239
|
+
maxSize: this.options.maxTextureSize ?? Infinity,
|
|
2240
|
+
anisotropyCap: this.options.anisotropyCap ?? Infinity,
|
|
2241
|
+
};
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
/**
|
|
2245
|
+
* 应用图层渲染 profile(颜色管线 / 各向异性上限)。
|
|
2246
|
+
*
|
|
2247
|
+
* `initial` = 构造期:只记状态并推给数据集管理器,不重建材质、不重传纹理
|
|
2248
|
+
* (那时还没有瓦片)。
|
|
2249
|
+
*/
|
|
2250
|
+
private _applyRenderProfile(
|
|
2251
|
+
patch: { colorPipeline?: ColorPipeline; anisotropyCap?: number },
|
|
2252
|
+
initial = false
|
|
2253
|
+
): void {
|
|
2254
|
+
const prev = this.colorPipeline;
|
|
2255
|
+
if (patch.colorPipeline !== undefined) {
|
|
2256
|
+
this._colorPipeline = normalizeColorPipeline(patch.colorPipeline) ?? 'linear';
|
|
2257
|
+
}
|
|
2258
|
+
if (patch.anisotropyCap !== undefined) this.options.anisotropyCap = patch.anisotropyCap;
|
|
2259
|
+
|
|
2260
|
+
const textureOptions = this._textureOptions();
|
|
2261
|
+
// 之后加载的瓦片按新参数走(已建好的数据集也要跟着换)
|
|
2262
|
+
this._datasetManager.setRenderProfile({ colorPipeline: this.colorPipeline, textureOptions });
|
|
2263
|
+
if (initial) return;
|
|
2264
|
+
|
|
2265
|
+
// 管线变了才需要换 program;只改各向异性时重建纯属浪费
|
|
2266
|
+
if (this.colorPipeline !== prev) this._rebuildStyledMaterials();
|
|
2267
|
+
this._syncCachedTextures(textureOptions);
|
|
2268
|
+
this._requestRepaint();
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
/**
|
|
2272
|
+
* 把纹理解码方式 / 各向异性同步到**已上传**的纹理上。
|
|
2273
|
+
*
|
|
2274
|
+
* 两者都不需要重新解码:`colorSpace` 改了 + `needsUpdate` 让 three 重传一次像素,
|
|
2275
|
+
* `anisotropy` 同理。所以切管线不用重拉瓦片,代价只有一次纹理上传。
|
|
2276
|
+
*/
|
|
2277
|
+
private _syncCachedTextures(opts: TextureCreateOptions): void {
|
|
2278
|
+
const want = opts.srgbDecode ? THREE.SRGBColorSpace : THREE.NoColorSpace;
|
|
2279
|
+
for (const entry of this.datasets.values()) {
|
|
2280
|
+
const cache = entry.tileset?.textureCache;
|
|
2281
|
+
if (!cache) continue;
|
|
2282
|
+
for (const tex of cache.values()) {
|
|
2283
|
+
if (tex.colorSpace === want) continue;
|
|
2284
|
+
tex.colorSpace = want;
|
|
2285
|
+
tex.needsUpdate = true;
|
|
2286
|
+
}
|
|
2287
|
+
applyAnisotropyCap(cache.values(), opts.anisotropyCap ?? Infinity);
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2118
2291
|
/** 按创建参数原位重建所有 styled 材质(materialQuality 切换用) */
|
|
2119
2292
|
private _rebuildStyledMaterials(): void {
|
|
2120
2293
|
// 逐材质 own uniform 的状态要在重建时搬过去,否则选中高亮 / 瓦片淡入会闪没
|
|
@@ -2134,9 +2307,14 @@ export class S3MViewer {
|
|
|
2134
2307
|
const old = raw as StyledMaterial | undefined;
|
|
2135
2308
|
if (!old?.userData?.isStyled || !old.userData?.s3mOpts) continue;
|
|
2136
2309
|
const opts = old.userData.s3mOpts as StyledMaterialOptions;
|
|
2137
|
-
//
|
|
2138
|
-
//
|
|
2139
|
-
const fresh = createStyledMaterial({
|
|
2310
|
+
// 显式传本层档位与管线:s3mOpts 里记的是创建时的参数,可能是 undefined(跟随全局),
|
|
2311
|
+
// 重建时必须钉成本层当前值,否则「别处改了全局」会把这一层带跑。
|
|
2312
|
+
const fresh = createStyledMaterial({
|
|
2313
|
+
...opts,
|
|
2314
|
+
uniforms: this.uniforms,
|
|
2315
|
+
materialQuality: this.materialQuality,
|
|
2316
|
+
colorPipeline: this.colorPipeline,
|
|
2317
|
+
});
|
|
2140
2318
|
for (const key of PRESERVED) {
|
|
2141
2319
|
const src = old.userData[key] as { value: unknown } | undefined;
|
|
2142
2320
|
const dst = fresh.userData[key] as { value: unknown } | undefined;
|
|
@@ -2297,6 +2475,208 @@ export class S3MViewer {
|
|
|
2297
2475
|
return this._picks.getFeatureBounds(smid);
|
|
2298
2476
|
}
|
|
2299
2477
|
|
|
2478
|
+
/* -------------------------------------------------------------- 三维空间分析 */
|
|
2479
|
+
|
|
2480
|
+
/**
|
|
2481
|
+
* 经纬度 + 高程 → 场景 ENU 米制坐标。
|
|
2482
|
+
*
|
|
2483
|
+
* 场景锚点可能还没就绪(没加数据集),此时按世界原点处理 —— 量算 API 会因此
|
|
2484
|
+
* 给出「以原点为基准」的结果,但不会抛错。真实使用里锚点在 addDataset 之后必然存在。
|
|
2485
|
+
*/
|
|
2486
|
+
private _sceneFromLngLat(p: LngLatAlt): Vec3 {
|
|
2487
|
+
const anchor = this.sceneAnchor;
|
|
2488
|
+
if (!anchor) return [0, 0, 0];
|
|
2489
|
+
const s = anchor.meterInMercator;
|
|
2490
|
+
const w = worldFromLngLat(this.geo, p.lng, p.lat, p.altitude);
|
|
2491
|
+
// 注意 y 取反:three 的 y 向上,墨卡托 y 向下(与 PickService.pickPosition 互逆)
|
|
2492
|
+
return [(w.x - anchor.x) / s, -(w.y - anchor.y) / s, p.altitude - (anchor.altitude ?? 0)];
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2495
|
+
/**
|
|
2496
|
+
* 经纬度 + 绝对高程 → 场景 ENU 米制坐标。
|
|
2497
|
+
*
|
|
2498
|
+
* 外围要做「分析代理几何」(底图建筑挤出)、或者在 three 里画自定义标注时,
|
|
2499
|
+
* 都得先把经纬度落到这个坐标系里 —— 场景的 ENU 原点在 `sceneAnchor`,
|
|
2500
|
+
* 不是经纬度,所以必须走这个换算。
|
|
2501
|
+
*/
|
|
2502
|
+
lngLatToPoint(p: LngLatAlt): Vec3 {
|
|
2503
|
+
return this._sceneFromLngLat(p);
|
|
2504
|
+
}
|
|
2505
|
+
|
|
2506
|
+
/** 场景 ENU 米制坐标 → 经纬度 + 绝对高程(`pointToLngLat` 的逆) */
|
|
2507
|
+
pointToLngLat(v: Vec3): LngLatAlt {
|
|
2508
|
+
return this._lngLatFromScene(v);
|
|
2509
|
+
}
|
|
2510
|
+
|
|
2511
|
+
/** 场景 ENU 米制坐标 → 经纬度 + 绝对高程 */
|
|
2512
|
+
private _lngLatFromScene(v: Vec3): LngLatAlt {
|
|
2513
|
+
const anchor = this.sceneAnchor;
|
|
2514
|
+
if (!anchor) return { lng: 0, lat: 0, altitude: v[2] };
|
|
2515
|
+
const s = anchor.meterInMercator;
|
|
2516
|
+
const [lng, lat] = lngLatFromWorld(this.geo, v[0] * s + anchor.x, -v[1] * s + anchor.y);
|
|
2517
|
+
return { lng, lat, altitude: v[2] + (anchor.altitude ?? 0) };
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2520
|
+
/**
|
|
2521
|
+
* 两点量算:三维距离 / 水平距离 / 高差。
|
|
2522
|
+
*
|
|
2523
|
+
* 输入是经纬度 + 绝对高程(`pickPosition` 的返回值直接能用),
|
|
2524
|
+
* 内部换算到 ENU 米再算,所以不会受投影单位影响。
|
|
2525
|
+
*/
|
|
2526
|
+
measureDistance(a: LngLatAlt, b: LngLatAlt): MeasureResult {
|
|
2527
|
+
return measureSegment(this._sceneFromLngLat(a), this._sceneFromLngLat(b));
|
|
2528
|
+
}
|
|
2529
|
+
|
|
2530
|
+
/** 折线长度(三维 + 水平投影两套口径) */
|
|
2531
|
+
measurePolyline(points: readonly LngLatAlt[]): { length3D: number; lengthHorizontal: number } {
|
|
2532
|
+
const scene = points.map((p) => this._sceneFromLngLat(p));
|
|
2533
|
+
return { length3D: polylineLength(scene), lengthHorizontal: polylineLengthHorizontal(scene) };
|
|
2534
|
+
}
|
|
2535
|
+
|
|
2536
|
+
/**
|
|
2537
|
+
* 多边形量算:坡面真实面积 / 水平投影面积 / 周长。
|
|
2538
|
+
*
|
|
2539
|
+
* 建筑面积、用地面积按 `areaHorizontal` 算;坡面覆盖面积按 `area3D`。
|
|
2540
|
+
*/
|
|
2541
|
+
measurePolygon(points: readonly LngLatAlt[]): {
|
|
2542
|
+
area3D: number;
|
|
2543
|
+
areaHorizontal: number;
|
|
2544
|
+
perimeter: number;
|
|
2545
|
+
} {
|
|
2546
|
+
const scene = points.map((p) => this._sceneFromLngLat(p));
|
|
2547
|
+
return {
|
|
2548
|
+
area3D: polygonArea(scene),
|
|
2549
|
+
areaHorizontal: polygonAreaHorizontal(scene),
|
|
2550
|
+
perimeter: polygonPerimeter(scene),
|
|
2551
|
+
};
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
/**
|
|
2555
|
+
* 通视分析:两点之间是否被已加载的 S3M 几何挡住。
|
|
2556
|
+
*
|
|
2557
|
+
* 走 CPU 射线求交(`Raycaster` 打全部 pickable mesh),几十万面量级是几十毫秒,
|
|
2558
|
+
* 适合点击式查询;要拖拽时逐帧刷新,得等深度图版本的批量采样。
|
|
2559
|
+
*
|
|
2560
|
+
* 注意范围:**只认 S3M 场景里的几何**。MapLibre 底图地形与 MVT 建筑不在这个场景里,
|
|
2561
|
+
* 它们不参与遮挡(见 `docs/API.md` 的「分析范围」)。
|
|
2562
|
+
*/
|
|
2563
|
+
lineOfSight(a: LngLatAlt, b: LngLatAlt, opts: LineOfSightOptions = {}): LineOfSightResult {
|
|
2564
|
+
const from = this._sceneFromLngLat(a);
|
|
2565
|
+
const to = this._sceneFromLngLat(b);
|
|
2566
|
+
return computeLineOfSight(from, to, (origin, dir, maxDistance) => {
|
|
2567
|
+
const hit = this._picks.raycastDistance(origin, dir, maxDistance);
|
|
2568
|
+
if (!hit) return null;
|
|
2569
|
+
return { distance: hit.distance, point: hit.point };
|
|
2570
|
+
}, opts);
|
|
2571
|
+
}
|
|
2572
|
+
|
|
2573
|
+
/**
|
|
2574
|
+
* 裁剪 / 剖切(平面、Box、十字)。
|
|
2575
|
+
*
|
|
2576
|
+
* 平面列表挂在**本图层的 renderer** 上(每个 `S3MLayer` 一个 `WebGLRenderer`),
|
|
2577
|
+
* 所以天然是图层级:同页别的图层不受影响。传 `null` 取消裁剪。
|
|
2578
|
+
*
|
|
2579
|
+
* 已知边界:只做「切掉」,不做**封口**——剖开后看到的是空壳/背面;
|
|
2580
|
+
* 凹多边形裁剪需要多组半空间并集,three 的 `clippingPlanes` 表达不了(只能凸)。
|
|
2581
|
+
*/
|
|
2582
|
+
setClipping(opt: ClipOptions | null): void {
|
|
2583
|
+
this._clip = opt;
|
|
2584
|
+
this._applyClipping();
|
|
2585
|
+
this._requestRepaint();
|
|
2586
|
+
}
|
|
2587
|
+
|
|
2588
|
+
/** 当前裁剪配置(`null` = 未裁剪) */
|
|
2589
|
+
get clipping(): ClipOptions | null {
|
|
2590
|
+
return this._clip;
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2593
|
+
private _applyClipping(): void {
|
|
2594
|
+
const r = this.renderer;
|
|
2595
|
+
if (!r) return; // mount 前先记着,`_createRenderer` 之后会补一次
|
|
2596
|
+
if (!this._clip) {
|
|
2597
|
+
r.clippingPlanes = [];
|
|
2598
|
+
return;
|
|
2599
|
+
}
|
|
2600
|
+
r.clippingPlanes = buildClipPlanes(this._clip).map(
|
|
2601
|
+
(p) => new THREE.Plane(new THREE.Vector3(p.normal[0], p.normal[1], p.normal[2]), p.constant)
|
|
2602
|
+
);
|
|
2603
|
+
}
|
|
2604
|
+
|
|
2605
|
+
/** 分析用的离屏采集上下文(renderer / scene / camera 都可能在 mount 前为空) */
|
|
2606
|
+
private _depthCtx(): DepthCaptureContext {
|
|
2607
|
+
return {
|
|
2608
|
+
renderer: () => this.renderer,
|
|
2609
|
+
scene: () => this.scene,
|
|
2610
|
+
camera: () => this.camera,
|
|
2611
|
+
};
|
|
2612
|
+
}
|
|
2613
|
+
|
|
2614
|
+
/**
|
|
2615
|
+
* 天际线分析:用当前视角渲染一张深度图,按列抽出「方位角 → 仰角」曲线。
|
|
2616
|
+
*
|
|
2617
|
+
* 一次调用 = 一趟额外渲染 + 一次全图回读,**不要每帧调**。
|
|
2618
|
+
* 典型用法是相机停下来(`moveend`)后算一次。
|
|
2619
|
+
*
|
|
2620
|
+
* @param opts.width 采集分辨率(默认 640)。精度由它决定:
|
|
2621
|
+
* 640 宽 ≈ 0.56°/列,1280 宽 ≈ 0.28°/列。
|
|
2622
|
+
*/
|
|
2623
|
+
skyline(opts: {
|
|
2624
|
+
width?: number;
|
|
2625
|
+
height?: number;
|
|
2626
|
+
step?: number;
|
|
2627
|
+
/** 额外参与遮挡的代理几何(底图建筑 / 地形网格),只在这一趟深度渲染里存在 */
|
|
2628
|
+
include?: readonly THREE.Object3D[];
|
|
2629
|
+
} = {}): SkylineResult | null {
|
|
2630
|
+
const cap = captureSkylineDepth(
|
|
2631
|
+
this._depthCtx(),
|
|
2632
|
+
opts.width ?? 640,
|
|
2633
|
+
opts.height ?? 360,
|
|
2634
|
+
opts.include
|
|
2635
|
+
);
|
|
2636
|
+
if (!cap) return null;
|
|
2637
|
+
return skylineFromDepth({
|
|
2638
|
+
rgba: cap.rgba,
|
|
2639
|
+
width: cap.width,
|
|
2640
|
+
height: cap.height,
|
|
2641
|
+
inverseViewProj: cap.inverseViewProj,
|
|
2642
|
+
cameraPos: cap.cameraPos,
|
|
2643
|
+
step: opts.step,
|
|
2644
|
+
});
|
|
2645
|
+
}
|
|
2646
|
+
|
|
2647
|
+
/**
|
|
2648
|
+
* 360° 可视域(地面栅格版):从观察点朝 6 个方向各渲染一次深度,
|
|
2649
|
+
* 再在 CPU 上对地面网格逐格判定可见性。
|
|
2650
|
+
*
|
|
2651
|
+
* 一次调用 = 6 趟额外渲染 + 6 次回读,成本明显高于天际线,
|
|
2652
|
+
* 只在点击/松手时算。返回的 `raster` 可以直接当贴图画在地面上。
|
|
2653
|
+
*/
|
|
2654
|
+
viewshed(opts: {
|
|
2655
|
+
/** 观察点(经纬度 + 地面高程;眼高由 `eyeHeight` 另加) */
|
|
2656
|
+
origin: LngLatAlt;
|
|
2657
|
+
/** 分析半径(米) */
|
|
2658
|
+
radius: number;
|
|
2659
|
+
/** 栅格边长(默认 96) */
|
|
2660
|
+
grid?: number;
|
|
2661
|
+
/** 眼高(米,默认 1.7) */
|
|
2662
|
+
eyeHeight?: number;
|
|
2663
|
+
/** 立方体深度图分辨率(默认 128;越大边缘越准,回读越慢) */
|
|
2664
|
+
size?: number;
|
|
2665
|
+
/** 额外参与遮挡的代理几何(底图建筑 / 地形网格),只在这一趟深度渲染里存在 */
|
|
2666
|
+
include?: readonly THREE.Object3D[];
|
|
2667
|
+
}): ViewshedResult | null {
|
|
2668
|
+
const eye = opts.eyeHeight ?? 1.7;
|
|
2669
|
+
const origin = this._sceneFromLngLat({ ...opts.origin, altitude: opts.origin.altitude + eye });
|
|
2670
|
+
const size = opts.size ?? 128;
|
|
2671
|
+
const faces = captureRadialCube(this._depthCtx(), origin, size, opts.radius, opts.include);
|
|
2672
|
+
if (!faces) return null;
|
|
2673
|
+
return computeViewshed(origin, makeCubeDistanceSampler(faces, size, opts.radius), {
|
|
2674
|
+
radius: opts.radius,
|
|
2675
|
+
grid: opts.grid,
|
|
2676
|
+
groundZ: 0,
|
|
2677
|
+
});
|
|
2678
|
+
}
|
|
2679
|
+
|
|
2300
2680
|
/** @internal 见 viewer/PickService.ts */
|
|
2301
2681
|
_intersectMeshAtRay(mesh: THREE.Mesh, ray: THREE.Ray, instanceIndex?: number): THREE.Vector3 | null {
|
|
2302
2682
|
return this._picks.intersectMeshAtRay(mesh, ray, instanceIndex);
|
|
@@ -2508,16 +2888,19 @@ export class S3MViewer {
|
|
|
2508
2888
|
}
|
|
2509
2889
|
|
|
2510
2890
|
/**
|
|
2511
|
-
*
|
|
2512
|
-
*
|
|
2513
|
-
*
|
|
2891
|
+
* 调整本图层的各向异性过滤上限(默认 Infinity = 取 GPU 最大值)。
|
|
2892
|
+
*
|
|
2893
|
+
* 与 0.4.22 之前不同:已上传的纹理**会立刻跟着变**(改 sampling 参数 + 重传,
|
|
2894
|
+
* 不需要重新解码,也不需要重载瓦片),所以切 `lightingPreset: 'supermap'` 时
|
|
2895
|
+
* 立面清晰度是当场变的,不再等新瓦片。
|
|
2514
2896
|
*/
|
|
2515
2897
|
setAnisotropyCap(cap: number): void {
|
|
2516
2898
|
const next = Number.isFinite(cap) && cap > 0 ? Math.max(1, cap) : Infinity;
|
|
2517
2899
|
this.options.anisotropyCap = next;
|
|
2900
|
+
// 同步模块级默认:直接调 createTexture / buildTileObjects 的老用法仍读它
|
|
2518
2901
|
const gpuMax = this.renderer?.capabilities.getMaxAnisotropy?.() ?? 1;
|
|
2519
2902
|
setMaxAnisotropy(gpuMax, next);
|
|
2520
|
-
this.
|
|
2903
|
+
this._applyRenderProfile({ anisotropyCap: next });
|
|
2521
2904
|
}
|
|
2522
2905
|
|
|
2523
2906
|
/** 当前纹理长边像素上限。Infinity = 不限制 */
|
|
@@ -2528,12 +2911,16 @@ export class S3MViewer {
|
|
|
2528
2911
|
/**
|
|
2529
2912
|
* 调整纹理长边像素上限(默认 Infinity = 不限制)。
|
|
2530
2913
|
* 只影响之后解码的纹理;非图集的 RGBA / WebP 产物按 2× 减半逼近上限。
|
|
2531
|
-
*
|
|
2914
|
+
*
|
|
2915
|
+
* 图层级:只改本层,同页其它图层不受影响(同时同步模块级默认,照顾直接调
|
|
2916
|
+
* `createTexture` 的老用法)。已解码的纹理不会重解 —— 那是另一件事。
|
|
2532
2917
|
*/
|
|
2533
2918
|
setMaxTextureSize(n: number): void {
|
|
2534
2919
|
const next = Number.isFinite(n) && n >= 1 ? n : Infinity;
|
|
2535
2920
|
this.options.maxTextureSize = next;
|
|
2536
2921
|
setTextureSizeCap(next);
|
|
2922
|
+
// 只推给数据集,不动材质也不重传纹理(尺寸上限只影响「之后解码」的纹理)
|
|
2923
|
+
this._datasetManager.setRenderProfile({ textureOptions: this._textureOptions() });
|
|
2537
2924
|
this._requestRepaint();
|
|
2538
2925
|
}
|
|
2539
2926
|
|
|
@@ -317,7 +317,9 @@ export function getMaterialQuality(): MaterialQuality {
|
|
|
317
317
|
* (0.8·NdotL + 0.7)、输出前抵消 renderer 的 OETF。与 `setSrgbTextureDecode(false)`
|
|
318
318
|
* 配套使用,`lightingPreset: 'supermap'` 会同时打开两者。
|
|
319
319
|
*
|
|
320
|
-
* 与 materialQuality
|
|
320
|
+
* 与 materialQuality 同一套约定:这里只是**模块级默认值**,不是全局开关 ——
|
|
321
|
+
* 图层显式传了 `colorPipeline` 就只认自己,没传才读这里。只影响之后创建的材质,
|
|
322
|
+
* 已加载瓦片要重载或在图层上调 `setColorPipeline` 才会换。
|
|
321
323
|
*/
|
|
322
324
|
export type ColorPipeline = 'linear' | 'direct';
|
|
323
325
|
let colorPipeline: ColorPipeline = 'linear';
|
|
@@ -330,6 +332,13 @@ export function getColorPipeline(): ColorPipeline {
|
|
|
330
332
|
return colorPipeline;
|
|
331
333
|
}
|
|
332
334
|
|
|
335
|
+
/** 归一颜色管线:只认 'linear' / 'direct',其余返回 undefined(= 去读模块级默认) */
|
|
336
|
+
export function normalizeColorPipeline(p: unknown): ColorPipeline | undefined {
|
|
337
|
+
if (p === 'direct') return 'direct';
|
|
338
|
+
if (p === 'linear') return 'linear';
|
|
339
|
+
return undefined;
|
|
340
|
+
}
|
|
341
|
+
|
|
333
342
|
export type StyledMaterial = (THREE.MeshStandardMaterial | THREE.MeshLambertMaterial) & {
|
|
334
343
|
userData: {
|
|
335
344
|
uSelBatch: { value: number };
|
|
@@ -1128,6 +1137,11 @@ export interface StyledMaterialOptions {
|
|
|
1128
1137
|
* 一路透传到这里,不再让所有图层共享同一份模块级状态。
|
|
1129
1138
|
*/
|
|
1130
1139
|
materialQuality?: MaterialQuality;
|
|
1140
|
+
/**
|
|
1141
|
+
* 颜色管线。不传 = 读模块级默认(`getColorPipeline()`),行为与旧版一致。
|
|
1142
|
+
* 与 `materialQuality` 同一套:图层把自己的 profile 透传进来,同页互不污染。
|
|
1143
|
+
*/
|
|
1144
|
+
colorPipeline?: ColorPipeline;
|
|
1131
1145
|
}
|
|
1132
1146
|
|
|
1133
1147
|
/**
|
|
@@ -1230,7 +1244,7 @@ export function createStyledMaterial(opts: StyledMaterialOptions): StyledMateria
|
|
|
1230
1244
|
if (texCoordZ) mat.defines = { ...(mat.defines as Record<string, string>), S3M_TEXCOORD_Z: '' };
|
|
1231
1245
|
if (secMap) mat.defines = { ...mat.defines, S3M_ATLAS_SEC: '' };
|
|
1232
1246
|
// supermap 完整版:官方 sRGB 直乘管线(配合 setSrgbTextureDecode(false))
|
|
1233
|
-
const direct = colorPipeline === 'direct';
|
|
1247
|
+
const direct = (opts.colorPipeline ?? colorPipeline) === 'direct';
|
|
1234
1248
|
if (direct) mat.defines = { ...mat.defines, S3M_DIRECT_COLOR: '' };
|
|
1235
1249
|
// 官方 uTextureDim = (base宽, base高, bake宽, bake高),取实际绑定纹理的像素尺寸
|
|
1236
1250
|
const uAtlasDim = { value: new THREE.Vector2(opts.map?.image?.width || 1, opts.map?.image?.height || 1) };
|