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/docs/API.md
CHANGED
|
@@ -172,10 +172,11 @@ new S3MViewer(options?: S3MViewerOptions)
|
|
|
172
172
|
| (随 `useWorker`)DXT 压缩直传 | 自动 | 自动 | 宿主 GL 同时支持 `WEBGL_compressed_texture_s3tc`(+srgb) 时,dxt1/dxt5(含 CRN 转码产物)不解码直接 `CompressedTexture` 上传,显存与上传带宽降为 1/4~1/8;块长与 4×4 对齐公式不符、flipY 图集、移动端无扩展时自动回退 RGBA 解码 |
|
|
173
173
|
| `lodRangeScale` | `number` | `1.0` | LOD 切换距离缩放系数,> 1 更倾向保留粗模,< 1 提前细化 |
|
|
174
174
|
| `lodPixelRatioCap` | `number` | `Infinity` | LOD 判定屏幕高度的 **DPR 上限**(不截断)。默认按 `canvasHeight` 物理像素算,与官方 `calcPixFromCam` 同口径;3x DPR 手机上由此会多细化、多拉瓦片,传 `2` 即按「CSS 高度 × 2」截断 |
|
|
175
|
-
| `anisotropyCap` | `number` | `Infinity` | 各向异性过滤上限(不封顶 = 取 GPU 最大值,通常 16)。移动端封到 4 / 8
|
|
176
|
-
| `maxTextureSize` | `number` | `Infinity` | 纹理长边像素上限(不限制)。非图集的 RGBA / WebP 产物按 2× 盒式滤波减半到上限内;DXT
|
|
175
|
+
| `anisotropyCap` | `number` | `Infinity` | 各向异性过滤上限(不封顶 = 取 GPU 最大值,通常 16)。移动端封到 4 / 8 肉眼通常无差,采样成本明显下降。**图层级**:运行时 `setAnisotropyCap()` 会立刻应用到已上传纹理(改采样参数 + 重传,不重解码、不重载瓦片),图集纹理恒为 1 |
|
|
176
|
+
| `maxTextureSize` | `number` | `Infinity` | 纹理长边像素上限(不限制)。非图集的 RGBA / WebP 产物按 2× 盒式滤波减半到上限内;DXT 压缩直传与图集不受影响。**图层级**,但只影响之后解码的纹理(已解码的不重解) |
|
|
177
177
|
| `maxImageCache` | `number` | `128` | 纹理缓存条数上限。一条 = 一张已解码的瓦片纹理,1024² RGBA 约 4MB,128 条即约 512MB 显存;低端设备 32~48。LRU,降下来下次淘汰时释放 |
|
|
178
178
|
| `materialQuality` | `'pbr' \| 'lambert'` | `'pbr'` | 光照模型。`lambert` 用 `MeshLambertMaterial` 替换 PBR,去掉 GGX 与环境球谐,片元约省一半;白模 / 纯色 / 按高度着色几乎无差。**图层级**:同页多个图层各用各的,互不覆盖;不传则跟随模块级默认(`setMaterialQuality`)。只影响之后创建的材质 |
|
|
179
|
+
| `colorPipeline` | `'linear' \| 'direct'` | `'linear'` | 颜色管线。`direct` = 官方 iClient3D 的 sRGB 直乘(同时关掉纹理解码),`lightingPreset: 'supermap'` 会选它。**图层级**:同页多图层各用各的;已加载瓦片可运行时 `setColorPipeline(q, { duration })` 走 fade-through 平滑切过去(重建材质 + 重传纹理,不重下瓦片) |
|
|
179
180
|
| `buildTile` | `BuildTileFn \| null` | — | 自定义瓦片构建器(测试 / 服务端渲染时注入) |
|
|
180
181
|
| `attributes` | `AttributeQueryOptions` | — | 可选的 iServer 数据服务配置,配了才能用 `colorByField()` 等便利方法;不配就自己取数 + `setFeaturesStyle()`,见[业务字段](#业务字段锚点在库内对应关系在外部) |
|
|
181
182
|
| `onRequestRepaint` | `() => void` | — | 宿主请求重画的信号:纯 three 宿主接自己的渲染循环 |
|
|
@@ -311,10 +312,30 @@ await viewer.setEnvironment('/env/city_2k.hdr', 1.0); // 等距柱状图(2:1
|
|
|
311
312
|
- **不传 = 不覆盖**(内部用 `-1` 当哨兵),保持数据自身的口径:`metalness` 恒 0,
|
|
312
313
|
`roughness` 由 S3M 的 `shininess` 映射(常见 0.95)。
|
|
313
314
|
- 只有 `materialQuality: 'pbr'`(默认)支持;`'lambert'` 模板里没有这两个 chunk,注入是 no-op。
|
|
315
|
+
- **图层级**:两个 uniform 是每个 viewer 自己的一份,同页多个图层互不影响。
|
|
316
|
+
- 过渡起点取数据侧常见值(`roughness` ≈ 0.95、`metalness` = 0),不是 0 —— 否则
|
|
317
|
+
`setStyle({ roughness: 0.2 }, { duration })` 会先跳到 0 再爬回来。
|
|
314
318
|
- ⚠️ 金属几乎没有漫反射,**没有环境贴图时 `metalness` 一高就整片发黑**,只剩平行光的
|
|
315
319
|
高光点。先 `setEnvironment()` 挂一张等距柱状图(内部会转成 PMREM 预滤波),
|
|
316
320
|
强度由 `scene.environmentIntensity` 控制。
|
|
317
321
|
|
|
322
|
+
### 颜色管线(`linear` / `direct`)与过渡
|
|
323
|
+
|
|
324
|
+
```ts
|
|
325
|
+
new S3MLayer({ colorPipeline: 'direct' }); // 建图层时定档(图层级)
|
|
326
|
+
await layer.setColorPipeline('direct', { duration: 400 }); // 运行时平滑切
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
- `'direct'` = 官方 iClient3D 的 sRGB 直乘管线(贴图字节值不解码 → 常数 Lambert 直乘
|
|
330
|
+
→ 输出前抵消 OETF),`lightingPreset: 'supermap'` 会同时选它并把各向异性收回 1。
|
|
331
|
+
- 切换牵扯三样东西:材质宏(换 program)、纹理 `colorSpace`(重传)、各向异性档位。
|
|
332
|
+
**都不需要重新下载瓦片** —— 改完 `needsUpdate` 重传一次像素即可。
|
|
333
|
+
- `duration > 0` 走 fade-through:压暗(`uFade` 到 0.35)→ 同一帧里换管线 → 亮回来。
|
|
334
|
+
不做逐像素混合:两条管线的差异有一部分在采样端(硬件 sRGB 解码 vs 原始字节),
|
|
335
|
+
要混合就得让贴图恒为原始字节并在片元里手动解码,那会改掉默认路径的采样数学。
|
|
336
|
+
- 同页多个图层各持自己的管线,`setColorPipeline` 只影响本层。
|
|
337
|
+
- `setAnisotropyCap()` 同样即时生效(改采样参数 + 重传),不再"只影响之后创建的纹理"。
|
|
338
|
+
|
|
318
339
|
### 单体样式与状态
|
|
319
340
|
|
|
320
341
|
```ts
|
|
@@ -389,6 +410,106 @@ getFeatureBounds(smid: number): [[number, number], [number, number]] | null
|
|
|
389
410
|
|
|
390
411
|
拾取走 GPU:把「物体编号 + batchId」编码成 RGBA 渲染到 **1×1** 离屏目标再读那一个像素——viewport 仍是整块画布、原点平移到点击点,于是整幅画面按原样光栅化,只有点击点那一格落在目标上。代价与三角面数无关,且天然只命中最前面的可见面。
|
|
391
412
|
|
|
413
|
+
### 三维空间分析(量算 / 通视 / 裁剪)
|
|
414
|
+
|
|
415
|
+
```ts
|
|
416
|
+
measureDistance(a: LngLatAlt, b: LngLatAlt): { distance3D; distanceHorizontal; heightDiff }
|
|
417
|
+
measurePolyline(points: LngLatAlt[]): { length3D; lengthHorizontal }
|
|
418
|
+
measurePolygon(points: LngLatAlt[]): { area3D; areaHorizontal; perimeter }
|
|
419
|
+
|
|
420
|
+
lineOfSight(a: LngLatAlt, b: LngLatAlt, opts?): {
|
|
421
|
+
visible: boolean; distance: number; blockedDistance: number | null;
|
|
422
|
+
blockedPoint: [number, number, number] | null; visibleRatio: number;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
setClipping(opt: ClipOptions | null): void // 平面 / Box / 十字
|
|
426
|
+
readonly clipping: ClipOptions | null
|
|
427
|
+
|
|
428
|
+
skyline(opts?: { width?; height?; step? }): SkylineResult | null
|
|
429
|
+
viewshed(opts: { origin: LngLatAlt; radius: number; grid?; eyeHeight?; size? }): ViewshedResult | null
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
- 入参是**经纬度 + 绝对高程**,直接用 `pickPosition(x, y)` 的返回值;交互(取点、拖拽、标签、撤销)属于应用层,不进库。
|
|
433
|
+
- 纯数学部分(距离 / 面积 / 通视判定 / 半空间生成)导出在 `s3m-three` 根上,**不依赖 WebGL**,可以在 Worker 或服务端复用。
|
|
434
|
+
- 面积有两套口径:`area3D` 是坡面真实面积(Newell 法),`areaHorizontal` 是水平投影(鞋带公式)。建筑面积、用地面积按后者。
|
|
435
|
+
- `lineOfSight` 走 CPU 射线求交(`Raycaster` 打全部 pickable mesh),几十万面是几十毫秒量级,适合点击式查询;要拖拽时逐帧刷新得等深度图批量采样版本。
|
|
436
|
+
- 裁剪是**图层级**的(每个 `S3MLayer` 一个 `WebGLRenderer`,平面挂在它的 `clippingPlanes` 上),同页其它图层不受影响。传 `null` 取消。
|
|
437
|
+
- **两个已知边界**:① 只做「切掉」不做**封口**,剖开后看到的是空壳/背面;② 半空间求交只能表达凸区域,凹多边形裁剪需要多组并集,three 的 `clippingPlanes` 表达不了。
|
|
438
|
+
- **分析范围只包含 S3M 场景里的几何**:MapLibre 底图地形与 MVT 建筑不在这个场景里,不参与遮挡判断。
|
|
439
|
+
|
|
440
|
+
**天际线 / 360° 可视域**(P1)走的是「一次离屏深度 pass + CPU 归约」:
|
|
441
|
+
|
|
442
|
+
- `skyline()`:用当前视角渲染一张 `RGBADepthPacking` 深度图,按列取最上面的几何像素,
|
|
443
|
+
反投影成(方位角, 仰角)。一次调用 = 一趟额外渲染 + 一次全图回读,**不要每帧调**,
|
|
444
|
+
相机停下后算一次。精度由 `width` 决定(640 宽 ≈ 0.56°/列)。
|
|
445
|
+
- 结果里除了角度,还有 `screen`(每列天际线像素的**归一化屏幕坐标** `[x0,y0,x1,y1,…]`)
|
|
446
|
+
和 `hasGeometry`(纯天空列)。要在**地图上描天际线**,直接用这两个:
|
|
447
|
+
深度图本来就是屏幕空间的产物,换算成角度只为读数,画线再投影一次反而多一层误差。
|
|
448
|
+
`hasGeometry = 0` 的列必须断开,否则线会横穿天空掉到画面底部。
|
|
449
|
+
- 天际线绑定位姿:镜头一动就作废,应用层要在 `movestart` 清掉、`moveend` 按需重算。
|
|
450
|
+
- `viewshed()`:从观察点朝 6 个方向各渲染一次「到观察点的距离」,再在 CPU 上对
|
|
451
|
+
地面网格逐格比较,返回 `raster`(1 可见 / 0 遮挡 / -1 无效)。一次调用 = 6 趟渲染 + 6 次回读,
|
|
452
|
+
成本明显更高,只在点击/松手时算。
|
|
453
|
+
- 两者都**不做建筑立面逐像素着色**(那要把立方体深度注入每种材质的 shader)。
|
|
454
|
+
- 已知限制:`overrideMaterial` 换材质时**实例化网格(点云/树木)不参与遮挡**。
|
|
455
|
+
|
|
456
|
+
#### 分析范围:只含 S3M 场景,底图内容要外围自己补
|
|
457
|
+
|
|
458
|
+
`skyline()` / `viewshed()` / `lineOfSight()` 都只看 **S3M 场景里的几何**:
|
|
459
|
+
|
|
460
|
+
- MapLibre 的 `fill-extrusion` 建筑、terrain 地形不在 three 场景里,**不参与遮挡**;
|
|
461
|
+
- raster 底图本来就没有高度,没什么可参与;
|
|
462
|
+
- 也**读不到** MapLibre 的深度:自定义图层画在主 framebuffer 里,帧内拿不出来;而深度 pass 用的是自己的 FBO + `overrideMaterial`,两套深度不共享。
|
|
463
|
+
|
|
464
|
+
要「整座城市的天际线」,只能**复制一份几何**进来。这一步属于外围,库提供两个必要的原语:
|
|
465
|
+
`lngLatToPoint()`(经纬度 → 场景 ENU)和 `include`(只参与这一趟深度渲染的额外几何)。
|
|
466
|
+
|
|
467
|
+
```ts
|
|
468
|
+
// 1) 从底图矢量源取当前视野的建筑(高度字段各家不同,见下面的坑)
|
|
469
|
+
const feats = map.querySourceFeatures('openmaptiles', { sourceLayer: 'building' });
|
|
470
|
+
|
|
471
|
+
// 2) 轮廓 → 场景 ENU → 挤出
|
|
472
|
+
const proxy = new THREE.Group();
|
|
473
|
+
for (const f of feats) {
|
|
474
|
+
const h = Number(f.properties.render_height ?? f.properties.height ?? 0);
|
|
475
|
+
if (!(h > 0)) continue;
|
|
476
|
+
const shape = new THREE.Shape(
|
|
477
|
+
f.geometry.coordinates[0].map(([lng, lat]) => {
|
|
478
|
+
const p = layer.lngLatToPoint({ lng, lat, altitude: 0 });
|
|
479
|
+
return new THREE.Vector2(p[0], p[1]);
|
|
480
|
+
})
|
|
481
|
+
);
|
|
482
|
+
proxy.add(new THREE.Mesh(new THREE.ExtrudeGeometry(shape, { depth: h, bevelEnabled: false })));
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// 3) 只喂给这一趟深度渲染:不进主渲染,不会和 MapLibre 重复画
|
|
486
|
+
const r = layer.skyline({ width: 1280, height: 720, include: [proxy] });
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
`include` 的语义:对象在采集期间临时挂进 viewer 场景,采集完立刻摘掉
|
|
490
|
+
(已经有 parent 的会被跳过)。`viewshed()` 同样支持这个参数。
|
|
491
|
+
|
|
492
|
+
**三个坑**:
|
|
493
|
+
|
|
494
|
+
1. **高度字段口径不统一** —— `render_height`(OpenMapTiles)/ `height`(OSM 原始)/ `building:levels × 3`,
|
|
495
|
+
要和底图样式里 `fill-extrusion-height` 的表达式保持一致,否则会出现「分析里有一栋、画面上没有」。
|
|
496
|
+
2. **要素会重复** —— `querySourceFeatures` 返回当前视野内所有已加载瓦片的要素,跨瓦片边界的建筑会出现多次,
|
|
497
|
+
需要按 id 或几何去重,否则代理几何叠在一起(对天际线无害,对统计有害)。
|
|
498
|
+
3. **代理 ≠ 画面** —— 底图如果用了 `fill-extrusion-height` 插值或 terrain 抬升,代理和实际渲染会有偏差;
|
|
499
|
+
要完全一致,就得让底图建筑也由自己渲染,那已经超出「分析」的范围了。
|
|
500
|
+
|
|
501
|
+
`demo/analysis.html` 里有可运行的例子:「含底图天际线」(只喂分析)+「显示代理」(把代理塞进画面看效果)。
|
|
502
|
+
|
|
503
|
+
```ts
|
|
504
|
+
type ClipOptions =
|
|
505
|
+
| { type: 'plane'; normal: [number, number, number]; point: [number, number, number] }
|
|
506
|
+
| { type: 'box'; a: [number, number, number]; b: [number, number, number] }
|
|
507
|
+
| { type: 'cross'; center: [number, number, number]; keepX: 1 | -1; keepY: 1 | -1;
|
|
508
|
+
horizontal?: boolean; keepZ?: 1 | -1 };
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
法线一律指向**保留侧**(与 three 的 `Plane` 口径一致:`normal·p + constant >= 0` 保留)。
|
|
512
|
+
|
|
392
513
|
### LOD 与统计
|
|
393
514
|
|
|
394
515
|
```ts
|
|
@@ -400,7 +521,7 @@ setMaxTriangles(n: number, datasetId?: string): void // Infinity = 不限制
|
|
|
400
521
|
readonly maxTriangles: number
|
|
401
522
|
setMaxMemory(bytes: number, datasetId?: string): void // 显存字节上限,Infinity = 不限制(默认)
|
|
402
523
|
readonly maxMemory: number
|
|
403
|
-
setAnisotropyCap(cap: number): void // Infinity = 取 GPU
|
|
524
|
+
setAnisotropyCap(cap: number): void // Infinity = 取 GPU 最大值(默认);即时应用到已上传纹理
|
|
404
525
|
readonly anisotropyCap: number
|
|
405
526
|
setMaxTextureSize(n: number): void // Infinity = 不限制(默认)
|
|
406
527
|
readonly maxTextureSize: number
|
package/package.json
CHANGED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 裁剪 / 剖切的纯数学部分:把「平面 / Box / 十字」三种交互输入翻译成一组半空间。
|
|
3
|
+
*
|
|
4
|
+
* 与 three 的 `THREE.Plane` 同构(`normal` 单位化,`constant = -normal·point`),
|
|
5
|
+
* 判据是 three 的口径:`normal·p + constant >= 0` 的片元**保留**,否则丢弃。
|
|
6
|
+
* 所以这里的法线一律朝「保留侧」。
|
|
7
|
+
*
|
|
8
|
+
* 不依赖 three,`S3MViewer.setClipping` 再把结果转成 `THREE.Plane` 挂到
|
|
9
|
+
* `renderer.clippingPlanes`(每个图层一个 renderer,天然图层级)。
|
|
10
|
+
*
|
|
11
|
+
* 已知边界:
|
|
12
|
+
* - 半空间求交只能表达**凸**区域,凹多边形要多组并集(three 的 clippingPlanes
|
|
13
|
+
* 是「全部满足才保留」,做不到并集),见 `docs/API.md` 的说明;
|
|
14
|
+
* - 不做封口:剖切后看到的是背面/空壳,封口要靠 stencil 或背面重绘,不在这一层。
|
|
15
|
+
*/
|
|
16
|
+
import type { Vec3 } from './measure.js';
|
|
17
|
+
|
|
18
|
+
/** 与 `THREE.Plane` 同构:`normal` 必须单位化 */
|
|
19
|
+
export interface PlaneLike {
|
|
20
|
+
normal: Vec3;
|
|
21
|
+
constant: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function normalize(v: Vec3): Vec3 {
|
|
25
|
+
const len = Math.hypot(v[0], v[1], v[2]);
|
|
26
|
+
if (len < 1e-9) return [0, 0, 1];
|
|
27
|
+
return [v[0] / len, v[1] / len, v[2] / len];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function dot(a: Vec3, b: Vec3): number {
|
|
31
|
+
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** 过一个点、法线指向**保留侧**的平面 */
|
|
35
|
+
export function planeFromPointNormal(normal: Vec3, point: Vec3): PlaneLike {
|
|
36
|
+
const n = normalize(normal);
|
|
37
|
+
return { normal: n, constant: -dot(n, point) };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 点是否在保留侧(含边界);用于调试与单测 */
|
|
41
|
+
export function isPointKept(planes: readonly PlaneLike[], p: Vec3): boolean {
|
|
42
|
+
for (const pl of planes) {
|
|
43
|
+
if (dot(pl.normal, p) + pl.constant < 0) return false;
|
|
44
|
+
}
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* ------------------------------------------------------------------ 输入形态 */
|
|
49
|
+
|
|
50
|
+
/** 平面裁剪:法线指向保留的一侧,切掉另一侧 */
|
|
51
|
+
export interface ClipPlaneOptions {
|
|
52
|
+
type: 'plane';
|
|
53
|
+
/** 指向保留侧的法线(不必单位化) */
|
|
54
|
+
normal: Vec3;
|
|
55
|
+
/** 平面过的一个点 */
|
|
56
|
+
point: Vec3;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Box 裁剪:保留盒内的部分 */
|
|
60
|
+
export interface ClipBoxOptions {
|
|
61
|
+
type: 'box';
|
|
62
|
+
/** 对角点(顺序无所谓,内部会取 min/max) */
|
|
63
|
+
a: Vec3;
|
|
64
|
+
b: Vec3;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 十字剖切:用两个竖直面切掉一个象限,露出两个互相垂直的剖面。
|
|
69
|
+
*
|
|
70
|
+
* `keepX` / `keepY` 指保留哪个方向:`1` 保留 +x 侧,`-1` 保留 -x 侧。
|
|
71
|
+
* 加 `horizontal: true` 再多一个水平面,变成「切掉一个角」的三面剖切。
|
|
72
|
+
*/
|
|
73
|
+
export interface ClipCrossOptions {
|
|
74
|
+
type: 'cross';
|
|
75
|
+
center: Vec3;
|
|
76
|
+
keepX: 1 | -1;
|
|
77
|
+
keepY: 1 | -1;
|
|
78
|
+
/** 是否再叠一个水平面(在 center.z 处切掉上方),默认 false */
|
|
79
|
+
horizontal?: boolean;
|
|
80
|
+
/** 水平面保留哪一侧:`1` 保留上方,`-1` 保留下方,默认 -1 */
|
|
81
|
+
keepZ?: 1 | -1;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export type ClipOptions = ClipPlaneOptions | ClipBoxOptions | ClipCrossOptions;
|
|
85
|
+
|
|
86
|
+
/* ------------------------------------------------------------------ 生成 */
|
|
87
|
+
|
|
88
|
+
export function planeClipping(opt: Omit<ClipPlaneOptions, 'type'>): PlaneLike[] {
|
|
89
|
+
return [planeFromPointNormal(opt.normal, opt.point)];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function boxClipping(opt: Omit<ClipBoxOptions, 'type'>): PlaneLike[] {
|
|
93
|
+
const min: Vec3 = [
|
|
94
|
+
Math.min(opt.a[0], opt.b[0]),
|
|
95
|
+
Math.min(opt.a[1], opt.b[1]),
|
|
96
|
+
Math.min(opt.a[2], opt.b[2]),
|
|
97
|
+
];
|
|
98
|
+
const max: Vec3 = [
|
|
99
|
+
Math.max(opt.a[0], opt.b[0]),
|
|
100
|
+
Math.max(opt.a[1], opt.b[1]),
|
|
101
|
+
Math.max(opt.a[2], opt.b[2]),
|
|
102
|
+
];
|
|
103
|
+
// 六个面,法线一律朝盒内 —— 六个半空间求交就是「盒内」
|
|
104
|
+
return [
|
|
105
|
+
{ normal: [1, 0, 0], constant: -min[0] },
|
|
106
|
+
{ normal: [-1, 0, 0], constant: max[0] },
|
|
107
|
+
{ normal: [0, 1, 0], constant: -min[1] },
|
|
108
|
+
{ normal: [0, -1, 0], constant: max[1] },
|
|
109
|
+
{ normal: [0, 0, 1], constant: -min[2] },
|
|
110
|
+
{ normal: [0, 0, -1], constant: max[2] },
|
|
111
|
+
];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function crossClipping(opt: Omit<ClipCrossOptions, 'type'>): PlaneLike[] {
|
|
115
|
+
const planes: PlaneLike[] = [
|
|
116
|
+
planeFromPointNormal([opt.keepX, 0, 0], opt.center),
|
|
117
|
+
planeFromPointNormal([0, opt.keepY, 0], opt.center),
|
|
118
|
+
];
|
|
119
|
+
if (opt.horizontal) {
|
|
120
|
+
planes.push(planeFromPointNormal([0, 0, opt.keepZ ?? -1], opt.center));
|
|
121
|
+
}
|
|
122
|
+
return planes;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** 统一入口:把交互输入翻成半空间列表 */
|
|
126
|
+
export function buildClipPlanes(opt: ClipOptions): PlaneLike[] {
|
|
127
|
+
switch (opt.type) {
|
|
128
|
+
case 'plane':
|
|
129
|
+
return planeClipping(opt);
|
|
130
|
+
case 'box':
|
|
131
|
+
return boxClipping(opt);
|
|
132
|
+
case 'cross':
|
|
133
|
+
return crossClipping(opt);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 深度图回读的数学部分:解包 + 反投影。
|
|
3
|
+
*
|
|
4
|
+
* GPU 那边只能把深度渲染成 RGBA8(three 的 `RGBADepthPacking`),
|
|
5
|
+
* 回读到 CPU 后要还原成 0~1 的窗口深度,再反投影成世界坐标 —— 这两步都在这里,
|
|
6
|
+
* 与 WebGL 无关,可以用构造出来的矩阵直接单测。
|
|
7
|
+
*
|
|
8
|
+
* 打包/解包的常数照抄 three 的 `packing.glsl.js`,不要自己改:
|
|
9
|
+
* 改一个常数,回读出来的深度就会整体偏移,而画面看起来完全正常。
|
|
10
|
+
*/
|
|
11
|
+
import type { Vec3 } from './measure.js';
|
|
12
|
+
|
|
13
|
+
/** three: `PackUpscale = 256 / 255` */
|
|
14
|
+
const PACK_UPSCALE = 256 / 255;
|
|
15
|
+
/** three: `UnpackDownscale = 255 / 256` */
|
|
16
|
+
const UNPACK_DOWNSCALE = 255 / 256;
|
|
17
|
+
/** three: `ShiftRight8 = 1 / 256` */
|
|
18
|
+
const SHIFT_RIGHT_8 = 1 / 256;
|
|
19
|
+
/** three: `PackFactors = (1, 256, 65536, 16777216)` */
|
|
20
|
+
const PACK_FACTOR_A = 16777216;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 把 0~1 的深度按 three 的 `RGBADepthPacking` 打包成 4 个字节(0~255)。
|
|
24
|
+
*
|
|
25
|
+
* 严格照抄 `packing.glsl.js` 的 `packDepthToRGBA`,**不要凭直觉改**:
|
|
26
|
+
* - `v <= 0` 返回全黑、`v >= 1` 返回**全白**(这两条特判是精度链条的一部分,
|
|
27
|
+
* 也正好让「远平面」和「清屏白」解包出来都是 1.0,背景判定因此简单);
|
|
28
|
+
* - 中间值是 `modf` 链拆出来的 32 位定点,通道顺序是 (粗 → 细) 的 r/g/b/a。
|
|
29
|
+
*
|
|
30
|
+
* 精度:解包误差约 1e-5(受 g 通道 8 位量化限制),换算成 141m 处的位置误差约 0.2m。
|
|
31
|
+
*/
|
|
32
|
+
export function packRGBADepth(depth: number): [number, number, number, number] {
|
|
33
|
+
const v = Math.min(1, Math.max(0, depth));
|
|
34
|
+
if (v <= 0) return [0, 0, 0, 0];
|
|
35
|
+
if (v >= 1) return [255, 255, 255, 255];
|
|
36
|
+
|
|
37
|
+
const af = modf(v * PACK_FACTOR_A);
|
|
38
|
+
const b = modf(af.int * SHIFT_RIGHT_8);
|
|
39
|
+
const g = modf(b.int * SHIFT_RIGHT_8);
|
|
40
|
+
|
|
41
|
+
const toByte = (x: number): number => Math.max(0, Math.min(255, Math.round(x * 255)));
|
|
42
|
+
return [
|
|
43
|
+
// r = floor(v*256)/255 —— 乘回 255 就是整数本身
|
|
44
|
+
toByte(g.int / 255),
|
|
45
|
+
toByte(g.frac * PACK_UPSCALE),
|
|
46
|
+
toByte(b.frac * PACK_UPSCALE),
|
|
47
|
+
toByte(af.frac),
|
|
48
|
+
];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 解包:RGBA 四个字节 → 0~1 的窗口深度(0 = 近平面,1 = 远平面 / 背景)。
|
|
53
|
+
*
|
|
54
|
+
* 与 `packing.glsl.js` 的 `unpackRGBAToDepth` 同式:
|
|
55
|
+
* `dot(rgba, (UnpackDownscale/1, UnpackDownscale/256, UnpackDownscale/65536, 1/2^24))`。
|
|
56
|
+
*
|
|
57
|
+
* @param rgba 回读出来的像素缓冲
|
|
58
|
+
* @param offset 字节偏移(第 i 个像素就是 i*4)
|
|
59
|
+
*/
|
|
60
|
+
export function unpackRGBADepth(rgba: ArrayLike<number>, offset = 0): number {
|
|
61
|
+
const r = rgba[offset]! / 255;
|
|
62
|
+
const g = rgba[offset + 1]! / 255;
|
|
63
|
+
const b = rgba[offset + 2]! / 255;
|
|
64
|
+
const a = rgba[offset + 3]! / 255;
|
|
65
|
+
return (
|
|
66
|
+
r * UNPACK_DOWNSCALE +
|
|
67
|
+
g * (UNPACK_DOWNSCALE / 256) +
|
|
68
|
+
b * (UNPACK_DOWNSCALE / 65536) +
|
|
69
|
+
a / PACK_FACTOR_A
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** GLSL `modf`:返回小数部分,整数部分写进 `int` */
|
|
74
|
+
function modf(x: number): { frac: number; int: number } {
|
|
75
|
+
const int = Math.floor(x);
|
|
76
|
+
return { frac: x - int, int };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* NDC + 窗口深度 → 世界坐标。
|
|
81
|
+
*
|
|
82
|
+
* @param ndcX -1~1(屏幕左侧到右侧)
|
|
83
|
+
* @param ndcY -1~1(屏幕底部到顶部;注意与像素行号反向)
|
|
84
|
+
* @param depth01 0~1 的窗口深度
|
|
85
|
+
* @param inverseViewProj 视图投影矩阵的逆(列主序 16 元,与 `THREE.Matrix4.elements` 同序)
|
|
86
|
+
*/
|
|
87
|
+
export function unprojectNdc(
|
|
88
|
+
ndcX: number,
|
|
89
|
+
ndcY: number,
|
|
90
|
+
depth01: number,
|
|
91
|
+
inverseViewProj: ArrayLike<number>
|
|
92
|
+
): Vec3 {
|
|
93
|
+
const z = depth01 * 2 - 1;
|
|
94
|
+
const m = inverseViewProj;
|
|
95
|
+
const x = m[0]! * ndcX + m[4]! * ndcY + m[8]! * z + m[12]!;
|
|
96
|
+
const y = m[1]! * ndcX + m[5]! * ndcY + m[9]! * z + m[13]!;
|
|
97
|
+
const zz = m[2]! * ndcX + m[6]! * ndcY + m[10]! * z + m[14]!;
|
|
98
|
+
const w = m[3]! * ndcX + m[7]! * ndcY + m[11]! * z + m[15]!;
|
|
99
|
+
const iw = Math.abs(w) > 1e-12 ? 1 / w : 1;
|
|
100
|
+
return [x * iw, y * iw, zz * iw];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** 像素坐标(左上角为 0,0)→ NDC */
|
|
104
|
+
export function pixelToNdc(px: number, py: number, width: number, height: number): [number, number] {
|
|
105
|
+
return [((px + 0.5) / width) * 2 - 1, 1 - ((py + 0.5) / height) * 2];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 把「观察点 → 目标点」拆成方位角 / 仰角(度)。
|
|
110
|
+
*
|
|
111
|
+
* 方位角:0 = 正北,顺时针增大(与地图 bearing 一致),ENU 下 x 向东、y 向北。
|
|
112
|
+
* 仰角:0 = 水平,正值向上,负值向下。
|
|
113
|
+
*/
|
|
114
|
+
export function azimuthElevation(
|
|
115
|
+
from: Vec3,
|
|
116
|
+
to: Vec3
|
|
117
|
+
): { azimuth: number; elevation: number; distance: number } {
|
|
118
|
+
const dx = to[0] - from[0];
|
|
119
|
+
const dy = to[1] - from[1];
|
|
120
|
+
const dz = to[2] - from[2];
|
|
121
|
+
const horizontal = Math.hypot(dx, dy);
|
|
122
|
+
const distance = Math.hypot(horizontal, dz);
|
|
123
|
+
const azimuth = ((Math.atan2(dx, dy) * 180) / Math.PI + 360) % 360;
|
|
124
|
+
const elevation = (Math.atan2(dz, horizontal) * 180) / Math.PI;
|
|
125
|
+
return { azimuth, elevation, distance };
|
|
126
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 三维空间分析内核(宿主无关、不依赖 WebGL)。
|
|
3
|
+
*
|
|
4
|
+
* 分工:
|
|
5
|
+
* - 本目录只做**纯数学**:量算、通视判定、裁剪半空间生成。可以直接单测。
|
|
6
|
+
* - 需要 GPU 的部分(贴地采样、天际线、可视域、阴影)在 `render/` 下,
|
|
7
|
+
* 复用 `Picker` 的离屏 pass 与状态恢复。
|
|
8
|
+
* - 交互(取点、拖拽、标签绘制)属于应用层,不进这个包。
|
|
9
|
+
*/
|
|
10
|
+
export {
|
|
11
|
+
distance3D,
|
|
12
|
+
distanceHorizontal,
|
|
13
|
+
heightDifference,
|
|
14
|
+
polylineLength,
|
|
15
|
+
polylineLengthHorizontal,
|
|
16
|
+
polygonPerimeter,
|
|
17
|
+
polygonArea,
|
|
18
|
+
polygonAreaHorizontal,
|
|
19
|
+
measureSegment,
|
|
20
|
+
} from './measure.js';
|
|
21
|
+
export type { Vec3, LngLatAlt, MeasureResult } from './measure.js';
|
|
22
|
+
|
|
23
|
+
export { lineOfSight } from './lineOfSight.js';
|
|
24
|
+
export type { RayHit, RayCast, LineOfSightOptions, LineOfSightResult } from './lineOfSight.js';
|
|
25
|
+
|
|
26
|
+
export {
|
|
27
|
+
planeFromPointNormal,
|
|
28
|
+
isPointKept,
|
|
29
|
+
planeClipping,
|
|
30
|
+
boxClipping,
|
|
31
|
+
crossClipping,
|
|
32
|
+
buildClipPlanes,
|
|
33
|
+
} from './clipping.js';
|
|
34
|
+
export type {
|
|
35
|
+
PlaneLike,
|
|
36
|
+
ClipPlaneOptions,
|
|
37
|
+
ClipBoxOptions,
|
|
38
|
+
ClipCrossOptions,
|
|
39
|
+
ClipOptions,
|
|
40
|
+
} from './clipping.js';
|
|
41
|
+
|
|
42
|
+
/* 深度图回读(P1:天际线 / 可视域的数学部分,GL 在 render/analysis/DepthPass) */
|
|
43
|
+
export { packRGBADepth, unpackRGBADepth, unprojectNdc, pixelToNdc, azimuthElevation } from './depth.js';
|
|
44
|
+
export { skylineFromDepth } from './skyline.js';
|
|
45
|
+
export type { SkylineArgs, SkylineResult } from './skyline.js';
|
|
46
|
+
export { computeViewshed, makeCubeDistanceSampler } from './viewshed.js';
|
|
47
|
+
export type { ViewshedOptions, ViewshedResult } from './viewshed.js';
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 通视分析(单条视线)。
|
|
3
|
+
*
|
|
4
|
+
* 求交本体由调用方注入 —— 内核只负责「沿视线找第一个遮挡、算可见比例」,
|
|
5
|
+
* 这样不依赖 three,也能在单测里用解析几何伪造求交结果。
|
|
6
|
+
*
|
|
7
|
+
* 场景里实际用的求交是 `THREE.Raycaster` 打全部 pickable mesh(与
|
|
8
|
+
* `PickService.drillPick` 同一条路径),由 `S3MViewer.lineOfSight` 装配。
|
|
9
|
+
*/
|
|
10
|
+
import { distance3D, type Vec3 } from './measure.js';
|
|
11
|
+
|
|
12
|
+
/** 一次射线求交的结果:只关心「最近的交点距离」 */
|
|
13
|
+
export interface RayHit {
|
|
14
|
+
/** 沿射线的距离(米) */
|
|
15
|
+
distance: number;
|
|
16
|
+
/** 交点位置(可选,用于画遮挡点标记) */
|
|
17
|
+
point?: Vec3;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 射线求交函数。
|
|
22
|
+
*
|
|
23
|
+
* @param origin 起点(场景 ENU 米)
|
|
24
|
+
* @param dir 单位方向
|
|
25
|
+
* @param maxDistance 只关心这个距离内的交点,超出可以返回 null(实现可据此提前剪枝)
|
|
26
|
+
*/
|
|
27
|
+
export type RayCast = (origin: Vec3, dir: Vec3, maxDistance: number) => RayHit | null;
|
|
28
|
+
|
|
29
|
+
export interface LineOfSightOptions {
|
|
30
|
+
/**
|
|
31
|
+
* 起点外推距离(米,默认 0.05)。
|
|
32
|
+
*
|
|
33
|
+
* 从建筑表面起步时,起点就落在三角面上,浮点误差会让射线立刻打中自己所在的
|
|
34
|
+
* 那个三角面,于是「永远不可见」。沿视线方向先推一小段再求交即可避开。
|
|
35
|
+
*/
|
|
36
|
+
originOffset?: number;
|
|
37
|
+
/**
|
|
38
|
+
* 终点内缩距离(米,默认 0.05)。同理:终点若贴在目标表面,不要把目标自身
|
|
39
|
+
* 当成遮挡物。
|
|
40
|
+
*/
|
|
41
|
+
targetOffset?: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface LineOfSightResult {
|
|
45
|
+
/** 起终点之间是否通视 */
|
|
46
|
+
visible: boolean;
|
|
47
|
+
/** 起点到终点的直线距离(米) */
|
|
48
|
+
distance: number;
|
|
49
|
+
/** 第一个遮挡点距起点的距离(米);通视时为 null */
|
|
50
|
+
blockedDistance: number | null;
|
|
51
|
+
/** 第一个遮挡点的位置;通视时为 null */
|
|
52
|
+
blockedPoint: Vec3 | null;
|
|
53
|
+
/** 可见段占全长的比例(0~1,1 = 完全通视)—— 直接用来画「绿段 / 红段」 */
|
|
54
|
+
visibleRatio: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 计算两点之间的通视情况。
|
|
59
|
+
*
|
|
60
|
+
* @param from 起点(场景 ENU 米)
|
|
61
|
+
* @param to 终点
|
|
62
|
+
* @param cast 射线求交函数
|
|
63
|
+
*/
|
|
64
|
+
export function lineOfSight(
|
|
65
|
+
from: Vec3,
|
|
66
|
+
to: Vec3,
|
|
67
|
+
cast: RayCast,
|
|
68
|
+
opts: LineOfSightOptions = {}
|
|
69
|
+
): LineOfSightResult {
|
|
70
|
+
const total = distance3D(from, to);
|
|
71
|
+
if (total <= 1e-6) {
|
|
72
|
+
return { visible: true, distance: 0, blockedDistance: null, blockedPoint: null, visibleRatio: 1 };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const dir: Vec3 = [
|
|
76
|
+
(to[0] - from[0]) / total,
|
|
77
|
+
(to[1] - from[1]) / total,
|
|
78
|
+
(to[2] - from[2]) / total,
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
const startOffset = Math.max(0, opts.originOffset ?? 0.05);
|
|
82
|
+
const endOffset = Math.max(0, opts.targetOffset ?? 0.05);
|
|
83
|
+
// 两端各缩一点;太短的视线就不缩了,免得区间翻负
|
|
84
|
+
const shrink = Math.min(startOffset + endOffset, total * 0.5);
|
|
85
|
+
const origin: Vec3 = [
|
|
86
|
+
from[0] + dir[0] * startOffset,
|
|
87
|
+
from[1] + dir[1] * startOffset,
|
|
88
|
+
from[2] + dir[2] * startOffset,
|
|
89
|
+
];
|
|
90
|
+
const limit = Math.max(0, total - shrink);
|
|
91
|
+
|
|
92
|
+
const hit = cast(origin, dir, limit);
|
|
93
|
+
if (hit && hit.distance < limit) {
|
|
94
|
+
// 距离要把起点外推量加回去,才是「从真实起点算起」
|
|
95
|
+
const blockedDistance = Math.min(total, hit.distance + startOffset);
|
|
96
|
+
return {
|
|
97
|
+
visible: false,
|
|
98
|
+
distance: total,
|
|
99
|
+
blockedDistance,
|
|
100
|
+
blockedPoint: hit.point ?? [
|
|
101
|
+
from[0] + dir[0] * blockedDistance,
|
|
102
|
+
from[1] + dir[1] * blockedDistance,
|
|
103
|
+
from[2] + dir[2] * blockedDistance,
|
|
104
|
+
],
|
|
105
|
+
visibleRatio: Math.max(0, Math.min(1, blockedDistance / total)),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { visible: true, distance: total, blockedDistance: null, blockedPoint: null, visibleRatio: 1 };
|
|
110
|
+
}
|