@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,250 @@
|
|
|
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 Peoples Gas Building, 122 South Michigan Avenue (D. H. Burnham & Co., 1911): the
|
|
5
|
+
// geographic layout's model, on the mapped outline 145498710 and its parts. The original
|
|
6
|
+
// layout has no model of it; the drawing shows it between the Lake View Building and Adams
|
|
7
|
+
// Street, with the Borg-Warner Building before its Adams front.
|
|
8
|
+
//
|
|
9
|
+
// Twenty storeys rise on the Michigan and Adams fronts between wide corner piers:
|
|
10
|
+
// - two behind granite columns;
|
|
11
|
+
// - fourteen of paired windows, eleven bays to Michigan and nine to Adams;
|
|
12
|
+
// - three behind a colonnade under an entablature;
|
|
13
|
+
// - an attic under a frieze of pendant arches, a projecting cornice and its cresting.
|
|
14
|
+
// A light court opens over the ground storey and widens over the seventeenth floor. The top
|
|
15
|
+
// is the City's 272 ft. The drawn cresting reads 86 m, so heights read on the drawing are
|
|
16
|
+
// scaled to meet it; see docs/peoples-gas-reference.md. Units are meters; +x is east, +z is
|
|
17
|
+
// south.
|
|
18
|
+
const drawn = (height) => height * 82.9 / 86;
|
|
19
|
+
export const peoplesGasLevels = Object.freeze({
|
|
20
|
+
court: drawn(7.2), // the light court's floor, over the ground storey
|
|
21
|
+
inner: drawn(66.3), // the court's inner wings' roof, at the seventeenth floor
|
|
22
|
+
colonnade: drawn(67.47), // the colonnade's foot
|
|
23
|
+
entablature: drawn(76.8), // the colonnade's entablature
|
|
24
|
+
frieze: drawn(82.1), // the frieze under the cornice
|
|
25
|
+
cornice: drawn(84.3), // the projecting cornice
|
|
26
|
+
roof: drawn(84.8), // the cornice's top and the roof
|
|
27
|
+
top: 82.9, // the cresting's top, the published height
|
|
28
|
+
});
|
|
29
|
+
const h = peoplesGasLevels;
|
|
30
|
+
// The skins' colours: the window tones, and the terracotta, granite, bands, the frieze's
|
|
31
|
+
// shadow and the court's white brick, which no window shares.
|
|
32
|
+
export const peoplesGasPalette = Object.freeze({ glass: [0x2c2c2c, 0x303030, 0x343434, 0x383838], lit: 0x6e6e6e, dim: 0x3c3c3c, stone: 0x7c7c7c, granite: 0x5a5a5a, band: 0x8e8e8e, shadow: 0x242424, court: 0x9c9c9c });
|
|
33
|
+
const palette = peoplesGasPalette;
|
|
34
|
+
const color = (hex) => new THREE.Color(hex);
|
|
35
|
+
const glassTones = palette.glass.map(color);
|
|
36
|
+
const litGlass = color(palette.lit), dimGlass = color(palette.dim);
|
|
37
|
+
const stone = color(palette.stone), granite = color(palette.granite), band = color(palette.band), shadow = color(palette.shadow), courtBrick = color(palette.court);
|
|
38
|
+
const core = color(0x262626), party = color(0x646464), roofing = color(0x3c3c3c);
|
|
39
|
+
function paneColor(row, bay, wall) {
|
|
40
|
+
const hash = (Math.imul(row + 61, 0x9e3779b1) ^ Math.imul(bay + 89, 0x85ebca77) ^ Math.imul(wall + 73, 0xc2b2ae3d)) >>> 0;
|
|
41
|
+
const value = hash % 101;
|
|
42
|
+
if (value < 4)
|
|
43
|
+
return litGlass;
|
|
44
|
+
if (value < 8)
|
|
45
|
+
return dimGlass;
|
|
46
|
+
return glassTones[value % glassTones.length];
|
|
47
|
+
}
|
|
48
|
+
const rows = [{ lo: 0.25, hi: drawn(0.6), kind: "granite", floor: 1 }];
|
|
49
|
+
const push = (hi, kind, floor, more = {}) => rows.push({ lo: rows.at(-1).hi, hi: drawn(hi), kind, floor, ...more });
|
|
50
|
+
push(6.2, "shop", 1);
|
|
51
|
+
push(7.2, "granite", 2);
|
|
52
|
+
push(11.6, "shop", 2);
|
|
53
|
+
push(12.4, "granite", 2);
|
|
54
|
+
push(13.3, "belt", 2);
|
|
55
|
+
const sill = (n) => 63.36 - (16 - n) * 3.79;
|
|
56
|
+
for (let n = 3; n <= 16; n += 1) {
|
|
57
|
+
push(sill(n), "wall", n);
|
|
58
|
+
push(sill(n) + 2.24, "glass", n, { pier: true });
|
|
59
|
+
}
|
|
60
|
+
push(66.3, "wall", 16);
|
|
61
|
+
push(67.47, "wall", 17);
|
|
62
|
+
push(69.7, "rail", 17, { pier: true });
|
|
63
|
+
push(69.9, "rail", 17);
|
|
64
|
+
push(70.9, "open", 18);
|
|
65
|
+
push(73.2, "open", 18, { pier: true });
|
|
66
|
+
push(74.56, "open", 19);
|
|
67
|
+
push(76.8, "open", 19, { pier: true });
|
|
68
|
+
push(78.3, "band", 19);
|
|
69
|
+
push(78.9, "wall", 20);
|
|
70
|
+
push(81, "attic", 20);
|
|
71
|
+
push(81.3, "attic", 20, { reach: 0.45 });
|
|
72
|
+
push(81.55, "pendant", 20, { reach: 0.45 });
|
|
73
|
+
push(82.1, "pendant", 20, { reach: 0.9 });
|
|
74
|
+
push(84.3, "frieze", 20);
|
|
75
|
+
push(84.8, "parapet", 20);
|
|
76
|
+
export function createPeoplesGasGeographicBuilding(record, projectPlan, offset) {
|
|
77
|
+
const kit = createBuilder(record.name, record.id);
|
|
78
|
+
const shell = kit.batch("Peoples Gas Building · shell", kit.material(0xffffff, { vertexColors: true }));
|
|
79
|
+
const wall = kit.batch("Peoples Gas Building · terracotta and windows", kit.material(0xffffff, { vertexColors: true }));
|
|
80
|
+
const cornice = kit.batch("Peoples Gas Building · cornice", kit.material(0x8e8e8e));
|
|
81
|
+
const corners = (coordinates) => orient(polygonOf(projectPlan(coordinates)));
|
|
82
|
+
const lot = corners(record.footprint.coordinates);
|
|
83
|
+
// The mapped parts: the twenty-level ring, the north wing within it, the inner wings
|
|
84
|
+
// rising to the seventeenth floor, the court, and the notch in the north wall that opens
|
|
85
|
+
// it to the Lake View Building's light court.
|
|
86
|
+
const part = (way) => corners(record.parts.find((p) => p.way === way).coordinates);
|
|
87
|
+
const mapped = [1179833660, 1179833658, 1179833661, 1179833659, 1179842408].map(part);
|
|
88
|
+
// The lot's south-east corner, on Michigan and Adams, with the Adams front running west to
|
|
89
|
+
// the alley and the Michigan front north to the Lake View Building.
|
|
90
|
+
const corner = lot.reduce((best, p) => (p[0] + p[1] > best[0] + best[1] ? p : best));
|
|
91
|
+
const northEast = lot.reduce((best, p) => (p[0] - p[1] > best[0] - best[1] ? p : best));
|
|
92
|
+
const southWest = lot.reduce((best, p) => (p[1] - p[0] > best[1] - best[0] ? p : best));
|
|
93
|
+
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]; };
|
|
94
|
+
const west = unit(southWest), north = unit(northEast);
|
|
95
|
+
const at = (a, b) => [corner[0] + west[0] * a + north[0] * b, corner[1] + west[1] * a + north[1] * b];
|
|
96
|
+
// The inverse of `at`: a point's distances along the Adams and Michigan fronts, which
|
|
97
|
+
// meet a little off square.
|
|
98
|
+
const skew = west[0] * north[1] - west[1] * north[0];
|
|
99
|
+
const inLot = (p) => {
|
|
100
|
+
const [x, z] = [p[0] - corner[0], p[1] - corner[1]];
|
|
101
|
+
return [(x * north[1] - z * north[0]) / skew, (west[0] * z - west[1] * x) / skew];
|
|
102
|
+
};
|
|
103
|
+
const [adams, michigan] = [inLot(southWest)[0], inLot(northEast)[1]];
|
|
104
|
+
// Corners and edges by value, since parts share the outline's nodes.
|
|
105
|
+
const key = (p) => `${p[0].toFixed(3)},${p[1].toFixed(3)}`;
|
|
106
|
+
// Where a part's edge joins two of the outline's corners a few nodes apart, it follows
|
|
107
|
+
// the outline between them, so that the roof meets the walls corner to corner: the ring
|
|
108
|
+
// skips two of the Michigan front's nodes.
|
|
109
|
+
const index = new Map(lot.map((p, i) => [key(p), i]));
|
|
110
|
+
const follow = (polygon) => polygon.flatMap((a, i) => {
|
|
111
|
+
const [from, to] = [index.get(key(a)), index.get(key(polygon[(i + 1) % polygon.length]))], n = lot.length;
|
|
112
|
+
if (from === undefined || to === undefined)
|
|
113
|
+
return [a];
|
|
114
|
+
const [ahead, back] = [(to - from + n) % n, (from - to + n) % n];
|
|
115
|
+
const step = ahead > 1 && ahead <= 3 ? 1 : back > 1 && back <= 3 ? -1 : 0, between = step === 1 ? ahead - 1 : step === -1 ? back - 1 : 0;
|
|
116
|
+
return [a, ...Array.from({ length: between }, (_, k) => lot[(from + step * (k + 1) + n) % n])];
|
|
117
|
+
});
|
|
118
|
+
// And a part takes every other part's corner within 2 cm of one of its edges: the ring's
|
|
119
|
+
// edge along the court passes 1.1 cm from the court's corner.
|
|
120
|
+
const conform = (polygon, others) => polygon.flatMap((a, i) => {
|
|
121
|
+
const b = polygon[(i + 1) % polygon.length], length = Math.hypot(b[0] - a[0], b[1] - a[1]);
|
|
122
|
+
const on = others.flat().map((p) => ({ p, t: ((p[0] - a[0]) * (b[0] - a[0]) + (p[1] - a[1]) * (b[1] - a[1])) / length ** 2 }))
|
|
123
|
+
.filter(({ p, t }) => t > 1e-6 && t < 1 - 1e-6 && Math.abs((p[0] - a[0]) * (b[1] - a[1]) - (p[1] - a[1]) * (b[0] - a[0])) / length < 0.02);
|
|
124
|
+
return [a, ...[...new Map(on.map((x) => [key(x.p), x])).values()].sort((x, y) => x.t - y.t).map((x) => x.p)];
|
|
125
|
+
});
|
|
126
|
+
const followed = mapped.map(follow);
|
|
127
|
+
const [ring, northWing, innerWings, court, notch] = followed.map((polygon) => conform(polygon, followed.filter((other) => other !== polygon)));
|
|
128
|
+
const edges = (polygon) => new Set(polygon.flatMap((a, i) => { const b = polygon[(i + 1) % polygon.length]; return [`${key(a)}|${key(b)}`, `${key(b)}|${key(a)}`]; }));
|
|
129
|
+
const has = (set, run) => set.has(`${key(run.at(0))}|${key(run.at(run.length))}`);
|
|
130
|
+
// Two outlines sharing a run of edges, as one: the edges neither shares, end to end.
|
|
131
|
+
const merge = (p, q) => {
|
|
132
|
+
const [ep, eq] = [edges(p), edges(q)];
|
|
133
|
+
const kept = [...p.map((a, i) => [a, p[(i + 1) % p.length]]).filter(([a, b]) => !eq.has(`${key(a)}|${key(b)}`)),
|
|
134
|
+
...q.map((a, i) => [a, q[(i + 1) % q.length]]).filter(([a, b]) => !ep.has(`${key(a)}|${key(b)}`))];
|
|
135
|
+
const next = new Map(kept.map(([a, b]) => [key(a), b]));
|
|
136
|
+
const out = [kept[0][0]];
|
|
137
|
+
for (let p1 = kept[0][1]; key(p1) !== key(out[0]); p1 = next.get(key(p1)))
|
|
138
|
+
out.push(p1);
|
|
139
|
+
return orient(out);
|
|
140
|
+
};
|
|
141
|
+
// The court's walls face into it, so their outlines run the other way.
|
|
142
|
+
const inward = (polygon) => [...polygon].reverse();
|
|
143
|
+
const [lower, upper, opening] = [inward(court), inward(merge(court, innerWings)), inward(notch)];
|
|
144
|
+
const [lotEdges, wingEdges, lowerEdges] = [edges(lot), edges(innerWings), edges(court)];
|
|
145
|
+
const face = (run, lo, hi, paint) => {
|
|
146
|
+
const a = run.at(0), b = run.at(run.length), n = run.normal(0);
|
|
147
|
+
kit.quad(shell, [[a[0], lo, a[1]], [b[0], lo, b[1]], [b[0], hi, b[1]], [a[0], hi, a[1]]], [[n[0], 0, n[1]]], paint);
|
|
148
|
+
};
|
|
149
|
+
// The outer walls to the roof, the north wall, above the Lake View Building, in common
|
|
150
|
+
// brick, and open over the court's floor at the notch. Walls split where the ones they
|
|
151
|
+
// meet end, at the court's floor and the inner wings' roof, so every edge is shared.
|
|
152
|
+
const notchEdges = edges(notch);
|
|
153
|
+
planOf(lot).forEach((run) => {
|
|
154
|
+
const paint = run.normal(0)[1] < -0.9 ? party : core;
|
|
155
|
+
face(run, 0, h.court, paint);
|
|
156
|
+
if (!has(notchEdges, run))
|
|
157
|
+
face(run, h.court, h.roof, paint);
|
|
158
|
+
});
|
|
159
|
+
// The court's walls: the inner wings' to their roof, the rest to the roof, and over the
|
|
160
|
+
// inner wings the widened court's; the notch's but its open side.
|
|
161
|
+
planOf(lower).forEach((run) => {
|
|
162
|
+
face(run, h.court, h.inner, courtBrick);
|
|
163
|
+
if (!has(wingEdges, run))
|
|
164
|
+
face(run, h.inner, h.roof, courtBrick);
|
|
165
|
+
});
|
|
166
|
+
planOf(upper).forEach((run) => { if (!has(lowerEdges, run))
|
|
167
|
+
face(run, h.inner, h.roof, courtBrick); });
|
|
168
|
+
planOf(opening).forEach((run) => { if (!has(lotEdges, run))
|
|
169
|
+
face(run, h.court, h.roof, courtBrick); });
|
|
170
|
+
paintedSlab(kit, shell, lot, 0, false, roofing);
|
|
171
|
+
for (const roof of [ring, northWing])
|
|
172
|
+
paintedSlab(kit, shell, roof, h.roof, true, roofing);
|
|
173
|
+
paintedSlab(kit, shell, innerWings, h.inner, true, roofing);
|
|
174
|
+
for (const floor of [court, notch])
|
|
175
|
+
paintedSlab(kit, shell, floor, h.court, true, roofing);
|
|
176
|
+
// The cornice projects 90 cm along both fronts, to 2 cm over the roof, stopping 3 cm
|
|
177
|
+
// short of the north and alley walls. It starts 8 cm inside the lines between the fronts'
|
|
178
|
+
// corners, which the mapped walls stand at most 2 cm inside and 12.4 cm outside, so it
|
|
179
|
+
// always meets them. Its cresting: blocks 50 cm wide and 1 m apart near its edge.
|
|
180
|
+
const [out, lip] = [0.9, 0.02];
|
|
181
|
+
kit.prism(cornice, planOf(orient([at(-out, michigan - 0.03), at(-out, -out), at(adams - 0.03, -out), at(adams - 0.03, 0.08), at(0.08, 0.08), at(0.08, michigan - 0.03)])), [h.cornice, h.roof + lip]);
|
|
182
|
+
const [east, south] = [[-north[1], north[0]], [west[1], -west[0]]];
|
|
183
|
+
for (let b = 0.5; b < michigan - 0.5; b += 1)
|
|
184
|
+
kit.box(cornice, at(-0.7, b), east, 0.25, -0.15, 0.15, h.roof + lip, h.top);
|
|
185
|
+
for (let a = 0.5; a < adams - 0.5; a += 1)
|
|
186
|
+
kit.box(cornice, at(a, -0.7), south, 0.25, -0.15, 0.15, h.roof + lip, h.top);
|
|
187
|
+
const skin = (plan, keep, from, to, seed, openings, plain) => {
|
|
188
|
+
const span = rows.filter((row) => row.lo >= from - 1e-9 && row.hi <= to + 1e-9), heights = [span[0].lo, ...span.map((row) => row.hi)];
|
|
189
|
+
const paints = { wall: plain, granite, band, shadow };
|
|
190
|
+
chainsOf(plan).forEach((chain, index) => {
|
|
191
|
+
if (!chain.runs.every((run) => keep(plan[run])))
|
|
192
|
+
return;
|
|
193
|
+
const first = chain.runs[0], last = chain.runs.at(-1);
|
|
194
|
+
const s0 = turnsAt(plan, first)[0] > 0 ? 0.09 : 0.02, s1 = chain.length - (turnsAt(plan, last)[1] > 0 ? 0.09 : 0.02);
|
|
195
|
+
const [columns, cell] = openings(chain.length);
|
|
196
|
+
chainSkin(kit, wall, plan, chain, 0, chain.runs.length - 1, s0, s1, columns, heights, 0.02, 0.07, (bay, r) => {
|
|
197
|
+
const row = span[r], shows = cell((columns[bay - 1] + columns[bay]) / 2, row);
|
|
198
|
+
return shows === "glass" ? paneColor(row.floor, bay, seed * 8 + index) : paints[shows];
|
|
199
|
+
}, plain);
|
|
200
|
+
});
|
|
201
|
+
};
|
|
202
|
+
const near = (list, x) => Math.min(...list.map((c) => Math.abs(x - c)));
|
|
203
|
+
const columnsOf = (length, cuts) => [0, ...[...new Set(cuts.map((c) => Math.round(c * 1e4) / 1e4))].sort((p, q) => p - q), length];
|
|
204
|
+
// A street front: corner piers 6 m to the first column's middle, each with a window 1.7 m
|
|
205
|
+
// wide 2.6 m from its corner; `count` bays between, each column 1.6 m wide. A bay holds a
|
|
206
|
+
// pair of windows 1.2 m wide either side of a 38 cm mullion, the colonnade's opening, and
|
|
207
|
+
// an attic window 1.7 m wide; a pendant hangs over each column.
|
|
208
|
+
const front = (count) => (length) => {
|
|
209
|
+
const pitch = (length - 12) / count, lines = Array.from({ length: count + 1 }, (_, k) => 6 + k * pitch);
|
|
210
|
+
const centres = lines.slice(1).map((line) => line - pitch / 2), piers = [2.6, length - 2.6];
|
|
211
|
+
const cuts = [...lines.flatMap((c) => [-0.9, -0.8, -0.45, 0.45, 0.8, 0.9].map((d) => c + d)), ...centres.flatMap((c) => [-1.39, -0.85, -0.19, 0.19, 0.85, 1.39].map((d) => c + d)), ...piers.flatMap((c) => [c - 0.85, c + 0.85])];
|
|
212
|
+
return [columnsOf(length, cuts), (mid, row) => {
|
|
213
|
+
const inside = mid > lines[0] && mid < lines.at(-1), line = near(lines, mid), centre = near(centres, mid);
|
|
214
|
+
const pierWindow = near(piers, mid) < 0.85;
|
|
215
|
+
switch (row.kind) {
|
|
216
|
+
case "granite": return "granite";
|
|
217
|
+
case "shop": return inside && line > 0.8 ? "glass" : "granite";
|
|
218
|
+
case "belt":
|
|
219
|
+
case "band": return "band";
|
|
220
|
+
case "glass": return (inside && centre > 0.19 && centre < 1.39) || pierWindow ? "glass" : "wall";
|
|
221
|
+
case "rail": return row.pier && pierWindow ? "glass" : inside && line > 0.8 ? "band" : "wall";
|
|
222
|
+
case "open": return (row.pier && pierWindow) || (inside && line > 0.8) ? "glass" : "wall";
|
|
223
|
+
case "attic": return row.reach !== undefined && line < row.reach ? "shadow" : (inside && centre < 0.85) || pierWindow ? "glass" : "wall";
|
|
224
|
+
case "pendant": return line < row.reach ? "shadow" : "wall";
|
|
225
|
+
case "frieze": return "shadow";
|
|
226
|
+
default: return "wall";
|
|
227
|
+
}
|
|
228
|
+
}];
|
|
229
|
+
};
|
|
230
|
+
// The alley's and the court's windows, which the drawing does not show: 1.6 m wide, spread
|
|
231
|
+
// evenly about 3.2 m apart, one to each floor from the third.
|
|
232
|
+
const spread = (length) => {
|
|
233
|
+
const count = Math.max(1, Math.round(length / 3.2)), centres = Array.from({ length: count }, (_, k) => length * (k + 0.5) / count);
|
|
234
|
+
return [columnsOf(length, centres.flatMap((c) => [c - 0.8, c + 0.8])), (mid, row) => ((row.kind === "glass" || row.kind === "attic" || row.pier === true) && near(centres, mid) < 0.8 ? "glass" : "wall")];
|
|
235
|
+
};
|
|
236
|
+
const facing = (x, z) => (run) => run.normal(0)[0] * x + run.normal(0)[1] * z > 0.9;
|
|
237
|
+
// The fronts to the cornice, the alley to the roof; the north wall, shared with the Lake
|
|
238
|
+
// View Building, stays plain.
|
|
239
|
+
skin(planOf(lot), facing(1, 0), rows[0].lo, h.cornice, 0, front(11), stone);
|
|
240
|
+
skin(planOf(lot), facing(0, 1), rows[0].lo, h.cornice, 1, front(9), stone);
|
|
241
|
+
skin(planOf(lot), facing(-1, 0), rows[0].lo, h.roof, 2, spread, stone);
|
|
242
|
+
skin(planOf(lower), () => true, h.court, h.inner, 3, spread, courtBrick);
|
|
243
|
+
skin(planOf(upper), () => true, h.inner, h.roof, 4, spread, courtBrick);
|
|
244
|
+
skin(planOf(opening), (run) => !has(lotEdges, run), h.court, h.roof, 5, spread, courtBrick);
|
|
245
|
+
const model = kit.finish({ height: h.top, outlines: [shell, cornice], opacity: 0.16 });
|
|
246
|
+
model.building.position.set(offset[0], 0, offset[1]);
|
|
247
|
+
model.building.userData["geography"] = record;
|
|
248
|
+
model.building.userData["geographicDetail"] = { levels: h, source: "docs/peoples-gas-reference.md" };
|
|
249
|
+
return model;
|
|
250
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
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 Railway Exchange Building, 224 South Michigan Avenue (Frederick P. Dinkelberg of D. H.
|
|
5
|
+
// Burnham & Co., 1904): the geographic layout's model, on the mapped outline 124873931. The
|
|
6
|
+
// original layout has no model of it; the drawing shows it at Michigan and Jackson, left of
|
|
7
|
+
// the Borg-Warner Building.
|
|
8
|
+
//
|
|
9
|
+
// White terracotta fronts on Michigan and Jackson rise seventeen storeys in eleven bays of
|
|
10
|
+
// paired windows, with a belt under the fourteenth floor. Round windows fill the frieze on
|
|
11
|
+
// the seventeenth, under a projecting cornice and a hipped copper roof. The cornice is the
|
|
12
|
+
// City's 235 ft and the roof's top the Skyscraper Center's 259 ft: each is the published
|
|
13
|
+
// height nearest its drawn reading. The drawn cornice reads 73.7 m, so heights read on the
|
|
14
|
+
// drawing are scaled to meet it; see docs/railway-exchange-reference.md. Units are meters;
|
|
15
|
+
// +x is east, +z is south.
|
|
16
|
+
const drawn = (height) => height * 71.6 / 73.7;
|
|
17
|
+
export const railwayExchangeLevels = Object.freeze({
|
|
18
|
+
belt: drawn(54.6), // the belt under the fourteenth floor
|
|
19
|
+
frieze: drawn(67.4), // the frieze and its round windows
|
|
20
|
+
cornice: drawn(71.8), // the projecting cornice
|
|
21
|
+
eaves: 71.6, // the cornice's top and the roof's foot, the City's 235 ft
|
|
22
|
+
top: 78.9, // the roof's top, the Skyscraper Center's 259 ft
|
|
23
|
+
});
|
|
24
|
+
const h = railwayExchangeLevels;
|
|
25
|
+
// The skins' colours: the window tones, and the terracotta, the belt and the plain walls,
|
|
26
|
+
// which no window shares.
|
|
27
|
+
export const railwayExchangePalette = Object.freeze({ glass: [0x2c2c2c, 0x303030, 0x343434, 0x383838], lit: 0x6e6e6e, dim: 0x3c3c3c, terracotta: 0x808080, band: 0x909090, plain: 0x626262 });
|
|
28
|
+
const palette = railwayExchangePalette;
|
|
29
|
+
const color = (hex) => new THREE.Color(hex);
|
|
30
|
+
const glassTones = palette.glass.map(color);
|
|
31
|
+
const litGlass = color(palette.lit), dimGlass = color(palette.dim);
|
|
32
|
+
const terracotta = color(palette.terracotta), band = color(palette.band), plain = color(palette.plain);
|
|
33
|
+
const core = color(0x262626), roofing = color(0x3c3c3c), copper = color(0x9c9c9c);
|
|
34
|
+
function paneColor(row, bay, wall) {
|
|
35
|
+
const hash = (Math.imul(row + 71, 0x9e3779b1) ^ Math.imul(bay + 101, 0x85ebca77) ^ Math.imul(wall + 83, 0xc2b2ae3d)) >>> 0;
|
|
36
|
+
const value = hash % 101;
|
|
37
|
+
if (value < 4)
|
|
38
|
+
return litGlass;
|
|
39
|
+
if (value < 8)
|
|
40
|
+
return dimGlass;
|
|
41
|
+
return glassTones[value % glassTones.length];
|
|
42
|
+
}
|
|
43
|
+
const rows = [{ lo: 0.25, hi: drawn(0.6), kind: "base", floor: 1 }];
|
|
44
|
+
const push = (hi, kind, floor, dy) => rows.push({ lo: rows.at(-1).hi, hi: drawn(hi), kind, floor, ...(dy === undefined ? {} : { dy }) });
|
|
45
|
+
push(7.4, "shop", 1);
|
|
46
|
+
const sill = (n) => (n <= 13 ? 51.53 - (13 - n) * 3.843 : 55.8 + (n - 14) * 3.85);
|
|
47
|
+
for (let n = 2; n <= 16; n += 1) {
|
|
48
|
+
if (n === 14) {
|
|
49
|
+
push(54.6, "wall", 13);
|
|
50
|
+
push(55.4, "band", 13);
|
|
51
|
+
}
|
|
52
|
+
push(sill(n), "wall", n);
|
|
53
|
+
push(sill(n) + 2.6, "glass", n);
|
|
54
|
+
}
|
|
55
|
+
push(67.4, "wall", 16);
|
|
56
|
+
push(67.8, "frieze", 17);
|
|
57
|
+
for (const [lo, hi] of [[67.8, 68.3], [68.3, 68.8], [68.8, 69.3], [69.3, 69.8], [69.8, 70.3]])
|
|
58
|
+
push(hi, "oculus", 17, Math.abs((lo + hi) / 2 - 69.05));
|
|
59
|
+
push(71.8, "frieze", 17);
|
|
60
|
+
// Over the cornice's foot, the plain walls' last row to the eaves.
|
|
61
|
+
rows.push({ lo: rows.at(-1).hi, hi: h.eaves, kind: "frieze", floor: 17 });
|
|
62
|
+
export function createRailwayExchangeGeographicBuilding(record, projectPlan, offset) {
|
|
63
|
+
const kit = createBuilder(record.name, record.id);
|
|
64
|
+
const shell = kit.batch("Railway Exchange Building · shell", kit.material(0xffffff, { vertexColors: true }));
|
|
65
|
+
const wall = kit.batch("Railway Exchange Building · terracotta and windows", kit.material(0xffffff, { vertexColors: true }));
|
|
66
|
+
const cornice = kit.batch("Railway Exchange Building · cornice", kit.material(0x8a8a8a));
|
|
67
|
+
const lot = orient(polygonOf(projectPlan(record.footprint.coordinates)));
|
|
68
|
+
// The lot's south-east corner, on Michigan and Jackson, with the Jackson front running
|
|
69
|
+
// west and the Michigan front north to Symphony Center.
|
|
70
|
+
const corner = lot.reduce((best, p) => (p[0] + p[1] > best[0] + best[1] ? p : best));
|
|
71
|
+
const northEast = lot.reduce((best, p) => (p[0] - p[1] > best[0] - best[1] ? p : best));
|
|
72
|
+
const southWest = lot.reduce((best, p) => (p[1] - p[0] > best[1] - best[0] ? p : best));
|
|
73
|
+
const northWest = lot.reduce((best, p) => (-p[0] - p[1] > -best[0] - best[1] ? p : best));
|
|
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(southWest), 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
|
+
const skew = west[0] * north[1] - west[1] * north[0];
|
|
78
|
+
const inLot = (p) => {
|
|
79
|
+
const [x, z] = [p[0] - corner[0], p[1] - corner[1]];
|
|
80
|
+
return [(x * north[1] - z * north[0]) / skew, (west[0] * z - west[1] * x) / skew];
|
|
81
|
+
};
|
|
82
|
+
const [jackson, michigan] = [inLot(southWest)[0], inLot(northEast)[1]];
|
|
83
|
+
const lift = ([x, z], y) => [x, y, z];
|
|
84
|
+
// The walls to the roof's foot.
|
|
85
|
+
planOf(lot).forEach((run) => {
|
|
86
|
+
const a = run.at(0), b = run.at(run.length), n = run.normal(0);
|
|
87
|
+
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);
|
|
88
|
+
});
|
|
89
|
+
paintedSlab(kit, shell, lot, 0, false, roofing);
|
|
90
|
+
paintedSlab(kit, shell, lot, h.eaves, true, roofing);
|
|
91
|
+
// The roof: a closed solid on the lot's four corners, 10 cm inside the lines between them,
|
|
92
|
+
// which the Jackson wall stands up to 6.7 cm inside, so that no face lies in the walls'.
|
|
93
|
+
// It rises from a foot 10 cm inside the block, up the eaves, to a flat top 10 m in from
|
|
94
|
+
// each side over the light well, as the photograph shows; the drawing simplifies it to a
|
|
95
|
+
// pyramid. Its slopes are in two triangles each, since the lot is out of square.
|
|
96
|
+
const northWestIn = inLot(northWest);
|
|
97
|
+
const ring = (inset) => orient([at(inset, inset), at(jackson - inset, inset), at(northWestIn[0] - inset, northWestIn[1] - inset), at(inset, michigan - inset)]);
|
|
98
|
+
const [eaves, crown] = [ring(0.1), ring(10)], foot = h.eaves - 0.1;
|
|
99
|
+
planOf(eaves).forEach((run) => {
|
|
100
|
+
const p = run.at(0), q = run.at(run.length), n = run.normal(0);
|
|
101
|
+
kit.quad(shell, [lift(p, foot), lift(q, foot), lift(q, h.eaves), lift(p, h.eaves)], [[n[0], 0, n[1]]], copper);
|
|
102
|
+
});
|
|
103
|
+
paintedSlab(kit, shell, eaves, foot, false, roofing);
|
|
104
|
+
paintedSlab(kit, shell, crown, h.top, true, copper);
|
|
105
|
+
// Each slope's outward normal, from its points and the roof's middle.
|
|
106
|
+
const middle = lift(at(jackson / 2, michigan / 2), (h.eaves + h.top) / 2);
|
|
107
|
+
const face = (points) => {
|
|
108
|
+
const [a, b, c] = points;
|
|
109
|
+
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();
|
|
110
|
+
const mid = [0, 1, 2].map((k) => (a[k] + b[k] + c[k]) / 3);
|
|
111
|
+
if (n.x * (mid[0] - middle[0]) + n.y * (mid[1] - middle[1]) + n.z * (mid[2] - middle[2]) < 0)
|
|
112
|
+
n.negate();
|
|
113
|
+
const normal = [n.x, n.y, n.z];
|
|
114
|
+
kit.triangle(shell, points, [normal, normal, normal], copper);
|
|
115
|
+
};
|
|
116
|
+
eaves.forEach((p, i) => {
|
|
117
|
+
const q = eaves[(i + 1) % 4], [r, s] = [crown[i], crown[(i + 1) % 4]];
|
|
118
|
+
face([lift(p, h.eaves), lift(q, h.eaves), lift(s, h.top)]);
|
|
119
|
+
face([lift(p, h.eaves), lift(s, h.top), lift(r, h.top)]);
|
|
120
|
+
});
|
|
121
|
+
// The cornice projects 90 cm along both fronts, from 12 cm inside the lines between their
|
|
122
|
+
// corners to 2 cm over the eaves, stopping 3 cm short of the north and alley walls.
|
|
123
|
+
kit.prism(cornice, planOf(orient([at(-0.9, michigan - 0.03), at(-0.9, -0.9), at(jackson - 0.03, -0.9), at(jackson - 0.03, 0.12), at(0.12, 0.12), at(0.12, michigan - 0.03)])), [h.cornice, h.eaves + 0.02]);
|
|
124
|
+
const paints = { wall: terracotta, band, plain };
|
|
125
|
+
const skin = (keep, span, seed, openings, sides) => {
|
|
126
|
+
const plan = planOf(lot), heights = [span[0].lo, ...span.map((row) => row.hi)];
|
|
127
|
+
chainsOf(plan).forEach((chain, index) => {
|
|
128
|
+
if (!chain.runs.every((run) => keep(plan[run])))
|
|
129
|
+
return;
|
|
130
|
+
const first = chain.runs[0], last = chain.runs.at(-1);
|
|
131
|
+
const s0 = turnsAt(plan, first)[0] > 0 ? 0.09 : 0.02, s1 = chain.length - (turnsAt(plan, last)[1] > 0 ? 0.09 : 0.02);
|
|
132
|
+
const [columns, cell] = openings(chain.length);
|
|
133
|
+
chainSkin(kit, wall, plan, chain, 0, chain.runs.length - 1, s0, s1, columns, heights, 0.02, 0.07, (bay, r) => {
|
|
134
|
+
const row = span[r], shows = cell((columns[bay - 1] + columns[bay]) / 2, row);
|
|
135
|
+
return shows === "glass" ? paneColor(row.floor, bay, seed * 8 + index) : paints[shows];
|
|
136
|
+
}, sides);
|
|
137
|
+
});
|
|
138
|
+
};
|
|
139
|
+
const near = (list, x) => Math.min(...list.map((c) => Math.abs(x - c)));
|
|
140
|
+
// A street front: eleven bays 4.4 m apart about its middle, each a pair of windows 1.44 m
|
|
141
|
+
// wide either side of a 47 cm mullion and a round window 2.6 m across over it, as drawn.
|
|
142
|
+
const front = (length) => {
|
|
143
|
+
const centres = Array.from({ length: 11 }, (_, k) => length / 2 + (k - 5) * 4.4);
|
|
144
|
+
const cuts = centres.flatMap((c) => [-1.675, -1.3, -0.9, -0.6, -0.235, 0.235, 0.6, 0.9, 1.3, 1.675].map((d) => c + d));
|
|
145
|
+
return [[0, ...cuts, length], (mid, row) => {
|
|
146
|
+
const dx = near(centres, mid);
|
|
147
|
+
if (row.kind === "band")
|
|
148
|
+
return "band";
|
|
149
|
+
if (row.kind === "shop")
|
|
150
|
+
return dx < 1.675 ? "glass" : "wall";
|
|
151
|
+
if (row.kind === "glass")
|
|
152
|
+
return dx > 0.235 && dx < 1.675 ? "glass" : "wall";
|
|
153
|
+
if (row.kind === "oculus")
|
|
154
|
+
return dx * dx + row.dy * row.dy <= 1.3 * 1.3 ? "glass" : "wall";
|
|
155
|
+
return "wall";
|
|
156
|
+
}];
|
|
157
|
+
};
|
|
158
|
+
// The alley's windows, which the drawing does not show: 1.6 m wide, spread evenly about
|
|
159
|
+
// 3.2 m apart, one to each floor above the ground floor, the seventeenth's as tall as the
|
|
160
|
+
// round windows' band.
|
|
161
|
+
const alley = (length) => {
|
|
162
|
+
const count = Math.max(1, Math.round(length / 3.2)), centres = Array.from({ length: count }, (_, k) => length * (k + 0.5) / count);
|
|
163
|
+
return [[0, ...centres.flatMap((c) => [c - 0.8, c + 0.8]), length], (mid, row) => ((row.kind === "glass" || row.kind === "oculus") && near(centres, mid) < 0.8 ? "glass" : "plain")];
|
|
164
|
+
};
|
|
165
|
+
const facing = (x, z) => (run) => run.normal(0)[0] * x + run.normal(0)[1] * z > 0.9;
|
|
166
|
+
// Michigan and Jackson carry the fronts; the alley has windows; the north wall, shared
|
|
167
|
+
// with Symphony Center, stays plain.
|
|
168
|
+
skin((run) => facing(1, 0)(run) || facing(0, 1)(run), rows.slice(0, -1), 0, front, terracotta);
|
|
169
|
+
skin(facing(-1, 0), rows, 1, alley, plain);
|
|
170
|
+
skin(facing(0, -1), rows, 2, (length) => [[0, length], () => "plain"], plain);
|
|
171
|
+
const model = kit.finish({ height: h.top, outlines: [shell, cornice], opacity: 0.16 });
|
|
172
|
+
model.building.position.set(offset[0], 0, offset[1]);
|
|
173
|
+
model.building.userData["geography"] = record;
|
|
174
|
+
model.building.userData["geographicDetail"] = { levels: h, source: "docs/railway-exchange-reference.md" };
|
|
175
|
+
return model;
|
|
176
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
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
|
+
// River Plaza, 405 North Wabash Avenue (Ezra Gordon–Jack M. Levin and Associates, 1977): the
|
|
5
|
+
// geographic layout's model, on the mapped outline 285867424 and its parts. The original
|
|
6
|
+
// layout has no model of it. From the photograph's camera its top stands behind Two Illinois
|
|
7
|
+
// Center, in the gap between Two Prudential Plaza and Aon Center that the drawing's
|
|
8
|
+
// `office-west-of-aon` group paints, the only mapped building on that sightline tall enough
|
|
9
|
+
// to reach the drawn top.
|
|
10
|
+
//
|
|
11
|
+
// A white concrete slab of 56 storeys with notched corners: a grid of exposed frame and
|
|
12
|
+
// punched windows over a low podium on the river side, to a flat roof at OpenStreetMap's 156 m
|
|
13
|
+
// under a box to the published 159.7 m. See docs/river-plaza-two-illinois-center-reference.md. Units are
|
|
14
|
+
// meters; +x is east, +z is south.
|
|
15
|
+
export const riverPlazaLevels = Object.freeze({
|
|
16
|
+
podium: 8, // the podium's roof, OpenStreetMap's
|
|
17
|
+
floors: 56,
|
|
18
|
+
roof: 156, // the slab's roof, OpenStreetMap's
|
|
19
|
+
box: 159.7, // the rooftop box's top, the published height and tip, the Skyscraper Center's 524 ft
|
|
20
|
+
bay: 1.6, // the frame's bays, an estimate from photographs
|
|
21
|
+
});
|
|
22
|
+
const h = riverPlazaLevels;
|
|
23
|
+
const pitch = h.roof / h.floors;
|
|
24
|
+
// The skins' colours: the window tones, and the concrete, which no window shares.
|
|
25
|
+
export const riverPlazaPalette = Object.freeze({ glass: [0x2c2c2c, 0x303030, 0x343434, 0x282828], lit: 0x8a8a8a, dim: 0x505050, concrete: 0x9a9a9a });
|
|
26
|
+
const palette = riverPlazaPalette;
|
|
27
|
+
const color = (hex) => new THREE.Color(hex);
|
|
28
|
+
const glassTones = palette.glass.map(color);
|
|
29
|
+
const litGlass = color(palette.lit), dimGlass = color(palette.dim), concrete = color(palette.concrete);
|
|
30
|
+
const core = color(0x6a6a6a), roofing = color(0x8a8a8a);
|
|
31
|
+
function paneColor(row, bay, wall) {
|
|
32
|
+
const hash = (Math.imul(row + 19, 0x9e3779b1) ^ Math.imul(bay + 37, 0x85ebca77) ^ Math.imul(wall + 29, 0xc2b2ae3d)) >>> 0;
|
|
33
|
+
const value = hash % 101;
|
|
34
|
+
if (value < 12)
|
|
35
|
+
return litGlass;
|
|
36
|
+
if (value < 18)
|
|
37
|
+
return dimGlass;
|
|
38
|
+
return glassTones[value % glassTones.length];
|
|
39
|
+
}
|
|
40
|
+
export function createRiverPlazaGeographicBuilding(record, projectPlan, offset) {
|
|
41
|
+
const kit = createBuilder(record.name, record.id);
|
|
42
|
+
const shell = kit.batch("River Plaza · shell", kit.material(0xffffff, { vertexColors: true }));
|
|
43
|
+
const wall = kit.batch("River Plaza · concrete and windows", kit.material(0xffffff, { vertexColors: true }));
|
|
44
|
+
const box = kit.batch("River Plaza · rooftop box", kit.material(0x9a9a9a));
|
|
45
|
+
const [tower, podium, top] = record.parts.map((part) => orient(polygonOf(projectPlan(part.coordinates))));
|
|
46
|
+
// The slab and the podium, each a closed shell on its mapped part.
|
|
47
|
+
for (const [plan, height] of [[tower, h.roof], [podium, h.podium]]) {
|
|
48
|
+
planOf(plan).forEach((run) => {
|
|
49
|
+
const a = run.at(0), b = run.at(run.length), n = run.normal(0);
|
|
50
|
+
kit.quad(shell, [[a[0], 0, a[1]], [b[0], 0, b[1]], [b[0], height, b[1]], [a[0], height, a[1]]], [[n[0], 0, n[1]]], core);
|
|
51
|
+
});
|
|
52
|
+
paintedSlab(kit, shell, plan, 0, false, roofing);
|
|
53
|
+
paintedSlab(kit, shell, plan, height, true, roofing);
|
|
54
|
+
}
|
|
55
|
+
// The box on the roof, on its mapped part.
|
|
56
|
+
kit.prism(box, planOf(top), [h.roof, h.box]);
|
|
57
|
+
// The slab's skin, one box to a run of a chain, held 2 cm clear of the chain's ends or 9 cm
|
|
58
|
+
// where the outline turns in: on each floor, a window 1.1 m wide and 1.5 m tall in each
|
|
59
|
+
// 1.6 m bay of the frame, from the chain's middle out. A run the podium stands against, its
|
|
60
|
+
// two ends on the podium's outline, starts over the podium's roof; a chain whose runs differ
|
|
61
|
+
// is skinned in parts, which meet at their joint.
|
|
62
|
+
const plan = planOf(tower);
|
|
63
|
+
const edges = planOf(podium);
|
|
64
|
+
const onEdge = (p) => edges.some((edge) => {
|
|
65
|
+
const a = edge.at(0), b = edge.at(edge.length), t = Math.max(0, Math.min(1, ((p[0] - a[0]) * (b[0] - a[0]) + (p[1] - a[1]) * (b[1] - a[1])) / (edge.length * edge.length)));
|
|
66
|
+
return Math.hypot(p[0] - a[0] - t * (b[0] - a[0]), p[1] - a[1] - t * (b[1] - a[1])) < 0.05;
|
|
67
|
+
});
|
|
68
|
+
const covered = (run) => onEdge(run.at(0)) && onEdge(run.at(run.length));
|
|
69
|
+
chainsOf(plan).forEach((chain, index) => {
|
|
70
|
+
const first = chain.runs[0], last = chain.runs.at(-1);
|
|
71
|
+
const s0 = turnsAt(plan, first)[0] > 0 ? 0.09 : 0.02, s1 = chain.length - (turnsAt(plan, last)[1] > 0 ? 0.09 : 0.02);
|
|
72
|
+
const count = Math.floor(chain.length / 2 / h.bay);
|
|
73
|
+
const centres = Array.from({ length: 2 * count }, (_, k) => chain.length / 2 + (k - count + 0.5) * h.bay);
|
|
74
|
+
// The chain's parts, each a span of runs the podium covers alike.
|
|
75
|
+
const parts = [];
|
|
76
|
+
chain.runs.forEach((run, k) => { const part = parts.at(-1); if (part && covered(plan[run]) === covered(plan[chain.runs[part[0]]]))
|
|
77
|
+
part[1] = k;
|
|
78
|
+
else
|
|
79
|
+
parts.push([k, k]); });
|
|
80
|
+
for (const [k0, k1] of parts) {
|
|
81
|
+
const base = covered(plan[chain.runs[k0]]) ? h.podium : 0.25;
|
|
82
|
+
const from = k0 === 0 ? s0 : chain.starts[k0], to = k1 === chain.runs.length - 1 ? s1 : chain.starts[k1] + plan[chain.runs[k1]].length;
|
|
83
|
+
const heights = [base];
|
|
84
|
+
for (let n = Math.ceil(base / pitch); n < h.floors; n += 1) {
|
|
85
|
+
const sill = n * pitch + 0.75;
|
|
86
|
+
if (sill > base + 0.2)
|
|
87
|
+
heights.push(sill, sill + 1.5);
|
|
88
|
+
}
|
|
89
|
+
heights.push(h.roof);
|
|
90
|
+
const cuts = centres.flatMap((c) => [c - 0.55, c + 0.55]).filter((s) => s > from + 0.01 && s < to - 0.01);
|
|
91
|
+
const columns = [from, ...cuts, to];
|
|
92
|
+
chainSkin(kit, wall, plan, chain, k0, k1, from, to, columns, heights, 0.02, 0.07, (bay, r) => {
|
|
93
|
+
const middle = (columns[bay - 1] + columns[bay]) / 2;
|
|
94
|
+
const glazed = r % 2 === 1 && centres.some((c) => Math.abs(middle - c) < 0.55);
|
|
95
|
+
return glazed ? paneColor(r, bay, index) : concrete;
|
|
96
|
+
}, concrete);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
const model = kit.finish({ height: h.box, outlines: [shell, box], opacity: 0.16 });
|
|
100
|
+
model.building.position.set(offset[0], 0, offset[1]);
|
|
101
|
+
model.building.userData["geography"] = record;
|
|
102
|
+
model.building.userData["geographicDetail"] = { levels: h, source: "docs/river-plaza-two-illinois-center-reference.md" };
|
|
103
|
+
return model;
|
|
104
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
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 Sheraton Grand Chicago Riverwalk, 301 East North Water Street (Solomon Cordwell Buenz,
|
|
5
|
+
// 1992): the geographic layout's model, on the mapped tower 592122464 and its corner part
|
|
6
|
+
// 1269924307. The original layout has no model of it. The drawing's `buckingham-east` group,
|
|
7
|
+
// right of The Buckingham, is its corner: a lit crown over a tower of windows.
|
|
8
|
+
//
|
|
9
|
+
// A cream precast tower on an L, its arms ending in round towers, of punched windows at the
|
|
10
|
+
// published 2.67 m floor-to-floor. The corner's round tower rises two floors over the arms,
|
|
11
|
+
// and each round end carries an open drum of maroon fins, floodlit at night. Heights stand on
|
|
12
|
+
// the Skyscraper Center's datum, its top floor at 97 m and the corner drum's top at 112.3 m;
|
|
13
|
+
// its 112.8 m tip, 50 cm higher, is not modelled. See docs/sheraton-grand-reference.md.
|
|
14
|
+
// Units are meters; +x is east, +z is south.
|
|
15
|
+
export const sheratonLevels = Object.freeze({
|
|
16
|
+
pitch: 2.67, // Emporis's floor-to-floor, 8.75 ft
|
|
17
|
+
topFloor: 97, // the Skyscraper Center's occupied height, the corner's top floor
|
|
18
|
+
corner: 100.1, // the corner's roof, a floor-to-roof of 3.1 m over it as Emporis gives it
|
|
19
|
+
arms: 100.1 - 2 * 2.67, // the arms' roof, two floors lower, at the mapped arms' 32 levels
|
|
20
|
+
drum: 12.2, // the drums' height, Emporis's architectural height over its main roof
|
|
21
|
+
top: 112.3, // the corner drum's top, the Skyscraper Center's architectural height
|
|
22
|
+
});
|
|
23
|
+
const h = sheratonLevels;
|
|
24
|
+
// The round ends the drums stand on, circles fitted to the mapped parts' arcs within 11 cm:
|
|
25
|
+
// the corner, part 1269924307, and the east and north arms' ends, parts 1269924306 and
|
|
26
|
+
// 1269924308. Centres are east and north metres from Crain's centre.
|
|
27
|
+
export const sheratonDrums = Object.freeze([
|
|
28
|
+
{ centre: [407.44, 465.95], radius: 7.97, base: h.corner },
|
|
29
|
+
{ centre: [439.52, 462.06], radius: 7.37, base: h.arms },
|
|
30
|
+
{ centre: [409.75, 498.32], radius: 7.39, base: h.arms },
|
|
31
|
+
]);
|
|
32
|
+
// The skins' colours: the window tones, the precast, and the drums' cream and maroon fins,
|
|
33
|
+
// which no window shares.
|
|
34
|
+
export const sheratonPalette = Object.freeze({ glass: [0x2a2a2a, 0x2e2e2e, 0x323232, 0x262626], lit: 0x8a8a8a, dim: 0x505050, precast: 0x848484, drum: 0xb4b4b4, fin: 0x5a5a5a });
|
|
35
|
+
const palette = sheratonPalette;
|
|
36
|
+
const color = (hex) => new THREE.Color(hex);
|
|
37
|
+
const glassTones = palette.glass.map(color);
|
|
38
|
+
const litGlass = color(palette.lit), dimGlass = color(palette.dim), precast = color(palette.precast), drumFace = color(palette.drum), fin = color(palette.fin);
|
|
39
|
+
const core = color(0x585858), roofing = color(0x686868);
|
|
40
|
+
function paneColor(row, bay, wall) {
|
|
41
|
+
const hash = (Math.imul(row + 17, 0x9e3779b1) ^ Math.imul(bay + 89, 0x85ebca77) ^ Math.imul(wall + 37, 0xc2b2ae3d)) >>> 0;
|
|
42
|
+
const value = hash % 101;
|
|
43
|
+
if (value < 14)
|
|
44
|
+
return litGlass;
|
|
45
|
+
if (value < 20)
|
|
46
|
+
return dimGlass;
|
|
47
|
+
return glassTones[value % glassTones.length];
|
|
48
|
+
}
|
|
49
|
+
// A wall's rows from `base` to `top`: a window 1.5 m tall 80 cm over each floor line, the
|
|
50
|
+
// floors counted down from the top floor at the published pitch, from the last floor line
|
|
51
|
+
// under the base, whose window may clear it.
|
|
52
|
+
function rowsFor(base, top) {
|
|
53
|
+
const heights = [base];
|
|
54
|
+
for (let level = h.topFloor - Math.ceil((h.topFloor - base) / h.pitch) * h.pitch; level < top; level += h.pitch) {
|
|
55
|
+
if (level + 0.8 > base + 0.2 && level + 2.3 < top - 0.2)
|
|
56
|
+
heights.push(level + 0.8, level + 2.3);
|
|
57
|
+
}
|
|
58
|
+
heights.push(top);
|
|
59
|
+
return heights;
|
|
60
|
+
}
|
|
61
|
+
export function createSheratonGrandGeographicBuilding(record, projectPlan, offset) {
|
|
62
|
+
const kit = createBuilder(record.name, record.id);
|
|
63
|
+
const shell = kit.batch("Sheraton Grand · shell", kit.material(0xffffff, { vertexColors: true }));
|
|
64
|
+
const wall = kit.batch("Sheraton Grand · precast and windows", kit.material(0xffffff, { vertexColors: true }));
|
|
65
|
+
const drums = kit.batch("Sheraton Grand · drums", kit.material(0xffffff, { vertexColors: true }));
|
|
66
|
+
const tower = orient(polygonOf(projectPlan(record.parts[0].coordinates)));
|
|
67
|
+
const circle = (centre, radius, sides = 24) => orient(Array.from({ length: sides }, (_, k) => {
|
|
68
|
+
const angle = (k / sides) * Math.PI * 2;
|
|
69
|
+
return [centre[0] + radius * Math.cos(angle), -centre[1] + radius * Math.sin(angle)];
|
|
70
|
+
}));
|
|
71
|
+
const cornerPlan = orient(polygonOf(projectPlan(record.parts[1].coordinates)));
|
|
72
|
+
// A closed solid of walls and slabs on an outline, painted by its runs.
|
|
73
|
+
const solid = (target, outline, lo, hi, paint) => {
|
|
74
|
+
planOf(outline).forEach((run, k) => {
|
|
75
|
+
const a = run.at(0), b = run.at(run.length), n = run.normal(0);
|
|
76
|
+
kit.quad(target, [[a[0], lo, a[1]], [b[0], lo, b[1]], [b[0], hi, b[1]], [a[0], hi, a[1]]], [[n[0], 0, n[1]]], paint(k));
|
|
77
|
+
});
|
|
78
|
+
paintedSlab(kit, target, outline, lo, false, roofing);
|
|
79
|
+
paintedSlab(kit, target, outline, hi, true, roofing);
|
|
80
|
+
};
|
|
81
|
+
// The tower to the arms' roof, and the corner's round tower, on its mapped part, two floors
|
|
82
|
+
// over it.
|
|
83
|
+
solid(shell, tower, 0, h.arms, () => core);
|
|
84
|
+
solid(shell, cornerPlan, h.arms, h.corner, () => core);
|
|
85
|
+
// Each drum: cream, a maroon fin every other facet, on its round end.
|
|
86
|
+
for (const { centre, radius, base } of sheratonDrums)
|
|
87
|
+
solid(drums, circle(centre, radius), base, base + h.drum, (k) => (k % 2 ? fin : drumFace));
|
|
88
|
+
// The skins, one box to a chain of runs turning less than 30°, so the round ends carry one:
|
|
89
|
+
// a window 1.4 m wide in each 2.4 m bay, set out from the chain's middle, on every floor.
|
|
90
|
+
const skin = (outline, base, top, seed) => {
|
|
91
|
+
const plan = planOf(outline), heights = rowsFor(base, top);
|
|
92
|
+
chainsOf(plan, 30 * Math.PI / 180).forEach((chain, index) => {
|
|
93
|
+
const first = chain.runs[0], last = chain.runs.at(-1);
|
|
94
|
+
const s0 = turnsAt(plan, first)[0] > 0 ? 0.09 : 0.02, s1 = chain.length - (turnsAt(plan, last)[1] > 0 ? 0.09 : 0.02);
|
|
95
|
+
const count = Math.floor((chain.length - 1.4) / 2 / 2.4);
|
|
96
|
+
const centres = Array.from({ length: 2 * count + 1 }, (_, k) => chain.length / 2 + (k - count) * 2.4);
|
|
97
|
+
const cuts = centres.flatMap((c) => [c - 0.7, c + 0.7]).filter((s) => s > s0 + 0.01 && s < s1 - 0.01);
|
|
98
|
+
const columns = [s0, ...cuts, s1];
|
|
99
|
+
chainSkin(kit, wall, plan, chain, 0, chain.runs.length - 1, s0, s1, columns, heights, 0.02, 0.07, (bay, r) => {
|
|
100
|
+
const middle = (columns[bay - 1] + columns[bay]) / 2;
|
|
101
|
+
return r % 2 === 1 && centres.some((c) => Math.abs(middle - c) < 0.7) ? paneColor(r, bay, seed * 16 + index) : precast;
|
|
102
|
+
}, precast);
|
|
103
|
+
});
|
|
104
|
+
};
|
|
105
|
+
skin(tower, 0.25, h.arms, 0);
|
|
106
|
+
skin(cornerPlan, h.arms, h.corner, 1);
|
|
107
|
+
const model = kit.finish({ height: h.top, outlines: [shell, drums], opacity: 0.16 });
|
|
108
|
+
model.building.position.set(offset[0], 0, offset[1]);
|
|
109
|
+
model.building.userData["geography"] = record;
|
|
110
|
+
model.building.userData["geographicDetail"] = { levels: h, source: "docs/sheraton-grand-reference.md" };
|
|
111
|
+
return model;
|
|
112
|
+
}
|