@a2f0/skyline 0.0.0-stage → 0.1.1
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/NOTICE.md +20 -0
- package/README.md +52 -2
- package/docs/package.md +222 -0
- package/docs/skyline-geography.md +503 -0
- package/lib/models/aon-center.d.ts +4 -0
- package/lib/models/aon-center.js +60 -0
- package/lib/models/aon-geographic.d.ts +9 -0
- package/lib/models/aon-geographic.js +35 -0
- package/lib/models/aon-tower.d.ts +23 -0
- package/lib/models/aon-tower.js +130 -0
- package/lib/models/blue-cross-geographic.d.ts +14 -0
- package/lib/models/blue-cross-geographic.js +248 -0
- package/lib/models/borg-warner-geographic.d.ts +20 -0
- package/lib/models/borg-warner-geographic.js +143 -0
- package/lib/models/buckingham-geographic.d.ts +10 -0
- package/lib/models/buckingham-geographic.js +121 -0
- package/lib/models/building-kit.d.ts +84 -0
- package/lib/models/building-kit.js +358 -0
- package/lib/models/celebrations.d.ts +39 -0
- package/lib/models/celebrations.js +44 -0
- package/lib/models/chicago-athletic-association-geographic.d.ts +32 -0
- package/lib/models/chicago-athletic-association-geographic.js +173 -0
- package/lib/models/crain-communications.d.ts +5 -0
- package/lib/models/crain-communications.js +75 -0
- package/lib/models/crain-geographic.d.ts +10 -0
- package/lib/models/crain-geographic.js +69 -0
- package/lib/models/crain-tower.d.ts +32 -0
- package/lib/models/crain-tower.js +378 -0
- package/lib/models/facade-grid.d.ts +18 -0
- package/lib/models/facade-grid.js +176 -0
- package/lib/models/gage-geographic.d.ts +19 -0
- package/lib/models/gage-geographic.js +154 -0
- package/lib/models/heritage-at-millennium-park.d.ts +3 -0
- package/lib/models/heritage-at-millennium-park.js +284 -0
- package/lib/models/heritage-geographic.d.ts +11 -0
- package/lib/models/heritage-geographic.js +208 -0
- package/lib/models/hyatt-west-tower-geographic.d.ts +26 -0
- package/lib/models/hyatt-west-tower-geographic.js +86 -0
- package/lib/models/keith-ascher-geographic.d.ts +29 -0
- package/lib/models/keith-ascher-geographic.js +110 -0
- package/lib/models/kemper-geographic.d.ts +8 -0
- package/lib/models/kemper-geographic.js +153 -0
- package/lib/models/kemper.d.ts +2 -0
- package/lib/models/kemper.js +78 -0
- package/lib/models/lake-view-geographic.d.ts +15 -0
- package/lib/models/lake-view-geographic.js +134 -0
- package/lib/models/maclean-center-geographic.d.ts +20 -0
- package/lib/models/maclean-center-geographic.js +162 -0
- package/lib/models/michigan-boulevard-geographic.d.ts +12 -0
- package/lib/models/michigan-boulevard-geographic.js +157 -0
- package/lib/models/michigan-plaza-south-geographic.d.ts +7 -0
- package/lib/models/michigan-plaza-south-geographic.js +84 -0
- package/lib/models/michigan-plaza-south.d.ts +2 -0
- package/lib/models/michigan-plaza-south.js +68 -0
- package/lib/models/millennium-park-plaza-geographic.d.ts +9 -0
- package/lib/models/millennium-park-plaza-geographic.js +103 -0
- package/lib/models/monroe-geographic.d.ts +19 -0
- package/lib/models/monroe-geographic.js +177 -0
- package/lib/models/north-michigan-180-geographic.d.ts +14 -0
- package/lib/models/north-michigan-180-geographic.js +117 -0
- package/lib/models/north-wabash-geographic.d.ts +14 -0
- package/lib/models/north-wabash-geographic.js +122 -0
- package/lib/models/on-the-park-geographic.d.ts +11 -0
- package/lib/models/on-the-park-geographic.js +245 -0
- package/lib/models/one-prudential-geographic.d.ts +12 -0
- package/lib/models/one-prudential-geographic.js +55 -0
- package/lib/models/one-prudential-plaza.d.ts +5 -0
- package/lib/models/one-prudential-plaza.js +72 -0
- package/lib/models/one-prudential-tower.d.ts +45 -0
- package/lib/models/one-prudential-tower.js +246 -0
- package/lib/models/peoples-gas-geographic.d.ts +23 -0
- package/lib/models/peoples-gas-geographic.js +250 -0
- package/lib/models/railway-exchange-geographic.d.ts +18 -0
- package/lib/models/railway-exchange-geographic.js +176 -0
- package/lib/models/river-plaza-geographic.d.ts +16 -0
- package/lib/models/river-plaza-geographic.js +104 -0
- package/lib/models/sheraton-grand-geographic.d.ts +24 -0
- package/lib/models/sheraton-grand-geographic.js +112 -0
- package/lib/models/six-north-michigan-geographic.d.ts +16 -0
- package/lib/models/six-north-michigan-geographic.js +180 -0
- package/lib/models/skyline-geography-data.d.ts +44 -0
- package/lib/models/skyline-geography-data.js +607 -0
- package/lib/models/skyline-geography.d.ts +23 -0
- package/lib/models/skyline-geography.js +322 -0
- package/lib/models/swissotel-geographic.d.ts +24 -0
- package/lib/models/swissotel-geographic.js +79 -0
- package/lib/models/three-illinois-center-geographic.d.ts +26 -0
- package/lib/models/three-illinois-center-geographic.js +86 -0
- package/lib/models/trump-geographic.d.ts +14 -0
- package/lib/models/trump-geographic.js +51 -0
- package/lib/models/trump-international-tower.d.ts +4 -0
- package/lib/models/trump-international-tower.js +84 -0
- package/lib/models/trump-tower.d.ts +43 -0
- package/lib/models/trump-tower.js +229 -0
- package/lib/models/two-illinois-center-geographic.d.ts +26 -0
- package/lib/models/two-illinois-center-geographic.js +85 -0
- package/lib/models/two-prudential-geographic.d.ts +25 -0
- package/lib/models/two-prudential-geographic.js +47 -0
- package/lib/models/two-prudential-plaza.d.ts +8 -0
- package/lib/models/two-prudential-plaza.js +55 -0
- package/lib/models/two-prudential-tower.d.ts +53 -0
- package/lib/models/two-prudential-tower.js +441 -0
- package/lib/models/university-club-geographic.d.ts +24 -0
- package/lib/models/university-club-geographic.js +217 -0
- package/lib/models/willoughby-tower-geographic.d.ts +17 -0
- package/lib/models/willoughby-tower-geographic.js +195 -0
- package/lib/models/window-illumination.d.ts +14 -0
- package/lib/models/window-illumination.js +64 -0
- package/lib/package-assets.d.ts +5 -0
- package/lib/package-assets.js +9 -0
- package/lib/skyline-package.d.ts +23 -0
- package/lib/skyline-package.js +25 -0
- package/lib/skyline-scene.d.ts +25 -0
- package/lib/skyline-scene.js +86 -0
- package/lib/study-types.d.ts +49 -0
- package/lib/study-types.js +1 -0
- package/lib/vendor/three-r186.d.ts +1 -0
- package/lib/vendor/three-r186.js +1 -0
- package/package.json +64 -4
- package/site/building-detail.html +54 -0
- package/site/building-detail.js +84 -0
- package/site/building-study.html +58 -0
- package/site/building-study.js +3 -0
- package/site/index.html +407 -0
- package/site/models/aon-center.js +60 -0
- package/site/models/aon-geographic.js +35 -0
- package/site/models/aon-tower.js +130 -0
- package/site/models/blue-cross-geographic.js +248 -0
- package/site/models/borg-warner-geographic.js +143 -0
- package/site/models/buckingham-geographic.js +121 -0
- package/site/models/building-kit.js +358 -0
- package/site/models/celebrations.js +44 -0
- package/site/models/chicago-athletic-association-geographic.js +173 -0
- package/site/models/crain-communications.js +75 -0
- package/site/models/crain-geographic.js +69 -0
- package/site/models/crain-reference.svg +10 -0
- package/site/models/crain-tower.js +378 -0
- package/site/models/facade-grid.js +176 -0
- package/site/models/gage-geographic.js +154 -0
- package/site/models/heritage-at-millennium-park.js +284 -0
- package/site/models/heritage-geographic.js +208 -0
- package/site/models/hyatt-west-tower-geographic.js +86 -0
- package/site/models/keith-ascher-geographic.js +110 -0
- package/site/models/kemper-geographic.js +153 -0
- package/site/models/kemper.js +78 -0
- package/site/models/lake-view-geographic.js +134 -0
- package/site/models/maclean-center-geographic.js +162 -0
- package/site/models/michigan-boulevard-geographic.js +157 -0
- package/site/models/michigan-plaza-south-geographic.js +84 -0
- package/site/models/michigan-plaza-south.js +68 -0
- package/site/models/millennium-park-plaza-geographic.js +103 -0
- package/site/models/monroe-geographic.js +177 -0
- package/site/models/north-michigan-180-geographic.js +117 -0
- package/site/models/north-wabash-geographic.js +122 -0
- package/site/models/on-the-park-geographic.js +245 -0
- package/site/models/one-prudential-geographic.js +55 -0
- package/site/models/one-prudential-plaza.js +72 -0
- package/site/models/one-prudential-tower.js +246 -0
- package/site/models/peoples-gas-geographic.js +250 -0
- package/site/models/railway-exchange-geographic.js +176 -0
- package/site/models/river-plaza-geographic.js +104 -0
- package/site/models/sheraton-grand-geographic.js +112 -0
- package/site/models/six-north-michigan-geographic.js +180 -0
- package/site/models/skyline-geography-data.js +607 -0
- package/site/models/skyline-geography.js +322 -0
- package/site/models/skyline-panorama.svg +3491 -0
- package/site/models/skyline-reference.svg +3090 -0
- package/site/models/swissotel-geographic.js +79 -0
- package/site/models/three-illinois-center-geographic.js +86 -0
- package/site/models/trump-geographic.js +51 -0
- package/site/models/trump-international-tower.js +84 -0
- package/site/models/trump-tower.js +229 -0
- package/site/models/two-illinois-center-geographic.js +85 -0
- package/site/models/two-prudential-geographic.js +47 -0
- package/site/models/two-prudential-plaza.js +55 -0
- package/site/models/two-prudential-tower.js +441 -0
- package/site/models/university-club-geographic.js +217 -0
- package/site/models/willoughby-tower-geographic.js +195 -0
- package/site/models/window-illumination.js +64 -0
- package/site/skyline-3d.html +92 -0
- package/site/skyline-3d.js +333 -0
- package/site/skyline-animated.svg +18795 -0
- package/site/skyline-comparison.js +97 -0
- package/site/skyline-original-fit.svg +18312 -0
- package/site/skyline-scene.js +86 -0
- package/site/skyline-study.html +83 -0
- package/site/skyline-study.js +109 -0
- package/site/skyline-webgl.html +667 -0
- package/site/stars.svg +176 -0
- package/site/study-loader.js +20 -0
- package/site/study-types.js +1 -0
- package/site/study-viewer.js +605 -0
- package/site/study.css +204 -0
- package/site/vendor/THREE-LICENSE.txt +21 -0
- package/site/vendor/three-r186.js +4228 -0
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
import * as THREE from "./vendor/three-r186.js";
|
|
2
|
+
export function createBuildingStudy({ models, defaultView = "quarter", views = {
|
|
3
|
+
front: { azimuth: Math.PI / 4, polar: Math.PI / 2 - 0.035, label: "front view" },
|
|
4
|
+
quarter: { azimuth: Math.PI / 4 + 0.22, polar: Math.PI / 2 - 0.15, label: "three-quarter view" },
|
|
5
|
+
side: { azimuth: Math.PI * 0.75, polar: Math.PI / 2 - 0.12, label: "side view" },
|
|
6
|
+
}, fov = 32, near = 1, far = 2000, clippingMargin = null, minimumDistanceRatio = 0.48, maximumDistanceRatio = 2, minimumZoom = 0.5, maximumZoom = 4, enablePan = false, target = [0, 85, 0], minimumCameraHeight = null, fit = { height: 230, width: 98 }, platform = { width: 76, depth: 76 }, lightPosition = [-110, 240, 170], shadowCamera = { left: -140, right: 140, top: 160, bottom: -160, near: 1, far: 600 }, extras = [], layouts = null, initialLayout = "original", onLayoutChange = () => { }, labels = [], turntable = false, }) {
|
|
7
|
+
const original = { models, extras, fit, target, platform, lightPosition, shadowCamera, clippingMargin };
|
|
8
|
+
let layout = "original";
|
|
9
|
+
const viewport = document.querySelector("#viewport");
|
|
10
|
+
const canvas = document.querySelector("#building");
|
|
11
|
+
const tooltip = document.querySelector("#tooltip");
|
|
12
|
+
// The studies name the active view in the viewport; the full-screen skyline leaves it to
|
|
13
|
+
// the pressed button.
|
|
14
|
+
const viewLabel = document.querySelector("#view-label");
|
|
15
|
+
const wireframeButton = document.querySelector("#wireframe");
|
|
16
|
+
const turntableButton = document.querySelector("#turntable");
|
|
17
|
+
const motionStatus = document.querySelector("#motion-status");
|
|
18
|
+
const reducedMotion = matchMedia("(prefers-reduced-motion: reduce)");
|
|
19
|
+
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
|
|
20
|
+
renderer.setPixelRatio(Math.min(devicePixelRatio || 1, 2));
|
|
21
|
+
renderer.shadowMap.enabled = true;
|
|
22
|
+
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
|
23
|
+
const scene = new THREE.Scene();
|
|
24
|
+
const labelLayer = document.createElement("div");
|
|
25
|
+
labelLayer.className = "study-annotations";
|
|
26
|
+
labelLayer.hidden = true;
|
|
27
|
+
labelLayer.setAttribute("aria-hidden", "true");
|
|
28
|
+
viewport.append(labelLayer);
|
|
29
|
+
let modelLabels = [];
|
|
30
|
+
let labelsPrepared = false;
|
|
31
|
+
function prepareLabels() {
|
|
32
|
+
labelLayer.replaceChildren();
|
|
33
|
+
// A label names a building only in the layouts that have it: the geographic layout
|
|
34
|
+
// maps buildings the drawing does not model.
|
|
35
|
+
modelLabels = labels.flatMap(({ id, text, placement = "above" }) => {
|
|
36
|
+
const model = models.find((entry) => entry.building.userData["buildingId"] === id);
|
|
37
|
+
if (!model)
|
|
38
|
+
return [];
|
|
39
|
+
const min = [Infinity, Infinity, Infinity], max = [-Infinity, -Infinity, -Infinity];
|
|
40
|
+
model.building.updateMatrixWorld(true);
|
|
41
|
+
model.building.traverse((child) => {
|
|
42
|
+
const mesh = child;
|
|
43
|
+
if (!mesh.isMesh)
|
|
44
|
+
return;
|
|
45
|
+
const positions = mesh.geometry.getAttribute("position"), point = new THREE.Vector3();
|
|
46
|
+
for (let i = 0; i < positions.count; i += 1) {
|
|
47
|
+
point.fromBufferAttribute(positions, i).applyMatrix4(mesh.matrixWorld);
|
|
48
|
+
min[0] = Math.min(min[0], point.x);
|
|
49
|
+
max[0] = Math.max(max[0], point.x);
|
|
50
|
+
min[1] = Math.min(min[1], point.y);
|
|
51
|
+
max[1] = Math.max(max[1], point.y);
|
|
52
|
+
min[2] = Math.min(min[2], point.z);
|
|
53
|
+
max[2] = Math.max(max[2], point.z);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
const element = document.createElement("span");
|
|
57
|
+
element.textContent = text;
|
|
58
|
+
element.dataset["placement"] = placement;
|
|
59
|
+
labelLayer.append(element);
|
|
60
|
+
return [{ element, position: new THREE.Vector3((min[0] + max[0]) / 2, max[1] + 5, (min[2] + max[2]) / 2) }];
|
|
61
|
+
});
|
|
62
|
+
labelsPrepared = true;
|
|
63
|
+
}
|
|
64
|
+
const perspectiveCamera = new THREE.PerspectiveCamera(fov, 1, near, far);
|
|
65
|
+
const planCamera = new THREE.OrthographicCamera(-500, 500, 500, -500, near, far);
|
|
66
|
+
let camera = perspectiveCamera;
|
|
67
|
+
const controls = new THREE.OrbitControls(camera, canvas);
|
|
68
|
+
controls.enablePan = enablePan;
|
|
69
|
+
controls.enableDamping = false;
|
|
70
|
+
controls.minPolarAngle = Math.PI * 0.12;
|
|
71
|
+
controls.maxPolarAngle = Math.PI * 0.52;
|
|
72
|
+
controls.target.fromArray(target);
|
|
73
|
+
controls.rotateSpeed = 0.65;
|
|
74
|
+
controls.zoomSpeed = 0.7;
|
|
75
|
+
scene.add(new THREE.AmbientLight(0xffffff, 0.7));
|
|
76
|
+
const keyLight = new THREE.DirectionalLight(0xffffff, 2.4);
|
|
77
|
+
keyLight.position.fromArray(lightPosition);
|
|
78
|
+
keyLight.target.position.set(0, 80, 0);
|
|
79
|
+
keyLight.castShadow = true;
|
|
80
|
+
keyLight.shadow.mapSize.set(2048, 2048);
|
|
81
|
+
Object.assign(keyLight.shadow.camera, shadowCamera);
|
|
82
|
+
keyLight.shadow.normalBias = 0.2;
|
|
83
|
+
scene.add(keyLight, keyLight.target);
|
|
84
|
+
const fillLight = new THREE.DirectionalLight(0xffffff, 0.55);
|
|
85
|
+
fillLight.position.set(100, 140, -140);
|
|
86
|
+
scene.add(fillLight);
|
|
87
|
+
let modelOwners = new Map(models.map((model) => [model.building, model]));
|
|
88
|
+
models.forEach((model) => scene.add(model.building));
|
|
89
|
+
extras.forEach((object) => scene.add(object));
|
|
90
|
+
const base = new THREE.Mesh(new THREE.BoxGeometry(platform.width, 2, platform.depth), new THREE.MeshToonMaterial({ color: platform.color ?? 0x3a3a3a }));
|
|
91
|
+
base.position.set(platform.x || 0, -1.1, platform.z || 0);
|
|
92
|
+
base.receiveShadow = true;
|
|
93
|
+
scene.add(base);
|
|
94
|
+
const baseEdges = new THREE.LineSegments(new THREE.EdgesGeometry(base.geometry), new THREE.LineBasicMaterial({ color: 0x555555 }));
|
|
95
|
+
baseEdges.position.copy(base.position);
|
|
96
|
+
scene.add(baseEdges);
|
|
97
|
+
const raycaster = new THREE.Raycaster();
|
|
98
|
+
const pointer = new THREE.Vector2();
|
|
99
|
+
let selectedModel = null;
|
|
100
|
+
let wireframe = false;
|
|
101
|
+
let turning = false;
|
|
102
|
+
let interacting = false;
|
|
103
|
+
let frameId = 0;
|
|
104
|
+
let lastTime = 0;
|
|
105
|
+
let fittedDistance = 400;
|
|
106
|
+
let activeView = defaultView;
|
|
107
|
+
let frameFit = fit;
|
|
108
|
+
let frameDistance = null;
|
|
109
|
+
let frameOffset = [0, 0];
|
|
110
|
+
let frameAlign = "center";
|
|
111
|
+
function requestRender() {
|
|
112
|
+
if (!frameId && !document.hidden)
|
|
113
|
+
frameId = requestAnimationFrame(render);
|
|
114
|
+
}
|
|
115
|
+
function render(time) {
|
|
116
|
+
frameId = 0;
|
|
117
|
+
if (turning && !document.hidden) {
|
|
118
|
+
const delta = lastTime ? Math.min((time - lastTime) / 1000, 0.1) : 0;
|
|
119
|
+
const offset = camera.position.clone().sub(controls.target);
|
|
120
|
+
offset.applyAxisAngle(new THREE.Vector3(0, 1, 0), delta * 0.16);
|
|
121
|
+
camera.position.copy(controls.target).add(offset);
|
|
122
|
+
controls.update();
|
|
123
|
+
}
|
|
124
|
+
lastTime = time;
|
|
125
|
+
renderer.render(scene, camera);
|
|
126
|
+
labelLayer.hidden = !camera.isOrthographicCamera;
|
|
127
|
+
const layerWidth = labelLayer.clientWidth, layerHeight = labelLayer.clientHeight;
|
|
128
|
+
for (const { element, position } of modelLabels) {
|
|
129
|
+
const point = position.clone().project(camera);
|
|
130
|
+
element.hidden = Math.abs(point.x) > 0.95 || Math.abs(point.y) > 0.95 || Math.abs(point.z) > 1;
|
|
131
|
+
// A label near the side slides along it rather than being cut by the layer's edge:
|
|
132
|
+
// centred labels keep half their width in from it, and one set to the right its
|
|
133
|
+
// whole width and offset. Near the top or bottom it slides the same way, by its
|
|
134
|
+
// height and offset above or below its point, or half its height beside it.
|
|
135
|
+
const x = (point.x + 1) / 2 * layerWidth, width = element.offsetWidth;
|
|
136
|
+
const [lo, hi] = element.dataset["placement"] === "right" ? [0, layerWidth - width - 14] : [width / 2, layerWidth - width / 2];
|
|
137
|
+
element.style.left = `${width && layerWidth ? Math.min(Math.max(x, lo), Math.max(lo, hi)) : x}px`;
|
|
138
|
+
const y = (1 - point.y) / 2 * layerHeight, height = element.offsetHeight, placement = element.dataset["placement"];
|
|
139
|
+
const [top, bottom] = placement === "right" ? [height / 2, layerHeight - height / 2]
|
|
140
|
+
: placement === "below" ? [-12, layerHeight - height - 12] : [height + 10, layerHeight + 10];
|
|
141
|
+
element.style.top = `${height && layerHeight ? Math.min(Math.max(y, top), Math.max(top, bottom)) : y}px`;
|
|
142
|
+
}
|
|
143
|
+
if (turning)
|
|
144
|
+
requestRender();
|
|
145
|
+
}
|
|
146
|
+
function clearHighlight() {
|
|
147
|
+
selectedModel?.setHighlighted(false);
|
|
148
|
+
selectedModel = null;
|
|
149
|
+
tooltip.hidden = true;
|
|
150
|
+
requestRender();
|
|
151
|
+
}
|
|
152
|
+
function markView(name) {
|
|
153
|
+
activeView = name;
|
|
154
|
+
if (viewLabel)
|
|
155
|
+
viewLabel.textContent = views[name]?.label || "orbit view";
|
|
156
|
+
document.querySelectorAll("[data-view]").forEach((button) => {
|
|
157
|
+
button.setAttribute("aria-pressed", String(button.dataset["view"] === name));
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
function setTurning(enabled) {
|
|
161
|
+
turning = enabled && !reducedMotion.matches;
|
|
162
|
+
lastTime = 0;
|
|
163
|
+
turntableButton.setAttribute("aria-pressed", String(turning));
|
|
164
|
+
turntableButton.textContent = turning ? "stop turntable" : "turntable";
|
|
165
|
+
if (turning) {
|
|
166
|
+
markView(null);
|
|
167
|
+
clearHighlight();
|
|
168
|
+
}
|
|
169
|
+
updateMotionStatus();
|
|
170
|
+
requestRender();
|
|
171
|
+
}
|
|
172
|
+
// Says how the camera moves: held still by reduced motion, circling on the turntable, or
|
|
173
|
+
// still until asked. The text changes only when that does, so it is not read out again.
|
|
174
|
+
function updateMotionStatus() {
|
|
175
|
+
const text = reducedMotion.matches
|
|
176
|
+
? "Reduced motion: turntable and drag movement paused. View buttons and keyboard controls change the view immediately."
|
|
177
|
+
: turning
|
|
178
|
+
? "Turntable on: the camera circles the model until you stop the turntable, choose a view, or drag."
|
|
179
|
+
: "Camera moves only when you interact or start the turntable.";
|
|
180
|
+
if (motionStatus.textContent !== text)
|
|
181
|
+
motionStatus.textContent = text;
|
|
182
|
+
}
|
|
183
|
+
function viewFor(name) {
|
|
184
|
+
return { ...views[name], ...layouts?.[layout]?.views?.[name] };
|
|
185
|
+
}
|
|
186
|
+
function positionView(name) {
|
|
187
|
+
const view = viewFor(name);
|
|
188
|
+
camera.position.set(fittedDistance * Math.sin(view.polar) * Math.sin(view.azimuth), fittedDistance * Math.cos(view.polar), fittedDistance * Math.sin(view.polar) * Math.cos(view.azimuth)).add(controls.target);
|
|
189
|
+
controls.update();
|
|
190
|
+
}
|
|
191
|
+
// The platform, and any extra marked as ground, such as mapped roads, take building
|
|
192
|
+
// shadows in every view but ground plan.
|
|
193
|
+
function setGroundShadows(enabled) {
|
|
194
|
+
base.receiveShadow = enabled;
|
|
195
|
+
extras.forEach((object) => object.traverse((child) => { if (child.userData["ground"])
|
|
196
|
+
child.receiveShadow = enabled; }));
|
|
197
|
+
}
|
|
198
|
+
function setView(name) {
|
|
199
|
+
setTurning(false);
|
|
200
|
+
if (!views[name])
|
|
201
|
+
return;
|
|
202
|
+
const view = viewFor(name);
|
|
203
|
+
camera = view.projection === "orthographic" ? planCamera : perspectiveCamera;
|
|
204
|
+
// Labels appear only in orthographic views. Their exact bounds scan every mesh
|
|
205
|
+
// vertex, so defer it until a view actually needs them and reuse it for this layout.
|
|
206
|
+
if (camera.isOrthographicCamera && !labelsPrepared)
|
|
207
|
+
prepareLabels();
|
|
208
|
+
controls.object = camera;
|
|
209
|
+
controls.enablePan = enablePan || !!camera.isOrthographicCamera;
|
|
210
|
+
controls.minPolarAngle = camera.isOrthographicCamera ? 0 : Math.PI * 0.12;
|
|
211
|
+
// A preset may look up further than free orbiting allows; OrbitControls
|
|
212
|
+
// would otherwise clamp it on its first update.
|
|
213
|
+
controls.maxPolarAngle = Math.max(Math.PI * 0.52, view.polar);
|
|
214
|
+
controls.minZoom = minimumZoom;
|
|
215
|
+
controls.maxZoom = maximumZoom;
|
|
216
|
+
camera.zoom = 1;
|
|
217
|
+
setGroundShadows(name !== "top");
|
|
218
|
+
frameFit = view.fit || fit;
|
|
219
|
+
frameDistance = view.distance ?? null;
|
|
220
|
+
frameOffset = view.offset ?? [0, 0];
|
|
221
|
+
frameAlign = view.align ?? "center";
|
|
222
|
+
controls.target.fromArray(view.target || target);
|
|
223
|
+
markView(name);
|
|
224
|
+
updateCameraHint();
|
|
225
|
+
resize();
|
|
226
|
+
clearHighlight();
|
|
227
|
+
}
|
|
228
|
+
function resize() {
|
|
229
|
+
const { width, height } = canvas.getBoundingClientRect();
|
|
230
|
+
// A frame with no size, such as the skyline viewer's 3D scene while another mode shows,
|
|
231
|
+
// has nothing to fit, and fitting to it would move an orbited eye.
|
|
232
|
+
if (!width || !height)
|
|
233
|
+
return;
|
|
234
|
+
const previousFit = fittedDistance;
|
|
235
|
+
const aspect = width / Math.max(height, 1);
|
|
236
|
+
camera.aspect = aspect;
|
|
237
|
+
renderer.setSize(width, height, false);
|
|
238
|
+
// Fit both the tower's height and its footprint at narrow mobile widths:
|
|
239
|
+
// by moving the eye, or at a fixed eye by widening the field of view.
|
|
240
|
+
if (frameDistance === null) {
|
|
241
|
+
const tangent = Math.tan(fov * Math.PI / 360);
|
|
242
|
+
perspectiveCamera.fov = fov;
|
|
243
|
+
fittedDistance = Math.max(frameFit.height / 2 / tangent, frameFit.width / 2 / (tangent * aspect));
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
perspectiveCamera.fov = Math.atan(Math.max(frameFit.height, frameFit.width / aspect) / 2 / frameDistance) * 360 / Math.PI;
|
|
247
|
+
fittedDistance = frameDistance;
|
|
248
|
+
}
|
|
249
|
+
if (camera.isOrthographicCamera) {
|
|
250
|
+
const plan = camera;
|
|
251
|
+
const height = Math.max(frameFit.height, frameFit.width / aspect);
|
|
252
|
+
plan.top = height / 2;
|
|
253
|
+
plan.bottom = -height / 2;
|
|
254
|
+
plan.left = -height * aspect / 2;
|
|
255
|
+
plan.right = height * aspect / 2;
|
|
256
|
+
controls.panSpeed = 1;
|
|
257
|
+
}
|
|
258
|
+
else
|
|
259
|
+
shiftLens(width, height, aspect);
|
|
260
|
+
controls.minDistance = fittedDistance * minimumDistanceRatio;
|
|
261
|
+
controls.maxDistance = fittedDistance * maximumDistanceRatio;
|
|
262
|
+
updateClipping();
|
|
263
|
+
camera.updateProjectionMatrix();
|
|
264
|
+
if (activeView)
|
|
265
|
+
positionView(activeView);
|
|
266
|
+
else {
|
|
267
|
+
camera.position.sub(controls.target).multiplyScalar(fittedDistance / previousFit).add(controls.target);
|
|
268
|
+
controls.update();
|
|
269
|
+
}
|
|
270
|
+
requestRender();
|
|
271
|
+
}
|
|
272
|
+
// Frames the view's fit box off the sightline, or on the viewport's bottom edge, by
|
|
273
|
+
// rendering the viewport as a window into a wider frustum about the same axis. Sizes are
|
|
274
|
+
// in the fit box's units at the fitted distance, where the window is the visible frame.
|
|
275
|
+
function shiftLens(width, height, aspect) {
|
|
276
|
+
const visible = Math.max(frameFit.height, frameFit.width / aspect);
|
|
277
|
+
const [x, y] = frameOffset;
|
|
278
|
+
const centre = frameAlign === "bottom" ? y + (visible - frameFit.height) / 2 : y;
|
|
279
|
+
if (!x && !centre) {
|
|
280
|
+
perspectiveCamera.clearViewOffset();
|
|
281
|
+
controls.panSpeed = 1;
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
const halfWidth = Math.abs(x) + visible * aspect / 2, halfHeight = Math.abs(centre) + visible / 2, pixel = visible / height;
|
|
285
|
+
perspectiveCamera.fov = Math.atan(halfHeight / fittedDistance) * 360 / Math.PI;
|
|
286
|
+
// OrbitControls pans by the field of view, now the wider frustum's; scale that back
|
|
287
|
+
// to the window so the scene follows the pointer.
|
|
288
|
+
controls.panSpeed = visible / 2 / halfHeight;
|
|
289
|
+
perspectiveCamera.setViewOffset(2 * halfWidth / pixel, 2 * halfHeight / pixel, (halfWidth + x - visible * aspect / 2) / pixel, (halfHeight - centre - visible / 2) / pixel, width, height);
|
|
290
|
+
}
|
|
291
|
+
function updateClipping() {
|
|
292
|
+
if (clippingMargin === null)
|
|
293
|
+
return;
|
|
294
|
+
// Follow the eye's distance from the scene, including after panning.
|
|
295
|
+
// A close near plane allows detail inspection; moving it out again at
|
|
296
|
+
// wider views preserves depth precision between thin facade layers.
|
|
297
|
+
const distance = Math.hypot(camera.position.x - target[0], camera.position.y - target[1], camera.position.z - target[2]);
|
|
298
|
+
const nextNear = Math.max(near, distance - clippingMargin);
|
|
299
|
+
const nextFar = Math.max(far, distance + clippingMargin);
|
|
300
|
+
if (camera.near === nextNear && camera.far === nextFar)
|
|
301
|
+
return;
|
|
302
|
+
camera.near = nextNear;
|
|
303
|
+
camera.far = nextFar;
|
|
304
|
+
camera.updateProjectionMatrix();
|
|
305
|
+
}
|
|
306
|
+
controls.addEventListener("change", () => {
|
|
307
|
+
// Long-lens scenes can reach below ground even at a shallow polar angle.
|
|
308
|
+
// Preserve zoom distance and azimuth while keeping the eye above the base.
|
|
309
|
+
if (minimumCameraHeight !== null && camera.position.y < minimumCameraHeight - 1e-6) {
|
|
310
|
+
const offset = camera.position.clone().sub(controls.target);
|
|
311
|
+
const distance = offset.length();
|
|
312
|
+
// Panning can place the target so far below grade that no allowed polar
|
|
313
|
+
// angle reaches the floor at this radius. Raise the target only enough
|
|
314
|
+
// to make the constraint feasible before asking OrbitControls to update.
|
|
315
|
+
// Otherwise its angle clamp undoes our correction and recurses forever.
|
|
316
|
+
const vertical = Math.min(minimumCameraHeight - controls.target.y, distance * Math.cos(controls.minPolarAngle));
|
|
317
|
+
controls.target.y = minimumCameraHeight - vertical;
|
|
318
|
+
const horizontal = Math.sqrt(Math.max(0, distance * distance - vertical * vertical));
|
|
319
|
+
const oldHorizontal = Math.hypot(offset.x, offset.z);
|
|
320
|
+
const direction = oldHorizontal > 1e-9 ? [offset.x / oldHorizontal, offset.z / oldHorizontal] : [0, 1];
|
|
321
|
+
camera.position.set(controls.target.x + direction[0] * horizontal, minimumCameraHeight, controls.target.z + direction[1] * horizontal);
|
|
322
|
+
controls.update();
|
|
323
|
+
}
|
|
324
|
+
updateClipping();
|
|
325
|
+
requestRender();
|
|
326
|
+
});
|
|
327
|
+
controls.addEventListener("start", () => {
|
|
328
|
+
interacting = true;
|
|
329
|
+
setTurning(false);
|
|
330
|
+
markView(null);
|
|
331
|
+
clearHighlight();
|
|
332
|
+
});
|
|
333
|
+
controls.addEventListener("end", () => { interacting = false; });
|
|
334
|
+
// The building at a point in the window, if any: the nearest surface there, so a nearer
|
|
335
|
+
// tower hides the one behind it.
|
|
336
|
+
function buildingAt(x, y) {
|
|
337
|
+
const bounds = canvas.getBoundingClientRect();
|
|
338
|
+
pointer.set((x - bounds.left) / bounds.width * 2 - 1, -(y - bounds.top) / bounds.height * 2 + 1);
|
|
339
|
+
raycaster.setFromCamera(pointer, camera);
|
|
340
|
+
// Intersect the full scene so the pedestal can occlude the tower base.
|
|
341
|
+
const hit = raycaster.intersectObjects(scene.children, true).find((intersection) => intersection.object.isMesh && intersection.object.visible);
|
|
342
|
+
let owner = hit?.object;
|
|
343
|
+
while (owner && !modelOwners.has(owner))
|
|
344
|
+
owner = owner.parent;
|
|
345
|
+
return modelOwners.get(owner) || null;
|
|
346
|
+
}
|
|
347
|
+
canvas.addEventListener("pointermove", (event) => {
|
|
348
|
+
if (interacting || turning || event.pointerType === "touch")
|
|
349
|
+
return;
|
|
350
|
+
const nextModel = buildingAt(event.clientX, event.clientY);
|
|
351
|
+
if (nextModel !== selectedModel) {
|
|
352
|
+
selectedModel?.setHighlighted(false);
|
|
353
|
+
selectedModel = nextModel;
|
|
354
|
+
selectedModel?.setHighlighted(true);
|
|
355
|
+
requestRender();
|
|
356
|
+
}
|
|
357
|
+
tooltip.hidden = !selectedModel;
|
|
358
|
+
if (selectedModel) {
|
|
359
|
+
const data = selectedModel.building.userData["geography"];
|
|
360
|
+
tooltip.textContent = data ? `${data.name} · ${data.height} m${data.tipHeight !== data.height ? ` / tip ${data.tipHeight} m` : ""}` : selectedModel.building.name;
|
|
361
|
+
const tooltipBounds = tooltip.getBoundingClientRect();
|
|
362
|
+
const x = event.clientX + 18 + tooltipBounds.width > innerWidth - 6 ? event.clientX - tooltipBounds.width - 18 : event.clientX + 18;
|
|
363
|
+
const y = event.clientY + 18 + tooltipBounds.height > innerHeight - 6 ? event.clientY - tooltipBounds.height - 18 : event.clientY + 18;
|
|
364
|
+
tooltip.style.left = `${Math.max(6, x)}px`;
|
|
365
|
+
tooltip.style.top = `${Math.max(6, y)}px`;
|
|
366
|
+
}
|
|
367
|
+
}, { passive: true });
|
|
368
|
+
canvas.addEventListener("pointerleave", clearHighlight);
|
|
369
|
+
canvas.addEventListener("blur", clearHighlight);
|
|
370
|
+
window.addEventListener("scroll", clearHighlight, { passive: true });
|
|
371
|
+
canvas.addEventListener("keydown", (event) => {
|
|
372
|
+
if (!["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "+", "=", "-", "Home"].includes(event.key))
|
|
373
|
+
return;
|
|
374
|
+
event.preventDefault();
|
|
375
|
+
if (event.key === "Home")
|
|
376
|
+
return setView(layouts?.[layout]?.defaultView || defaultView);
|
|
377
|
+
setTurning(false);
|
|
378
|
+
const offset = camera.position.clone().sub(controls.target);
|
|
379
|
+
const distance = offset.length();
|
|
380
|
+
let polar = Math.acos(offset.y / distance);
|
|
381
|
+
let azimuth = Math.atan2(offset.x, offset.z);
|
|
382
|
+
let nextDistance = distance;
|
|
383
|
+
if (event.key === "ArrowLeft")
|
|
384
|
+
azimuth -= Math.PI / 18;
|
|
385
|
+
if (event.key === "ArrowRight")
|
|
386
|
+
azimuth += Math.PI / 18;
|
|
387
|
+
if (event.key === "ArrowUp")
|
|
388
|
+
polar -= Math.PI / 36;
|
|
389
|
+
if (event.key === "ArrowDown")
|
|
390
|
+
polar += Math.PI / 36;
|
|
391
|
+
if (camera.isOrthographicCamera && ["+", "=", "-"].includes(event.key)) {
|
|
392
|
+
camera.zoom = Math.max(controls.minZoom, Math.min(controls.maxZoom, camera.zoom * (event.key === "-" ? 0.9 : 1 / 0.9)));
|
|
393
|
+
camera.updateProjectionMatrix();
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
if (event.key === "+" || event.key === "=")
|
|
397
|
+
nextDistance *= 0.9;
|
|
398
|
+
if (event.key === "-")
|
|
399
|
+
nextDistance /= 0.9;
|
|
400
|
+
}
|
|
401
|
+
polar = Math.max(controls.minPolarAngle, Math.min(controls.maxPolarAngle, polar));
|
|
402
|
+
nextDistance = Math.max(controls.minDistance, Math.min(controls.maxDistance, nextDistance));
|
|
403
|
+
camera.position.set(nextDistance * Math.sin(polar) * Math.sin(azimuth), nextDistance * Math.cos(polar), nextDistance * Math.sin(polar) * Math.cos(azimuth)).add(controls.target);
|
|
404
|
+
controls.update();
|
|
405
|
+
markView(null);
|
|
406
|
+
clearHighlight();
|
|
407
|
+
});
|
|
408
|
+
document.querySelectorAll("[data-view]").forEach((button) => button.addEventListener("click", () => setView(button.dataset["view"])));
|
|
409
|
+
document.querySelector("#reset").addEventListener("click", () => setView(layouts?.[layout]?.defaultView || defaultView));
|
|
410
|
+
wireframeButton.addEventListener("click", () => {
|
|
411
|
+
wireframe = !wireframe;
|
|
412
|
+
models.forEach((model) => model.setWireframe(wireframe));
|
|
413
|
+
wireframeButton.setAttribute("aria-pressed", String(wireframe));
|
|
414
|
+
requestRender();
|
|
415
|
+
});
|
|
416
|
+
turntableButton.addEventListener("click", () => setTurning(!turning));
|
|
417
|
+
function setLayout(name) {
|
|
418
|
+
if (name === layout || (name !== "original" && !layouts?.[name]))
|
|
419
|
+
return;
|
|
420
|
+
const previousView = activeView;
|
|
421
|
+
const keepPose = camera.isOrthographicCamera;
|
|
422
|
+
const pose = { position: camera.position.clone(), target: controls.target.clone(), zoom: camera.zoom, groundShadows: base.receiveShadow };
|
|
423
|
+
clearHighlight();
|
|
424
|
+
setTurning(false);
|
|
425
|
+
models.forEach((model) => scene.remove(model.building));
|
|
426
|
+
extras.forEach((object) => scene.remove(object));
|
|
427
|
+
const next = { ...original, ...layouts?.[name] };
|
|
428
|
+
layout = name;
|
|
429
|
+
({ models, fit, target, platform, lightPosition, shadowCamera, clippingMargin } = next);
|
|
430
|
+
extras = next.extras || [];
|
|
431
|
+
models.forEach((model) => { scene.add(model.building); model.setWireframe(wireframe); });
|
|
432
|
+
extras.forEach((object) => scene.add(object));
|
|
433
|
+
modelOwners = new Map(models.map((model) => [model.building, model]));
|
|
434
|
+
labelsPrepared = false;
|
|
435
|
+
base.geometry.dispose();
|
|
436
|
+
baseEdges.geometry.dispose();
|
|
437
|
+
base.geometry = new THREE.BoxGeometry(platform.width, 2, platform.depth);
|
|
438
|
+
baseEdges.geometry = new THREE.EdgesGeometry(base.geometry);
|
|
439
|
+
base.position.set(platform.x || 0, -1.1, platform.z || 0);
|
|
440
|
+
baseEdges.position.copy(base.position);
|
|
441
|
+
keyLight.position.fromArray(lightPosition);
|
|
442
|
+
Object.assign(keyLight.shadow.camera, shadowCamera);
|
|
443
|
+
keyLight.shadow.camera.updateProjectionMatrix();
|
|
444
|
+
const nextView = previousView && previousView !== defaultView ? previousView : next.defaultView || defaultView;
|
|
445
|
+
setView(keepPose ? previousView || "top" : nextView);
|
|
446
|
+
if (keepPose && camera.isOrthographicCamera) {
|
|
447
|
+
camera.position.copy(pose.position);
|
|
448
|
+
controls.target.copy(pose.target);
|
|
449
|
+
camera.zoom = pose.zoom;
|
|
450
|
+
setGroundShadows(pose.groundShadows);
|
|
451
|
+
camera.updateProjectionMatrix();
|
|
452
|
+
controls.update();
|
|
453
|
+
if (!previousView)
|
|
454
|
+
markView(null);
|
|
455
|
+
}
|
|
456
|
+
markLayout(name);
|
|
457
|
+
onLayoutChange(name);
|
|
458
|
+
requestRender();
|
|
459
|
+
}
|
|
460
|
+
function markLayout(name) {
|
|
461
|
+
document.querySelectorAll("[data-layout]").forEach((button) => button.setAttribute("aria-pressed", String(button.dataset["layout"] === name)));
|
|
462
|
+
}
|
|
463
|
+
document.querySelectorAll("[data-layout]").forEach((button) => button.addEventListener("click", () => setLayout(button.dataset["layout"])));
|
|
464
|
+
function updateCameraHint() {
|
|
465
|
+
document.querySelector("#camera-hint").innerHTML = reducedMotion.matches
|
|
466
|
+
? '<span class="wide-hint">Use the view buttons or arrow keys to inspect<br />+ / − zoom · Home resets</span><span class="narrow-hint">Use the view buttons to inspect</span>'
|
|
467
|
+
: controls.enablePan
|
|
468
|
+
? '<span class="wide-hint">Drag to orbit · shift-drag to pan · scroll to zoom<br />Arrow keys rotate · + / − zoom · Home resets</span><span class="narrow-hint">Drag to orbit · two fingers pan / zoom</span>'
|
|
469
|
+
: '<span class="wide-hint">Drag to orbit · scroll or pinch to zoom<br />Arrow keys rotate · + / − zoom · Home resets</span><span class="narrow-hint">Drag to orbit · pinch to zoom</span>';
|
|
470
|
+
}
|
|
471
|
+
function updateMotionPreference() {
|
|
472
|
+
controls.enabled = !reducedMotion.matches;
|
|
473
|
+
turntableButton.disabled = reducedMotion.matches;
|
|
474
|
+
if (reducedMotion.matches)
|
|
475
|
+
setTurning(false);
|
|
476
|
+
updateMotionStatus();
|
|
477
|
+
updateCameraHint();
|
|
478
|
+
clearHighlight();
|
|
479
|
+
}
|
|
480
|
+
reducedMotion.addEventListener("change", updateMotionPreference);
|
|
481
|
+
document.addEventListener("visibilitychange", () => {
|
|
482
|
+
lastTime = 0;
|
|
483
|
+
if (document.hidden) {
|
|
484
|
+
cancelAnimationFrame(frameId);
|
|
485
|
+
frameId = 0;
|
|
486
|
+
}
|
|
487
|
+
else
|
|
488
|
+
requestRender();
|
|
489
|
+
});
|
|
490
|
+
canvas.addEventListener("webglcontextlost", (event) => {
|
|
491
|
+
event.preventDefault();
|
|
492
|
+
setTurning(false);
|
|
493
|
+
const loading = document.querySelector("#loading");
|
|
494
|
+
loading.textContent = "The graphics context was interrupted. Reload to restore the 3D preview.";
|
|
495
|
+
loading.hidden = false;
|
|
496
|
+
if (window.parent !== window)
|
|
497
|
+
window.parent.postMessage({ type: "skyline:unavailable" }, location.origin);
|
|
498
|
+
});
|
|
499
|
+
new ResizeObserver(resize).observe(viewport);
|
|
500
|
+
updateMotionPreference();
|
|
501
|
+
// Open on the default view with its own frame, target, and eye distance, if it sets any.
|
|
502
|
+
setView(defaultView);
|
|
503
|
+
// Open on the page's layout, and bring the page's controls and text to it, whichever
|
|
504
|
+
// layout its markup starts in.
|
|
505
|
+
if (initialLayout !== layout)
|
|
506
|
+
setLayout(initialLayout);
|
|
507
|
+
else {
|
|
508
|
+
markLayout(layout);
|
|
509
|
+
onLayoutChange(layout);
|
|
510
|
+
}
|
|
511
|
+
if (turntable)
|
|
512
|
+
setTurning(true);
|
|
513
|
+
renderer.render(scene, camera);
|
|
514
|
+
document.querySelector("#loading").hidden = true;
|
|
515
|
+
window.__buildingStudy = {
|
|
516
|
+
ready: true,
|
|
517
|
+
get modelName() { return models[0].building.name; },
|
|
518
|
+
get modelNames() { return models.map((model) => model.building.name); },
|
|
519
|
+
get triangleCount() { return models.reduce((total, model) => total + model.triangleCount, 0); },
|
|
520
|
+
get illuminations() { return models.flatMap((model) => model.illumination ? [{ building: model.building.userData["buildingId"], active: model.illumination.active, litWindows: model.illumination.litWindows }] : []); },
|
|
521
|
+
get layout() { return layout; },
|
|
522
|
+
get projection() { return camera.isOrthographicCamera ? "orthographic" : "perspective"; },
|
|
523
|
+
get zoom() { return camera.zoom; },
|
|
524
|
+
get groundShadows() { return base.receiveShadow; },
|
|
525
|
+
get activeView() { return activeView; },
|
|
526
|
+
get selectedBuilding() { return selectedModel?.building.userData["buildingId"] || null; },
|
|
527
|
+
get shadowBounds() {
|
|
528
|
+
// Actual rendered geometry in the shadow camera's clip space, for coverage checks.
|
|
529
|
+
const min = [Infinity, Infinity, Infinity], max = [-Infinity, -Infinity, -Infinity];
|
|
530
|
+
const point = new THREE.Vector3();
|
|
531
|
+
for (const object of [...models.map((model) => model.building), base]) {
|
|
532
|
+
object.traverse((child) => {
|
|
533
|
+
const mesh = child;
|
|
534
|
+
if (!mesh.isMesh)
|
|
535
|
+
return;
|
|
536
|
+
const positions = mesh.geometry.getAttribute("position");
|
|
537
|
+
for (let i = 0; i < positions.count; i += 1) {
|
|
538
|
+
point.fromBufferAttribute(positions, i).applyMatrix4(mesh.matrixWorld).project(keyLight.shadow.camera);
|
|
539
|
+
point.toArray().forEach((value, axis) => { min[axis] = Math.min(min[axis], value); max[axis] = Math.max(max[axis], value); });
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
return { min, max };
|
|
544
|
+
},
|
|
545
|
+
projectPoint(buildingId, coordinates) {
|
|
546
|
+
const model = models.find((entry) => entry.building.userData["buildingId"] === buildingId);
|
|
547
|
+
const point = model.building.localToWorld(new THREE.Vector3(...coordinates)).project(camera);
|
|
548
|
+
return [(point.x + 1) / 2, (1 - point.y) / 2];
|
|
549
|
+
},
|
|
550
|
+
screenBounds(buildingId) {
|
|
551
|
+
// Every vertex, since geographic models carry their placement in their geometry.
|
|
552
|
+
const model = models.find((entry) => entry.building.userData["buildingId"] === buildingId);
|
|
553
|
+
const bounds = [Infinity, Infinity, -Infinity, -Infinity];
|
|
554
|
+
const point = new THREE.Vector3();
|
|
555
|
+
camera.updateMatrixWorld();
|
|
556
|
+
model.building.updateMatrixWorld(true);
|
|
557
|
+
model.building.traverse((child) => {
|
|
558
|
+
const mesh = child;
|
|
559
|
+
if (!mesh.isMesh)
|
|
560
|
+
return;
|
|
561
|
+
const positions = mesh.geometry.getAttribute("position");
|
|
562
|
+
for (let i = 0; i < positions.count; i += 1) {
|
|
563
|
+
point.fromBufferAttribute(positions, i).applyMatrix4(mesh.matrixWorld).project(camera);
|
|
564
|
+
const u = (point.x + 1) / 2, v = (1 - point.y) / 2;
|
|
565
|
+
bounds[0] = Math.min(bounds[0], u);
|
|
566
|
+
bounds[1] = Math.min(bounds[1], v);
|
|
567
|
+
bounds[2] = Math.max(bounds[2], u);
|
|
568
|
+
bounds[3] = Math.max(bounds[3], v);
|
|
569
|
+
}
|
|
570
|
+
});
|
|
571
|
+
return bounds;
|
|
572
|
+
},
|
|
573
|
+
get cameraPosition() { return camera.position.toArray(); },
|
|
574
|
+
get highlighted() { return Boolean(selectedModel); },
|
|
575
|
+
get turning() { return turning; },
|
|
576
|
+
get renderCount() { return renderer.info.render.frame; },
|
|
577
|
+
get modelBounds() {
|
|
578
|
+
return models.map((model) => {
|
|
579
|
+
const min = [Infinity, Infinity, Infinity], max = [-Infinity, -Infinity, -Infinity];
|
|
580
|
+
model.building.updateMatrixWorld(true);
|
|
581
|
+
model.building.traverse((child) => {
|
|
582
|
+
const mesh = child;
|
|
583
|
+
if (!mesh.isMesh)
|
|
584
|
+
return;
|
|
585
|
+
const positions = mesh.geometry.getAttribute("position"), point = new THREE.Vector3();
|
|
586
|
+
for (let i = 0; i < positions.count; i += 1) {
|
|
587
|
+
point.fromBufferAttribute(positions, i).applyMatrix4(mesh.matrixWorld).toArray().forEach((n, axis) => {
|
|
588
|
+
min[axis] = Math.min(min[axis], n);
|
|
589
|
+
max[axis] = Math.max(max[axis], n);
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
});
|
|
593
|
+
return { id: model.building.userData["buildingId"], min, max };
|
|
594
|
+
});
|
|
595
|
+
},
|
|
596
|
+
get platformBounds() {
|
|
597
|
+
base.geometry.computeBoundingBox();
|
|
598
|
+
const { min, max } = base.geometry.boundingBox;
|
|
599
|
+
return { min: min.clone().add(base.position).toArray(), max: max.clone().add(base.position).toArray() };
|
|
600
|
+
},
|
|
601
|
+
};
|
|
602
|
+
if (window.parent !== window)
|
|
603
|
+
window.parent.postMessage({ type: "skyline:ready" }, location.origin);
|
|
604
|
+
return { setLayout, setView, requestRender, buildingAt };
|
|
605
|
+
}
|