zero2geospatial3d-sdk 0.1.0__py3-none-any.whl
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.
- zero2geospatial3d/__init__.py +105 -0
- zero2geospatial3d/acoustics.py +284 -0
- zero2geospatial3d/classify.py +379 -0
- zero2geospatial3d/cli.py +367 -0
- zero2geospatial3d/comfort.py +687 -0
- zero2geospatial3d/data/epw_stations.json.gz +0 -0
- zero2geospatial3d/daylight.py +337 -0
- zero2geospatial3d/dispersion.py +249 -0
- zero2geospatial3d/epw.py +162 -0
- zero2geospatial3d/geo.py +138 -0
- zero2geospatial3d/grid.py +119 -0
- zero2geospatial3d/microclimate.py +388 -0
- zero2geospatial3d/morphology.py +283 -0
- zero2geospatial3d/nsga2.py +315 -0
- zero2geospatial3d/osm.py +450 -0
- zero2geospatial3d/osm_tags.py +180 -0
- zero2geospatial3d/py.typed +0 -0
- zero2geospatial3d/results.py +231 -0
- zero2geospatial3d/road_sources.py +141 -0
- zero2geospatial3d/scene.py +555 -0
- zero2geospatial3d/server.py +332 -0
- zero2geospatial3d/shading.py +443 -0
- zero2geospatial3d/solar.py +207 -0
- zero2geospatial3d/terrain.py +610 -0
- zero2geospatial3d/urban_form.py +326 -0
- zero2geospatial3d/walkability.py +380 -0
- zero2geospatial3d/weather.py +373 -0
- zero2geospatial3d/web/index.html +50 -0
- zero2geospatial3d/web/src/analysis/climate.js +278 -0
- zero2geospatial3d/web/src/analysis/compliance.js +345 -0
- zero2geospatial3d/web/src/analysis/gpu.js +431 -0
- zero2geospatial3d/web/src/analysis/optimize.js +208 -0
- zero2geospatial3d/web/src/analysis/runners.js +1644 -0
- zero2geospatial3d/web/src/analysis/terrain_results.js +145 -0
- zero2geospatial3d/web/src/app.js +630 -0
- zero2geospatial3d/web/src/core/acoustics.js +341 -0
- zero2geospatial3d/web/src/core/classify.js +158 -0
- zero2geospatial3d/web/src/core/comfort.js +686 -0
- zero2geospatial3d/web/src/core/daylight_rule.js +199 -0
- zero2geospatial3d/web/src/core/dispersion.js +264 -0
- zero2geospatial3d/web/src/core/microclimate.js +372 -0
- zero2geospatial3d/web/src/core/morphology.js +153 -0
- zero2geospatial3d/web/src/core/nsga2.js +181 -0
- zero2geospatial3d/web/src/core/party_walls.js +60 -0
- zero2geospatial3d/web/src/core/road_sources.js +127 -0
- zero2geospatial3d/web/src/core/solar.js +112 -0
- zero2geospatial3d/web/src/core/terrain_analysis.js +267 -0
- zero2geospatial3d/web/src/core/terrain_math.js +279 -0
- zero2geospatial3d/web/src/core/urban_form.js +405 -0
- zero2geospatial3d/web/src/core/walkability.js +316 -0
- zero2geospatial3d/web/src/core/weather.js +225 -0
- zero2geospatial3d/web/src/core/wind.js +131 -0
- zero2geospatial3d/web/src/export/check_pdf.js +74 -0
- zero2geospatial3d/web/src/export/exporters.js +556 -0
- zero2geospatial3d/web/src/export/pdf.js +149 -0
- zero2geospatial3d/web/src/export/snapshot.js +234 -0
- zero2geospatial3d/web/src/export/snapshot_scene.js +150 -0
- zero2geospatial3d/web/src/main.js +75 -0
- zero2geospatial3d/web/src/scene/objects.js +645 -0
- zero2geospatial3d/web/src/scene/overlay.js +145 -0
- zero2geospatial3d/web/src/scene/terrain.js +546 -0
- zero2geospatial3d/web/src/scene/viewer.js +275 -0
- zero2geospatial3d/web/src/scene/wind_streamlines.js +336 -0
- zero2geospatial3d/web/src/style.css +225 -0
- zero2geospatial3d/web/src/ui/building_report.js +404 -0
- zero2geospatial3d/web/src/ui/charts.js +441 -0
- zero2geospatial3d/web/src/ui/colorize.js +101 -0
- zero2geospatial3d/web/src/ui/compare.js +362 -0
- zero2geospatial3d/web/src/ui/dom.js +104 -0
- zero2geospatial3d/web/src/ui/panel_analyze.js +742 -0
- zero2geospatial3d/web/src/ui/panel_compare.js +294 -0
- zero2geospatial3d/web/src/ui/panel_layers.js +172 -0
- zero2geospatial3d/web/src/ui/panel_terrain.js +277 -0
- zero2geospatial3d/web/src/ui/qgis_link.js +127 -0
- zero2geospatial3d/web/src/ui/sunclock.js +45 -0
- zero2geospatial3d/web/src/ui/tools.js +197 -0
- zero2geospatial3d/web/src/ui/viewtools.js +173 -0
- zero2geospatial3d/web/vendor/three/addons/controls/OrbitControls.js +1417 -0
- zero2geospatial3d/web/vendor/three/addons/exporters/GLTFExporter.js +3309 -0
- zero2geospatial3d/web/vendor/three/addons/utils/TextureUtils.js +98 -0
- zero2geospatial3d/web/vendor/three/three.module.js +53044 -0
- zero2geospatial3d/wind.py +178 -0
- zero2geospatial3d_sdk-0.1.0.dist-info/METADATA +219 -0
- zero2geospatial3d_sdk-0.1.0.dist-info/RECORD +89 -0
- zero2geospatial3d_sdk-0.1.0.dist-info/WHEEL +5 -0
- zero2geospatial3d_sdk-0.1.0.dist-info/entry_points.txt +2 -0
- zero2geospatial3d_sdk-0.1.0.dist-info/licenses/LICENSE +21 -0
- zero2geospatial3d_sdk-0.1.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +52 -0
- zero2geospatial3d_sdk-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
// Analysis drape: a result grid shown on every upward-facing surface (ground
|
|
3
|
+
// and roofs) by sampling a texture at the fragment's world position. Patched
|
|
4
|
+
// into MeshStandardMaterial with onBeforeCompile so terrain and buildings
|
|
5
|
+
// share one overlay.
|
|
6
|
+
import * as THREE from 'three';
|
|
7
|
+
import { rampStops, rampColor } from '../core/classify.js';
|
|
8
|
+
|
|
9
|
+
const blank = new THREE.DataTexture(new Uint8Array([0, 0, 0, 0]), 1, 1);
|
|
10
|
+
blank.needsUpdate = true;
|
|
11
|
+
|
|
12
|
+
export const overlayUniforms = {
|
|
13
|
+
uOverlay: { value: /** @type {THREE.Texture} */ (blank) },
|
|
14
|
+
uOverlayRect: { value: new THREE.Vector4(0, 1, 0, 1) }, // minX, maxX, minZ, maxZ
|
|
15
|
+
uOverlayOn: { value: 0 },
|
|
16
|
+
uOverlayOpacity: { value: 0.92 },
|
|
17
|
+
uOverlayRoofs: { value: 1 } // 0 for maps of the ground itself (terrain analyses)
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** Make a MeshStandardMaterial show the shared overlay; ground: the terrain's own material. */
|
|
21
|
+
export function patchMaterial(material, { ground = false } = {}) {
|
|
22
|
+
material.onBeforeCompile = (shader) => {
|
|
23
|
+
Object.assign(shader.uniforms, overlayUniforms);
|
|
24
|
+
shader.uniforms.uOvGround = { value: ground ? 1 : 0 };
|
|
25
|
+
shader.vertexShader = shader.vertexShader
|
|
26
|
+
.replace('#include <common>', '#include <common>\nvarying vec3 vOvWorld;\nvarying float vOvNy;')
|
|
27
|
+
.replace('#include <project_vertex>', `#include <project_vertex>
|
|
28
|
+
vOvWorld = (modelMatrix * vec4(transformed, 1.0)).xyz;
|
|
29
|
+
vOvNy = normalize(mat3(modelMatrix) * objectNormal).y;`);
|
|
30
|
+
shader.fragmentShader = shader.fragmentShader
|
|
31
|
+
.replace('#include <common>', `#include <common>
|
|
32
|
+
varying vec3 vOvWorld;
|
|
33
|
+
varying float vOvNy;
|
|
34
|
+
uniform sampler2D uOverlay;
|
|
35
|
+
uniform vec4 uOverlayRect;
|
|
36
|
+
uniform float uOverlayOn;
|
|
37
|
+
uniform float uOverlayOpacity;
|
|
38
|
+
uniform float uOverlayRoofs;
|
|
39
|
+
uniform float uOvGround;`)
|
|
40
|
+
.replace('#include <color_fragment>', `#include <color_fragment>
|
|
41
|
+
vec4 ovSample = vec4(0.0);
|
|
42
|
+
if (uOverlayOn > 0.5 && vOvNy > 0.55 && (uOvGround > 0.5 || uOverlayRoofs > 0.5)) {
|
|
43
|
+
vec2 ouv = vec2((vOvWorld.x - uOverlayRect.x) / (uOverlayRect.y - uOverlayRect.x),
|
|
44
|
+
(vOvWorld.z - uOverlayRect.z) / (uOverlayRect.w - uOverlayRect.z));
|
|
45
|
+
if (ouv.x >= 0.0 && ouv.x <= 1.0 && ouv.y >= 0.0 && ouv.y <= 1.0) {
|
|
46
|
+
ovSample = texture2D(uOverlay, ouv);
|
|
47
|
+
ovSample.a *= uOverlayOpacity;
|
|
48
|
+
diffuseColor.rgb = mix(diffuseColor.rgb, ovSample.rgb, ovSample.a);
|
|
49
|
+
}
|
|
50
|
+
}`)
|
|
51
|
+
.replace('#include <emissivemap_fragment>', `#include <emissivemap_fragment>
|
|
52
|
+
totalEmissiveRadiance = mix(totalEmissiveRadiance, ovSample.rgb * 0.38, ovSample.a);`);
|
|
53
|
+
};
|
|
54
|
+
material.customProgramCacheKey = () => 'g3d-overlay';
|
|
55
|
+
return material;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Show a grid result. grid: { cols, rows, cell, minX, maxY } (local x east,
|
|
60
|
+
* y north, cell centres), values row-major from the north-west.
|
|
61
|
+
*/
|
|
62
|
+
/** RGBA pixels (row 0 = north) for a result grid, as drawn on the map. */
|
|
63
|
+
export function overlayPixels(grid, values, { min, max, ramp = 'viridis', classes = null, categorical = null } = {}) {
|
|
64
|
+
const { cols, rows } = grid;
|
|
65
|
+
const data = new Uint8Array(cols * rows * 4);
|
|
66
|
+
const stops = rampStops(ramp);
|
|
67
|
+
const c = [0, 0, 0];
|
|
68
|
+
const span = max - min || 1;
|
|
69
|
+
for (let i = 0; i < cols * rows; i++) {
|
|
70
|
+
const v = values[i];
|
|
71
|
+
if (!Number.isFinite(v)) continue;
|
|
72
|
+
if (categorical) {
|
|
73
|
+
const hex = categorical[v];
|
|
74
|
+
if (!hex) continue;
|
|
75
|
+
const n = parseInt(hex.slice(1), 16);
|
|
76
|
+
data[i * 4] = (n >> 16) & 255; data[i * 4 + 1] = (n >> 8) & 255; data[i * 4 + 2] = n & 255; data[i * 4 + 3] = 255;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
let t = (v - min) / span;
|
|
80
|
+
if (classes) t = (Math.min(classes - 1, Math.floor(Math.max(0, Math.min(0.9999, t)) * classes)) + 0.5) / classes;
|
|
81
|
+
rampColor(stops, t, c);
|
|
82
|
+
data[i * 4] = c[0] * 255; data[i * 4 + 1] = c[1] * 255; data[i * 4 + 2] = c[2] * 255; data[i * 4 + 3] = 255;
|
|
83
|
+
}
|
|
84
|
+
return data;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Cell-edge bounds of a grid in three coordinates (x east, z = -north). */
|
|
88
|
+
export function gridRect(grid) {
|
|
89
|
+
const minX = grid.minX - grid.cell / 2;
|
|
90
|
+
const maxX = grid.minX + (grid.cols - 0.5) * grid.cell;
|
|
91
|
+
const northY = grid.maxY + grid.cell / 2;
|
|
92
|
+
const southY = grid.maxY - (grid.rows - 0.5) * grid.cell;
|
|
93
|
+
return { minX, maxX, minZ: -northY, maxZ: -southY };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Show a grid result. grid: { cols, rows, cell, minX, maxY } (local x east,
|
|
98
|
+
* y north, cell centres), values row-major from the north-west.
|
|
99
|
+
*/
|
|
100
|
+
export function showOverlay(grid, values, display = {}) {
|
|
101
|
+
const { cols, rows } = grid;
|
|
102
|
+
const data = overlayPixels(grid, values, display);
|
|
103
|
+
const tex = new THREE.DataTexture(data, cols, rows, THREE.RGBAFormat, THREE.UnsignedByteType);
|
|
104
|
+
tex.colorSpace = THREE.SRGBColorSpace;
|
|
105
|
+
tex.magFilter = THREE.NearestFilter;
|
|
106
|
+
tex.minFilter = THREE.LinearFilter;
|
|
107
|
+
tex.needsUpdate = true;
|
|
108
|
+
const old = overlayUniforms.uOverlay.value;
|
|
109
|
+
if (old !== blank && !old.userData.cached) old.dispose();
|
|
110
|
+
overlayUniforms.uOverlay.value = tex;
|
|
111
|
+
const r = gridRect(grid);
|
|
112
|
+
overlayUniforms.uOverlayRect.value.set(r.minX, r.maxX, r.minZ, r.maxZ);
|
|
113
|
+
overlayUniforms.uOverlayOn.value = 1;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** A result's drape texture, built once and kept on the result (compare views switch between them). */
|
|
117
|
+
export function resultTexture(result) {
|
|
118
|
+
if (result._tex) return result._tex;
|
|
119
|
+
const { cols, rows } = result.grid;
|
|
120
|
+
const tex = new THREE.DataTexture(overlayPixels(result.grid, result.values, result.display), cols, rows, THREE.RGBAFormat, THREE.UnsignedByteType);
|
|
121
|
+
tex.colorSpace = THREE.SRGBColorSpace;
|
|
122
|
+
tex.magFilter = THREE.NearestFilter;
|
|
123
|
+
tex.minFilter = THREE.LinearFilter;
|
|
124
|
+
tex.userData.cached = true;
|
|
125
|
+
tex.needsUpdate = true;
|
|
126
|
+
result._tex = tex;
|
|
127
|
+
return tex;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Drape a cached texture (no copy, no dispose); roofs: false keeps it on the ground. */
|
|
131
|
+
export function useOverlay(tex, grid, { roofs = true } = {}) {
|
|
132
|
+
overlayUniforms.uOverlayRoofs.value = roofs ? 1 : 0;
|
|
133
|
+
overlayUniforms.uOverlay.value = tex;
|
|
134
|
+
const r = gridRect(grid);
|
|
135
|
+
overlayUniforms.uOverlayRect.value.set(r.minX, r.maxX, r.minZ, r.maxZ);
|
|
136
|
+
overlayUniforms.uOverlayOn.value = 1;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function hideOverlay() {
|
|
140
|
+
overlayUniforms.uOverlayOn.value = 0;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function setOverlayOpacity(v) {
|
|
144
|
+
overlayUniforms.uOverlayOpacity.value = v;
|
|
145
|
+
}
|
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
// Terrain mesh, its painted texture (shading + draped surfaces + rasters),
|
|
3
|
+
// contour lines and the flood surface.
|
|
4
|
+
import * as THREE from 'three';
|
|
5
|
+
import { LAYER_SURFACE, LAYER_OCCLUDER } from './viewer.js';
|
|
6
|
+
import { patchMaterial } from './overlay.js';
|
|
7
|
+
import { sample, slopeAspect, hillshade, contours as contourLines } from '../core/terrain_math.js';
|
|
8
|
+
import { rampStops, rampColor, hexToRgb } from '../core/classify.js';
|
|
9
|
+
|
|
10
|
+
const TILE_CELLS = 256;
|
|
11
|
+
const LOD_STEPS = [1, 2, 4, 8, 16];
|
|
12
|
+
const LOD_THRESHOLD = 1100 * 1100;
|
|
13
|
+
const VERTEX_BUDGET = 6_000_000;
|
|
14
|
+
|
|
15
|
+
export const SHADING_MODES = {
|
|
16
|
+
natural: 'Natural',
|
|
17
|
+
elevation: 'Elevation',
|
|
18
|
+
slope: 'Slope',
|
|
19
|
+
aspect: 'Aspect',
|
|
20
|
+
hillshade: 'Hillshade',
|
|
21
|
+
plain: 'Plain'
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export class Terrain {
|
|
25
|
+
/**
|
|
26
|
+
* @param {import('./viewer.js').Viewer} viewer
|
|
27
|
+
* @param {{ cols: number, rows: number, cell: number, minX: number, maxY: number, values: Float32Array }} grid
|
|
28
|
+
* @param {Uint8Array|null} [mask] 1 = DEM data, 0 = no data. A DEM clipped to
|
|
29
|
+
* a polygon has holes: they are not drawn, not hit and not analysed. grid
|
|
30
|
+
* still carries a filled-in height there, so objects get a base.
|
|
31
|
+
*/
|
|
32
|
+
constructor(viewer, grid, mask = null) {
|
|
33
|
+
this.viewer = viewer;
|
|
34
|
+
this.grid = grid;
|
|
35
|
+
this.mask = mask && mask.length === grid.cols * grid.rows && mask.includes(0) ? mask : null;
|
|
36
|
+
this._dataGrid = null;
|
|
37
|
+
let lo = Infinity, hi = -Infinity;
|
|
38
|
+
const m = this.mask;
|
|
39
|
+
grid.values.forEach((v, i) => { if (m && !m[i]) return; if (v < lo) lo = v; if (v > hi) hi = v; });
|
|
40
|
+
if (!(lo <= hi)) lo = hi = 0;
|
|
41
|
+
this.min = lo;
|
|
42
|
+
this.max = hi;
|
|
43
|
+
this.zBase = Math.floor(lo);
|
|
44
|
+
this.exaggeration = 1;
|
|
45
|
+
this.shading = 'natural';
|
|
46
|
+
this.surfaces = []; // [{ layer, colors }] painted on the texture
|
|
47
|
+
this.drape = null; // { values, min, max, ramp, opacity }
|
|
48
|
+
this.contourInterval = 0;
|
|
49
|
+
this.derived = null; // cached slope/aspect/hillshade
|
|
50
|
+
this.canvas = document.createElement('canvas');
|
|
51
|
+
this.texture = new THREE.CanvasTexture(this.canvas);
|
|
52
|
+
this.texture.flipY = false;
|
|
53
|
+
this.texture.colorSpace = THREE.SRGBColorSpace;
|
|
54
|
+
this.texture.anisotropy = 8;
|
|
55
|
+
this.material = patchMaterial(new THREE.MeshStandardMaterial({ map: this.texture, roughness: 0.96, metalness: 0 }), { ground: true });
|
|
56
|
+
this.group = new THREE.Group();
|
|
57
|
+
this.group.name = 'terrain';
|
|
58
|
+
viewer.world.add(this.group);
|
|
59
|
+
// Grids above about 1024 x 1024 are drawn in tiles whose density follows
|
|
60
|
+
// the distance to the camera; smaller ones are drawn whole, at full
|
|
61
|
+
// resolution, exactly as before.
|
|
62
|
+
this.lodEnabled = grid.cols * grid.rows > LOD_THRESHOLD;
|
|
63
|
+
this.lock = null;
|
|
64
|
+
this._makeTiles();
|
|
65
|
+
this._lastCam = new THREE.Vector3(Infinity, Infinity, Infinity);
|
|
66
|
+
this.updateLod(true);
|
|
67
|
+
viewer.onFrame(() => this.updateLod());
|
|
68
|
+
this.contourGroup = new THREE.Group();
|
|
69
|
+
viewer.helpers.add(this.contourGroup);
|
|
70
|
+
this.water = null;
|
|
71
|
+
this.paint();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The DEM as measured: the same grid with NaN where there is no data. Use it
|
|
76
|
+
* for anything that reads the ground as data (contours, profiles, flooding,
|
|
77
|
+
* cut and fill, terrain analyses); `grid` is only for placing objects.
|
|
78
|
+
*/
|
|
79
|
+
get dataGrid() {
|
|
80
|
+
if (!this.mask) return this.grid;
|
|
81
|
+
if (!this._dataGrid) {
|
|
82
|
+
const values = Float32Array.from(this.grid.values, (v, i) => (this.mask[i] ? v : NaN));
|
|
83
|
+
this._dataGrid = { ...this.grid, values };
|
|
84
|
+
}
|
|
85
|
+
return this._dataGrid;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Whether local (x, y) lies on DEM data (nearest cell). */
|
|
89
|
+
hasData(x, y) {
|
|
90
|
+
if (!this.mask) return true;
|
|
91
|
+
const g = this.grid;
|
|
92
|
+
const c = Math.round((x - g.minX) / g.cell), r = Math.round((g.maxY - y) / g.cell);
|
|
93
|
+
if (c < 0 || r < 0 || c >= g.cols || r >= g.rows) return false;
|
|
94
|
+
return this.mask[r * g.cols + c] === 1;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Local (x, y) -> absolute elevation. */
|
|
98
|
+
heightAt(x, y) {
|
|
99
|
+
return sample(this.grid, x, y);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Absolute elevation -> scene y. */
|
|
103
|
+
toY(z) {
|
|
104
|
+
return (z - this.zBase) * this.exaggeration;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
fromY(y) {
|
|
108
|
+
return y / this.exaggeration + this.zBase;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Scene y of the ground at local (x, y). */
|
|
112
|
+
groundY(x, y) {
|
|
113
|
+
return this.toY(this.heightAt(x, y));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ------------------------------------------------------------ tiled mesh
|
|
117
|
+
/** Vertex normal at grid point (c, r) from its neighbours, so tiles meet without seams. */
|
|
118
|
+
_normalAt(c, r, out, k) {
|
|
119
|
+
const { cols, rows, cell, values: z } = this.grid;
|
|
120
|
+
const ex = this.exaggeration;
|
|
121
|
+
const ru = Math.max(0, r - 1), rd = Math.min(rows - 1, r + 1);
|
|
122
|
+
const cl = Math.max(0, c - 1), cr = Math.min(cols - 1, c + 1);
|
|
123
|
+
const dx = ((z[r * cols + cr] - z[r * cols + cl]) * ex) / (cell * (cr - cl || 1));
|
|
124
|
+
// Rows run north -> south, i.e. towards +z in the scene.
|
|
125
|
+
const dz = ((z[rd * cols + c] - z[ru * cols + c]) * ex) / (cell * (rd - ru || 1));
|
|
126
|
+
const len = Math.hypot(dx, 1, dz);
|
|
127
|
+
out[k * 3] = -dx / len; out[k * 3 + 1] = 1 / len; out[k * 3 + 2] = -dz / len;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
_makeTiles() {
|
|
131
|
+
const { cols, rows, cell, minX, maxY } = this.grid;
|
|
132
|
+
const size = this.lodEnabled ? TILE_CELLS : Math.max(cols, rows);
|
|
133
|
+
this.tiles = [];
|
|
134
|
+
for (let r0 = 0; r0 < rows - 1; r0 += size) {
|
|
135
|
+
for (let c0 = 0; c0 < cols - 1; c0 += size) {
|
|
136
|
+
const c1 = Math.min(cols - 1, c0 + size);
|
|
137
|
+
const r1 = Math.min(rows - 1, r0 + size);
|
|
138
|
+
const cx = minX + ((c0 + c1) / 2) * cell;
|
|
139
|
+
const cz = -(maxY - ((r0 + r1) / 2) * cell);
|
|
140
|
+
this.tiles.push({ c0, r0, c1, r1, cx, cz, radius: (Math.hypot(c1 - c0, r1 - r0) * cell) / 2, meshes: new Map(), step: 0 });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
_indices(from, to, step) {
|
|
146
|
+
const out = [];
|
|
147
|
+
for (let i = from; i < to; i += step) out.push(i);
|
|
148
|
+
out.push(to);
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
_buildTileMesh(tile, step) {
|
|
153
|
+
const { cols, rows, cell, minX, maxY, values } = this.grid;
|
|
154
|
+
const cs = this._indices(tile.c0, tile.c1, step);
|
|
155
|
+
const rs = this._indices(tile.r0, tile.r1, step);
|
|
156
|
+
const nc = cs.length, nr = rs.length;
|
|
157
|
+
const skirt = this.lodEnabled;
|
|
158
|
+
const ring = [];
|
|
159
|
+
if (skirt) {
|
|
160
|
+
for (let i = 0; i < nc; i++) ring.push([i, 0]);
|
|
161
|
+
for (let j = 1; j < nr; j++) ring.push([nc - 1, j]);
|
|
162
|
+
for (let i = nc - 2; i >= 0; i--) ring.push([i, nr - 1]);
|
|
163
|
+
for (let j = nr - 2; j >= 0; j--) ring.push([0, j]);
|
|
164
|
+
}
|
|
165
|
+
const count = nc * nr + ring.length;
|
|
166
|
+
const pos = new Float32Array(count * 3);
|
|
167
|
+
const nrm = new Float32Array(count * 3);
|
|
168
|
+
const uv = new Float32Array(count * 2);
|
|
169
|
+
const put = (k, c, r, drop) => {
|
|
170
|
+
const g = r * cols + c;
|
|
171
|
+
pos[k * 3] = minX + c * cell;
|
|
172
|
+
pos[k * 3 + 1] = this.toY(values[g]) - drop;
|
|
173
|
+
pos[k * 3 + 2] = -(maxY - r * cell);
|
|
174
|
+
this._normalAt(c, r, nrm, k);
|
|
175
|
+
uv[k * 2] = (c + 0.5) / cols;
|
|
176
|
+
uv[k * 2 + 1] = (r + 0.5) / rows;
|
|
177
|
+
};
|
|
178
|
+
for (let j = 0; j < nr; j++) for (let i = 0; i < nc; i++) put(j * nc + i, cs[i], rs[j], 0);
|
|
179
|
+
// Typed index of the largest size needed: no growing JS array to collect later.
|
|
180
|
+
const index = new (count > 65535 ? Uint32Array : Uint16Array)((nc - 1) * (nr - 1) * 6 + ring.length * 12);
|
|
181
|
+
const mask = this.mask;
|
|
182
|
+
const on = mask ? (i, j) => mask[rs[j] * cols + cs[i]] === 1 : () => true;
|
|
183
|
+
let q6 = 0;
|
|
184
|
+
for (let j = 0; j < nr - 1; j++) {
|
|
185
|
+
for (let i = 0; i < nc - 1; i++) {
|
|
186
|
+
const a = j * nc + i, b = a + 1, d = a + nc, e = d + 1;
|
|
187
|
+
// No data at a corner: no ground there (a DEM clipped to a polygon).
|
|
188
|
+
// Each half of the quad is kept when its own three corners have data,
|
|
189
|
+
// so the edge follows the outline diagonally rather than in steps.
|
|
190
|
+
const oa = on(i, j), ob = on(i + 1, j), od = on(i, j + 1), oe = on(i + 1, j + 1);
|
|
191
|
+
// Counter-clockwise seen from above (+y): a(north-west) -> d(south-west) -> b.
|
|
192
|
+
if (oa && od && ob) { index[q6++] = a; index[q6++] = d; index[q6++] = b; }
|
|
193
|
+
if (ob && od && oe) { index[q6++] = b; index[q6++] = d; index[q6++] = e; }
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (skirt) {
|
|
197
|
+
// A curtain hanging below each tile edge hides the cracks where a fine
|
|
198
|
+
// tile meets a coarser neighbour. Both windings, so it shows from any side.
|
|
199
|
+
const drop = cell * step * 2 + 2 * this.exaggeration;
|
|
200
|
+
let k = nc * nr;
|
|
201
|
+
const top = [];
|
|
202
|
+
const bottom = [];
|
|
203
|
+
for (const [i, j] of ring) {
|
|
204
|
+
top.push(j * nc + i);
|
|
205
|
+
put(k, cs[i], rs[j], drop);
|
|
206
|
+
bottom.push(k++);
|
|
207
|
+
}
|
|
208
|
+
top.push(top[0]);
|
|
209
|
+
bottom.push(bottom[0]);
|
|
210
|
+
ring.push(ring[0]);
|
|
211
|
+
for (let q = 0; q < top.length - 1; q++) {
|
|
212
|
+
// No curtain under a hole in the DEM.
|
|
213
|
+
if (!on(ring[q][0], ring[q][1]) || !on(ring[q + 1][0], ring[q + 1][1])) continue;
|
|
214
|
+
const a = top[q], b = top[q + 1], c2 = bottom[q], d = bottom[q + 1];
|
|
215
|
+
index.set([a, c2, b, b, c2, d, a, b, c2, b, d, c2], q6);
|
|
216
|
+
q6 += 12;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const geom = new THREE.BufferGeometry();
|
|
220
|
+
geom.setAttribute('position', new THREE.BufferAttribute(pos, 3));
|
|
221
|
+
geom.setAttribute('normal', new THREE.BufferAttribute(nrm, 3));
|
|
222
|
+
geom.setAttribute('uv', new THREE.BufferAttribute(uv, 2));
|
|
223
|
+
geom.setIndex(new THREE.BufferAttribute(q6 < index.length ? index.slice(0, q6) : index, 1));
|
|
224
|
+
geom.computeBoundingBox();
|
|
225
|
+
geom.computeBoundingSphere();
|
|
226
|
+
const mesh = new THREE.Mesh(geom, this.material);
|
|
227
|
+
mesh.receiveShadow = true;
|
|
228
|
+
mesh.castShadow = true;
|
|
229
|
+
mesh.layers.enable(LAYER_SURFACE);
|
|
230
|
+
mesh.layers.enable(LAYER_OCCLUDER);
|
|
231
|
+
mesh.visible = false;
|
|
232
|
+
mesh.userData.vertices = count;
|
|
233
|
+
this.group.add(mesh);
|
|
234
|
+
return mesh;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
_stepFor(tile, cam) {
|
|
238
|
+
if (!this.lodEnabled) return 1;
|
|
239
|
+
const cell = this.grid.cell;
|
|
240
|
+
const L = this.lock;
|
|
241
|
+
if (L && tile.cx + tile.radius >= L.minX && tile.cx - tile.radius <= L.maxX &&
|
|
242
|
+
-tile.cz + tile.radius >= L.minY && -tile.cz - tile.radius <= L.maxY) return L.step;
|
|
243
|
+
const dist = Math.max(0, Math.hypot(cam.x - tile.cx, cam.y - this.toY((this.min + this.max) / 2), cam.z - tile.cz) - tile.radius);
|
|
244
|
+
const spacing = Math.max(cell, dist * 0.003);
|
|
245
|
+
let step = 1;
|
|
246
|
+
for (const s of LOD_STEPS) if (s * cell <= spacing) step = s;
|
|
247
|
+
return step;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Pick a density per tile for the current camera. Returns true when the scene changed. */
|
|
251
|
+
updateLod(force = false) {
|
|
252
|
+
const cam = this.viewer.camera.position;
|
|
253
|
+
if (!force) {
|
|
254
|
+
if (!this.lodEnabled) return false;
|
|
255
|
+
const moved = cam.distanceTo(this._lastCam);
|
|
256
|
+
if (!this._pending && moved < Math.max(1, 0.02 * cam.distanceTo(this.viewer.controls.target))) return false;
|
|
257
|
+
}
|
|
258
|
+
this._lastCam.copy(cam);
|
|
259
|
+
const wanted = this.tiles.map((t) => this._stepFor(t, cam));
|
|
260
|
+
// Keep the total under the vertex budget by coarsening the farthest tiles first.
|
|
261
|
+
const verts = (t, s) => (Math.floor((t.c1 - t.c0) / s) + 2) * (Math.floor((t.r1 - t.r0) / s) + 2);
|
|
262
|
+
let total = this.tiles.reduce((sum, t, i) => sum + verts(t, wanted[i]), 0);
|
|
263
|
+
if (total > VERTEX_BUDGET) {
|
|
264
|
+
const order = this.tiles.map((t, i) => [Math.hypot(cam.x - t.cx, cam.z - t.cz), i]).sort((a, b) => b[0] - a[0]);
|
|
265
|
+
for (const [, i] of order) {
|
|
266
|
+
while (total > VERTEX_BUDGET && wanted[i] < LOD_STEPS[LOD_STEPS.length - 1]) {
|
|
267
|
+
total -= verts(this.tiles[i], wanted[i]);
|
|
268
|
+
wanted[i] *= 2;
|
|
269
|
+
total += verts(this.tiles[i], wanted[i]);
|
|
270
|
+
}
|
|
271
|
+
if (total <= VERTEX_BUDGET) break;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
let changed = false;
|
|
275
|
+
// Building tiles costs time: spread it over frames (about 12 ms each) so a
|
|
276
|
+
// jump of the camera never freezes the view. Nearest tiles go first; a
|
|
277
|
+
// forced update (analysis, first frame) builds everything at once.
|
|
278
|
+
const deadline = performance.now() + (force ? Infinity : 12);
|
|
279
|
+
this._pending = false;
|
|
280
|
+
const order = this.tiles.map((t, i) => [Math.hypot(cam.x - t.cx, cam.z - t.cz), i]).sort((a, b) => a[0] - b[0]);
|
|
281
|
+
for (const [, i] of order) {
|
|
282
|
+
const t = this.tiles[i];
|
|
283
|
+
const step = wanted[i];
|
|
284
|
+
if (t.step === step) continue;
|
|
285
|
+
let mesh = t.meshes.get(step);
|
|
286
|
+
if (!mesh) {
|
|
287
|
+
if (performance.now() > deadline && t.step) { this._pending = true; continue; }
|
|
288
|
+
mesh = this._buildTileMesh(t, step);
|
|
289
|
+
t.meshes.set(step, mesh);
|
|
290
|
+
}
|
|
291
|
+
const old = t.meshes.get(t.step);
|
|
292
|
+
if (old) old.visible = false;
|
|
293
|
+
mesh.visible = true;
|
|
294
|
+
t.step = step;
|
|
295
|
+
changed = true;
|
|
296
|
+
// Keep at most two densities per tile in memory.
|
|
297
|
+
for (const [s, m] of t.meshes) {
|
|
298
|
+
if (s !== step && t.meshes.size > 2) { this.group.remove(m); m.geometry.dispose(); t.meshes.delete(s); }
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
this.stats = { tiles: this.tiles.length, vertices: total };
|
|
302
|
+
return changed;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Draw an area at (at least) a given cell size for an analysis; null releases it. */
|
|
306
|
+
lockForAnalysis(area, cell) {
|
|
307
|
+
if (!this.lodEnabled) return;
|
|
308
|
+
if (!area) { this.lock = null; this.updateLod(true); return; }
|
|
309
|
+
const g = this.grid.cell;
|
|
310
|
+
let step = 1;
|
|
311
|
+
for (const s of LOD_STEPS) if (s * g <= Math.max(g, cell)) step = s;
|
|
312
|
+
const cells = ((area.maxX - area.minX) / g) * ((area.maxY - area.minY) / g);
|
|
313
|
+
while (cells / (step * step) > VERTEX_BUDGET && step < LOD_STEPS[LOD_STEPS.length - 1]) step *= 2;
|
|
314
|
+
this.lock = { ...area, step };
|
|
315
|
+
this.updateLod(true);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
_disposeTiles() {
|
|
319
|
+
for (const t of this.tiles) {
|
|
320
|
+
for (const m of t.meshes.values()) { this.group.remove(m); m.geometry.dispose(); }
|
|
321
|
+
t.meshes.clear();
|
|
322
|
+
t.step = 0;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
setExaggeration(ex) {
|
|
327
|
+
this.exaggeration = ex;
|
|
328
|
+
this._disposeTiles();
|
|
329
|
+
this.updateLod(true);
|
|
330
|
+
if (this.contourInterval) this.setContours(this.contourInterval);
|
|
331
|
+
if (this.water) this.setWater(this.water.level, this.water.depth);
|
|
332
|
+
this.viewer.requestRender();
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** Hillshade always; slope and aspect only while a mode shows them (64 MB each on a 4096 grid). */
|
|
336
|
+
_derived(needSlope = false) {
|
|
337
|
+
if (!this.derived) this.derived = { shade: hillshade(this.grid, 315, 40, 1.5) };
|
|
338
|
+
if (needSlope && !this.derived.slope) Object.assign(this.derived, slopeAspect(this.grid));
|
|
339
|
+
if (!needSlope && this.derived.slope) { delete this.derived.slope; delete this.derived.aspect; }
|
|
340
|
+
return this.derived;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** Repaint the terrain texture: shading, raster drape, then surfaces. */
|
|
344
|
+
paint() {
|
|
345
|
+
const { cols, rows } = this.grid;
|
|
346
|
+
const longest = Math.max(cols, rows);
|
|
347
|
+
const scale = Math.min(4, Math.min(4096, Math.max(2048, longest)) / longest);
|
|
348
|
+
const W = Math.round(cols * scale);
|
|
349
|
+
const H = Math.round(rows * scale);
|
|
350
|
+
this.canvas.width = W;
|
|
351
|
+
this.canvas.height = H;
|
|
352
|
+
const ctx = /** @type {CanvasRenderingContext2D} */ (this.canvas.getContext('2d'));
|
|
353
|
+
// 1. Shading at grid resolution, scaled up.
|
|
354
|
+
const base = document.createElement('canvas');
|
|
355
|
+
base.width = cols;
|
|
356
|
+
base.height = rows;
|
|
357
|
+
const bctx = /** @type {CanvasRenderingContext2D} */ (base.getContext('2d'));
|
|
358
|
+
const img = bctx.createImageData(cols, rows);
|
|
359
|
+
const flat = this.max - this.min < 0.5;
|
|
360
|
+
const d = flat ? null : this._derived(this.shading === 'slope' || this.shading === 'aspect');
|
|
361
|
+
const elevStops = rampStops('terrain');
|
|
362
|
+
const slopeStops = rampStops('magma-r');
|
|
363
|
+
const c = [0, 0, 0];
|
|
364
|
+
for (let i = 0; i < cols * rows; i++) {
|
|
365
|
+
const z = this.grid.values[i];
|
|
366
|
+
const shade = d ? d.shade[i] : 0.75;
|
|
367
|
+
const t = (z - this.min) / Math.max(1e-6, this.max - this.min);
|
|
368
|
+
switch (this.shading) {
|
|
369
|
+
case 'elevation': rampColor(elevStops, t, c); mul(c, 0.55 + 0.45 * shade); break;
|
|
370
|
+
case 'slope': rampColor(slopeStops, d ? Math.min(1, d.slope[i] / 45) : 0, c); mul(c, 0.75 + 0.25 * shade); break;
|
|
371
|
+
case 'aspect': {
|
|
372
|
+
const a = d ? d.aspect[i] : -1;
|
|
373
|
+
if (a < 0) { c[0] = c[1] = c[2] = 0.8; } else hsl(a / 360, 0.55, 0.6, c);
|
|
374
|
+
mul(c, 0.7 + 0.3 * shade);
|
|
375
|
+
break;
|
|
376
|
+
}
|
|
377
|
+
case 'hillshade': c[0] = c[1] = c[2] = 0.15 + 0.85 * shade; break;
|
|
378
|
+
case 'plain': c[0] = 0.86; c[1] = 0.86; c[2] = 0.84; mul(c, 0.82 + 0.18 * shade); break;
|
|
379
|
+
default: {
|
|
380
|
+
// Natural: muted ground tones by height, lit by the hillshade.
|
|
381
|
+
c[0] = 0.78 - 0.08 * t; c[1] = 0.79 - 0.02 * t; c[2] = 0.70 + 0.02 * t;
|
|
382
|
+
mul(c, 0.68 + 0.32 * shade);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
img.data[i * 4] = c[0] * 255; img.data[i * 4 + 1] = c[1] * 255; img.data[i * 4 + 2] = c[2] * 255; img.data[i * 4 + 3] = 255;
|
|
386
|
+
}
|
|
387
|
+
if (this.drape) {
|
|
388
|
+
const { values, min, max, ramp, opacity } = this.drape;
|
|
389
|
+
const stops = rampStops(ramp);
|
|
390
|
+
for (let i = 0; i < cols * rows; i++) {
|
|
391
|
+
const v = values[i];
|
|
392
|
+
if (!Number.isFinite(v)) continue;
|
|
393
|
+
rampColor(stops, (v - min) / Math.max(1e-9, max - min), c);
|
|
394
|
+
for (let k = 0; k < 3; k++) img.data[i * 4 + k] = img.data[i * 4 + k] * (1 - opacity) + c[k] * 255 * opacity;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
bctx.putImageData(img, 0, 0);
|
|
398
|
+
ctx.imageSmoothingEnabled = true;
|
|
399
|
+
ctx.drawImage(base, 0, 0, W, H);
|
|
400
|
+
// 2. Draped surfaces (parks, water, squares ...), drawn as vectors.
|
|
401
|
+
const g = this.grid;
|
|
402
|
+
const toPx = (x, y) => [((x - (g.minX - g.cell / 2)) / g.cell) * scale, (((g.maxY + g.cell / 2) - y) / g.cell) * scale];
|
|
403
|
+
for (const { features, colors, visible } of this.surfaces) {
|
|
404
|
+
if (!visible) continue;
|
|
405
|
+
for (let f = 0; f < features.length; f++) {
|
|
406
|
+
const geom = features[f].geometry;
|
|
407
|
+
if (!geom) continue;
|
|
408
|
+
const rgb = colors.subarray(f * 3, f * 3 + 3);
|
|
409
|
+
ctx.fillStyle = `rgba(${rgb[0] * 255 | 0},${rgb[1] * 255 | 0},${rgb[2] * 255 | 0},0.82)`;
|
|
410
|
+
ctx.beginPath();
|
|
411
|
+
for (const poly of geom.coordinates) {
|
|
412
|
+
for (const ring of poly) {
|
|
413
|
+
ring.forEach(([x, y], i) => { const [px, py] = toPx(x, y); if (i) ctx.lineTo(px, py); else ctx.moveTo(px, py); });
|
|
414
|
+
ctx.closePath();
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
ctx.fill('evenodd');
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
this.texture.needsUpdate = true;
|
|
421
|
+
this.viewer.requestRender();
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
setShading(mode) {
|
|
425
|
+
this.shading = mode;
|
|
426
|
+
this.paint();
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
setDrape(drape) {
|
|
430
|
+
this.drape = drape;
|
|
431
|
+
this.paint();
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
addSurfaceLayer(entry) {
|
|
435
|
+
this.surfaces.push(entry);
|
|
436
|
+
this.paint();
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
setContours(interval) {
|
|
440
|
+
this.contourInterval = interval;
|
|
441
|
+
for (const child of [...this.contourGroup.children]) {
|
|
442
|
+
this.contourGroup.remove(child);
|
|
443
|
+
/** @type {any} */ (child).geometry.dispose();
|
|
444
|
+
}
|
|
445
|
+
if (!(interval > 0)) { this.viewer.requestRender(); return; }
|
|
446
|
+
const sets = contourLines(this.dataGrid, interval, { maxLevels: 300 });
|
|
447
|
+
const minor = [];
|
|
448
|
+
const major = [];
|
|
449
|
+
for (const { level, segments } of sets) {
|
|
450
|
+
const target = Math.abs(level / (interval * 5) - Math.round(level / (interval * 5))) < 1e-6 ? major : minor;
|
|
451
|
+
const y = this.toY(level) + 0.25;
|
|
452
|
+
for (let i = 0; i < segments.length; i += 4) {
|
|
453
|
+
target.push(segments[i], y, -segments[i + 1], segments[i + 2], y, -segments[i + 3]);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
for (const [arr, color, opacity] of [[minor, 0x5b4a3a, 0.55], [major, 0x3b2a1a, 0.9]]) {
|
|
457
|
+
if (!arr.length) continue;
|
|
458
|
+
const geom = new THREE.BufferGeometry();
|
|
459
|
+
geom.setAttribute('position', new THREE.Float32BufferAttribute(arr, 3));
|
|
460
|
+
const mat = new THREE.LineBasicMaterial({ color, transparent: true, opacity, depthWrite: false });
|
|
461
|
+
this.contourGroup.add(new THREE.LineSegments(geom, mat));
|
|
462
|
+
}
|
|
463
|
+
this.viewer.requestRender();
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/** Water surface over the flooded cells. depth: Float32Array (NaN = dry). */
|
|
467
|
+
setWater(level, depth) {
|
|
468
|
+
if (this.waterMesh) {
|
|
469
|
+
this.viewer.helpers.remove(this.waterMesh);
|
|
470
|
+
this.waterMesh.geometry.dispose();
|
|
471
|
+
this.waterMesh = null;
|
|
472
|
+
}
|
|
473
|
+
this.water = depth ? { level, depth } : null;
|
|
474
|
+
if (!depth) { this.viewer.requestRender(); return; }
|
|
475
|
+
const { cols, rows, cell, minX, maxY } = this.grid;
|
|
476
|
+
const pos = [];
|
|
477
|
+
const y = this.toY(level);
|
|
478
|
+
const h = cell / 2;
|
|
479
|
+
for (let r = 0; r < rows; r++) {
|
|
480
|
+
for (let c = 0; c < cols; c++) {
|
|
481
|
+
if (!Number.isFinite(depth[r * cols + c])) continue;
|
|
482
|
+
const x = minX + c * cell;
|
|
483
|
+
const z = -(maxY - r * cell);
|
|
484
|
+
pos.push(x - h, y, z - h, x - h, y, z + h, x + h, y, z - h, x + h, y, z - h, x - h, y, z + h, x + h, y, z + h);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
if (!pos.length) { this.viewer.requestRender(); return; }
|
|
488
|
+
const geom = new THREE.BufferGeometry();
|
|
489
|
+
geom.setAttribute('position', new THREE.Float32BufferAttribute(pos, 3));
|
|
490
|
+
geom.computeVertexNormals();
|
|
491
|
+
const mat = new THREE.MeshStandardMaterial({ color: 0x2f7fc1, transparent: true, opacity: 0.62, roughness: 0.15, metalness: 0.1, depthWrite: false });
|
|
492
|
+
this.waterMesh = new THREE.Mesh(geom, mat);
|
|
493
|
+
this.waterMesh.renderOrder = 2;
|
|
494
|
+
this.viewer.helpers.add(this.waterMesh);
|
|
495
|
+
this.viewer.requestRender();
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Intersect a ray (three coordinates) with the terrain by marching over
|
|
500
|
+
* the height grid. Returns the hit point or null.
|
|
501
|
+
*/
|
|
502
|
+
raycast(origin, dir, maxDist = 100000) {
|
|
503
|
+
const step = Math.max(0.5, this.grid.cell / 2);
|
|
504
|
+
let prevT = 0;
|
|
505
|
+
// Over a hole in the DEM there is no ground: the ray is always above it.
|
|
506
|
+
const above = (x, y, h) => !this.hasData(x, y) || h - this.groundY(x, y) > 0;
|
|
507
|
+
let prevAbove = above(origin.x, -origin.z, origin.y);
|
|
508
|
+
if (!prevAbove) return null;
|
|
509
|
+
// Coarse march with growing steps, then bisection.
|
|
510
|
+
let t = step;
|
|
511
|
+
while (t < maxDist) {
|
|
512
|
+
const x = origin.x + dir.x * t, y = origin.y + dir.y * t, z = origin.z + dir.z * t;
|
|
513
|
+
const gx = x, gy = -z;
|
|
514
|
+
const inside = gx >= this.grid.minX - this.grid.cell && gx <= this.grid.minX + this.grid.cols * this.grid.cell &&
|
|
515
|
+
gy <= this.grid.maxY + this.grid.cell && gy >= this.grid.maxY - this.grid.rows * this.grid.cell;
|
|
516
|
+
const isAbove = above(gx, gy, y);
|
|
517
|
+
if (inside && !isAbove && prevAbove) {
|
|
518
|
+
let lo = prevT, hi = t;
|
|
519
|
+
for (let i = 0; i < 30; i++) {
|
|
520
|
+
const mid = (lo + hi) / 2;
|
|
521
|
+
const yy = origin.y + dir.y * mid;
|
|
522
|
+
if (above(origin.x + dir.x * mid, -(origin.z + dir.z * mid), yy)) lo = mid; else hi = mid;
|
|
523
|
+
}
|
|
524
|
+
return new THREE.Vector3().copy(origin).addScaledVector(dir, hi);
|
|
525
|
+
}
|
|
526
|
+
prevAbove = isAbove || !inside;
|
|
527
|
+
prevT = t;
|
|
528
|
+
t += step * Math.max(1, t / 400);
|
|
529
|
+
}
|
|
530
|
+
return null;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function mul(c, k) { c[0] *= k; c[1] *= k; c[2] *= k; }
|
|
535
|
+
|
|
536
|
+
function hsl(h, s, l, out) {
|
|
537
|
+
const f = (n) => {
|
|
538
|
+
const k = (n + h * 12) % 12;
|
|
539
|
+
const a = s * Math.min(l, 1 - l);
|
|
540
|
+
return l - a * Math.max(-1, Math.min(k - 3, Math.min(9 - k, 1)));
|
|
541
|
+
};
|
|
542
|
+
out[0] = f(0); out[1] = f(8); out[2] = f(4);
|
|
543
|
+
return out;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
export { hexToRgb };
|