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,431 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
// GPU exposure engine.
|
|
3
|
+
//
|
|
4
|
+
// Samples are either the top surface of the scene seen from straight above
|
|
5
|
+
// (a height field: ground and roofs, one per texel) or an explicit list of
|
|
6
|
+
// points with normals (building facades). For each direction (sun position,
|
|
7
|
+
// sky direction) a depth map is rendered from that direction, like a shadow
|
|
8
|
+
// map, and a full-screen pass adds the direction's weight to every sample
|
|
9
|
+
// the depth map shows as unobstructed. Viewsheds use six perspective views
|
|
10
|
+
// from the observer instead. One read-back returns all samples.
|
|
11
|
+
//
|
|
12
|
+
// Output channels: R = sum of weight x visibility x cosine term,
|
|
13
|
+
// G = sum of weight x cosine term (the unobstructed potential).
|
|
14
|
+
// Cosine modes: 0 none, 1 weight by max(0, n.d), 2 gate (count only n.d > 0).
|
|
15
|
+
//
|
|
16
|
+
// Large areas are split into tiles so every tile keeps its own depth maps
|
|
17
|
+
// sharp; each tile's depth cameras cover the tile plus a margin for distant
|
|
18
|
+
// occluders.
|
|
19
|
+
import * as THREE from 'three';
|
|
20
|
+
import { LAYER_SURFACE, LAYER_OCCLUDER, LAYER_ID, makeIdMaterial, decodeKey } from '../scene/viewer.js';
|
|
21
|
+
|
|
22
|
+
const FULLSCREEN_VERTEX = `
|
|
23
|
+
varying vec2 vUv;
|
|
24
|
+
void main() { vUv = uv; gl_Position = vec4(position.xy, 0.0, 1.0); }`;
|
|
25
|
+
|
|
26
|
+
const ACCUMULATE_FRAGMENT = `
|
|
27
|
+
precision highp float;
|
|
28
|
+
#include <packing>
|
|
29
|
+
varying vec2 vUv;
|
|
30
|
+
uniform sampler2D tHeight;
|
|
31
|
+
uniform sampler2D tPos;
|
|
32
|
+
uniform sampler2D tNrm;
|
|
33
|
+
uniform float uPoints;
|
|
34
|
+
uniform sampler2D tDepth;
|
|
35
|
+
uniform mat4 uTopInvViewProj;
|
|
36
|
+
uniform mat4 uDirViewProj;
|
|
37
|
+
uniform vec3 uDir;
|
|
38
|
+
uniform vec3 uNormal;
|
|
39
|
+
uniform float uCosMode;
|
|
40
|
+
uniform float uWeight;
|
|
41
|
+
uniform float uBias;
|
|
42
|
+
uniform float uLift;
|
|
43
|
+
uniform float uPerspective;
|
|
44
|
+
uniform mat4 uDirView;
|
|
45
|
+
uniform vec3 uEye;
|
|
46
|
+
uniform float uNear;
|
|
47
|
+
uniform float uFar;
|
|
48
|
+
uniform float uBiasMeters;
|
|
49
|
+
uniform float uMaxDistance;
|
|
50
|
+
void main() {
|
|
51
|
+
vec3 world;
|
|
52
|
+
vec3 n;
|
|
53
|
+
if (uPoints > 0.5) {
|
|
54
|
+
vec4 p = texture2D(tPos, vUv);
|
|
55
|
+
if (p.w < 0.5) discard;
|
|
56
|
+
world = p.xyz;
|
|
57
|
+
n = normalize(texture2D(tNrm, vUv).xyz);
|
|
58
|
+
} else {
|
|
59
|
+
float d = texture2D(tHeight, vUv).r;
|
|
60
|
+
if (d <= 0.0) discard;
|
|
61
|
+
vec4 w = uTopInvViewProj * vec4(vUv * 2.0 - 1.0, (1.0 - d) * 2.0 - 1.0, 1.0);
|
|
62
|
+
world = w.xyz / w.w;
|
|
63
|
+
n = uNormal;
|
|
64
|
+
}
|
|
65
|
+
world += n * uLift;
|
|
66
|
+
if (uPerspective > 0.5) {
|
|
67
|
+
vec4 clip = uDirViewProj * vec4(world, 1.0);
|
|
68
|
+
if (clip.w <= 0.0) discard;
|
|
69
|
+
vec3 pndc = clip.xyz / clip.w;
|
|
70
|
+
vec2 puv = pndc.xy * 0.5 + 0.5;
|
|
71
|
+
if (puv.x < 0.0 || puv.x > 1.0 || puv.y < 0.0 || puv.y > 1.0 || pndc.z > 1.0) discard;
|
|
72
|
+
float dist = length(world - uEye);
|
|
73
|
+
if (dist > uMaxDistance) discard;
|
|
74
|
+
float selfDepth = -(uDirView * vec4(world, 1.0)).z;
|
|
75
|
+
float occ = 1.0 - texture2D(tDepth, puv).r;
|
|
76
|
+
float occDepth = occ >= 1.0 ? 1e9 : -perspectiveDepthToViewZ(occ, uNear, uFar);
|
|
77
|
+
float seen = selfDepth <= occDepth + uBiasMeters ? 1.0 : 0.0;
|
|
78
|
+
// A surface seen edge-on or from behind does not count as visible.
|
|
79
|
+
if (uPoints > 0.5 && dot(n, uEye - world) <= 0.0) seen = 0.0;
|
|
80
|
+
gl_FragColor = vec4(uWeight * seen, uWeight, 0.0, 1.0);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
float c = dot(n, uDir);
|
|
84
|
+
float cosf = uCosMode > 1.5 ? (c > 0.0 ? 1.0 : 0.0) : (uCosMode > 0.5 ? max(c, 0.0) : 1.0);
|
|
85
|
+
if (cosf <= 0.0) { gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0); return; }
|
|
86
|
+
vec4 clip = uDirViewProj * vec4(world, 1.0);
|
|
87
|
+
vec3 ndc = clip.xyz / clip.w;
|
|
88
|
+
vec2 duv = ndc.xy * 0.5 + 0.5;
|
|
89
|
+
float lit = 1.0;
|
|
90
|
+
if (duv.x >= 0.0 && duv.x <= 1.0 && duv.y >= 0.0 && duv.y <= 1.0) {
|
|
91
|
+
float occluder = 1.0 - texture2D(tDepth, duv).r;
|
|
92
|
+
float self = ndc.z * 0.5 + 0.5;
|
|
93
|
+
lit = self <= occluder + uBias ? 1.0 : 0.0;
|
|
94
|
+
}
|
|
95
|
+
gl_FragColor = vec4(uWeight * lit * cosf, uWeight * cosf, 0.0, 1.0);
|
|
96
|
+
}`;
|
|
97
|
+
|
|
98
|
+
export class GpuEngine {
|
|
99
|
+
/** @param {THREE.WebGLRenderer} renderer */
|
|
100
|
+
constructor(renderer) {
|
|
101
|
+
this.renderer = renderer;
|
|
102
|
+
const gl = renderer.getContext();
|
|
103
|
+
this.floatOk = renderer.extensions.has('EXT_color_buffer_float') && renderer.extensions.has('EXT_float_blend');
|
|
104
|
+
this.type = this.floatOk ? THREE.FloatType : THREE.HalfFloatType;
|
|
105
|
+
this.maxTexture = gl.getParameter(gl.MAX_TEXTURE_SIZE);
|
|
106
|
+
this.depthMaterial = new THREE.MeshDepthMaterial({ depthPacking: THREE.BasicDepthPacking, side: THREE.DoubleSide });
|
|
107
|
+
this.idMaterial = makeIdMaterial();
|
|
108
|
+
this.quadScene = new THREE.Scene();
|
|
109
|
+
this.quadCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
|
|
110
|
+
const blank = new THREE.DataTexture(new Float32Array(4), 1, 1, THREE.RGBAFormat, THREE.FloatType);
|
|
111
|
+
blank.needsUpdate = true;
|
|
112
|
+
this.accumulate = new THREE.ShaderMaterial({
|
|
113
|
+
vertexShader: FULLSCREEN_VERTEX,
|
|
114
|
+
fragmentShader: ACCUMULATE_FRAGMENT,
|
|
115
|
+
uniforms: {
|
|
116
|
+
tHeight: { value: blank }, tPos: { value: blank }, tNrm: { value: blank }, uPoints: { value: 0 },
|
|
117
|
+
tDepth: { value: null }, uTopInvViewProj: { value: new THREE.Matrix4() }, uDirViewProj: { value: new THREE.Matrix4() },
|
|
118
|
+
uDir: { value: new THREE.Vector3(0, 1, 0) }, uNormal: { value: new THREE.Vector3(0, 1, 0) }, uCosMode: { value: 0 },
|
|
119
|
+
uWeight: { value: 1 }, uBias: { value: 0.0005 }, uLift: { value: 0.3 }, uPerspective: { value: 0 },
|
|
120
|
+
uDirView: { value: new THREE.Matrix4() }, uEye: { value: new THREE.Vector3() }, uNear: { value: 0.5 },
|
|
121
|
+
uFar: { value: 1000 }, uBiasMeters: { value: 0.6 }, uMaxDistance: { value: 1e9 }
|
|
122
|
+
},
|
|
123
|
+
blending: THREE.CustomBlending,
|
|
124
|
+
blendEquation: THREE.AddEquation,
|
|
125
|
+
blendSrc: THREE.OneFactor,
|
|
126
|
+
blendDst: THREE.OneFactor,
|
|
127
|
+
depthTest: false,
|
|
128
|
+
depthWrite: false
|
|
129
|
+
});
|
|
130
|
+
const quad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), this.accumulate);
|
|
131
|
+
quad.frustumCulled = false;
|
|
132
|
+
this.quadScene.add(quad);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
_target(w, h, type = this.type) {
|
|
136
|
+
return new THREE.WebGLRenderTarget(w, h, {
|
|
137
|
+
type, format: THREE.RGBAFormat, minFilter: THREE.NearestFilter, magFilter: THREE.NearestFilter, depthBuffer: true
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
_render(scene, camera, target, material, layer) {
|
|
142
|
+
const r = this.renderer;
|
|
143
|
+
const prev = { override: scene.overrideMaterial, bg: scene.background, target: r.getRenderTarget(),
|
|
144
|
+
clear: r.getClearColor(new THREE.Color()), alpha: r.getClearAlpha(), mask: camera.layers.mask, shadow: r.shadowMap.autoUpdate };
|
|
145
|
+
scene.overrideMaterial = material;
|
|
146
|
+
scene.background = null;
|
|
147
|
+
camera.layers.set(layer);
|
|
148
|
+
r.shadowMap.autoUpdate = false;
|
|
149
|
+
r.setRenderTarget(target);
|
|
150
|
+
r.setClearColor(0x000000, 0);
|
|
151
|
+
r.clear(true, true, false);
|
|
152
|
+
r.render(scene, camera);
|
|
153
|
+
r.setRenderTarget(prev.target);
|
|
154
|
+
r.setClearColor(prev.clear, prev.alpha);
|
|
155
|
+
r.shadowMap.autoUpdate = prev.shadow;
|
|
156
|
+
camera.layers.mask = prev.mask;
|
|
157
|
+
scene.overrideMaterial = prev.override;
|
|
158
|
+
scene.background = prev.bg;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
_readChannels(target, w, h, channels) {
|
|
162
|
+
const n = w * h;
|
|
163
|
+
const raw = this.type === THREE.FloatType ? new Float32Array(n * 4) : new Uint16Array(n * 4);
|
|
164
|
+
this.renderer.readRenderTargetPixels(target, 0, 0, w, h, raw);
|
|
165
|
+
const out = channels.map(() => new Float32Array(n));
|
|
166
|
+
for (let i = 0; i < n; i++) {
|
|
167
|
+
for (let k = 0; k < channels.length; k++) {
|
|
168
|
+
const v = raw[i * 4 + channels[k]];
|
|
169
|
+
out[k][i] = this.type === THREE.FloatType ? v : THREE.DataUtils.fromHalfFloat(v);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return out;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Top camera looking straight down over a tile, north (-z) up. */
|
|
176
|
+
_topCamera(tile) {
|
|
177
|
+
const cx = (tile.minX + tile.maxX) / 2;
|
|
178
|
+
const cz = (tile.minZ + tile.maxZ) / 2;
|
|
179
|
+
const hx = (tile.maxX - tile.minX) / 2;
|
|
180
|
+
const hz = (tile.maxZ - tile.minZ) / 2;
|
|
181
|
+
const top = tile.maxY + 50;
|
|
182
|
+
const cam = new THREE.OrthographicCamera(-hx, hx, hz, -hz, 1, top - tile.minY + 100);
|
|
183
|
+
cam.position.set(cx, top, cz);
|
|
184
|
+
cam.up.set(0, 0, -1);
|
|
185
|
+
cam.lookAt(cx, tile.minY, cz);
|
|
186
|
+
cam.updateMatrixWorld();
|
|
187
|
+
cam.updateProjectionMatrix();
|
|
188
|
+
return cam;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Resolve once the GPU has run every command issued so far, polling a
|
|
193
|
+
* WebGL2 fence without blocking the page (a plain setTimeout yield when
|
|
194
|
+
* fences are not available).
|
|
195
|
+
*/
|
|
196
|
+
async _gpuDone() {
|
|
197
|
+
const gl = /** @type {WebGL2RenderingContext} */ (this.renderer.getContext());
|
|
198
|
+
if (typeof gl.fenceSync !== 'function') { await new Promise((res) => setTimeout(res, 0)); return; }
|
|
199
|
+
const sync = gl.fenceSync(gl.SYNC_GPU_COMMANDS_COMPLETE, 0);
|
|
200
|
+
if (!sync) { await new Promise((res) => setTimeout(res, 0)); return; }
|
|
201
|
+
gl.flush();
|
|
202
|
+
try {
|
|
203
|
+
for (let k = 0; k < 20000; k++) {
|
|
204
|
+
const st = gl.clientWaitSync(sync, 0, 0);
|
|
205
|
+
if (st === gl.ALREADY_SIGNALED || st === gl.CONDITION_SATISFIED || st === gl.WAIT_FAILED) break;
|
|
206
|
+
await new Promise((res) => setTimeout(res, 4));
|
|
207
|
+
}
|
|
208
|
+
} finally {
|
|
209
|
+
gl.deleteSync(sync);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Feature key per texel of a tile (row 0 = north), 0 where no feature. */
|
|
214
|
+
idGrid(scene, tile, cols, rows) {
|
|
215
|
+
const cam = this._topCamera(tile);
|
|
216
|
+
const target = this._target(cols, rows, THREE.UnsignedByteType);
|
|
217
|
+
this._render(scene, cam, target, this.idMaterial, LAYER_ID);
|
|
218
|
+
const raw = new Uint8Array(cols * rows * 4);
|
|
219
|
+
this.renderer.readRenderTargetPixels(target, 0, 0, cols, rows, raw);
|
|
220
|
+
target.dispose();
|
|
221
|
+
const out = new Int32Array(cols * rows);
|
|
222
|
+
for (let y = 0; y < rows; y++) {
|
|
223
|
+
const src = (rows - 1 - y) * cols;
|
|
224
|
+
for (let x = 0; x < cols; x++) {
|
|
225
|
+
const i = (src + x) * 4;
|
|
226
|
+
out[y * cols + x] = raw[i + 3] ? decodeKey(raw[i], raw[i + 1], raw[i + 2], raw[i + 3]) : 0;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return out;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Run one tile.
|
|
234
|
+
* @param {object} a
|
|
235
|
+
* @param {THREE.Scene} a.scene
|
|
236
|
+
* @param {{ minX: number, maxX: number, minZ: number, maxZ: number, minY: number, maxY: number }} a.tile
|
|
237
|
+
* @param {number} a.cols texels across (heightfield mode)
|
|
238
|
+
* @param {number} a.rows texels down
|
|
239
|
+
* @param {{ dir: number[], weight: number }[]} [a.directions]
|
|
240
|
+
* @param {number} [a.cosMode]
|
|
241
|
+
* @param {number[]} [a.normal] normal of heightfield samples
|
|
242
|
+
* @param {number} [a.lift] metres the sample is lifted along its normal
|
|
243
|
+
* @param {{ positions: Float32Array, normals: Float32Array, count: number }} [a.points] point mode
|
|
244
|
+
* @param {THREE.Vector3} [a.eye] viewshed mode
|
|
245
|
+
* @param {number} [a.maxDistance]
|
|
246
|
+
* @param {number} [a.margin] metres of occluders around the tile
|
|
247
|
+
* @param {number} [a.depthResolution]
|
|
248
|
+
* @param {(f: number) => void} [a.onProgress]
|
|
249
|
+
*/
|
|
250
|
+
async run({ scene, tile, cols, rows, directions = [], cosMode = 0, normal = [0, 1, 0], lift = 0.3, points = null,
|
|
251
|
+
eye = null, maxDistance = 1000, margin = 200, depthResolution = 2048, onProgress }) {
|
|
252
|
+
const r = this.renderer;
|
|
253
|
+
const u = this.accumulate.uniforms;
|
|
254
|
+
let w = cols, h = rows;
|
|
255
|
+
let heightTarget = null;
|
|
256
|
+
let posTex = null, nrmTex = null;
|
|
257
|
+
let topInv = null;
|
|
258
|
+
if (points) {
|
|
259
|
+
w = Math.min(this.maxTexture, Math.ceil(Math.sqrt(points.count)));
|
|
260
|
+
h = Math.ceil(points.count / w);
|
|
261
|
+
const pos = new Float32Array(w * h * 4);
|
|
262
|
+
const nrm = new Float32Array(w * h * 4);
|
|
263
|
+
for (let i = 0; i < points.count; i++) {
|
|
264
|
+
pos.set([points.positions[i * 3], points.positions[i * 3 + 1], points.positions[i * 3 + 2], 1], i * 4);
|
|
265
|
+
nrm.set([points.normals[i * 3], points.normals[i * 3 + 1], points.normals[i * 3 + 2], 0], i * 4);
|
|
266
|
+
}
|
|
267
|
+
posTex = new THREE.DataTexture(pos, w, h, THREE.RGBAFormat, THREE.FloatType);
|
|
268
|
+
nrmTex = new THREE.DataTexture(nrm, w, h, THREE.RGBAFormat, THREE.FloatType);
|
|
269
|
+
posTex.needsUpdate = true;
|
|
270
|
+
nrmTex.needsUpdate = true;
|
|
271
|
+
u.tPos.value = posTex;
|
|
272
|
+
u.tNrm.value = nrmTex;
|
|
273
|
+
u.uPoints.value = 1;
|
|
274
|
+
} else {
|
|
275
|
+
const topCam = this._topCamera(tile);
|
|
276
|
+
heightTarget = this._target(cols, rows);
|
|
277
|
+
this._render(scene, topCam, heightTarget, this.depthMaterial, LAYER_SURFACE);
|
|
278
|
+
topInv = new THREE.Matrix4().multiplyMatrices(topCam.projectionMatrix, topCam.matrixWorldInverse).invert();
|
|
279
|
+
u.tHeight.value = heightTarget.texture;
|
|
280
|
+
u.uTopInvViewProj.value.copy(topInv);
|
|
281
|
+
u.uPoints.value = 0;
|
|
282
|
+
}
|
|
283
|
+
const dSize = Math.min(depthResolution, this.maxTexture);
|
|
284
|
+
const depthTarget = this._target(dSize, dSize);
|
|
285
|
+
const accum = this._target(w, h);
|
|
286
|
+
const prevTarget = r.getRenderTarget();
|
|
287
|
+
r.setRenderTarget(accum);
|
|
288
|
+
r.setClearColor(0x000000, 0);
|
|
289
|
+
r.clear(true, true, false);
|
|
290
|
+
r.setRenderTarget(prevTarget);
|
|
291
|
+
|
|
292
|
+
const cx = (tile.minX + tile.maxX) / 2;
|
|
293
|
+
const cz = (tile.minZ + tile.maxZ) / 2;
|
|
294
|
+
const half = Math.max(tile.maxX - tile.minX, tile.maxZ - tile.minZ) / 2 + margin;
|
|
295
|
+
const radius = Math.hypot(half, half, (tile.maxY - tile.minY) / 2) + 20;
|
|
296
|
+
const centre = new THREE.Vector3(cx, (tile.minY + tile.maxY) / 2, cz);
|
|
297
|
+
const dirCam = new THREE.OrthographicCamera(-radius, radius, radius, -radius, 1, 4 * radius);
|
|
298
|
+
u.tDepth.value = depthTarget.texture;
|
|
299
|
+
u.uNormal.value.set(normal[0], normal[1], normal[2]).normalize();
|
|
300
|
+
u.uCosMode.value = cosMode;
|
|
301
|
+
u.uLift.value = lift;
|
|
302
|
+
// About 1 m of depth bias, more where depth texels are coarse.
|
|
303
|
+
u.uBias.value = Math.max(1, ((2 * radius) / dSize) * 2) / (4 * radius);
|
|
304
|
+
|
|
305
|
+
const views = [];
|
|
306
|
+
if (eye) {
|
|
307
|
+
const faces = [[1, 0, 0], [-1, 0, 0], [0, 1, 0], [0, -1, 0], [0, 0, 1], [0, 0, -1]];
|
|
308
|
+
for (const [x, y, z] of faces) {
|
|
309
|
+
const cam = new THREE.PerspectiveCamera(90, 1, 0.3, maxDistance * 1.8);
|
|
310
|
+
cam.position.copy(eye);
|
|
311
|
+
cam.up.set(0, 1, 0);
|
|
312
|
+
if (y !== 0) cam.up.set(0, 0, -1);
|
|
313
|
+
cam.lookAt(eye.x + x, eye.y + y, eye.z + z);
|
|
314
|
+
cam.updateMatrixWorld();
|
|
315
|
+
cam.updateProjectionMatrix();
|
|
316
|
+
views.push({ camera: cam, weight: 1 });
|
|
317
|
+
}
|
|
318
|
+
u.uEye.value.copy(eye);
|
|
319
|
+
} else {
|
|
320
|
+
for (const d of directions) views.push(d);
|
|
321
|
+
}
|
|
322
|
+
u.uPerspective.value = eye ? 1 : 0;
|
|
323
|
+
u.uMaxDistance.value = eye ? maxDistance : 1e9;
|
|
324
|
+
let batch = 3, inBatch = 0, batchStart = performance.now();
|
|
325
|
+
for (let i = 0; i < views.length; i++) {
|
|
326
|
+
const view = views[i];
|
|
327
|
+
let cam = view.camera;
|
|
328
|
+
if (!cam) {
|
|
329
|
+
const d = new THREE.Vector3(view.dir[0], view.dir[1], view.dir[2]).normalize();
|
|
330
|
+
cam = dirCam;
|
|
331
|
+
dirCam.position.copy(centre).addScaledVector(d, 2 * radius);
|
|
332
|
+
dirCam.up.set(0, 1, 0);
|
|
333
|
+
if (Math.abs(d.y) > 0.99) dirCam.up.set(0, 0, -1);
|
|
334
|
+
dirCam.lookAt(centre);
|
|
335
|
+
dirCam.updateMatrixWorld();
|
|
336
|
+
u.uDir.value.copy(d);
|
|
337
|
+
}
|
|
338
|
+
this._render(scene, cam, depthTarget, this.depthMaterial, LAYER_OCCLUDER);
|
|
339
|
+
u.uDirViewProj.value.multiplyMatrices(cam.projectionMatrix, cam.matrixWorldInverse);
|
|
340
|
+
u.uDirView.value.copy(cam.matrixWorldInverse);
|
|
341
|
+
u.uNear.value = cam.near;
|
|
342
|
+
u.uFar.value = cam.far;
|
|
343
|
+
u.uWeight.value = view.weight;
|
|
344
|
+
const before = r.getRenderTarget();
|
|
345
|
+
const autoClear = r.autoClear;
|
|
346
|
+
r.autoClear = false;
|
|
347
|
+
r.setRenderTarget(accum);
|
|
348
|
+
r.render(this.quadScene, this.quadCamera);
|
|
349
|
+
r.setRenderTarget(before);
|
|
350
|
+
r.autoClear = autoClear;
|
|
351
|
+
if (++inBatch >= batch || i === views.length - 1) {
|
|
352
|
+
// Let the GPU finish this batch while the page stays responsive;
|
|
353
|
+
// otherwise every view queues up and the readback blocks for all.
|
|
354
|
+
// Batches shrink to one view when views are slow (large grids, weak
|
|
355
|
+
// GPUs) and grow to twelve when they are fast.
|
|
356
|
+
await this._gpuDone();
|
|
357
|
+
const took = performance.now() - batchStart;
|
|
358
|
+
batch = took > 200 ? 1 : Math.min(12, batch * 2);
|
|
359
|
+
inBatch = 0;
|
|
360
|
+
batchStart = performance.now();
|
|
361
|
+
onProgress?.((i + 1) / views.length);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
const [valuesRaw, potentialRaw] = this._readChannels(accum, w, h, [0, 1]);
|
|
365
|
+
let heights = null;
|
|
366
|
+
let values = valuesRaw;
|
|
367
|
+
let potential = potentialRaw;
|
|
368
|
+
if (!points) {
|
|
369
|
+
// Render-target rows start in the south: flip to north-first.
|
|
370
|
+
values = flipRows(valuesRaw, w, h);
|
|
371
|
+
potential = flipRows(potentialRaw, w, h);
|
|
372
|
+
const [hRaw] = this._readChannels(heightTarget, w, h, [0]);
|
|
373
|
+
heights = new Float32Array(w * h);
|
|
374
|
+
const p = new THREE.Vector4();
|
|
375
|
+
for (let y = 0; y < h; y++) {
|
|
376
|
+
for (let x = 0; x < w; x++) {
|
|
377
|
+
const d = hRaw[(h - 1 - y) * w + x];
|
|
378
|
+
if (d > 0) {
|
|
379
|
+
p.set(((x + 0.5) / w) * 2 - 1, ((h - 1 - y + 0.5) / h) * 2 - 1, (1 - d) * 2 - 1, 1).applyMatrix4(/** @type {THREE.Matrix4} */ (topInv));
|
|
380
|
+
heights[y * w + x] = p.y / p.w;
|
|
381
|
+
} else heights[y * w + x] = NaN;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
heightTarget?.dispose();
|
|
385
|
+
} else {
|
|
386
|
+
values = valuesRaw.subarray(0, points.count);
|
|
387
|
+
potential = potentialRaw.subarray(0, points.count);
|
|
388
|
+
posTex?.dispose();
|
|
389
|
+
nrmTex?.dispose();
|
|
390
|
+
}
|
|
391
|
+
accum.dispose();
|
|
392
|
+
depthTarget.dispose();
|
|
393
|
+
return { values, potential, heights };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
dispose() {
|
|
397
|
+
this.depthMaterial.dispose();
|
|
398
|
+
this.idMaterial.dispose();
|
|
399
|
+
this.accumulate.dispose();
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function flipRows(src, w, h) {
|
|
404
|
+
const out = new Float32Array(w * h);
|
|
405
|
+
for (let y = 0; y < h; y++) out.set(src.subarray((h - 1 - y) * w, (h - y) * w), y * w);
|
|
406
|
+
return out;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Split an analysis area into tiles of at most maxCells texels a side.
|
|
411
|
+
* area: { minX, maxX, minY(north-south local y), maxY } in local metres
|
|
412
|
+
* (x east, y north). Returns grid info and tiles with three-coordinate
|
|
413
|
+
* bounds and their offsets into the full grid.
|
|
414
|
+
*/
|
|
415
|
+
export function planTiles(area, cell, maxCells = 1024) {
|
|
416
|
+
const cols = Math.max(1, Math.ceil((area.maxX - area.minX) / cell));
|
|
417
|
+
const rows = Math.max(1, Math.ceil((area.maxY - area.minY) / cell));
|
|
418
|
+
const grid = { cols, rows, cell, minX: area.minX + cell / 2, maxY: area.maxY - cell / 2 };
|
|
419
|
+
const tiles = [];
|
|
420
|
+
for (let r0 = 0; r0 < rows; r0 += maxCells) {
|
|
421
|
+
for (let c0 = 0; c0 < cols; c0 += maxCells) {
|
|
422
|
+
const tc = Math.min(maxCells, cols - c0);
|
|
423
|
+
const tr = Math.min(maxCells, rows - r0);
|
|
424
|
+
const minX = area.minX + c0 * cell;
|
|
425
|
+
const northY = area.maxY - r0 * cell;
|
|
426
|
+
tiles.push({ c0, r0, cols: tc, rows: tr,
|
|
427
|
+
bounds: { minX, maxX: minX + tc * cell, minZ: -northY, maxZ: -(northY - tr * cell) } });
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return { grid, tiles };
|
|
431
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
// Building-height optimisation: NSGA-II over the floor counts of selected
|
|
3
|
+
// buildings, trading floor area against sun on the open ground around them,
|
|
4
|
+
// optionally within planning constraints. Each candidate is evaluated with
|
|
5
|
+
// one GPU sun-hours pass at a coarse cell; the constraints add no pass of
|
|
6
|
+
// their own:
|
|
7
|
+
// - sun on the ground around each building reuses that pass, with the
|
|
8
|
+
// open ground shared out among the buildings once (footprints do not
|
|
9
|
+
// change, only heights);
|
|
10
|
+
// - the BRE 209 25° test for neighbouring windows is prepared once from
|
|
11
|
+
// the footprints and is a few multiplications per design.
|
|
12
|
+
// Both follow BRE 209 for places that are built up already: meet the limit,
|
|
13
|
+
// or keep at least 0.8 times the value now (VSC for windows, sunny area for
|
|
14
|
+
// open ground). "Now" is the current design as the optimiser sees it
|
|
15
|
+
// (heights rounded to whole floors).
|
|
16
|
+
// Analyses too slow to run per design (sky view, the climate year) belong to
|
|
17
|
+
// the planning check of the design that is finally chosen.
|
|
18
|
+
import { nsga2 } from '../core/nsga2.js';
|
|
19
|
+
import { sunPath } from '../core/solar.js';
|
|
20
|
+
import { typicalDoy } from '../core/weather.js';
|
|
21
|
+
import { surroundings } from './climate.js';
|
|
22
|
+
import { featureKey, ExtrudedLayer } from '../scene/objects.js';
|
|
23
|
+
import { prepareRule, checkRule, reach25, BRE_FACTOR } from '../core/daylight_rule.js';
|
|
24
|
+
|
|
25
|
+
/** Height of the centre of a neighbour's lowest window above its floor (m). */
|
|
26
|
+
export const WINDOW_HEIGHT = 1.6;
|
|
27
|
+
|
|
28
|
+
/** Shadow-map size whose texels are at most half a cell over the depth camera's view (1024..4096). */
|
|
29
|
+
export function optimiserDepthResolution(area, cell, margin) {
|
|
30
|
+
const side = 2 * (Math.max(area.maxX - area.minX, area.maxY - area.minY) / 2 + margin);
|
|
31
|
+
let size = 1024;
|
|
32
|
+
while (size < 4096 && side / size > cell / 2) size *= 2;
|
|
33
|
+
return size;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function outerRings(geometry) {
|
|
37
|
+
if (!geometry) return [];
|
|
38
|
+
const polys = geometry.type === 'Polygon' ? [geometry.coordinates] : geometry.type === 'MultiPolygon' ? geometry.coordinates : [];
|
|
39
|
+
return polys.map((p) => p[0]).filter((r) => r && r.length >= 3);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Bounding box of rings of [x, y] points. */
|
|
43
|
+
function ringsBox(rings) {
|
|
44
|
+
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
|
|
45
|
+
for (const ring of rings) for (const [x, y] of ring) { minX = Math.min(minX, x); maxX = Math.max(maxX, x); minY = Math.min(minY, y); maxY = Math.max(maxY, y); }
|
|
46
|
+
return { minX, maxX, minY, maxY };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The 25° test prepared for the targets and the other buildings around them. */
|
|
50
|
+
function prepareDaylight(app, targets, o, topsNow) {
|
|
51
|
+
const targetSet = new Set(targets.map(({ layer, f }) => `${layer.index}:${f}`));
|
|
52
|
+
const tallest = Math.max(...targets.map(({ layer, f }) => layer.baseZ[f])) + o.maxFloors * o.floorHeight;
|
|
53
|
+
const tg = targets.map(({ layer, f }) => ({ rings: outerRings(layer.features[f].geometry) }));
|
|
54
|
+
const box = ringsBox(tg.flatMap((t) => t.rings));
|
|
55
|
+
// Every other building, then only those whose footprint comes within the
|
|
56
|
+
// 25° reach of the tallest allowed roof (measured from the target walls,
|
|
57
|
+
// not from the scored area): windows farther away pass the 25° test alone.
|
|
58
|
+
const candidates = [];
|
|
59
|
+
for (const layer of app.layers) {
|
|
60
|
+
if (!(layer instanceof ExtrudedLayer) || layer.isWall || !layer.visible) continue;
|
|
61
|
+
for (let f = 0; f < layer.count; f++) {
|
|
62
|
+
if (targetSet.has(`${layer.index}:${f}`)) continue;
|
|
63
|
+
const windowZ = layer.baseZ[f] + layer.minH[f] + WINDOW_HEIGHT;
|
|
64
|
+
for (const ring of outerRings(layer.features[f].geometry)) candidates.push({ ring, windowZ, box: ringsBox([ring]) });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Each window has its own reach: the rise from it to the tallest roof.
|
|
68
|
+
const neighbours = [];
|
|
69
|
+
let maxDist = 1;
|
|
70
|
+
for (const n of candidates) {
|
|
71
|
+
const reach = reach25(tallest - n.windowZ) + 1;
|
|
72
|
+
if (n.box.maxX < box.minX - reach || n.box.minX > box.maxX + reach
|
|
73
|
+
|| n.box.maxY < box.minY - reach || n.box.minY > box.maxY + reach) continue;
|
|
74
|
+
neighbours.push({ ring: n.ring, windowZ: n.windowZ });
|
|
75
|
+
if (reach > maxDist) maxDist = reach;
|
|
76
|
+
}
|
|
77
|
+
return prepareRule(neighbours, tg, topsNow, { maxDist });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @param {any} app
|
|
82
|
+
* @param {object} o
|
|
83
|
+
* @param {{ layer: any, f: number }[]} o.targets
|
|
84
|
+
* @param {number} o.floorHeight metres per floor
|
|
85
|
+
* @param {number} o.minFloors
|
|
86
|
+
* @param {number} o.maxFloors
|
|
87
|
+
* @param {number} o.minGfa required floor area (m2), 0 for none
|
|
88
|
+
* @param {{ month: number, day: number }} o.date
|
|
89
|
+
* @param {number} o.startHour
|
|
90
|
+
* @param {number} o.endHour
|
|
91
|
+
* @param {number} o.margin metres of ground around the targets that are scored
|
|
92
|
+
* @param {number} o.populationSize
|
|
93
|
+
* @param {number} o.generations
|
|
94
|
+
* @param {number} o.seed
|
|
95
|
+
* @param {{ amenity?: { on: boolean, share: number, radius?: number }, daylight?: { on: boolean } }} [o.constraints]
|
|
96
|
+
* amenity: at least `share` (0..1) of the open ground within `radius` m of each target gets 2 h of sun on the day;
|
|
97
|
+
* daylight: the BRE 209 25° test for the windows of the buildings around them.
|
|
98
|
+
* @param {(g: any) => void} [o.onGeneration]
|
|
99
|
+
* @param {() => boolean} [o.shouldStop]
|
|
100
|
+
*/
|
|
101
|
+
export async function optimiseHeights(app, o) {
|
|
102
|
+
const { targets } = o;
|
|
103
|
+
const cons = o.constraints || {};
|
|
104
|
+
const s = app.site;
|
|
105
|
+
const dirs = sunPath(typicalDoy(o.date.month, o.date.day), s.lat, s.lon, s.utcOffset,
|
|
106
|
+
{ startHour: o.startHour, endHour: o.endHour, stepMinutes: 30 });
|
|
107
|
+
if (!dirs.length) throw new Error('No sun in the chosen period.');
|
|
108
|
+
// Scored area: the targets' bounding box plus a margin, inside the scene.
|
|
109
|
+
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
|
|
110
|
+
for (const { layer, f } of targets) {
|
|
111
|
+
const x = layer.centroid[f * 2], y = layer.centroid[f * 2 + 1];
|
|
112
|
+
minX = Math.min(minX, x); maxX = Math.max(maxX, x); minY = Math.min(minY, y); maxY = Math.max(maxY, y);
|
|
113
|
+
}
|
|
114
|
+
const sc = app.sceneArea();
|
|
115
|
+
const area = { minX: Math.max(sc.minX, minX - o.margin), maxX: Math.min(sc.maxX, maxX + o.margin),
|
|
116
|
+
minY: Math.max(sc.minY, minY - o.margin), maxY: Math.min(sc.maxY, maxY + o.margin) };
|
|
117
|
+
const cell = Math.max(2, Math.sqrt(((area.maxX - area.minX) * (area.maxY - area.minY)) / 90000));
|
|
118
|
+
// Shadow maps fine enough for the coarse cell: same ranking of designs as the
|
|
119
|
+
// full analysis (checked against it), at a fraction of the cost.
|
|
120
|
+
const depthResolution = optimiserDepthResolution(area, cell, app.analyst.occluderMargin());
|
|
121
|
+
const original = targets.map(({ layer, f }) => layer.heights[f]);
|
|
122
|
+
const byLayer = new Map();
|
|
123
|
+
for (const t of targets) { if (!byLayer.has(t.layer)) byLayer.set(t.layer, []); byLayer.get(t.layer).push(t.f); }
|
|
124
|
+
const setFloors = (x) => {
|
|
125
|
+
targets.forEach(({ layer, f }, i) => { layer.heights[f] = x[i] * o.floorHeight; });
|
|
126
|
+
for (const [layer, fs] of byLayer) layer.applyHeightsFor(fs);
|
|
127
|
+
};
|
|
128
|
+
const gfa = (x) => targets.reduce((sum, { layer, f }, i) => sum + x[i] * layer.footprint[f], 0);
|
|
129
|
+
const roofs = (x) => targets.map(({ layer, f }, i) => layer.baseZ[f] + x[i] * o.floorHeight);
|
|
130
|
+
|
|
131
|
+
const current = targets.map(({ layer, f }) => Math.max(o.minFloors, Math.min(o.maxFloors, Math.round(layer.heights[f] / o.floorHeight) || o.minFloors)));
|
|
132
|
+
const amenity = cons.amenity?.on ? { share: cons.amenity.share, radius: cons.amenity.radius ?? 15 } : null;
|
|
133
|
+
const daylight = cons.daylight?.on ? prepareDaylight(app, targets, o, roofs(current)) : null;
|
|
134
|
+
const keyIndex = new Map(targets.map(({ layer, f }, i) => [featureKey(layer.index, f + 1), i]));
|
|
135
|
+
// Footprints do not change between designs: building ids are drawn once,
|
|
136
|
+
// and the open ground is shared out among the targets once.
|
|
137
|
+
let ids = null, owner = null, nowShare = null;
|
|
138
|
+
const okCells = new Int32Array(targets.length), allCells = new Int32Array(targets.length);
|
|
139
|
+
|
|
140
|
+
const evaluate = async (x) => {
|
|
141
|
+
setFloors(x);
|
|
142
|
+
const run = await app.analyst.gridRun({ area, cell, directions: dirs, withIds: !ids, depthResolution, lock: false });
|
|
143
|
+
if (!ids) {
|
|
144
|
+
ids = run.ids;
|
|
145
|
+
if (amenity) {
|
|
146
|
+
const label = surroundings(ids, new Float32Array(ids.length), run.grid.cols, run.grid.rows, Math.max(1, Math.round(amenity.radius / cell)), (k) => keyIndex.has(k));
|
|
147
|
+
owner = new Int16Array(ids.length).fill(-1);
|
|
148
|
+
for (let i = 0; i < label.length; i++) if (!ids[i] && label[i]) owner[i] = keyIndex.get(label[i]) ?? -1;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
okCells.fill(0); allCells.fill(0);
|
|
152
|
+
let sum = 0, n = 0, twoH = 0;
|
|
153
|
+
for (let i = 0; i < run.values.length; i++) {
|
|
154
|
+
const v = run.values[i];
|
|
155
|
+
if (ids[i] !== 0 || !Number.isFinite(v)) continue;
|
|
156
|
+
sum += v; n++;
|
|
157
|
+
if (v >= 2) twoH++;
|
|
158
|
+
if (owner && owner[i] >= 0) { allCells[owner[i]]++; if (v >= 2) okCells[owner[i]]++; }
|
|
159
|
+
}
|
|
160
|
+
const sun = n ? sum / n : 0;
|
|
161
|
+
const area2 = gfa(x);
|
|
162
|
+
let violation = o.minGfa > 0 ? Math.max(0, (o.minGfa - area2) / o.minGfa) : 0;
|
|
163
|
+
const extra = { sun, gfa: area2, twoHourShare: n ? twoH / n : 0 };
|
|
164
|
+
if (amenity) {
|
|
165
|
+
// The first design evaluated is the current one: it sets "now".
|
|
166
|
+
const shares = Array.from(allCells, (nc, t) => (nc ? okCells[t] / nc : NaN));
|
|
167
|
+
nowShare ??= shares;
|
|
168
|
+
let pass = 0, checked = 0, short = 0, worse = 0;
|
|
169
|
+
for (let t = 0; t < targets.length; t++) {
|
|
170
|
+
if (!Number.isFinite(shares[t])) continue;
|
|
171
|
+
checked++;
|
|
172
|
+
if (shares[t] >= amenity.share) { pass++; continue; }
|
|
173
|
+
const limit = Math.min(amenity.share, BRE_FACTOR * nowShare[t]);
|
|
174
|
+
if (shares[t] < limit - 1e-9) { worse++; short += (limit - shares[t]) / amenity.share; }
|
|
175
|
+
}
|
|
176
|
+
violation += short;
|
|
177
|
+
Object.assign(extra, { amenityPass: pass, amenityChecked: checked, amenityWorse: worse });
|
|
178
|
+
}
|
|
179
|
+
if (daylight) {
|
|
180
|
+
const r = checkRule(daylight, roofs(x));
|
|
181
|
+
violation += r.excessDeg / 25;
|
|
182
|
+
Object.assign(extra, { windowsOver: r.failing, neighboursHit: r.neighboursHit, worstDeg: r.worstDeg });
|
|
183
|
+
}
|
|
184
|
+
return { objectives: [-sun, -area2], violation, extra };
|
|
185
|
+
};
|
|
186
|
+
app.terrain.lockForAnalysis(area, cell);
|
|
187
|
+
try {
|
|
188
|
+
const baseline = await evaluate(current);
|
|
189
|
+
const seeds = [current, targets.map(() => o.minFloors), targets.map(() => o.maxFloors)];
|
|
190
|
+
if (amenity || daylight) {
|
|
191
|
+
// Under constraints the feasible designs sit close to the current one:
|
|
192
|
+
// start half the population one floor up on a single building each.
|
|
193
|
+
const room = Math.floor(o.populationSize / 2);
|
|
194
|
+
for (let i = 0; i < targets.length && seeds.length < room + 3; i++) {
|
|
195
|
+
if (current[i] < o.maxFloors) seeds.push(current.map((v, k) => (k === i ? v + 1 : v)));
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const run = await nsga2({
|
|
199
|
+
lower: targets.map(() => o.minFloors), upper: targets.map(() => o.maxFloors), integer: true, evaluate, seeds
|
|
200
|
+
}, { populationSize: o.populationSize, generations: o.generations, seed: o.seed, onGeneration: o.onGeneration, shouldStop: o.shouldStop });
|
|
201
|
+
return { ...run, baseline: { x: current, ...baseline }, area, cell, dirs: dirs.length, depthResolution,
|
|
202
|
+
windows: daylight?.windows ?? 0, constraints: { amenity, daylight: !!daylight, minGfa: o.minGfa } };
|
|
203
|
+
} finally {
|
|
204
|
+
app.terrain.lockForAnalysis(null);
|
|
205
|
+
targets.forEach(({ layer, f }, i) => { layer.heights[f] = original[i]; });
|
|
206
|
+
for (const [layer, fs] of byLayer) layer.applyHeightsFor(fs);
|
|
207
|
+
}
|
|
208
|
+
}
|