s3m-three 0.4.23 → 0.4.25
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/assets/{s3mWorker-DvAUA8NJ.js → s3mWorker-jc9Oly5K.js} +1 -1
- package/dist/browser/s3m-three.iife.ext-three.js +21 -4
- package/dist/browser/s3m-three.iife.js +21 -4
- package/dist/core/DXTTextureDecode.d.ts +5 -2
- package/dist/core/DXTTextureDecode.d.ts.map +1 -1
- package/dist/core/DXTTextureDecode.js +17 -7
- package/dist/core/DXTTextureDecode.js.map +1 -1
- 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 +17 -0
- package/dist/render/viewer/PickService.d.ts.map +1 -1
- package/dist/render/viewer/PickService.js +42 -1
- package/dist/render/viewer/PickService.js.map +1 -1
- package/dist/worker/s3mWorker.js +1 -1
- package/dist/worker/s3mWorker.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/core/DXTTextureDecode.ts +17 -7
- 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 +48 -1
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 分析用的离屏深度采集(P1 的 GPU 部分)。
|
|
3
|
+
*
|
|
4
|
+
* 这里只干一件事:把场景渲染成「一张能回读的深度图」,判断逻辑全在
|
|
5
|
+
* `analysis/skyline.ts` 与 `analysis/viewshed.ts` 里(纯数学、有单测)。
|
|
6
|
+
* 这样 GPU 侧出错只可能是「图不对」(demo 里一眼看得出),
|
|
7
|
+
* 而不会把归约逻辑一起搅进去。
|
|
8
|
+
*
|
|
9
|
+
* 两个采集器:
|
|
10
|
+
* - `captureSkylineDepth`:用当前相机视角渲染一次,给天际线用(three 的 RGBADepthPacking);
|
|
11
|
+
* - `captureRadialCube`:从观察点朝 6 个方向各渲染一次,存「到观察点的距离」,
|
|
12
|
+
* 给 360° 可视域用。
|
|
13
|
+
*
|
|
14
|
+
* 三条硬约束(都是踩过的坑):
|
|
15
|
+
* 1. **必须 `resetState()`**:MapLibre 在上一次 draw 之后改过 GL 状态,
|
|
16
|
+
* three 的缓存还停在上一次自己的 render 上,不重置会出现深度测试失效。
|
|
17
|
+
* 2. **清屏成白色**:`packDepthToRGBA(1)` 返回全白,清屏也用白,
|
|
18
|
+
* 于是「背景」和「远平面」解包后都是 1.0,背景判定只需一个阈值。
|
|
19
|
+
* 3. **回读是行倒序的**:WebGL 的 `readPixels` 原点在左下,这里翻成正序再交出去。
|
|
20
|
+
*/
|
|
21
|
+
import * as THREE from 'three';
|
|
22
|
+
import type { Vec3 } from '../../analysis/measure.js';
|
|
23
|
+
|
|
24
|
+
export interface DepthCaptureContext {
|
|
25
|
+
renderer: () => THREE.WebGLRenderer | null;
|
|
26
|
+
scene: () => THREE.Scene | null;
|
|
27
|
+
camera: () => THREE.Camera | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 参与深度采集的**额外几何**(分析代理)。
|
|
32
|
+
*
|
|
33
|
+
* 用途:分析只看得见 S3M 场景里的东西。要让 MapLibre 的 MVT 建筑 / 地形也参与
|
|
34
|
+
* 遮挡,外围得自己把它们挤出成几何,通过这个参数临时挂进场景 —— 只在这一趟
|
|
35
|
+
* 深度渲染里存在,**不进主渲染、不影响画面**,渲染完立刻摘掉。
|
|
36
|
+
*
|
|
37
|
+
* 注意:
|
|
38
|
+
* - 传进来的对象不要已经有 parent(会被临时 add 到 viewer 的场景上再移除);
|
|
39
|
+
* - 代理几何的材质会被 `overrideMaterial` 覆盖,所以给什么材质都行;
|
|
40
|
+
* - 代理和 MapLibre 实际画出来的可能有细微偏差(表达式插值、terrain 抬升等),
|
|
41
|
+
* 这是「复制一份几何」这条路固有的代价。
|
|
42
|
+
*/
|
|
43
|
+
export interface DepthCaptureInclude {
|
|
44
|
+
include?: readonly THREE.Object3D[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 临时把代理几何挂进场景,返回「摘掉」的函数 */
|
|
48
|
+
function attachProxy(scene: THREE.Scene, include?: readonly THREE.Object3D[]): () => void {
|
|
49
|
+
if (!include?.length) return () => {};
|
|
50
|
+
const added = include.filter((o) => o && !o.parent);
|
|
51
|
+
for (const o of added) scene.add(o);
|
|
52
|
+
return () => {
|
|
53
|
+
for (const o of added) scene.remove(o);
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* ------------------------------------------------------------------ 渲染器状态 */
|
|
58
|
+
|
|
59
|
+
interface RendererState {
|
|
60
|
+
target: THREE.WebGLRenderTarget | null;
|
|
61
|
+
toneMapping: THREE.ToneMapping;
|
|
62
|
+
colorSpace: THREE.ColorSpace;
|
|
63
|
+
autoClear: boolean;
|
|
64
|
+
clearColor: THREE.Color;
|
|
65
|
+
clearAlpha: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function saveState(r: THREE.WebGLRenderer): RendererState {
|
|
69
|
+
return {
|
|
70
|
+
target: r.getRenderTarget(),
|
|
71
|
+
toneMapping: r.toneMapping,
|
|
72
|
+
colorSpace: r.outputColorSpace,
|
|
73
|
+
autoClear: r.autoClear,
|
|
74
|
+
clearColor: r.getClearColor(new THREE.Color()),
|
|
75
|
+
clearAlpha: r.getClearAlpha(),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function restoreState(r: THREE.WebGLRenderer, s: RendererState): void {
|
|
80
|
+
r.setRenderTarget(s.target);
|
|
81
|
+
r.toneMapping = s.toneMapping;
|
|
82
|
+
r.outputColorSpace = s.colorSpace;
|
|
83
|
+
r.autoClear = s.autoClear;
|
|
84
|
+
r.setClearColor(s.clearColor, s.clearAlpha);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** 回读并把像素翻成正序(行 0 = 画面顶部) */
|
|
88
|
+
function readFlipped(r: THREE.WebGLRenderer, rt: THREE.WebGLRenderTarget, width: number, height: number): Uint8Array {
|
|
89
|
+
const raw = new Uint8Array(width * height * 4);
|
|
90
|
+
r.readRenderTargetPixels(rt, 0, 0, width, height, raw);
|
|
91
|
+
const out = new Uint8Array(raw.length);
|
|
92
|
+
const rowBytes = width * 4;
|
|
93
|
+
for (let y = 0; y < height; y++) {
|
|
94
|
+
out.set(raw.subarray((height - 1 - y) * rowBytes, (height - y) * rowBytes), y * rowBytes);
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function makeTarget(w: number, h: number): THREE.WebGLRenderTarget {
|
|
100
|
+
return new THREE.WebGLRenderTarget(w, h, {
|
|
101
|
+
format: THREE.RGBAFormat,
|
|
102
|
+
type: THREE.UnsignedByteType,
|
|
103
|
+
minFilter: THREE.NearestFilter,
|
|
104
|
+
magFilter: THREE.NearestFilter,
|
|
105
|
+
depthBuffer: true,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* ------------------------------------------------------------------ 天际线 */
|
|
110
|
+
|
|
111
|
+
export interface SkylineCapture {
|
|
112
|
+
rgba: Uint8Array;
|
|
113
|
+
width: number;
|
|
114
|
+
height: number;
|
|
115
|
+
inverseViewProj: Float32Array;
|
|
116
|
+
cameraPos: Vec3;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 用当前相机渲染一张深度图(RGBADepthPacking)。
|
|
121
|
+
*
|
|
122
|
+
* @param width 采集分辨率。精度由它决定:1280 宽 ≈ 0.28°/列;320 宽 ≈ 1.1°/列。
|
|
123
|
+
*/
|
|
124
|
+
export function captureSkylineDepth(
|
|
125
|
+
ctx: DepthCaptureContext,
|
|
126
|
+
width: number,
|
|
127
|
+
height: number,
|
|
128
|
+
include?: readonly THREE.Object3D[]
|
|
129
|
+
): SkylineCapture | null {
|
|
130
|
+
const r = ctx.renderer();
|
|
131
|
+
const scene = ctx.scene();
|
|
132
|
+
const camera = ctx.camera();
|
|
133
|
+
if (!r || !scene || !camera) return null;
|
|
134
|
+
|
|
135
|
+
const w = Math.max(1, Math.floor(width));
|
|
136
|
+
const h = Math.max(1, Math.floor(height));
|
|
137
|
+
const rt = makeTarget(w, h);
|
|
138
|
+
const depthMat = new THREE.MeshDepthMaterial({ depthPacking: THREE.RGBADepthPacking });
|
|
139
|
+
|
|
140
|
+
const prevOverride = scene.overrideMaterial;
|
|
141
|
+
const state = saveState(r);
|
|
142
|
+
const detach = attachProxy(scene, include);
|
|
143
|
+
let rgba: Uint8Array | null = null;
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
r.resetState();
|
|
147
|
+
r.setRenderTarget(rt);
|
|
148
|
+
r.toneMapping = THREE.NoToneMapping;
|
|
149
|
+
r.outputColorSpace = THREE.LinearSRGBColorSpace;
|
|
150
|
+
r.autoClear = true;
|
|
151
|
+
r.setClearColor(0xffffff, 1);
|
|
152
|
+
r.clear();
|
|
153
|
+
|
|
154
|
+
scene.overrideMaterial = depthMat;
|
|
155
|
+
camera.updateMatrixWorld();
|
|
156
|
+
r.render(scene, camera);
|
|
157
|
+
rgba = readFlipped(r, rt, w, h);
|
|
158
|
+
} finally {
|
|
159
|
+
detach();
|
|
160
|
+
scene.overrideMaterial = prevOverride;
|
|
161
|
+
restoreState(r, state);
|
|
162
|
+
depthMat.dispose();
|
|
163
|
+
rt.dispose();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const vp = new THREE.Matrix4().multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
|
|
167
|
+
const inv = new Float32Array(vp.clone().invert().elements);
|
|
168
|
+
const pos = new THREE.Vector3().setFromMatrixPosition(camera.matrixWorld);
|
|
169
|
+
|
|
170
|
+
return { rgba, width: w, height: h, inverseViewProj: inv, cameraPos: [pos.x, pos.y, pos.z] };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/* ------------------------------------------------------------------ 360° 可视域 */
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* 6 个面的朝向。**与 `analysis/viewshed.ts` 的采样器约定必须逐字一致**,
|
|
177
|
+
* 否则可视域会镜像到错误的一侧(而且看起来还挺像真的)。
|
|
178
|
+
*
|
|
179
|
+
* 约定:`right = forward × up`;采样器里 `u = dot(dir, right) / dot(dir, forward)`,
|
|
180
|
+
* `v = dot(dir, up) / dot(dir, forward)`,像素 = `((u + 1) / 2 * size, (v + 1) / 2 * size)`。
|
|
181
|
+
*/
|
|
182
|
+
export const CUBE_FACES: readonly { forward: Vec3; up: Vec3 }[] = [
|
|
183
|
+
{ forward: [1, 0, 0], up: [0, -1, 0] },
|
|
184
|
+
{ forward: [-1, 0, 0], up: [0, -1, 0] },
|
|
185
|
+
{ forward: [0, 1, 0], up: [0, 0, 1] },
|
|
186
|
+
{ forward: [0, -1, 0], up: [0, 0, -1] },
|
|
187
|
+
{ forward: [0, 0, 1], up: [0, -1, 0] },
|
|
188
|
+
{ forward: [0, 0, -1], up: [0, -1, 0] },
|
|
189
|
+
];
|
|
190
|
+
|
|
191
|
+
/** 按上面的约定造 6 台 90° 相机;抽出来是为了能脱离 WebGL 单测朝向 */
|
|
192
|
+
export function buildCubeCameras(origin: Vec3, near: number, far: number): THREE.PerspectiveCamera[] {
|
|
193
|
+
return CUBE_FACES.map(({ forward, up }) => {
|
|
194
|
+
const cam = new THREE.PerspectiveCamera(90, 1, near, far);
|
|
195
|
+
cam.up.set(up[0], up[1], up[2]);
|
|
196
|
+
cam.position.set(origin[0], origin[1], origin[2]);
|
|
197
|
+
cam.lookAt(origin[0] + forward[0], origin[1] + forward[1], origin[2] + forward[2]);
|
|
198
|
+
cam.updateMatrixWorld(true);
|
|
199
|
+
return cam;
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* 从观察点朝 6 个方向渲染「到观察点的距离」,归一化到 [0,1] 后按
|
|
205
|
+
* three 的 `packDepthToRGBA` 打包。
|
|
206
|
+
*
|
|
207
|
+
* ⚠️ 用 `overrideMaterial` 换材质,**实例化网格(InstancedMesh)不会被正确变换** ——
|
|
208
|
+
* 简化着色器不处理 `instanceMatrix`。点云 / 树木这类实例化内容不参与遮挡。
|
|
209
|
+
*/
|
|
210
|
+
export function captureRadialCube(
|
|
211
|
+
ctx: DepthCaptureContext,
|
|
212
|
+
origin: Vec3,
|
|
213
|
+
size: number,
|
|
214
|
+
maxDist: number,
|
|
215
|
+
include?: readonly THREE.Object3D[]
|
|
216
|
+
): Uint8Array[] | null {
|
|
217
|
+
const r = ctx.renderer();
|
|
218
|
+
const scene = ctx.scene();
|
|
219
|
+
if (!r || !scene) return null;
|
|
220
|
+
|
|
221
|
+
const s = Math.max(8, Math.floor(size));
|
|
222
|
+
const rt = makeTarget(s, s);
|
|
223
|
+
const mat = new THREE.ShaderMaterial({
|
|
224
|
+
uniforms: {
|
|
225
|
+
uOrigin: { value: new THREE.Vector3(origin[0], origin[1], origin[2]) },
|
|
226
|
+
uMaxDist: { value: maxDist },
|
|
227
|
+
},
|
|
228
|
+
vertexShader: /* glsl */ `
|
|
229
|
+
varying vec3 vWorld;
|
|
230
|
+
void main() {
|
|
231
|
+
vec4 world = modelMatrix * vec4( position, 1.0 );
|
|
232
|
+
vWorld = world.xyz;
|
|
233
|
+
gl_Position = projectionMatrix * viewMatrix * world;
|
|
234
|
+
}
|
|
235
|
+
`,
|
|
236
|
+
fragmentShader: /* glsl */ `
|
|
237
|
+
#include <common>
|
|
238
|
+
#include <packing>
|
|
239
|
+
uniform vec3 uOrigin;
|
|
240
|
+
uniform float uMaxDist;
|
|
241
|
+
varying vec3 vWorld;
|
|
242
|
+
void main() {
|
|
243
|
+
float d = clamp( length( vWorld - uOrigin ) / uMaxDist, 0.0, 1.0 );
|
|
244
|
+
gl_FragColor = packDepthToRGBA( d );
|
|
245
|
+
}
|
|
246
|
+
`,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const prevOverride = scene.overrideMaterial;
|
|
250
|
+
const state = saveState(r);
|
|
251
|
+
const detach = attachProxy(scene, include);
|
|
252
|
+
const cameras = buildCubeCameras(origin, 0.1, maxDist);
|
|
253
|
+
const faces: Uint8Array[] = [];
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
r.resetState();
|
|
257
|
+
r.setRenderTarget(rt);
|
|
258
|
+
r.toneMapping = THREE.NoToneMapping;
|
|
259
|
+
r.outputColorSpace = THREE.LinearSRGBColorSpace;
|
|
260
|
+
r.autoClear = true;
|
|
261
|
+
r.setClearColor(0xffffff, 1); // 白 = 该方向没有几何(与 pack(1) 一致)
|
|
262
|
+
scene.overrideMaterial = mat;
|
|
263
|
+
|
|
264
|
+
for (const cam of cameras) {
|
|
265
|
+
r.clear();
|
|
266
|
+
r.render(scene, cam);
|
|
267
|
+
faces.push(readFlipped(r, rt, s, s));
|
|
268
|
+
}
|
|
269
|
+
} finally {
|
|
270
|
+
detach();
|
|
271
|
+
scene.overrideMaterial = prevOverride;
|
|
272
|
+
restoreState(r, state);
|
|
273
|
+
mat.dispose();
|
|
274
|
+
rt.dispose();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return faces;
|
|
278
|
+
}
|
|
@@ -41,7 +41,7 @@ interface MeshUserData {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/** 拾取 ID 只有 2 个字节(RG 两个通道),65534 是安全上限 */
|
|
44
|
-
const MAX_PICK_ID = 65534;
|
|
44
|
+
export const MAX_PICK_ID = 65534;
|
|
45
45
|
|
|
46
46
|
export interface PickHost {
|
|
47
47
|
readonly picker: Picker | null;
|
|
@@ -407,6 +407,53 @@ export class PickService {
|
|
|
407
407
|
return null;
|
|
408
408
|
}
|
|
409
409
|
|
|
410
|
+
/**
|
|
411
|
+
* 沿给定射线求**最近**交点距离(通视分析用)。
|
|
412
|
+
*
|
|
413
|
+
* 与 `pick` 不同:不走 GPU、不认批次,只回答「这条线上最先撞到什么」,
|
|
414
|
+
* 所以一次查询的代价是 O(已加载三角面),几十万面的场景在几十毫秒量级,
|
|
415
|
+
* 适合点击式查询;要逐帧连续更新得换深度图方案(见 `docs/API.md`)。
|
|
416
|
+
*
|
|
417
|
+
* @param origin 起点(场景 ENU 米)
|
|
418
|
+
* @param dir 单位方向
|
|
419
|
+
* @param maxDistance 只关心这个距离内的交点
|
|
420
|
+
*/
|
|
421
|
+
raycastDistance(
|
|
422
|
+
origin: readonly [number, number, number],
|
|
423
|
+
dir: readonly [number, number, number],
|
|
424
|
+
maxDistance: number
|
|
425
|
+
): { distance: number; point: [number, number, number] } | null {
|
|
426
|
+
if (!this.host.scene) return null;
|
|
427
|
+
const meshes = [...this.pickables.values()]
|
|
428
|
+
.map((p) => p.mesh)
|
|
429
|
+
.filter((m) => {
|
|
430
|
+
// 被隐藏的单体所在网格、以及整层隐藏的都要排除,否则会「撞到看不见的墙」
|
|
431
|
+
let curr: THREE.Object3D | null = m;
|
|
432
|
+
while (curr) {
|
|
433
|
+
if (!curr.visible) return false;
|
|
434
|
+
curr = curr.parent;
|
|
435
|
+
}
|
|
436
|
+
return true;
|
|
437
|
+
});
|
|
438
|
+
if (!meshes.length) return null;
|
|
439
|
+
|
|
440
|
+
const raycaster = new THREE.Raycaster();
|
|
441
|
+
raycaster.set(
|
|
442
|
+
new THREE.Vector3(origin[0], origin[1], origin[2]),
|
|
443
|
+
new THREE.Vector3(dir[0], dir[1], dir[2]).normalize()
|
|
444
|
+
);
|
|
445
|
+
raycaster.near = 0;
|
|
446
|
+
raycaster.far = maxDistance;
|
|
447
|
+
|
|
448
|
+
const hits = raycaster.intersectObjects(meshes, false);
|
|
449
|
+
if (!hits.length) return null;
|
|
450
|
+
const h = hits[0];
|
|
451
|
+
return {
|
|
452
|
+
distance: h.distance,
|
|
453
|
+
point: [h.point.x, h.point.y, h.point.z],
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
410
457
|
/**
|
|
411
458
|
* 沿视线方向穿透拾取多个单体,按由近至远排序返回。
|
|
412
459
|
*/
|