@a2f0/skyline 0.0.0-stage → 0.1.2
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 +224 -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,84 @@
|
|
|
1
|
+
import { createBuilder } from "./building-kit.js";
|
|
2
|
+
// Michigan Plaza South at 205 North Michigan Avenue: the geographic layout's
|
|
3
|
+
// detailed model. The mapped ground outline stays exact; the 168.6 m top is
|
|
4
|
+
// the published architectural height. The dark curtain-wall grid, recessed
|
|
5
|
+
// panes with scattered lit units, and pale mullions reuse the fitted model's
|
|
6
|
+
// vocabulary at meter scale. Window-row and bay spacings are estimates; see
|
|
7
|
+
// docs/michigan-plaza-south-geographic-reference.md. Units are meters; +x is
|
|
8
|
+
// east, +z is south.
|
|
9
|
+
export const michiganPlazaSouthGeographicLevels = Object.freeze({
|
|
10
|
+
roof: 168.6, // published architectural height
|
|
11
|
+
floors: 44,
|
|
12
|
+
});
|
|
13
|
+
const h = michiganPlazaSouthGeographicLevels;
|
|
14
|
+
const shaftTop = h.roof - 0.8;
|
|
15
|
+
const pitch = shaftTop / h.floors;
|
|
16
|
+
export function createMichiganPlazaSouthGeographicBuilding(record, projectPlan, offset) {
|
|
17
|
+
const kit = createBuilder(record.name, record.id);
|
|
18
|
+
// The neutral palette of the fitted model: a mid-gray frame grid over dark
|
|
19
|
+
// glazing with scattered lit panes.
|
|
20
|
+
const shell = kit.batch("Michigan Plaza South · mapped tower shell", kit.material(0x555555));
|
|
21
|
+
const mullions = kit.batch("Michigan Plaza South · mullions and bands", kit.material(0x7a7a7a));
|
|
22
|
+
const lit = kit.batch("Michigan Plaza South · lit glazing", kit.material(0x5a5a5a));
|
|
23
|
+
const dim = kit.batch("Michigan Plaza South · dim glazing", kit.material(0x464646));
|
|
24
|
+
const tones = [0x2a2a2a, 0x2e2e2e, 0x333333, 0x262626].map((color, i) => kit.batch(`Michigan Plaza South · glazing ${i + 1}`, kit.material(color)));
|
|
25
|
+
const paneTone = (row, bay, side) => {
|
|
26
|
+
const hash = (Math.imul(row + 5, 0x9e3779b1) ^ Math.imul(bay + 17, 0x85ebca77) ^ Math.imul(side + 3, 0xc2b2ae3d)) >>> 0;
|
|
27
|
+
const value = hash % 97;
|
|
28
|
+
if (value < 3)
|
|
29
|
+
return lit;
|
|
30
|
+
if (value < 6)
|
|
31
|
+
return dim;
|
|
32
|
+
return tones[value % tones.length];
|
|
33
|
+
};
|
|
34
|
+
const ground = projectPlan(record.footprint.coordinates);
|
|
35
|
+
// The mapped volume: the outline itself is the tower, with a roof parapet.
|
|
36
|
+
kit.prism(shell, ground, [0, shaftTop]);
|
|
37
|
+
// A closed shallow solid on a run. Offsets separate the visible surfaces from
|
|
38
|
+
// their backing wall, and the ends stop short of polygon corners so adjacent
|
|
39
|
+
// walls' projecting returns cannot overlap at the same elevation.
|
|
40
|
+
const strip = (batch, run, from, to, y0, y1, back, front) => {
|
|
41
|
+
const clearance = Math.max(Math.abs(back), Math.abs(front)) + 0.025;
|
|
42
|
+
from = Math.max(from, clearance);
|
|
43
|
+
to = Math.min(to, run.length - clearance);
|
|
44
|
+
if (to - from < 0.03 || y1 <= y0)
|
|
45
|
+
return;
|
|
46
|
+
kit.box(batch, run.at((from + to) / 2), run.normal(0), (to - from) / 2, back, front, y0, y1);
|
|
47
|
+
};
|
|
48
|
+
// The dense Miesian grid: one pane per bay per row between full-height
|
|
49
|
+
// mullions, with a pale base course at grade. Mullions stand proudest.
|
|
50
|
+
ground.forEach((run, side) => {
|
|
51
|
+
if (run.length < 2)
|
|
52
|
+
return;
|
|
53
|
+
const bays = Math.max(1, Math.round(run.length / 3.1));
|
|
54
|
+
const width = run.length / bays;
|
|
55
|
+
for (let row = 1; row <= h.floors; row += 1) {
|
|
56
|
+
const bottom = Math.max((row - 1) * pitch, 0.85), top = row * pitch;
|
|
57
|
+
for (let bay = 0; bay < bays; bay += 1) {
|
|
58
|
+
strip(paneTone(row, bay, side), run, bay * width + 0.18, (bay + 1) * width - 0.18, bottom + 0.28, top - 0.24, 0.03, 0.07);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
for (let i = 0; i <= bays; i += 1) {
|
|
62
|
+
const s = Math.max(0.18, Math.min(run.length - 0.18, i * width));
|
|
63
|
+
strip(mullions, run, s - 0.06, s + 0.06, 0.25, shaftTop - 0.05, 0.05, 0.16);
|
|
64
|
+
}
|
|
65
|
+
strip(mullions, run, 0.015, run.length - 0.015, 0.3, 0.75, 0.06, 0.18);
|
|
66
|
+
});
|
|
67
|
+
// The roof parapet wraps the mapped outline at the published 168.6 m top.
|
|
68
|
+
// The outline's notch has concave corners, which a closed band cannot turn,
|
|
69
|
+
// so the parapet is per-segment boxes that stop short of every corner.
|
|
70
|
+
ground.forEach((run) => {
|
|
71
|
+
if (run.length < 2)
|
|
72
|
+
return;
|
|
73
|
+
const count = Math.max(1, Math.ceil(run.length / 1.5));
|
|
74
|
+
for (let index = 0; index < count; index += 1) {
|
|
75
|
+
const from = index * run.length / count, to = (index + 1) * run.length / count;
|
|
76
|
+
strip(mullions, run, from, to, shaftTop, h.roof, 0.05, 0.35);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
const model = kit.finish({ height: h.roof, outlines: [shell, mullions], opacity: 0.16 });
|
|
80
|
+
model.building.position.set(offset[0], 0, offset[1]);
|
|
81
|
+
model.building.userData["geography"] = record;
|
|
82
|
+
model.building.userData["geographicDetail"] = { levels: h, floors: 44, source: "docs/michigan-plaza-south-geographic-reference.md" };
|
|
83
|
+
return model;
|
|
84
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import * as THREE from "../vendor/three-r186.js";
|
|
2
|
+
// The roof outline and visible window spacing follow the source SVG. The
|
|
3
|
+
// footprint, hidden faces, and lower floors obscured in the drawing are inferred.
|
|
4
|
+
export function createMichiganPlazaSouthBuilding() {
|
|
5
|
+
const building = new THREE.Group();
|
|
6
|
+
building.name = "Michigan Plaza South";
|
|
7
|
+
building.userData["buildingId"] = "building-michigan-plaza-south-tower";
|
|
8
|
+
const width = 46.7, depth = 46.7, height = 180;
|
|
9
|
+
const gradient = new THREE.DataTexture(new Uint8Array([90, 145, 205, 255]), 4, 1, THREE.RedFormat);
|
|
10
|
+
gradient.minFilter = gradient.magFilter = THREE.NearestFilter;
|
|
11
|
+
gradient.needsUpdate = true;
|
|
12
|
+
const material = (color) => new THREE.MeshToonMaterial({ color, gradientMap: gradient });
|
|
13
|
+
const frame = material(0x555555);
|
|
14
|
+
const glass = material(0x323232);
|
|
15
|
+
const recess = material(0x252525);
|
|
16
|
+
const materials = [frame, glass, recess];
|
|
17
|
+
let triangleCount = 0;
|
|
18
|
+
function box(name, size, position, surface) {
|
|
19
|
+
const mesh = new THREE.Mesh(new THREE.BoxGeometry(...size), surface);
|
|
20
|
+
mesh.name = name;
|
|
21
|
+
mesh.position.set(...position);
|
|
22
|
+
mesh.castShadow = mesh.receiveShadow = true;
|
|
23
|
+
building.add(mesh);
|
|
24
|
+
triangleCount += 12;
|
|
25
|
+
}
|
|
26
|
+
box("closed tower shell", [width, height, depth], [0, height / 2, 0], frame);
|
|
27
|
+
box("inset flat roof", [width - 1, 0.15, depth - 1], [0, height + 0.025, 0], recess);
|
|
28
|
+
const windows = [], reveals = [];
|
|
29
|
+
const quad = (positions, a, b, c, d) => {
|
|
30
|
+
for (const point of [a, b, c, a, c, d])
|
|
31
|
+
positions.push(...point);
|
|
32
|
+
};
|
|
33
|
+
const corners = [[-width / 2, depth / 2], [width / 2, depth / 2], [width / 2, -depth / 2], [-width / 2, -depth / 2]];
|
|
34
|
+
for (let face = 0; face < 4; face += 1) {
|
|
35
|
+
const a = corners[face], b = corners[(face + 1) % 4];
|
|
36
|
+
const length = Math.hypot(b[0] - a[0], b[1] - a[1]);
|
|
37
|
+
const normal = [(a[1] - b[1]) / length, (b[0] - a[0]) / length];
|
|
38
|
+
const point = (t, y, offset) => [a[0] + (b[0] - a[0]) * t + normal[0] * offset, y, a[1] + (b[1] - a[1]) * t + normal[1] * offset];
|
|
39
|
+
// Match the 24- and 23-column grids drawn on the two visible SVG faces.
|
|
40
|
+
const columns = face % 2 === 0 ? 24 : 23;
|
|
41
|
+
for (let row = 0; row < 40; row += 1) {
|
|
42
|
+
const top = height - 13 - row * 4.1;
|
|
43
|
+
const bottom = top - 2.7;
|
|
44
|
+
for (let column = 0; column < columns; column += 1) {
|
|
45
|
+
const left = (column + 0.22) / columns, right = (column + 0.88) / columns;
|
|
46
|
+
// A dark surround and offset pane separate each opening without
|
|
47
|
+
// allocating thousands of individual meshes or external textures.
|
|
48
|
+
quad(reveals, point(left - 0.002, bottom - 0.08, 0.025), point(right + 0.002, bottom - 0.08, 0.025), point(right + 0.002, top + 0.08, 0.025), point(left - 0.002, top + 0.08, 0.025));
|
|
49
|
+
quad(windows, point(left, bottom, 0.055), point(right, bottom, 0.055), point(right, top, 0.055), point(left, top, 0.055));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
for (const [name, positions, surface] of [["window surrounds", reveals, recess], ["window grid", windows, glass]]) {
|
|
54
|
+
const geometry = new THREE.BufferGeometry();
|
|
55
|
+
geometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3));
|
|
56
|
+
geometry.computeVertexNormals();
|
|
57
|
+
const mesh = new THREE.Mesh(geometry, surface);
|
|
58
|
+
mesh.name = name;
|
|
59
|
+
mesh.castShadow = mesh.receiveShadow = true;
|
|
60
|
+
building.add(mesh);
|
|
61
|
+
triangleCount += positions.length / 9;
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
building, height, triangleCount,
|
|
65
|
+
setHighlighted(highlighted) { materials.forEach((surface) => surface.emissive.setHex(highlighted ? 0x222222 : 0)); },
|
|
66
|
+
setWireframe(enabled) { materials.forEach((surface) => { surface.wireframe = enabled; }); },
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { BuildingModel, Plan } from "./building-kit.js";
|
|
2
|
+
import type { GeoBuilding } from "./skyline-geography-data.js";
|
|
3
|
+
export declare const millenniumParkPlazaLevels: Readonly<{
|
|
4
|
+
lobby: 4.4;
|
|
5
|
+
office: 3.7;
|
|
6
|
+
apartment: 2.8;
|
|
7
|
+
top: 121.9;
|
|
8
|
+
}>;
|
|
9
|
+
export declare function createMillenniumParkPlazaGeographicBuilding(record: GeoBuilding, projectPlan: (coordinates: [number, number][]) => Plan, offset: [number, number]): BuildingModel;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import * as THREE from "../vendor/three-r186.js";
|
|
2
|
+
import { createBuilder, polygonOf } from "./building-kit.js";
|
|
3
|
+
import { chainSkin, chainsOf, orient, paintedSlab, planOf, turnsAt } from "./facade-grid.js";
|
|
4
|
+
// Millennium Park Plaza, 151–155 North Michigan Avenue (Reinheimer & Associates, 1982, as
|
|
5
|
+
// Doral Plaza): the geographic layout's model, on the mapped outline 127107026. The drawing
|
|
6
|
+
// shows it in front of Michigan Plaza, where the geographic camera names it; the original
|
|
7
|
+
// layout has no model of it.
|
|
8
|
+
//
|
|
9
|
+
// A 90 m concrete slab along Michigan Avenue, its south end chamfered at both corners:
|
|
10
|
+
// offices on the first seven floors and apartments above. The narrow ends are solid wall
|
|
11
|
+
// but for four slender window strips, each topped by a small window on the top floor; the
|
|
12
|
+
// long faces carry punched windows between piers. Floor levels are estimated from the
|
|
13
|
+
// published 40 storeys and 121.9 m; see docs/millennium-park-plaza-reference.md. Units are
|
|
14
|
+
// meters; +x is east, +z is south.
|
|
15
|
+
export const millenniumParkPlazaLevels = Object.freeze({
|
|
16
|
+
lobby: 4.4, // the second floor, over the lobby
|
|
17
|
+
office: 3.7, // floors 2 to 8
|
|
18
|
+
apartment: 2.8, // floors 8 to 41, the roof's
|
|
19
|
+
top: 121.9, // the parapet's top, the published architectural height
|
|
20
|
+
});
|
|
21
|
+
const h = millenniumParkPlazaLevels;
|
|
22
|
+
// Each floor's level, from the second to a 41st at the top floor's ceiling.
|
|
23
|
+
const floor = (n) => (n <= 8 ? h.lobby + (n - 2) * h.office : h.lobby + 6 * h.office + (n - 8) * h.apartment);
|
|
24
|
+
const color = (hex) => new THREE.Color(hex);
|
|
25
|
+
const glassTones = [0x2e2e2e, 0x323232, 0x363636, 0x3a3a3a].map(color);
|
|
26
|
+
const litGlass = color(0x727272), dimGlass = color(0x484848);
|
|
27
|
+
const concrete = color(0x666666), strip = color(0x3e3e3e), lobby = color(0x3a3a3a), core = color(0x262626), roofing = color(0x3e3e3e);
|
|
28
|
+
function paneColor(row, bay, wall) {
|
|
29
|
+
const hash = (Math.imul(row + 23, 0x9e3779b1) ^ Math.imul(bay + 47, 0x85ebca77) ^ Math.imul(wall + 37, 0xc2b2ae3d)) >>> 0;
|
|
30
|
+
const value = hash % 101;
|
|
31
|
+
if (value < 4)
|
|
32
|
+
return litGlass;
|
|
33
|
+
if (value < 8)
|
|
34
|
+
return dimGlass;
|
|
35
|
+
return glassTones[value % glassTones.length];
|
|
36
|
+
}
|
|
37
|
+
const rows = [{ lo: 0.25, hi: floor(2) - 0.3, kind: "lobby", floor: 1 }];
|
|
38
|
+
for (let n = 2; n <= 40; n += 1) {
|
|
39
|
+
rows.push({ lo: floor(n) - 0.3, hi: floor(n) + 0.6, kind: "spandrel", floor: n });
|
|
40
|
+
rows.push({ lo: floor(n) + 0.6, hi: floor(n + 1) - 0.3, kind: "glass", floor: n });
|
|
41
|
+
}
|
|
42
|
+
rows.push({ lo: floor(41) - 0.3, hi: h.top, kind: "spandrel", floor: 41 });
|
|
43
|
+
const heights = [rows[0].lo, ...rows.map((row) => row.hi)];
|
|
44
|
+
export function createMillenniumParkPlazaGeographicBuilding(record, projectPlan, offset) {
|
|
45
|
+
const kit = createBuilder(record.name, record.id);
|
|
46
|
+
const shell = kit.batch("Millennium Park Plaza · shell", kit.material(0xffffff, { vertexColors: true }));
|
|
47
|
+
const wall = kit.batch("Millennium Park Plaza · walls and windows", kit.material(0xffffff, { vertexColors: true }));
|
|
48
|
+
const corners = orient(polygonOf(projectPlan(record.footprint.coordinates)));
|
|
49
|
+
// The slab, its walls the shell's dark core.
|
|
50
|
+
planOf(corners).forEach((run) => {
|
|
51
|
+
const a = run.at(0), b = run.at(run.length), n = run.normal(0);
|
|
52
|
+
kit.quad(shell, [[a[0], 0, a[1]], [b[0], 0, b[1]], [b[0], h.top, b[1]], [a[0], h.top, a[1]]], [[n[0], 0, n[1]]], core);
|
|
53
|
+
});
|
|
54
|
+
paintedSlab(kit, shell, corners, 0, false, roofing);
|
|
55
|
+
paintedSlab(kit, shell, corners, h.top, true, roofing);
|
|
56
|
+
// Every wall's skin, one box a chain, held 2 cm clear of its ends. A narrow end carries
|
|
57
|
+
// four window strips, 1.25 m wide and symmetrical about its middle, from the eighth floor,
|
|
58
|
+
// the first of apartments, with the top floor's window standing apart over each; below,
|
|
59
|
+
// the offices' windows run across it. A long face has a punched window every 3 m between
|
|
60
|
+
// 0.8 m piers. The chamfers are solid.
|
|
61
|
+
const plan = planOf(corners);
|
|
62
|
+
chainsOf(plan).forEach((chain, index) => {
|
|
63
|
+
const first = chain.runs[0], last = chain.runs.at(-1);
|
|
64
|
+
const s0 = turnsAt(plan, first)[0] > 0 ? 0.09 : 0.02, s1 = chain.length - (turnsAt(plan, last)[1] > 0 ? 0.09 : 0.02);
|
|
65
|
+
const end = chain.length > 12 && chain.length < 30, long = chain.length >= 30;
|
|
66
|
+
let columns, isWindow;
|
|
67
|
+
if (end) {
|
|
68
|
+
const middle = chain.length / 2, centres = [-4.1, -1.6, 1.6, 4.1].map((d) => middle + d);
|
|
69
|
+
columns = [0, ...centres.flatMap((c) => [c - 0.625, c + 0.625]), chain.length];
|
|
70
|
+
isWindow = (bay) => bay % 2 === 0 && bay >= 2 && bay <= 8;
|
|
71
|
+
}
|
|
72
|
+
else if (long) {
|
|
73
|
+
const bays = Math.round(chain.length / 3);
|
|
74
|
+
columns = [0, ...Array.from({ length: bays }, (_, i) => [chain.length * i / bays + 0.4, chain.length * (i + 1) / bays - 0.4]).flat(), chain.length];
|
|
75
|
+
isWindow = (bay) => bay % 2 === 0 && bay >= 2 && bay <= 2 * bays;
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
columns = [0, chain.length];
|
|
79
|
+
isWindow = () => false;
|
|
80
|
+
}
|
|
81
|
+
chainSkin(kit, wall, plan, chain, 0, chain.runs.length - 1, s0, s1, columns, heights, 0.02, 0.07, (bay, r) => {
|
|
82
|
+
const row = rows[r];
|
|
83
|
+
if (row.kind === "lobby")
|
|
84
|
+
return lobby;
|
|
85
|
+
const office = row.floor < 8;
|
|
86
|
+
if (end && !office && isWindow(bay)) {
|
|
87
|
+
if (row.kind === "glass")
|
|
88
|
+
return paneColor(row.floor, bay, index);
|
|
89
|
+
return row.floor >= 40 ? concrete : strip;
|
|
90
|
+
}
|
|
91
|
+
if (row.kind === "spandrel")
|
|
92
|
+
return concrete;
|
|
93
|
+
if (end)
|
|
94
|
+
return office ? paneColor(row.floor, bay, index) : concrete;
|
|
95
|
+
return isWindow(bay) ? paneColor(row.floor, bay, index) : concrete;
|
|
96
|
+
}, concrete);
|
|
97
|
+
});
|
|
98
|
+
const model = kit.finish({ height: h.top, outlines: [shell], opacity: 0.16 });
|
|
99
|
+
model.building.position.set(offset[0], 0, offset[1]);
|
|
100
|
+
model.building.userData["geography"] = record;
|
|
101
|
+
model.building.userData["geographicDetail"] = { levels: h, source: "docs/millennium-park-plaza-reference.md" };
|
|
102
|
+
return model;
|
|
103
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { BuildingModel, Plan } from "./building-kit.js";
|
|
2
|
+
import type { GeoBuilding } from "./skyline-geography-data.js";
|
|
3
|
+
export declare const monroeLevels: Readonly<{
|
|
4
|
+
granite: number;
|
|
5
|
+
belt: number;
|
|
6
|
+
beltTop: number;
|
|
7
|
+
cornice: number;
|
|
8
|
+
eaves: number;
|
|
9
|
+
ridge: 69;
|
|
10
|
+
}>;
|
|
11
|
+
export declare const monroePalette: Readonly<{
|
|
12
|
+
glass: number[];
|
|
13
|
+
lit: 9605778;
|
|
14
|
+
dim: 4737096;
|
|
15
|
+
terracotta: 11711154;
|
|
16
|
+
granite: 10132122;
|
|
17
|
+
band: 12763842;
|
|
18
|
+
}>;
|
|
19
|
+
export declare function createMonroeGeographicBuilding(record: GeoBuilding, projectPlan: (coordinates: [number, number][]) => Plan, offset: [number, number]): BuildingModel;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import * as THREE from "../vendor/three-r186.js";
|
|
2
|
+
import { createBuilder, polygonOf } from "./building-kit.js";
|
|
3
|
+
import { chainSkin, chainsOf, orient, paintedSlab, planOf, turnsAt } from "./facade-grid.js";
|
|
4
|
+
// The Monroe Building, 104 South Michigan Avenue (Holabird & Roche, 1912): the geographic
|
|
5
|
+
// layout's model, on the mapped outline 145498713. The original layout has no model of it;
|
|
6
|
+
// the drawing shows it left of the University Club, across Monroe Street.
|
|
7
|
+
//
|
|
8
|
+
// Twelve storeys of terracotta over two of granite, in bays of paired windows, with a belt
|
|
9
|
+
// course over the twelfth floor and a cornice under a steep gable roof that holds two more
|
|
10
|
+
// floors, sixteen in all, its gable facing Michigan with small arched windows. The ridge is
|
|
11
|
+
// the Skyscraper Center's 69 m. The drawn ridge reads 67.7 m, so heights read on the drawing
|
|
12
|
+
// are scaled to meet it; see docs/monroe-reference.md. Units are meters; +x is east, +z is
|
|
13
|
+
// south.
|
|
14
|
+
const drawn = (height) => height * 69 / 67.7;
|
|
15
|
+
export const monroeLevels = Object.freeze({
|
|
16
|
+
granite: drawn(14.5), // the two granite storeys, under the third floor
|
|
17
|
+
belt: drawn(49.9), // the belt course over the twelfth floor
|
|
18
|
+
beltTop: drawn(51.1),
|
|
19
|
+
cornice: drawn(56.9), // the cornice under the roof
|
|
20
|
+
eaves: drawn(58.2), // the roof's foot and the gable's
|
|
21
|
+
ridge: 69, // the ridge, the published height
|
|
22
|
+
});
|
|
23
|
+
const h = monroeLevels;
|
|
24
|
+
// The skins' colours: the window tones, and the terracotta, granite and bands, which no
|
|
25
|
+
// window shares.
|
|
26
|
+
export const monroePalette = Object.freeze({ glass: [0x4e4e4e, 0x525252, 0x565656, 0x5a5a5a], lit: 0x929292, dim: 0x484848, terracotta: 0xb2b2b2, granite: 0x9a9a9a, band: 0xc2c2c2 });
|
|
27
|
+
const palette = monroePalette;
|
|
28
|
+
const color = (hex) => new THREE.Color(hex);
|
|
29
|
+
const glassTones = palette.glass.map(color);
|
|
30
|
+
const litGlass = color(palette.lit), dimGlass = color(palette.dim);
|
|
31
|
+
const terracotta = color(palette.terracotta), granite = color(palette.granite), band = color(palette.band);
|
|
32
|
+
const core = color(0x2e2e2e), roofing = color(0x303030), flat = color(0x484848);
|
|
33
|
+
function paneColor(row, bay, wall) {
|
|
34
|
+
const hash = (Math.imul(row + 47, 0x9e3779b1) ^ Math.imul(bay + 73, 0x85ebca77) ^ Math.imul(wall + 61, 0xc2b2ae3d)) >>> 0;
|
|
35
|
+
const value = hash % 101;
|
|
36
|
+
if (value < 4)
|
|
37
|
+
return litGlass;
|
|
38
|
+
if (value < 8)
|
|
39
|
+
return dimGlass;
|
|
40
|
+
return glassTones[value % glassTones.length];
|
|
41
|
+
}
|
|
42
|
+
const rows = [
|
|
43
|
+
{ lo: 0.25, hi: drawn(1), kind: "granite", floor: 1 },
|
|
44
|
+
{ lo: drawn(1), hi: drawn(7), kind: "glass", floor: 1 },
|
|
45
|
+
{ lo: drawn(7), hi: drawn(8.5), kind: "granite", floor: 2 },
|
|
46
|
+
{ lo: drawn(8.5), hi: drawn(13.5), kind: "glass", floor: 2 },
|
|
47
|
+
{ lo: drawn(13.5), hi: h.granite, kind: "granite", floor: 2 },
|
|
48
|
+
];
|
|
49
|
+
const bases = [...Array.from({ length: 10 }, (_, k) => 14.5 + k * 3.55), 50.3, 53.7];
|
|
50
|
+
bases.forEach((base, k) => {
|
|
51
|
+
const floor = k + 3, below = rows.at(-1).hi;
|
|
52
|
+
if (floor === 13) {
|
|
53
|
+
rows.push({ lo: below, hi: h.belt, kind: "wall", floor });
|
|
54
|
+
rows.push({ lo: h.belt, hi: h.beltTop, kind: "band", floor });
|
|
55
|
+
rows.push({ lo: h.beltTop, hi: drawn(base + 0.9), kind: "wall", floor });
|
|
56
|
+
}
|
|
57
|
+
else
|
|
58
|
+
rows.push({ lo: below, hi: drawn(base + 0.9), kind: "wall", floor });
|
|
59
|
+
rows.push({ lo: drawn(base + 0.9), hi: drawn(base + 3), kind: "glass", floor });
|
|
60
|
+
});
|
|
61
|
+
rows.push({ lo: rows.at(-1).hi, hi: h.cornice, kind: "wall", floor: 14 });
|
|
62
|
+
rows.push({ lo: h.cornice, hi: h.eaves, kind: "band", floor: 14 });
|
|
63
|
+
export function createMonroeGeographicBuilding(record, projectPlan, offset) {
|
|
64
|
+
const kit = createBuilder(record.name, record.id);
|
|
65
|
+
const shell = kit.batch("Monroe Building · shell", kit.material(0xffffff, { vertexColors: true }));
|
|
66
|
+
const wall = kit.batch("Monroe Building · terracotta and windows", kit.material(0xffffff, { vertexColors: true }));
|
|
67
|
+
const attic = kit.batch("Monroe Building · attic windows", kit.material(0x505050));
|
|
68
|
+
const lot = orient(polygonOf(projectPlan(record.footprint.coordinates)));
|
|
69
|
+
// The lot's south-east corner, with its south wall running west and its Michigan front
|
|
70
|
+
// north to Monroe.
|
|
71
|
+
const corner = lot.reduce((best, p) => (p[0] + p[1] > best[0] + best[1] ? p : best));
|
|
72
|
+
const northEast = lot.reduce((best, p) => (p[0] - p[1] > best[0] - best[1] ? p : best));
|
|
73
|
+
const neighbours = [lot[(lot.indexOf(corner) + 1) % lot.length], lot[(lot.indexOf(corner) + lot.length - 1) % lot.length]];
|
|
74
|
+
const unit = (p) => { const d = Math.hypot(p[0] - corner[0], p[1] - corner[1]); return [(p[0] - corner[0]) / d, (p[1] - corner[1]) / d]; };
|
|
75
|
+
const west = unit(neighbours[0][0] < neighbours[1][0] ? neighbours[0] : neighbours[1]), north = unit(northEast);
|
|
76
|
+
const at = (a, b) => [corner[0] + west[0] * a + north[0] * b, corner[1] + west[1] * a + north[1] * b];
|
|
77
|
+
// The inverse of `at`: a point's distances along the south wall and the Michigan front,
|
|
78
|
+
// which meet a little off square.
|
|
79
|
+
const skew = west[0] * north[1] - west[1] * north[0];
|
|
80
|
+
const inLot = (p) => {
|
|
81
|
+
const [x, z] = [p[0] - corner[0], p[1] - corner[1]];
|
|
82
|
+
return [(x * north[1] - z * north[0]) / skew, (west[0] * z - west[1] * x) / skew];
|
|
83
|
+
};
|
|
84
|
+
const rect = (a0, a1, b0, b1) => orient([at(a0, b0), at(a1, b0), at(a1, b1), at(a0, b1)]);
|
|
85
|
+
const lift = ([x, z], y) => [x, y, z];
|
|
86
|
+
// The lot's four corners, in lot coordinates: its north and south walls are a little out
|
|
87
|
+
// of parallel.
|
|
88
|
+
const inPlan = lot.map(inLot), extreme = (score) => inPlan.reduce((best, p) => (score(p) > score(best) ? p : best));
|
|
89
|
+
const [southWest, northWest, northEastIn] = [extreme(([a, b]) => a - b), extreme(([a, b]) => a + b), inLot(northEast)];
|
|
90
|
+
// The walls to the roof's foot.
|
|
91
|
+
planOf(lot).forEach((run) => {
|
|
92
|
+
const a = run.at(0), b = run.at(run.length), n = run.normal(0);
|
|
93
|
+
kit.quad(shell, [[a[0], 0, a[1]], [b[0], 0, b[1]], [b[0], h.eaves, b[1]], [a[0], h.eaves, a[1]]], [[n[0], 0, n[1]]], core);
|
|
94
|
+
});
|
|
95
|
+
paintedSlab(kit, shell, lot, 0, false, flat);
|
|
96
|
+
paintedSlab(kit, shell, lot, h.eaves, true, flat);
|
|
97
|
+
// The roof: a closed solid 5 cm inside the walls, so that no face lies in theirs, from a
|
|
98
|
+
// foot 10 cm inside the block to a ridge between its gables' middles; its gables are
|
|
99
|
+
// terracotta, and its slopes roofing, each in two triangles since the lot is out of square.
|
|
100
|
+
const inset = (a, b, da, db) => at(a + da * 0.05, b + db * 0.05);
|
|
101
|
+
const roofCorners = [inset(0, 0, 1, 1), inset(southWest[0], southWest[1], -1, 1), inset(northWest[0], northWest[1], -1, -1), inset(0, northEastIn[1], 1, -1)];
|
|
102
|
+
const [se, sw, nw, ne] = roofCorners, foot = h.eaves - 0.1;
|
|
103
|
+
const footprint = orient([...roofCorners]);
|
|
104
|
+
planOf(footprint).forEach((run) => {
|
|
105
|
+
const p = run.at(0), q = run.at(run.length), n = run.normal(0);
|
|
106
|
+
kit.quad(shell, [lift(p, foot), lift(q, foot), lift(q, h.eaves), lift(p, h.eaves)], [[n[0], 0, n[1]]], core);
|
|
107
|
+
});
|
|
108
|
+
paintedSlab(kit, shell, footprint, foot, false, flat);
|
|
109
|
+
const halfway = (p, q) => [(p[0] + q[0]) / 2, (p[1] + q[1]) / 2];
|
|
110
|
+
const [eastRidge, westRidge] = [lift(halfway(se, ne), h.ridge), lift(halfway(sw, nw), h.ridge)];
|
|
111
|
+
const centre = lift(halfway(halfway(se, nw), halfway(sw, ne)), (h.eaves + h.ridge) / 2);
|
|
112
|
+
// Each face's outward normal, from its points and the roof's middle.
|
|
113
|
+
const face = (points, paint) => {
|
|
114
|
+
const [a, b, c] = points;
|
|
115
|
+
const n = new THREE.Vector3(b[0] - a[0], b[1] - a[1], b[2] - a[2]).cross(new THREE.Vector3(c[0] - a[0], c[1] - a[1], c[2] - a[2])).normalize();
|
|
116
|
+
const mid = [0, 1, 2].map((k) => (a[k] + b[k] + c[k]) / 3);
|
|
117
|
+
if (n.x * (mid[0] - centre[0]) + n.y * (mid[1] - centre[1]) + n.z * (mid[2] - centre[2]) < 0)
|
|
118
|
+
n.negate();
|
|
119
|
+
const normal = [n.x, n.y, n.z];
|
|
120
|
+
kit.triangle(shell, points, [normal, normal, normal], paint);
|
|
121
|
+
};
|
|
122
|
+
const eave = (p) => lift(p, h.eaves);
|
|
123
|
+
face([eave(se), eave(sw), westRidge], roofing);
|
|
124
|
+
face([eave(se), westRidge, eastRidge], roofing);
|
|
125
|
+
face([eave(ne), eave(nw), westRidge], roofing);
|
|
126
|
+
face([eave(ne), westRidge, eastRidge], roofing);
|
|
127
|
+
face([eave(se), eave(ne), eastRidge], terracotta);
|
|
128
|
+
face([eave(sw), eave(nw), westRidge], terracotta);
|
|
129
|
+
const middle = (inLot(halfway(se, ne))[1]);
|
|
130
|
+
// A wall's skin, one box to a chain the `keep` test accepts, held 2 cm clear of its ends
|
|
131
|
+
// or 9 cm where the outline turns in, with paired windows `pairs` along it from its start;
|
|
132
|
+
// a `plain` wall carries neither windows nor bands.
|
|
133
|
+
const skin = (seed, keep, pairs, plain = false) => {
|
|
134
|
+
const plan = planOf(lot), heights = [rows[0].lo, ...rows.map((row) => row.hi)];
|
|
135
|
+
chainsOf(plan).forEach((chain, index) => {
|
|
136
|
+
if (!chain.runs.every((run) => keep(plan[run])))
|
|
137
|
+
return;
|
|
138
|
+
const first = chain.runs[0], last = chain.runs.at(-1);
|
|
139
|
+
const s0 = turnsAt(plan, first)[0] > 0 ? 0.09 : 0.02, s1 = chain.length - (turnsAt(plan, last)[1] > 0 ? 0.09 : 0.02);
|
|
140
|
+
// Each pair: two windows 1.3 m wide either side of a 35 cm mullion.
|
|
141
|
+
const centres = pairs(chain.length, plan[first].at(0), plan[last].at(plan[last].length)).filter((c) => c - 1.475 > 0.1 && c + 1.475 < chain.length - 0.1).sort((p, q) => p - q);
|
|
142
|
+
const columns = [0, ...centres.flatMap((c) => [c - 1.475, c - 0.175, c + 0.175, c + 1.475]), chain.length];
|
|
143
|
+
chainSkin(kit, wall, plan, chain, 0, chain.runs.length - 1, s0, s1, columns, heights, 0.02, 0.07, (bay, r) => {
|
|
144
|
+
const row = rows[r], window = bay <= 4 * centres.length && (bay % 4 === 2 || bay % 4 === 0);
|
|
145
|
+
if (row.kind === "band")
|
|
146
|
+
return plain ? terracotta : band;
|
|
147
|
+
if (row.kind === "glass" && window)
|
|
148
|
+
return paneColor(row.floor, bay, seed * 8 + index);
|
|
149
|
+
return row.kind === "granite" || row.lo < h.granite ? granite : terracotta;
|
|
150
|
+
}, terracotta);
|
|
151
|
+
});
|
|
152
|
+
};
|
|
153
|
+
// Bays 5.3 m apart centred on each wall, as the drawing's five on Michigan.
|
|
154
|
+
const bays = (length) => { const count = Math.max(1, Math.round(length / 5.3)); return Array.from({ length: count }, (_, k) => length * (k + 0.5) / count); };
|
|
155
|
+
const facing = (x, z) => (run) => run.normal(0)[0] * x + run.normal(0)[1] * z > 0.9;
|
|
156
|
+
// The south wall, shared with the taller MacLean Center, stays blank.
|
|
157
|
+
skin(0, (run) => !facing(0, 1)(run), bays);
|
|
158
|
+
skin(1, facing(0, 1), () => [], true);
|
|
159
|
+
// The gable's small arched windows, dark panels 1 m wide standing 3 cm proud of it, 5 mm
|
|
160
|
+
// clear, their heads stepped to the middle half for the arch's last 30 cm as drawn and set
|
|
161
|
+
// 5 mm back so that no face lies in the panel's: three pairs 2.3 m apart in the lower row,
|
|
162
|
+
// one pair in the middle above.
|
|
163
|
+
const window = (b, lo, hi) => {
|
|
164
|
+
kit.prism(attic, planOf(rect(0.02, 0.045, b - 0.5, b + 0.5)), [lo, hi - drawn(0.3)]);
|
|
165
|
+
kit.prism(attic, planOf(rect(0.025, 0.045, b - 0.25, b + 0.25)), [hi - drawn(0.3), hi]);
|
|
166
|
+
};
|
|
167
|
+
for (const c of [middle - 5.6, middle, middle + 5.6])
|
|
168
|
+
for (const d of [-1.15, 1.15])
|
|
169
|
+
window(c + d, drawn(58.3), drawn(60.1));
|
|
170
|
+
for (const d of [-1.15, 1.15])
|
|
171
|
+
window(middle + d, drawn(61.75), drawn(63.4));
|
|
172
|
+
const model = kit.finish({ height: h.ridge, outlines: [shell, attic], opacity: 0.16 });
|
|
173
|
+
model.building.position.set(offset[0], 0, offset[1]);
|
|
174
|
+
model.building.userData["geography"] = record;
|
|
175
|
+
model.building.userData["geographicDetail"] = { levels: h, source: "docs/monroe-reference.md" };
|
|
176
|
+
return model;
|
|
177
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { BuildingModel, Plan } from "./building-kit.js";
|
|
2
|
+
import type { GeoBuilding } from "./skyline-geography-data.js";
|
|
3
|
+
export declare const northMichigan180Levels: Readonly<{
|
|
4
|
+
base: 9.85;
|
|
5
|
+
top: 86.3;
|
|
6
|
+
}>;
|
|
7
|
+
export declare const northMichigan180Palette: Readonly<{
|
|
8
|
+
glass: number[];
|
|
9
|
+
lit: 10132122;
|
|
10
|
+
dim: 5395026;
|
|
11
|
+
masonry: 8026746;
|
|
12
|
+
course: 8947848;
|
|
13
|
+
}>;
|
|
14
|
+
export declare function createNorthMichigan180GeographicBuilding(record: GeoBuilding, projectPlan: (coordinates: [number, number][]) => Plan, offset: [number, number]): BuildingModel;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import * as THREE from "../vendor/three-r186.js";
|
|
2
|
+
import { createBuilder, polygonOf } from "./building-kit.js";
|
|
3
|
+
import { chainSkin, chainsOf, orient, paintedSlab, planOf, turnsAt } from "./facade-grid.js";
|
|
4
|
+
// 180 North Michigan Avenue, the Harvester Building (1927): the geographic layout's model,
|
|
5
|
+
// on the mapped outline 210671714. The original layout has no model of it; the drawing
|
|
6
|
+
// shows its south wall behind Millennium Park Plaza, which hides its Michigan front.
|
|
7
|
+
//
|
|
8
|
+
// A brown masonry block of twenty-four storeys over the whole mapped lot, on a ground floor
|
|
9
|
+
// as tall as three of its floors, with string courses over the seventeenth and twentieth floors and a top floor of
|
|
10
|
+
// arched windows under a band and a parapet at 86.3 m. No height is published; the parapet
|
|
11
|
+
// is read on the drawing, whose nearby tops fall within about a metre of their published
|
|
12
|
+
// heights, and the storeys are Marc Realty's. See docs/north-michigan-180-reference.md.
|
|
13
|
+
// Units are meters; +x is east, +z is south.
|
|
14
|
+
export const northMichigan180Levels = Object.freeze({
|
|
15
|
+
base: 9.85, // the tall ground floor, under the second
|
|
16
|
+
top: 86.3, // the parapet, read on the drawing
|
|
17
|
+
});
|
|
18
|
+
const h = northMichigan180Levels;
|
|
19
|
+
// Each floor's level: the tall ground floor, then 3.25 m floors, the twenty-fourth
|
|
20
|
+
// the last.
|
|
21
|
+
const floor = (n) => (n < 2 ? 0 : h.base + (n - 2) * 3.25);
|
|
22
|
+
// The skins' colours: the window tones, and the masonry and its courses, which no window
|
|
23
|
+
// shares.
|
|
24
|
+
export const northMichigan180Palette = Object.freeze({ glass: [0x585858, 0x5c5c5c, 0x606060, 0x646464], lit: 0x9a9a9a, dim: 0x525252, masonry: 0x7a7a7a, course: 0x888888 });
|
|
25
|
+
const palette = northMichigan180Palette;
|
|
26
|
+
const color = (hex) => new THREE.Color(hex);
|
|
27
|
+
const glassTones = palette.glass.map(color);
|
|
28
|
+
const litGlass = color(palette.lit), dimGlass = color(palette.dim);
|
|
29
|
+
const masonry = color(palette.masonry), course = color(palette.course), core = color(0x303030), roofing = color(0x505050);
|
|
30
|
+
function paneColor(row, bay, wall) {
|
|
31
|
+
const hash = (Math.imul(row + 41, 0x9e3779b1) ^ Math.imul(bay + 67, 0x85ebca77) ^ Math.imul(wall + 53, 0xc2b2ae3d)) >>> 0;
|
|
32
|
+
const value = hash % 101;
|
|
33
|
+
if (value < 4)
|
|
34
|
+
return litGlass;
|
|
35
|
+
if (value < 8)
|
|
36
|
+
return dimGlass;
|
|
37
|
+
return glassTones[value % glassTones.length];
|
|
38
|
+
}
|
|
39
|
+
const rows = [
|
|
40
|
+
{ lo: 0.25, hi: 1, kind: "wall", floor: 1 },
|
|
41
|
+
{ lo: 1, hi: 8.6, kind: "glass", floor: 1 },
|
|
42
|
+
];
|
|
43
|
+
for (let n = 2; n <= 23; n += 1) {
|
|
44
|
+
rows.push({ lo: rows.at(-1).hi, hi: floor(n) + 0.6, kind: n === 18 || n === 21 ? "course" : "wall", floor: n });
|
|
45
|
+
rows.push({ lo: floor(n) + 0.6, hi: floor(n) + 2.65, kind: "glass", floor: n });
|
|
46
|
+
}
|
|
47
|
+
rows.push({ lo: rows.at(-1).hi, hi: 81.75, kind: "wall", floor: 24 });
|
|
48
|
+
rows.push({ lo: 81.75, hi: 82.3, kind: "course", floor: 24 });
|
|
49
|
+
rows.push({ lo: 82.3, hi: 85.4, kind: "glass", floor: 24 });
|
|
50
|
+
rows.push({ lo: 85.4, hi: 86, kind: "head", floor: 24 });
|
|
51
|
+
rows.push({ lo: 86, hi: h.top, kind: "wall", floor: 24 });
|
|
52
|
+
export function createNorthMichigan180GeographicBuilding(record, projectPlan, offset) {
|
|
53
|
+
const kit = createBuilder(record.name, record.id);
|
|
54
|
+
const shell = kit.batch("180 North Michigan · shell", kit.material(0xffffff, { vertexColors: true }));
|
|
55
|
+
const wall = kit.batch("180 North Michigan · masonry and windows", kit.material(0xffffff, { vertexColors: true }));
|
|
56
|
+
const lot = orient(polygonOf(projectPlan(record.footprint.coordinates)));
|
|
57
|
+
// The lot's south-east corner, with its south wall running west and its Michigan front
|
|
58
|
+
// north to Lake Street.
|
|
59
|
+
const corner = lot.reduce((best, p) => (p[0] + p[1] > best[0] + best[1] ? p : best));
|
|
60
|
+
const northEast = lot.reduce((best, p) => (p[0] - p[1] > best[0] - best[1] ? p : best));
|
|
61
|
+
const neighbours = [lot[(lot.indexOf(corner) + 1) % lot.length], lot[(lot.indexOf(corner) + lot.length - 1) % lot.length]];
|
|
62
|
+
const unit = (p) => { const d = Math.hypot(p[0] - corner[0], p[1] - corner[1]); return [(p[0] - corner[0]) / d, (p[1] - corner[1]) / d]; };
|
|
63
|
+
const west = unit(neighbours[0][0] < neighbours[1][0] ? neighbours[0] : neighbours[1]), north = unit(northEast);
|
|
64
|
+
// A point's distances along the south wall and the Michigan front, which meet a little off
|
|
65
|
+
// square.
|
|
66
|
+
const skew = west[0] * north[1] - west[1] * north[0];
|
|
67
|
+
const inLot = (p) => {
|
|
68
|
+
const [x, z] = [p[0] - corner[0], p[1] - corner[1]];
|
|
69
|
+
return [(x * north[1] - z * north[0]) / skew, (west[0] * z - west[1] * x) / skew];
|
|
70
|
+
};
|
|
71
|
+
planOf(lot).forEach((run) => {
|
|
72
|
+
const a = run.at(0), b = run.at(run.length), n = run.normal(0);
|
|
73
|
+
kit.quad(shell, [[a[0], 0, a[1]], [b[0], 0, b[1]], [b[0], h.top, b[1]], [a[0], h.top, a[1]]], [[n[0], 0, n[1]]], core);
|
|
74
|
+
});
|
|
75
|
+
paintedSlab(kit, shell, lot, 0, false, roofing);
|
|
76
|
+
paintedSlab(kit, shell, lot, h.top, true, roofing);
|
|
77
|
+
const skin = (seed, keep, openings) => {
|
|
78
|
+
const plan = planOf(lot), heights = [rows[0].lo, ...rows.map((row) => row.hi)];
|
|
79
|
+
chainsOf(plan).forEach((chain, index) => {
|
|
80
|
+
if (!chain.runs.every((run) => keep(plan[run])))
|
|
81
|
+
return;
|
|
82
|
+
const first = chain.runs[0], last = chain.runs.at(-1);
|
|
83
|
+
const s0 = turnsAt(plan, first)[0] > 0 ? 0.09 : 0.02, s1 = chain.length - (turnsAt(plan, last)[1] > 0 ? 0.09 : 0.02);
|
|
84
|
+
const [columns, opening] = openings(chain.length, plan[first].at(0), plan[last].at(plan[last].length));
|
|
85
|
+
chainSkin(kit, wall, plan, chain, 0, chain.runs.length - 1, s0, s1, columns, heights, 0.02, 0.07, (bay, r) => {
|
|
86
|
+
const row = rows[r], open = opening(bay);
|
|
87
|
+
if (row.kind === "course")
|
|
88
|
+
return course;
|
|
89
|
+
const glazed = row.kind === "glass" ? open !== "wall" : row.kind === "head" && open === "middle";
|
|
90
|
+
return glazed ? paneColor(row.floor, bay, seed * 8 + index) : masonry;
|
|
91
|
+
}, masonry);
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
// Windows 1.4 m wide centred at `centres`, positions along a wall from its start; each
|
|
95
|
+
// splits into sides and a middle half for the top floor's arched heads.
|
|
96
|
+
const windows = (length, centres) => {
|
|
97
|
+
const inside = centres.filter((c) => c - 0.7 > 0.1 && c + 0.7 < length - 0.1).sort((p, q) => p - q);
|
|
98
|
+
const columns = [0, ...inside.flatMap((c) => [c - 0.7, c - 0.35, c + 0.35, c + 0.7]), length];
|
|
99
|
+
// Bays count from one: a pier, then side, middle, side for each window, then the last pier.
|
|
100
|
+
return [columns, (bay) => { const k = (bay - 1) % 4; return bay > 4 * inside.length || k === 0 ? "wall" : k === 2 ? "middle" : "side"; }];
|
|
101
|
+
};
|
|
102
|
+
// The south wall's windows stand 2.7 m apart from 2.8 m west of Michigan, as the drawing
|
|
103
|
+
// puts its visible five; the other walls carry the same spacing, centred on each.
|
|
104
|
+
const south = (length, start, end) => {
|
|
105
|
+
const [a0, a1] = [inLot(start)[0], inLot(end)[0]];
|
|
106
|
+
return windows(length, Array.from({ length: 15 }, (_, k) => 2.8 + 2.7 * k).map((a) => length * (a - a0) / (a1 - a0)));
|
|
107
|
+
};
|
|
108
|
+
const spaced = (length) => { const count = Math.max(1, Math.round(length / 2.7)); return windows(length, Array.from({ length: count }, (_, k) => length * (k + 0.5) / count)); };
|
|
109
|
+
const isSouth = (run) => run.normal(0)[1] > 0.9;
|
|
110
|
+
skin(0, isSouth, south);
|
|
111
|
+
skin(1, (run) => !isSouth(run), spaced);
|
|
112
|
+
const model = kit.finish({ height: h.top, outlines: [shell], opacity: 0.16 });
|
|
113
|
+
model.building.position.set(offset[0], 0, offset[1]);
|
|
114
|
+
model.building.userData["geography"] = record;
|
|
115
|
+
model.building.userData["geographicDetail"] = { levels: h, source: "docs/north-michigan-180-reference.md" };
|
|
116
|
+
return model;
|
|
117
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { BuildingModel, Plan } from "./building-kit.js";
|
|
2
|
+
import type { GeoBuilding } from "./skyline-geography-data.js";
|
|
3
|
+
export declare const northWabashLevels: Readonly<{
|
|
4
|
+
plaza: 7.6;
|
|
5
|
+
lobbyTop: 15.5;
|
|
6
|
+
pitch: 3.81;
|
|
7
|
+
lowerFloors: 15;
|
|
8
|
+
plant: 4.66;
|
|
9
|
+
upperFloors: 31;
|
|
10
|
+
roof: 211.84;
|
|
11
|
+
module: 1.524;
|
|
12
|
+
}>;
|
|
13
|
+
export declare const northWabashFloors: number[];
|
|
14
|
+
export declare function createNorthWabashGeographicBuilding(record: GeoBuilding, projectPlan: (coordinates: [number, number][]) => Plan, offset: [number, number]): BuildingModel;
|