s3m-three 0.4.23 → 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 +20 -3
- package/dist/browser/s3m-three.iife.js +20 -3
- 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/S3MViewer.d.ts +106 -0
- package/dist/render/S3MViewer.d.ts.map +1 -1
- package/dist/render/S3MViewer.js +174 -1
- package/dist/render/S3MViewer.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/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/docs/API.md +100 -0
- 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/S3MViewer.ts +233 -2
- package/src/render/analysis/DepthPass.ts +278 -0
- package/src/render/viewer/PickService.ts +47 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 360° 可视域:地面栅格版。
|
|
3
|
+
*
|
|
4
|
+
* 思路与「从观察点做阴影贴图」一致,但**判定在 CPU 上做**:
|
|
5
|
+
* GPU 只负责产出一个 `方向 → 最近遮挡距离` 的采样函数(6 面立方体深度图),
|
|
6
|
+
* 这里拿它在观察点周围的地面网格上逐格比较距离。
|
|
7
|
+
*
|
|
8
|
+
* 为什么不在片元里逐像素判定(建筑立面着色版):
|
|
9
|
+
* 那要把立方体深度注入到每种材质(pbr / lambert / 棱线 / 点云 / 泼溅)的 shader 里,
|
|
10
|
+
* 成本和风险高一个量级;地面栅格版能覆盖绝大多数规划类需求,且结果可以直接
|
|
11
|
+
* 当成一张贴图/热力图叠在地面上。
|
|
12
|
+
*
|
|
13
|
+
* 纯数学,不碰 WebGL:采样函数由 `render/analysis/DepthPass` 提供,单测里用假的。
|
|
14
|
+
*/
|
|
15
|
+
import { unpackRGBADepth } from './depth.js';
|
|
16
|
+
import type { Vec3 } from './measure.js';
|
|
17
|
+
|
|
18
|
+
export interface ViewshedOptions {
|
|
19
|
+
/** 分析半径(米) */
|
|
20
|
+
radius: number;
|
|
21
|
+
/** 栅格边长(格数,默认 96;96² = 9216 个采样点,CPU 侧约 1~3 ms) */
|
|
22
|
+
grid?: number;
|
|
23
|
+
/**
|
|
24
|
+
* 遮挡容差(米,默认 0.5)。
|
|
25
|
+
*
|
|
26
|
+
* 采样点到观察点的距离要**小于等于**该方向上的遮挡距离才算可见;
|
|
27
|
+
* 留一点容差是为了吸收深度图的离散化误差与地面自身的起伏。
|
|
28
|
+
*/
|
|
29
|
+
bias?: number;
|
|
30
|
+
/** 地面高程(场景 ENU 的 z,默认 0) */
|
|
31
|
+
groundZ?: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ViewshedResult {
|
|
35
|
+
/**
|
|
36
|
+
* `grid × grid` 的可见性,行优先(`y * grid + x`):
|
|
37
|
+
* `1` = 可见,`0` = 被遮挡,`-1` = 超出半径或没有数据。
|
|
38
|
+
*/
|
|
39
|
+
raster: Float32Array;
|
|
40
|
+
grid: number;
|
|
41
|
+
/** 栅格左下角(场景 ENU 米) */
|
|
42
|
+
minX: number;
|
|
43
|
+
minY: number;
|
|
44
|
+
/** 每格边长(米) */
|
|
45
|
+
cellSize: number;
|
|
46
|
+
/** 有效格子里可见的比例(0~1) */
|
|
47
|
+
visibleRatio: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param origin 观察点(场景 ENU 米,已含眼高)
|
|
52
|
+
* @param sampleDistance 沿单位方向取「最近的遮挡距离」;该方向没有几何时返回 `Infinity`
|
|
53
|
+
*/
|
|
54
|
+
export function computeViewshed(
|
|
55
|
+
origin: Vec3,
|
|
56
|
+
sampleDistance: (dir: Vec3) => number,
|
|
57
|
+
opts: ViewshedOptions
|
|
58
|
+
): ViewshedResult {
|
|
59
|
+
const radius = Math.max(1e-3, opts.radius);
|
|
60
|
+
const grid = Math.max(2, Math.floor(opts.grid ?? 96));
|
|
61
|
+
const bias = Math.max(0, opts.bias ?? 0.5);
|
|
62
|
+
const groundZ = opts.groundZ ?? 0;
|
|
63
|
+
|
|
64
|
+
const cellSize = (radius * 2) / grid;
|
|
65
|
+
const minX = origin[0] - radius;
|
|
66
|
+
const minY = origin[1] - radius;
|
|
67
|
+
const raster = new Float32Array(grid * grid);
|
|
68
|
+
|
|
69
|
+
let visible = 0;
|
|
70
|
+
let counted = 0;
|
|
71
|
+
|
|
72
|
+
for (let iy = 0; iy < grid; iy++) {
|
|
73
|
+
const wy = minY + (iy + 0.5) * cellSize;
|
|
74
|
+
for (let ix = 0; ix < grid; ix++) {
|
|
75
|
+
const wx = minX + (ix + 0.5) * cellSize;
|
|
76
|
+
const dx = wx - origin[0];
|
|
77
|
+
const dy = wy - origin[1];
|
|
78
|
+
const dz = groundZ - origin[2];
|
|
79
|
+
const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
80
|
+
const idx = iy * grid + ix;
|
|
81
|
+
|
|
82
|
+
if (dist > radius || dist < 1e-6) {
|
|
83
|
+
raster[idx] = -1;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const dir: Vec3 = [dx / dist, dy / dist, dz / dist];
|
|
88
|
+
const blockedAt = sampleDistance(dir);
|
|
89
|
+
const ok = dist <= blockedAt + bias;
|
|
90
|
+
raster[idx] = ok ? 1 : 0;
|
|
91
|
+
counted++;
|
|
92
|
+
if (ok) visible++;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
raster,
|
|
98
|
+
grid,
|
|
99
|
+
minX,
|
|
100
|
+
minY,
|
|
101
|
+
cellSize,
|
|
102
|
+
visibleRatio: counted > 0 ? visible / counted : 0,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 从 6 面立方体深度图构造 `sampleDistance`。
|
|
108
|
+
*
|
|
109
|
+
* 立方体每个面是 `size × size` 的 RGBA8,存的是 `距离 / maxDist` 的打包值
|
|
110
|
+
* (打包方式同 three 的 `packDepthToRGBA`,与 `RGBADepthPacking` 一致)。
|
|
111
|
+
* 采样用最近邻 —— 可视域本身是栅格化的,插值不会让结果更准。
|
|
112
|
+
*
|
|
113
|
+
* 面序与 three 的 `WebGLCubeRenderTarget` / `CubeCamera` 一致:
|
|
114
|
+
* `+x, -x, +y, -y, +z, -z`。
|
|
115
|
+
*/
|
|
116
|
+
export function makeCubeDistanceSampler(
|
|
117
|
+
faces: readonly (ArrayLike<number> | null | undefined)[],
|
|
118
|
+
size: number,
|
|
119
|
+
maxDist: number
|
|
120
|
+
): (dir: Vec3) => number {
|
|
121
|
+
return (dir) => {
|
|
122
|
+
const [x, y, z] = dir;
|
|
123
|
+
const ax = Math.abs(x);
|
|
124
|
+
const ay = Math.abs(y);
|
|
125
|
+
const az = Math.abs(z);
|
|
126
|
+
|
|
127
|
+
// 主轴决定落在哪个面:|x| 最大 → ±x 面,依此类推
|
|
128
|
+
let faceIndex: number;
|
|
129
|
+
let u: number;
|
|
130
|
+
let v: number;
|
|
131
|
+
if (ax >= ay && ax >= az) {
|
|
132
|
+
faceIndex = x >= 0 ? 0 : 1;
|
|
133
|
+
const d = ax;
|
|
134
|
+
// three 的立方体面约定:+x 面 u = -z/d, v = -y/d
|
|
135
|
+
u = x >= 0 ? -z / d : z / d;
|
|
136
|
+
v = -y / d;
|
|
137
|
+
} else if (ay >= az) {
|
|
138
|
+
faceIndex = y >= 0 ? 2 : 3;
|
|
139
|
+
const d = ay;
|
|
140
|
+
u = x / d;
|
|
141
|
+
v = y >= 0 ? z / d : -z / d;
|
|
142
|
+
} else {
|
|
143
|
+
faceIndex = z >= 0 ? 4 : 5;
|
|
144
|
+
const d = az;
|
|
145
|
+
// 面朝向相反时 u 轴也翻向,否则 -z 面会左右镜像(可视域会镜像到错误一侧)
|
|
146
|
+
u = z >= 0 ? x / d : -x / d;
|
|
147
|
+
v = -y / d;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const face = faces[faceIndex];
|
|
151
|
+
if (!face) return Infinity;
|
|
152
|
+
const px = Math.min(size - 1, Math.max(0, Math.floor(((u + 1) / 2) * size)));
|
|
153
|
+
const py = Math.min(size - 1, Math.max(0, Math.floor(((v + 1) / 2) * size)));
|
|
154
|
+
const d = unpackRGBADepth(face, (py * size + px) * 4);
|
|
155
|
+
// 打包值是归一化距离:1 = 远平面(该方向没有几何)。three 的 pack 对 v>=1
|
|
156
|
+
// 直接返回全白,解包正好是 1.0,所以阈值取 0.9999 就够。
|
|
157
|
+
if (d >= 0.9999) return Infinity;
|
|
158
|
+
return d * maxDist;
|
|
159
|
+
};
|
|
160
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -138,6 +138,11 @@ export { setMaterialQuality, getMaterialQuality } from './render/StyleMaterial.j
|
|
|
138
138
|
|
|
139
139
|
export { Picker } from './render/Picker.js';
|
|
140
140
|
export { GpuEdgePass } from './render/GpuEdgePass.js';
|
|
141
|
+
|
|
142
|
+
/* 三维空间分析内核:纯数学,不依赖 WebGL,可单独用于服务端 / Worker */
|
|
143
|
+
export * from './analysis/index.js';
|
|
144
|
+
export { captureSkylineDepth, captureRadialCube, buildCubeCameras, CUBE_FACES } from './render/analysis/DepthPass.js';
|
|
145
|
+
export type { DepthCaptureContext, SkylineCapture } from './render/analysis/DepthPass.js';
|
|
141
146
|
export type { GpuEdgeParams } from './render/GpuEdgePass.js';
|
|
142
147
|
export type { PickHit } from './render/Picker.js';
|
|
143
148
|
|
package/src/render/S3MViewer.ts
CHANGED
|
@@ -70,7 +70,30 @@ import type { DegradationKind, UnsupportedPolicy } from '../core/degradations.js
|
|
|
70
70
|
import { Picker } from './Picker.js';
|
|
71
71
|
import { GpuEdgePass } from './GpuEdgePass.js';
|
|
72
72
|
import { Animator } from '../anim/Animator.js';
|
|
73
|
-
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';
|
|
74
97
|
import { Path } from '../anim/Path.js';
|
|
75
98
|
import type { EasingFn, EasingName } from '../anim/easing.js';
|
|
76
99
|
import type { LineSharedUniforms, ThickLineMaterial } from './EdgeLines.js';
|
|
@@ -88,7 +111,7 @@ import type {
|
|
|
88
111
|
StyleUniforms,
|
|
89
112
|
} from '../core/types.js';
|
|
90
113
|
|
|
91
|
-
import { lngLatFromWorld, type GeoAdapter } from './GeoAdapter.js';
|
|
114
|
+
import { lngLatFromWorld, worldFromLngLat, type GeoAdapter } from './GeoAdapter.js';
|
|
92
115
|
|
|
93
116
|
import {
|
|
94
117
|
DatasetManager,
|
|
@@ -282,6 +305,8 @@ export class S3MViewer {
|
|
|
282
305
|
private _colorPipeline: ColorPipeline | undefined;
|
|
283
306
|
/** fade-through 编排的代次:中途再切一次,旧编排作废(与 setMode 同一套) */
|
|
284
307
|
private _pipelineGen = 0;
|
|
308
|
+
/** 当前裁剪配置;renderer 就绪前后都要能设,所以单独存一份(见 `_applyClipping`) */
|
|
309
|
+
private _clip: ClipOptions | null = null;
|
|
285
310
|
/** 单体样式、专题着色与状态纹理管理器 */
|
|
286
311
|
private readonly _featureStyleManager: FeatureStyleManager;
|
|
287
312
|
/** GPU 路径动画管理器 */
|
|
@@ -566,6 +591,8 @@ export class S3MViewer {
|
|
|
566
591
|
this.renderer = this._createRenderer(canvas, gl);
|
|
567
592
|
setMaxAnisotropy(this.renderer.capabilities.getMaxAnisotropy(), this.options.anisotropyCap ?? Infinity);
|
|
568
593
|
this._syncTextureCapabilities(gl);
|
|
594
|
+
// 裁剪是构造期就能设的(还没 mount 时 `_applyClipping` 会先跳过),这里补挂一次
|
|
595
|
+
this._applyClipping();
|
|
569
596
|
|
|
570
597
|
this.scene = new THREE.Scene();
|
|
571
598
|
this.scene.add(this.groundManager.group);
|
|
@@ -608,6 +635,8 @@ export class S3MViewer {
|
|
|
608
635
|
this.renderer = this._createRenderer(this.canvas, gl);
|
|
609
636
|
setMaxAnisotropy(this.renderer.capabilities.getMaxAnisotropy(), this.options.anisotropyCap ?? Infinity);
|
|
610
637
|
this._syncTextureCapabilities(gl);
|
|
638
|
+
// 新 renderer 的 clippingPlanes 是空的,裁剪状态要重新挂上去
|
|
639
|
+
this._applyClipping();
|
|
611
640
|
this.picker?.dispose();
|
|
612
641
|
this.picker = new Picker(this.renderer);
|
|
613
642
|
// GPU 描边的 RT / 材质绑定在旧上下文上,整体丢弃,下一帧按需重建
|
|
@@ -2446,6 +2475,208 @@ export class S3MViewer {
|
|
|
2446
2475
|
return this._picks.getFeatureBounds(smid);
|
|
2447
2476
|
}
|
|
2448
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
|
+
|
|
2449
2680
|
/** @internal 见 viewer/PickService.ts */
|
|
2450
2681
|
_intersectMeshAtRay(mesh: THREE.Mesh, ray: THREE.Ray, instanceIndex?: number): THREE.Vector3 | null {
|
|
2451
2682
|
return this._picks.intersectMeshAtRay(mesh, ray, instanceIndex);
|