@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,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,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,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,122 @@
|
|
|
1
|
+
import * as THREE from "../vendor/three-r186.js";
|
|
2
|
+
import { createBuilder, polygonOf } from "./building-kit.js";
|
|
3
|
+
import { chainSkin, chainsOf, mitredBox, orient, paintedSlab, planOf, turnsAt } from "./facade-grid.js";
|
|
4
|
+
// 330 North Wabash (Ludwig Mies van der Rohe with C. F. Murphy, 1972), the former IBM
|
|
5
|
+
// Building and now AMA Plaza: the geographic layout's model, on the mapped outline 64596068
|
|
6
|
+
// and its mapped 211.84 m (695 ft) height. It is the tower the drawing labels Michigan Plaza
|
|
7
|
+
// South: through the photograph's camera its roof corners land on the drawn ones, and One
|
|
8
|
+
// Prudential covers its east face where the drawing does.
|
|
9
|
+
//
|
|
10
|
+
// A black slab, 120 by 270 ft, of bronze-anodized aluminium: I-beam mullions on the 5 ft
|
|
11
|
+
// module stand in front of bronze-tinted glass, with a bronze spandrel at every floor. It
|
|
12
|
+
// stands on a plaza 25 ft above the street, where bronze-clad columns carry it over a glazed
|
|
13
|
+
// lobby, and louvers in place of glass mark its mechanical floors: two above the sixteenth,
|
|
14
|
+
// and the three at the top, which the photograph shows dark. See docs/north-wabash-reference.md.
|
|
15
|
+
// Units are meters; +x is east, +z is south.
|
|
16
|
+
export const northWabashLevels = Object.freeze({
|
|
17
|
+
plaza: 7.6, // 25 ft above the street
|
|
18
|
+
lobbyTop: 15.5, // the lobby's 26 ft
|
|
19
|
+
pitch: 3.81, // 12 ft 6 in office floors
|
|
20
|
+
lowerFloors: 15,
|
|
21
|
+
plant: 4.66, // each of the two mechanical floors above the sixteenth
|
|
22
|
+
upperFloors: 31,
|
|
23
|
+
roof: 211.84, // mapped height, the published 695 ft
|
|
24
|
+
module: 1.524, // 5 ft
|
|
25
|
+
});
|
|
26
|
+
const h = northWabashLevels;
|
|
27
|
+
const lowerPlant = h.lobbyTop + h.lowerFloors * h.pitch, upperStart = lowerPlant + 2 * h.plant;
|
|
28
|
+
const topPlant = upperStart + h.upperFloors * h.pitch;
|
|
29
|
+
// Every floor line from the lobby's top to the top mechanical floors, and what the storey
|
|
30
|
+
// above it is.
|
|
31
|
+
const floors = [
|
|
32
|
+
...Array.from({ length: h.lowerFloors }, (_, i) => ({ level: h.lobbyTop + i * h.pitch, plant: false })),
|
|
33
|
+
{ level: lowerPlant, plant: true }, { level: lowerPlant + h.plant, plant: true },
|
|
34
|
+
...Array.from({ length: h.upperFloors }, (_, i) => ({ level: upperStart + i * h.pitch, plant: false })),
|
|
35
|
+
...Array.from({ length: 3 }, (_, i) => ({ level: topPlant + i * (h.roof - topPlant) / 3, plant: true })),
|
|
36
|
+
];
|
|
37
|
+
export const northWabashFloors = floors.map(({ level }) => level);
|
|
38
|
+
const spandrelBelow = 0.32, spandrelAbove = 0.75, fascia = 0.55;
|
|
39
|
+
const mullionHalf = 0.05, mullionDepth = 0.18, columnHalf = 0.45, columnDepth = 0.6;
|
|
40
|
+
// The columns stand 30 ft apart along the long faces and 40 ft along the short ones.
|
|
41
|
+
const columnBays = [9.144, 12.192];
|
|
42
|
+
const color = (hex) => new THREE.Color(hex);
|
|
43
|
+
const glassTones = [0x1f1f1f, 0x232323, 0x272727, 0x1c1c1c].map(color);
|
|
44
|
+
const litGlass = color(0x7a7a7a), dimGlass = color(0x4a4a4a);
|
|
45
|
+
const spandrel = color(0x262626), louver = color(0x353535), lobby = color(0x3a3a3a), granite = color(0x464646), roofing = color(0x3a3a3a);
|
|
46
|
+
function paneColor(row, bay, wall) {
|
|
47
|
+
const hash = (Math.imul(row + 7, 0x9e3779b1) ^ Math.imul(bay + 41, 0x85ebca77) ^ Math.imul(wall + 19, 0xc2b2ae3d)) >>> 0;
|
|
48
|
+
const value = hash % 101;
|
|
49
|
+
if (value < 4)
|
|
50
|
+
return litGlass;
|
|
51
|
+
if (value < 8)
|
|
52
|
+
return dimGlass;
|
|
53
|
+
return glassTones[value % glassTones.length];
|
|
54
|
+
}
|
|
55
|
+
export function createNorthWabashGeographicBuilding(record, projectPlan, offset) {
|
|
56
|
+
const kit = createBuilder(record.name, record.id);
|
|
57
|
+
const shell = kit.batch("330 North Wabash · shell", kit.material(0xffffff, { vertexColors: true }));
|
|
58
|
+
const wall = kit.batch("330 North Wabash · glass, spandrels and louvers", kit.material(0xffffff, { vertexColors: true }));
|
|
59
|
+
const frames = kit.batch("330 North Wabash · bronze mullions and columns", kit.material(0x3c3c3c));
|
|
60
|
+
const outline = orient(polygonOf(projectPlan(record.footprint.coordinates))), plan = planOf(outline);
|
|
61
|
+
// The slab: granite below the plaza, bronze above it, and a flat roof.
|
|
62
|
+
plan.forEach((run) => {
|
|
63
|
+
const a = run.at(0), b = run.at(run.length), n = run.normal(0);
|
|
64
|
+
kit.quad(shell, [[a[0], 0, a[1]], [b[0], 0, b[1]], [b[0], h.plaza, b[1]], [a[0], h.plaza, a[1]]], [[n[0], 0, n[1]]], granite);
|
|
65
|
+
kit.quad(shell, [[a[0], h.plaza, a[1]], [b[0], h.plaza, b[1]], [b[0], h.roof, b[1]], [a[0], h.roof, a[1]]], [[n[0], 0, n[1]]], spandrel);
|
|
66
|
+
});
|
|
67
|
+
paintedSlab(kit, shell, outline, 0, false, roofing);
|
|
68
|
+
paintedSlab(kit, shell, outline, h.roof, true, roofing);
|
|
69
|
+
const rows = [{ lo: h.plaza + 0.1, hi: floors[0].level - spandrelBelow, kind: "lobby", floor: 0 }];
|
|
70
|
+
floors.forEach(({ level, plant }, i) => {
|
|
71
|
+
const next = floors[i + 1]?.level ?? h.roof + spandrelBelow;
|
|
72
|
+
rows.push({ lo: level - spandrelBelow, hi: level + spandrelAbove, kind: "spandrel", floor: i + 1 });
|
|
73
|
+
rows.push({ lo: level + spandrelAbove, hi: Math.min(next - spandrelBelow, h.roof - fascia), kind: plant ? "louver" : "glass", floor: i + 1 });
|
|
74
|
+
});
|
|
75
|
+
const cuts = [rows[0].lo, ...rows.map((row) => row.hi)];
|
|
76
|
+
// Each wall: one skin of cells on each floor; I-beam mullions one module
|
|
77
|
+
// apart, whole on one run and held back from the corners, from the lobby's top to under
|
|
78
|
+
// the fascia; and bronze-clad columns on the column bays through the lobby.
|
|
79
|
+
chainsOf(plan).forEach((chain, index) => {
|
|
80
|
+
const count = Math.max(1, Math.round(chain.length / h.module));
|
|
81
|
+
const stations = Array.from({ length: count + 1 }, (_, i) => chain.length * i / count);
|
|
82
|
+
// The long faces run north and south.
|
|
83
|
+
const run0 = plan[chain.runs[0]], n = run0.normal(0), bay = columnBays[Math.abs(n[0]) > 0.7 ? 0 : 1];
|
|
84
|
+
const columnCount = Math.max(1, Math.round(chain.length / bay));
|
|
85
|
+
const columns = Array.from({ length: columnCount + 1 }, (_, i) => chain.length * i / columnCount);
|
|
86
|
+
// Cells span two modules, so a lit window reads as an office's width.
|
|
87
|
+
chainSkin(kit, wall, plan, chain, 0, chain.runs.length - 1, 0.02, chain.length - 0.02, stations.filter((_, i) => i % 2 === 0), cuts, 0.02, 0.07, (bay, r) => {
|
|
88
|
+
const row = rows[r];
|
|
89
|
+
if (row.kind === "spandrel")
|
|
90
|
+
return spandrel;
|
|
91
|
+
if (row.kind === "louver")
|
|
92
|
+
return louver;
|
|
93
|
+
if (row.kind === "lobby")
|
|
94
|
+
return lobby;
|
|
95
|
+
return paneColor(row.floor, bay, index);
|
|
96
|
+
}, spandrel);
|
|
97
|
+
const place = (s, half, depth, lo, hi) => {
|
|
98
|
+
const k = chain.starts.findIndex((start, i) => s < start + plan[chain.runs[i]].length + 1e-9);
|
|
99
|
+
if (k < 0)
|
|
100
|
+
return;
|
|
101
|
+
const run = plan[chain.runs[k]], start = chain.starts[k];
|
|
102
|
+
const ends = (other) => (other < 0 || other >= chain.runs.length ? depth + 0.02 : 0.03) + half;
|
|
103
|
+
const lowS = ends(k - 1), highS = run.length - ends(k + 1);
|
|
104
|
+
if (highS >= lowS)
|
|
105
|
+
kit.box(frames, run.at(Math.min(Math.max(s - start, lowS), highS)), run.normal(0), half, 0, depth, lo, hi);
|
|
106
|
+
};
|
|
107
|
+
for (const s of stations)
|
|
108
|
+
place(s, mullionHalf, mullionDepth, h.lobbyTop, h.roof - fascia - 0.1);
|
|
109
|
+
for (const s of columns)
|
|
110
|
+
place(s, columnHalf, columnDepth, h.plaza + 0.05, h.lobbyTop);
|
|
111
|
+
});
|
|
112
|
+
// The fascia that caps the curtain wall, mitred round the corners.
|
|
113
|
+
plan.forEach((run, i) => {
|
|
114
|
+
const [before, after] = turnsAt(plan, i);
|
|
115
|
+
mitredBox(kit, frames, run, 0, run.length, before, after, 0.25, h.roof - fascia, h.roof);
|
|
116
|
+
});
|
|
117
|
+
const model = kit.finish({ height: h.roof, outlines: [shell, frames], opacity: 0.16 });
|
|
118
|
+
model.building.position.set(offset[0], 0, offset[1]);
|
|
119
|
+
model.building.userData["geography"] = record;
|
|
120
|
+
model.building.userData["geographicDetail"] = { levels: h, source: "docs/north-wabash-reference.md" };
|
|
121
|
+
return model;
|
|
122
|
+
}
|