@a2f0/skyline 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE.md +20 -0
- package/README.md +52 -2
- package/docs/package.md +222 -0
- package/docs/skyline-geography.md +503 -0
- package/lib/models/aon-center.d.ts +4 -0
- package/lib/models/aon-center.js +60 -0
- package/lib/models/aon-geographic.d.ts +9 -0
- package/lib/models/aon-geographic.js +35 -0
- package/lib/models/aon-tower.d.ts +23 -0
- package/lib/models/aon-tower.js +130 -0
- package/lib/models/blue-cross-geographic.d.ts +14 -0
- package/lib/models/blue-cross-geographic.js +248 -0
- package/lib/models/borg-warner-geographic.d.ts +20 -0
- package/lib/models/borg-warner-geographic.js +143 -0
- package/lib/models/buckingham-geographic.d.ts +10 -0
- package/lib/models/buckingham-geographic.js +121 -0
- package/lib/models/building-kit.d.ts +84 -0
- package/lib/models/building-kit.js +358 -0
- package/lib/models/celebrations.d.ts +39 -0
- package/lib/models/celebrations.js +44 -0
- package/lib/models/chicago-athletic-association-geographic.d.ts +32 -0
- package/lib/models/chicago-athletic-association-geographic.js +173 -0
- package/lib/models/crain-communications.d.ts +5 -0
- package/lib/models/crain-communications.js +75 -0
- package/lib/models/crain-geographic.d.ts +10 -0
- package/lib/models/crain-geographic.js +69 -0
- package/lib/models/crain-tower.d.ts +32 -0
- package/lib/models/crain-tower.js +378 -0
- package/lib/models/facade-grid.d.ts +18 -0
- package/lib/models/facade-grid.js +176 -0
- package/lib/models/gage-geographic.d.ts +19 -0
- package/lib/models/gage-geographic.js +154 -0
- package/lib/models/heritage-at-millennium-park.d.ts +3 -0
- package/lib/models/heritage-at-millennium-park.js +284 -0
- package/lib/models/heritage-geographic.d.ts +11 -0
- package/lib/models/heritage-geographic.js +208 -0
- package/lib/models/hyatt-west-tower-geographic.d.ts +26 -0
- package/lib/models/hyatt-west-tower-geographic.js +86 -0
- package/lib/models/keith-ascher-geographic.d.ts +29 -0
- package/lib/models/keith-ascher-geographic.js +110 -0
- package/lib/models/kemper-geographic.d.ts +8 -0
- package/lib/models/kemper-geographic.js +153 -0
- package/lib/models/kemper.d.ts +2 -0
- package/lib/models/kemper.js +78 -0
- package/lib/models/lake-view-geographic.d.ts +15 -0
- package/lib/models/lake-view-geographic.js +134 -0
- package/lib/models/maclean-center-geographic.d.ts +20 -0
- package/lib/models/maclean-center-geographic.js +162 -0
- package/lib/models/michigan-boulevard-geographic.d.ts +12 -0
- package/lib/models/michigan-boulevard-geographic.js +157 -0
- package/lib/models/michigan-plaza-south-geographic.d.ts +7 -0
- package/lib/models/michigan-plaza-south-geographic.js +84 -0
- package/lib/models/michigan-plaza-south.d.ts +2 -0
- package/lib/models/michigan-plaza-south.js +68 -0
- package/lib/models/millennium-park-plaza-geographic.d.ts +9 -0
- package/lib/models/millennium-park-plaza-geographic.js +103 -0
- package/lib/models/monroe-geographic.d.ts +19 -0
- package/lib/models/monroe-geographic.js +177 -0
- package/lib/models/north-michigan-180-geographic.d.ts +14 -0
- package/lib/models/north-michigan-180-geographic.js +117 -0
- package/lib/models/north-wabash-geographic.d.ts +14 -0
- package/lib/models/north-wabash-geographic.js +122 -0
- package/lib/models/on-the-park-geographic.d.ts +11 -0
- package/lib/models/on-the-park-geographic.js +245 -0
- package/lib/models/one-prudential-geographic.d.ts +12 -0
- package/lib/models/one-prudential-geographic.js +55 -0
- package/lib/models/one-prudential-plaza.d.ts +5 -0
- package/lib/models/one-prudential-plaza.js +72 -0
- package/lib/models/one-prudential-tower.d.ts +45 -0
- package/lib/models/one-prudential-tower.js +246 -0
- package/lib/models/peoples-gas-geographic.d.ts +23 -0
- package/lib/models/peoples-gas-geographic.js +250 -0
- package/lib/models/railway-exchange-geographic.d.ts +18 -0
- package/lib/models/railway-exchange-geographic.js +176 -0
- package/lib/models/river-plaza-geographic.d.ts +16 -0
- package/lib/models/river-plaza-geographic.js +104 -0
- package/lib/models/sheraton-grand-geographic.d.ts +24 -0
- package/lib/models/sheraton-grand-geographic.js +112 -0
- package/lib/models/six-north-michigan-geographic.d.ts +16 -0
- package/lib/models/six-north-michigan-geographic.js +180 -0
- package/lib/models/skyline-geography-data.d.ts +44 -0
- package/lib/models/skyline-geography-data.js +607 -0
- package/lib/models/skyline-geography.d.ts +23 -0
- package/lib/models/skyline-geography.js +322 -0
- package/lib/models/swissotel-geographic.d.ts +24 -0
- package/lib/models/swissotel-geographic.js +79 -0
- package/lib/models/three-illinois-center-geographic.d.ts +26 -0
- package/lib/models/three-illinois-center-geographic.js +86 -0
- package/lib/models/trump-geographic.d.ts +14 -0
- package/lib/models/trump-geographic.js +51 -0
- package/lib/models/trump-international-tower.d.ts +4 -0
- package/lib/models/trump-international-tower.js +84 -0
- package/lib/models/trump-tower.d.ts +43 -0
- package/lib/models/trump-tower.js +229 -0
- package/lib/models/two-illinois-center-geographic.d.ts +26 -0
- package/lib/models/two-illinois-center-geographic.js +85 -0
- package/lib/models/two-prudential-geographic.d.ts +25 -0
- package/lib/models/two-prudential-geographic.js +47 -0
- package/lib/models/two-prudential-plaza.d.ts +8 -0
- package/lib/models/two-prudential-plaza.js +55 -0
- package/lib/models/two-prudential-tower.d.ts +53 -0
- package/lib/models/two-prudential-tower.js +441 -0
- package/lib/models/university-club-geographic.d.ts +24 -0
- package/lib/models/university-club-geographic.js +217 -0
- package/lib/models/willoughby-tower-geographic.d.ts +17 -0
- package/lib/models/willoughby-tower-geographic.js +195 -0
- package/lib/models/window-illumination.d.ts +14 -0
- package/lib/models/window-illumination.js +64 -0
- package/lib/package-assets.d.ts +5 -0
- package/lib/package-assets.js +9 -0
- package/lib/skyline-package.d.ts +23 -0
- package/lib/skyline-package.js +25 -0
- package/lib/skyline-scene.d.ts +25 -0
- package/lib/skyline-scene.js +86 -0
- package/lib/study-types.d.ts +49 -0
- package/lib/study-types.js +1 -0
- package/lib/vendor/three-r186.d.ts +1 -0
- package/lib/vendor/three-r186.js +1 -0
- package/package.json +64 -4
- package/site/building-detail.html +54 -0
- package/site/building-detail.js +84 -0
- package/site/building-study.html +58 -0
- package/site/building-study.js +3 -0
- package/site/index.html +407 -0
- package/site/models/aon-center.js +60 -0
- package/site/models/aon-geographic.js +35 -0
- package/site/models/aon-tower.js +130 -0
- package/site/models/blue-cross-geographic.js +248 -0
- package/site/models/borg-warner-geographic.js +143 -0
- package/site/models/buckingham-geographic.js +121 -0
- package/site/models/building-kit.js +358 -0
- package/site/models/celebrations.js +44 -0
- package/site/models/chicago-athletic-association-geographic.js +173 -0
- package/site/models/crain-communications.js +75 -0
- package/site/models/crain-geographic.js +69 -0
- package/site/models/crain-reference.svg +10 -0
- package/site/models/crain-tower.js +378 -0
- package/site/models/facade-grid.js +176 -0
- package/site/models/gage-geographic.js +154 -0
- package/site/models/heritage-at-millennium-park.js +284 -0
- package/site/models/heritage-geographic.js +208 -0
- package/site/models/hyatt-west-tower-geographic.js +86 -0
- package/site/models/keith-ascher-geographic.js +110 -0
- package/site/models/kemper-geographic.js +153 -0
- package/site/models/kemper.js +78 -0
- package/site/models/lake-view-geographic.js +134 -0
- package/site/models/maclean-center-geographic.js +162 -0
- package/site/models/michigan-boulevard-geographic.js +157 -0
- package/site/models/michigan-plaza-south-geographic.js +84 -0
- package/site/models/michigan-plaza-south.js +68 -0
- package/site/models/millennium-park-plaza-geographic.js +103 -0
- package/site/models/monroe-geographic.js +177 -0
- package/site/models/north-michigan-180-geographic.js +117 -0
- package/site/models/north-wabash-geographic.js +122 -0
- package/site/models/on-the-park-geographic.js +245 -0
- package/site/models/one-prudential-geographic.js +55 -0
- package/site/models/one-prudential-plaza.js +72 -0
- package/site/models/one-prudential-tower.js +246 -0
- package/site/models/peoples-gas-geographic.js +250 -0
- package/site/models/railway-exchange-geographic.js +176 -0
- package/site/models/river-plaza-geographic.js +104 -0
- package/site/models/sheraton-grand-geographic.js +112 -0
- package/site/models/six-north-michigan-geographic.js +180 -0
- package/site/models/skyline-geography-data.js +607 -0
- package/site/models/skyline-geography.js +322 -0
- package/site/models/skyline-panorama.svg +3491 -0
- package/site/models/skyline-reference.svg +3090 -0
- package/site/models/swissotel-geographic.js +79 -0
- package/site/models/three-illinois-center-geographic.js +86 -0
- package/site/models/trump-geographic.js +51 -0
- package/site/models/trump-international-tower.js +84 -0
- package/site/models/trump-tower.js +229 -0
- package/site/models/two-illinois-center-geographic.js +85 -0
- package/site/models/two-prudential-geographic.js +47 -0
- package/site/models/two-prudential-plaza.js +55 -0
- package/site/models/two-prudential-tower.js +441 -0
- package/site/models/university-club-geographic.js +217 -0
- package/site/models/willoughby-tower-geographic.js +195 -0
- package/site/models/window-illumination.js +64 -0
- package/site/skyline-3d.html +92 -0
- package/site/skyline-3d.js +333 -0
- package/site/skyline-animated.svg +18795 -0
- package/site/skyline-comparison.js +97 -0
- package/site/skyline-original-fit.svg +18312 -0
- package/site/skyline-scene.js +86 -0
- package/site/skyline-study.html +83 -0
- package/site/skyline-study.js +109 -0
- package/site/skyline-webgl.html +667 -0
- package/site/stars.svg +176 -0
- package/site/study-loader.js +20 -0
- package/site/study-types.js +1 -0
- package/site/study-viewer.js +605 -0
- package/site/study.css +204 -0
- package/site/vendor/THREE-LICENSE.txt +21 -0
- package/site/vendor/three-r186.js +4228 -0
|
@@ -0,0 +1,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,16 @@
|
|
|
1
|
+
import type { BuildingModel, Plan } from "./building-kit.js";
|
|
2
|
+
import type { GeoBuilding } from "./skyline-geography-data.js";
|
|
3
|
+
export declare const riverPlazaLevels: Readonly<{
|
|
4
|
+
podium: 8;
|
|
5
|
+
floors: 56;
|
|
6
|
+
roof: 156;
|
|
7
|
+
box: 159.7;
|
|
8
|
+
bay: 1.6;
|
|
9
|
+
}>;
|
|
10
|
+
export declare const riverPlazaPalette: Readonly<{
|
|
11
|
+
glass: number[];
|
|
12
|
+
lit: 9079434;
|
|
13
|
+
dim: 5263440;
|
|
14
|
+
concrete: 10132122;
|
|
15
|
+
}>;
|
|
16
|
+
export declare function createRiverPlazaGeographicBuilding(record: GeoBuilding, projectPlan: (coordinates: [number, number][]) => Plan, offset: [number, number]): BuildingModel;
|
|
@@ -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,24 @@
|
|
|
1
|
+
import type { BuildingModel, Plan, Vec2 } from "./building-kit.js";
|
|
2
|
+
import type { GeoBuilding } from "./skyline-geography-data.js";
|
|
3
|
+
export declare const sheratonLevels: Readonly<{
|
|
4
|
+
pitch: 2.67;
|
|
5
|
+
topFloor: 97;
|
|
6
|
+
corner: 100.1;
|
|
7
|
+
arms: number;
|
|
8
|
+
drum: 12.2;
|
|
9
|
+
top: 112.3;
|
|
10
|
+
}>;
|
|
11
|
+
export declare const sheratonDrums: readonly {
|
|
12
|
+
centre: Vec2;
|
|
13
|
+
radius: number;
|
|
14
|
+
base: number;
|
|
15
|
+
}[];
|
|
16
|
+
export declare const sheratonPalette: Readonly<{
|
|
17
|
+
glass: number[];
|
|
18
|
+
lit: 9079434;
|
|
19
|
+
dim: 5263440;
|
|
20
|
+
precast: 8684676;
|
|
21
|
+
drum: 11842740;
|
|
22
|
+
fin: 5921370;
|
|
23
|
+
}>;
|
|
24
|
+
export declare function createSheratonGrandGeographicBuilding(record: GeoBuilding, projectPlan: (coordinates: [number, number][]) => Plan, offset: [number, number]): BuildingModel;
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { BuildingModel, Plan } from "./building-kit.js";
|
|
2
|
+
import type { GeoBuilding } from "./skyline-geography-data.js";
|
|
3
|
+
export declare const sixNorthLevels: Readonly<{
|
|
4
|
+
block: 62.1;
|
|
5
|
+
cornice: 63.6;
|
|
6
|
+
band: 65;
|
|
7
|
+
bandTop: 66.9;
|
|
8
|
+
arches: 75.6;
|
|
9
|
+
towerCornice: 78;
|
|
10
|
+
stage: 84.5;
|
|
11
|
+
top: 86;
|
|
12
|
+
towerSouth: 7.5;
|
|
13
|
+
towerNorth: 20;
|
|
14
|
+
towerDepth: 12;
|
|
15
|
+
}>;
|
|
16
|
+
export declare function createSixNorthMichiganGeographicBuilding(record: GeoBuilding, projectPlan: (coordinates: [number, number][]) => Plan, offset: [number, number]): BuildingModel;
|
|
@@ -0,0 +1,180 @@
|
|
|
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
|
+
// Six North Michigan, the Montgomery Ward Building (Schmidt, Garden & Martin, 1899; four
|
|
5
|
+
// floors added by Holabird & Roche, 1923): the geographic layout's model, on the mapped
|
|
6
|
+
// outline 126982631. The original layout has no model of it; the drawing shows it right of
|
|
7
|
+
// Willoughby Tower, outside the excerpt it is fitted to.
|
|
8
|
+
//
|
|
9
|
+
// A sixteen-storey block, 27.5 m on Michigan by 49.6 m on Madison, under a projecting
|
|
10
|
+
// cornice. The tower, built in the middle of the Michigan front rather than on the corner
|
|
11
|
+
// first planned, rises flush with it: a floor above the block, a band, an arched stage of
|
|
12
|
+
// three tall windows to a face under a cornice, and a top stage of panels under a cap at
|
|
13
|
+
// 86 m, where the pyramid and statue came off in 1947. Heights are measured on the drawing
|
|
14
|
+
// down from that top; see docs/six-north-michigan-reference.md. Units are meters; +x is
|
|
15
|
+
// east, +z is south.
|
|
16
|
+
export const sixNorthLevels = Object.freeze({
|
|
17
|
+
block: 62.1, // the block's walls, under its cornice
|
|
18
|
+
cornice: 63.6, // the block's cornice and roof
|
|
19
|
+
band: 65, // the tower's band, over the floor above the block
|
|
20
|
+
bandTop: 66.9,
|
|
21
|
+
arches: 75.6, // the arched stage, under the tower's cornice
|
|
22
|
+
towerCornice: 78, // the tower's cornice, under the top stage
|
|
23
|
+
stage: 84.5, // the top stage, under its cap
|
|
24
|
+
top: 86, // the cap, the published height
|
|
25
|
+
towerSouth: 7.5, // the tower's south wall, north of Madison
|
|
26
|
+
towerNorth: 20, // its north wall
|
|
27
|
+
towerDepth: 12, // west of the Michigan front
|
|
28
|
+
});
|
|
29
|
+
const h = sixNorthLevels;
|
|
30
|
+
// Each floor's level: a 6 m ground floor, then 3.74 m floors to the block's cornice.
|
|
31
|
+
const floor = (n) => (n < 2 ? 0 : 6 + (n - 2) * 3.74);
|
|
32
|
+
const color = (hex) => new THREE.Color(hex);
|
|
33
|
+
const glassTones = [0x4c4c4c, 0x505050, 0x545454, 0x585858].map(color);
|
|
34
|
+
const litGlass = color(0x8a8a8a), dimGlass = color(0x5e5e5e);
|
|
35
|
+
const brick = color(0x6e6e6e), panel = color(0x9c9c9c), core = color(0x2e2e2e), roofing = color(0x484848);
|
|
36
|
+
function paneColor(row, bay, wall) {
|
|
37
|
+
const hash = (Math.imul(row + 31, 0x9e3779b1) ^ Math.imul(bay + 57, 0x85ebca77) ^ Math.imul(wall + 43, 0xc2b2ae3d)) >>> 0;
|
|
38
|
+
const value = hash % 101;
|
|
39
|
+
if (value < 4)
|
|
40
|
+
return litGlass;
|
|
41
|
+
if (value < 8)
|
|
42
|
+
return dimGlass;
|
|
43
|
+
return glassTones[value % glassTones.length];
|
|
44
|
+
}
|
|
45
|
+
const blockRows = [
|
|
46
|
+
{ lo: 0.25, hi: 1, kind: "brick", floor: 1 },
|
|
47
|
+
{ lo: 1, hi: 5.2, kind: "glass", floor: 1 },
|
|
48
|
+
{ lo: 5.2, hi: floor(2) - 0.35, kind: "brick", floor: 1 },
|
|
49
|
+
];
|
|
50
|
+
for (let n = 2; n <= 16; n += 1) {
|
|
51
|
+
blockRows.push({ lo: floor(n) - 0.35, hi: floor(n) + 1.25, kind: "brick", floor: n });
|
|
52
|
+
blockRows.push({ lo: floor(n) + 1.25, hi: floor(n + 1) - 0.35, kind: "glass", floor: n });
|
|
53
|
+
}
|
|
54
|
+
blockRows.push({ lo: floor(17) - 0.35, hi: h.block, kind: "brick", floor: 17 });
|
|
55
|
+
// The tower's rows over the block: the seventeenth floor's windows under the band; then the
|
|
56
|
+
// arched stage's small windows over the band and its tall windows, their heads narrowing
|
|
57
|
+
// for the last half-metre; then the top stage's panel.
|
|
58
|
+
const towerRows = [
|
|
59
|
+
{ lo: h.block, hi: 62.95, kind: "brick", floor: 17 },
|
|
60
|
+
{ lo: 62.95, hi: 64.77, kind: "floor17", floor: 17 },
|
|
61
|
+
{ lo: 64.77, hi: h.band, kind: "brick", floor: 17 },
|
|
62
|
+
{ lo: h.bandTop, hi: 68.2, kind: "small", floor: 18 },
|
|
63
|
+
{ lo: 68.2, hi: 70.7, kind: "brick", floor: 18 },
|
|
64
|
+
{ lo: 70.7, hi: 73.7, kind: "arch", floor: 19 },
|
|
65
|
+
{ lo: 73.7, hi: 74.2, kind: "head", floor: 19 },
|
|
66
|
+
{ lo: 74.2, hi: h.arches, kind: "brick", floor: 20 },
|
|
67
|
+
{ lo: h.towerCornice, hi: 83.2, kind: "panel", floor: 21 },
|
|
68
|
+
{ lo: 83.2, hi: h.stage, kind: "brick", floor: 21 },
|
|
69
|
+
];
|
|
70
|
+
const rowsBetween = (rows, lo, hi) => rows
|
|
71
|
+
.map((row) => ({ ...row, lo: Math.max(row.lo, lo), hi: Math.min(row.hi, hi) }))
|
|
72
|
+
.filter((row) => row.hi - row.lo > 0.05);
|
|
73
|
+
export function createSixNorthMichiganGeographicBuilding(record, projectPlan, offset) {
|
|
74
|
+
const kit = createBuilder(record.name, record.id);
|
|
75
|
+
const shell = kit.batch("Six North Michigan · shell", kit.material(0xffffff, { vertexColors: true }));
|
|
76
|
+
const wall = kit.batch("Six North Michigan · brick and windows", kit.material(0xffffff, { vertexColors: true }));
|
|
77
|
+
const cornices = kit.batch("Six North Michigan · cornices", kit.material(0x747474));
|
|
78
|
+
const lot = orient(polygonOf(projectPlan(record.footprint.coordinates)));
|
|
79
|
+
// The lot's south-east corner, and its Madison and Michigan fronts running from it.
|
|
80
|
+
const corner = lot.reduce((best, p) => (p[0] + p[1] > best[0] + best[1] ? p : best));
|
|
81
|
+
const neighbours = [lot[(lot.indexOf(corner) + 1) % lot.length], lot[(lot.indexOf(corner) + lot.length - 1) % lot.length]];
|
|
82
|
+
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]; };
|
|
83
|
+
const [west, north] = neighbours[0][0] < neighbours[1][0] ? [unit(neighbours[0]), unit(neighbours[1])] : [unit(neighbours[1]), unit(neighbours[0])];
|
|
84
|
+
const at = (a, b) => [corner[0] + west[0] * a + north[0] * b, corner[1] + west[1] * a + north[1] * b];
|
|
85
|
+
// The inverse of `at`: a point's distances along Madison and Michigan, whose fronts meet
|
|
86
|
+
// a little off square.
|
|
87
|
+
const skew = west[0] * north[1] - west[1] * north[0];
|
|
88
|
+
const inLot = (p) => {
|
|
89
|
+
const [x, z] = [p[0] - corner[0], p[1] - corner[1]];
|
|
90
|
+
return [(x * north[1] - z * north[0]) / skew, (west[0] * z - west[1] * x) / skew];
|
|
91
|
+
};
|
|
92
|
+
const rect = (a0, a1, b0, b1) => orient([at(a0, b0), at(a1, b0), at(a1, b1), at(a0, b1)]);
|
|
93
|
+
const [depth, front] = [Math.max(...lot.map((p) => inLot(p)[0])), Math.max(...lot.map((p) => inLot(p)[1]))];
|
|
94
|
+
const tower = (grow) => rect(-grow, h.towerDepth + grow, h.towerSouth - grow, h.towerNorth + grow);
|
|
95
|
+
// A solid on an outline between two heights.
|
|
96
|
+
const solid = (corners, lo, hi) => {
|
|
97
|
+
planOf(corners).forEach((run) => {
|
|
98
|
+
const a = run.at(0), b = run.at(run.length), n = run.normal(0);
|
|
99
|
+
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]]], core);
|
|
100
|
+
});
|
|
101
|
+
paintedSlab(kit, shell, corners, lo, false, roofing);
|
|
102
|
+
paintedSlab(kit, shell, corners, hi, true, roofing);
|
|
103
|
+
};
|
|
104
|
+
solid(lot, 0, h.block);
|
|
105
|
+
solid(tower(0), h.block, h.arches);
|
|
106
|
+
solid(tower(-0.8), h.towerCornice, h.stage);
|
|
107
|
+
// The block's cornice overhangs its walls by a metre, stopping either side of the tower,
|
|
108
|
+
// which rises through it, 2 cm clear of its walls so that the two undersides do not share
|
|
109
|
+
// an edge; the tower's band, cornice and cap are its outline grown or shrunk.
|
|
110
|
+
const [notchNorth, notchSouth, notchBack] = [h.towerNorth + 0.02, h.towerSouth - 0.02, h.towerDepth + 0.02];
|
|
111
|
+
const corniceOutline = orient([at(-1, -1), at(depth + 1, -1), at(depth + 1, front + 1), at(-1, front + 1), at(-1, notchNorth), at(notchBack, notchNorth), at(notchBack, notchSouth), at(-1, notchSouth)]);
|
|
112
|
+
kit.prism(cornices, planOf(corniceOutline), [h.block, h.cornice]);
|
|
113
|
+
kit.prism(cornices, planOf(tower(0.5)), [h.band, h.bandTop]);
|
|
114
|
+
kit.prism(cornices, planOf(tower(1.2)), [h.arches, h.towerCornice]);
|
|
115
|
+
kit.prism(cornices, planOf(tower(-0.3)), [h.stage, h.top]);
|
|
116
|
+
// A wall's skin, one box to a chain the `keep` test accepts, held 2 cm clear of its ends
|
|
117
|
+
// or 9 cm where the outline turns in. `openings` lays a wall's columns out across it,
|
|
118
|
+
// from its start to its end, and says how far each reaches into a window: 0 is brick; a
|
|
119
|
+
// floor's window takes any other; the tower's tall windows 1 and over, its seventeenth
|
|
120
|
+
// floor's 2, its small windows 3, and their heads and the top stage's panel 4. Floors
|
|
121
|
+
// below `fromFloor` stay brick.
|
|
122
|
+
const glassFrom = { brick: 99, glass: 1, floor17: 2, small: 3, arch: 1, head: 4, panel: 4 };
|
|
123
|
+
const skin = (corners, rows, lo, hi, seed, keep, openings, fromFloor = 1) => {
|
|
124
|
+
const plan = planOf(corners), cut = rowsBetween(rows, lo, hi), heights = [cut[0].lo, ...cut.map((row) => row.hi)];
|
|
125
|
+
chainsOf(plan).forEach((chain, index) => {
|
|
126
|
+
if (!chain.runs.every((run) => keep(plan[run])))
|
|
127
|
+
return;
|
|
128
|
+
const first = chain.runs[0], last = chain.runs.at(-1);
|
|
129
|
+
const s0 = turnsAt(plan, first)[0] > 0 ? 0.09 : 0.02, s1 = chain.length - (turnsAt(plan, last)[1] > 0 ? 0.09 : 0.02);
|
|
130
|
+
const [columns, reach] = openings(chain.length, plan[first].at(0), plan[last].at(plan[last].length));
|
|
131
|
+
chainSkin(kit, wall, plan, chain, 0, chain.runs.length - 1, s0, s1, columns, heights, 0.02, 0.07, (bay, r) => {
|
|
132
|
+
const row = cut[r], into = reach(bay);
|
|
133
|
+
if (row.kind === "panel")
|
|
134
|
+
return into >= 4 ? panel : brick;
|
|
135
|
+
if (row.floor < fromFloor)
|
|
136
|
+
return brick;
|
|
137
|
+
return into >= glassFrom[row.kind] ? paneColor(row.floor, bay, seed * 8 + index) : brick;
|
|
138
|
+
}, brick);
|
|
139
|
+
});
|
|
140
|
+
};
|
|
141
|
+
// Windows centred at `centres`, `width` wide, along a wall from its start.
|
|
142
|
+
const windows = (length, centres, width) => {
|
|
143
|
+
const inside = centres.filter((c) => c - width / 2 > 0.1 && c + width / 2 < length - 0.1);
|
|
144
|
+
return [[0, ...inside.flatMap((c) => [c - width / 2, c + width / 2]), length], (bay) => (bay % 2 === 0 && bay <= 2 * inside.length ? 1 : 0)];
|
|
145
|
+
};
|
|
146
|
+
// A position along a wall from its start, given in lot coordinates across it.
|
|
147
|
+
const along = (length, start, end, target, axis) => length * (target - inLot(start)[axis]) / (inLot(end)[axis] - inLot(start)[axis]);
|
|
148
|
+
const michigan = (run) => run.normal(0)[0] > 0.9, party = (run) => run.normal(0)[1] < -0.9;
|
|
149
|
+
// The Michigan front: three windows to each wing either side of the tower, and three to
|
|
150
|
+
// the tower between them, 3 m apart on the centres of the windows above.
|
|
151
|
+
const michiganFront = (length, start, end) => windows(length, [1.6, 3.75, 5.9, 10.75, 13.75, 16.75, 21.6, 23.75, 25.9].map((b) => along(length, start, end, b, 1)), 1.3);
|
|
152
|
+
// Madison, the alley and the north wall: a window every 2.75 m. The north wall is shared
|
|
153
|
+
// with 20 North Michigan, eight storeys high, and stays blank below its ninth floor.
|
|
154
|
+
const punched = (length) => { const bays = Math.round(length / 2.75); return windows(length, Array.from({ length: bays }, (_, i) => length * (i + 0.5) / bays), 1.1); };
|
|
155
|
+
skin(lot, blockRows, 0.25, h.block, 0, michigan, michiganFront);
|
|
156
|
+
skin(lot, blockRows, 0.25, h.block, 1, (run) => !michigan(run) && !party(run), punched);
|
|
157
|
+
skin(lot, blockRows, 0.25, h.block, 6, party, punched, 9);
|
|
158
|
+
// The tower's faces: three windows to a face, centred 3 m apart. A tall window is 2 m
|
|
159
|
+
// wide, its head the middle metre; the seventeenth floor's are 1.3 m and the small ones
|
|
160
|
+
// 1.1 m, all on the same centres.
|
|
161
|
+
const towerFace = (length) => {
|
|
162
|
+
const steps = [-1, -0.65, -0.55, -0.5, 0.5, 0.55, 0.65, 1];
|
|
163
|
+
const columns = [0, ...[-3, 0, 3].flatMap((d) => steps.map((s) => length / 2 + d + s)), length];
|
|
164
|
+
// Bays count from one: a pier, then 1, 2, 3, 4, 3, 2, 1 across each window.
|
|
165
|
+
return [columns, (bay) => (bay > 24 || (bay - 1) % 8 === 0 ? 0 : [0, 1, 2, 3, 4, 3, 2, 1][(bay - 1) % 8])];
|
|
166
|
+
};
|
|
167
|
+
// The top stage's panel, 7.2 m wide in the middle of each face.
|
|
168
|
+
const panelled = (length) => [[0, length / 2 - 3.6, length / 2 + 3.6, length], (bay) => (bay === 2 ? 4 : 0)];
|
|
169
|
+
// Over the block, the tower's Michigan face shows from the block's walls; the cornice
|
|
170
|
+
// hides its other faces' foot.
|
|
171
|
+
skin(tower(0), towerRows, h.block, h.band, 2, michigan, towerFace);
|
|
172
|
+
skin(tower(0), towerRows, h.cornice, h.band, 3, (run) => !michigan(run), towerFace);
|
|
173
|
+
skin(tower(0), towerRows, h.bandTop, h.arches, 4, () => true, towerFace);
|
|
174
|
+
skin(tower(-0.8), towerRows, h.towerCornice, h.stage, 5, () => true, panelled);
|
|
175
|
+
const model = kit.finish({ height: h.top, outlines: [shell, cornices], opacity: 0.16 });
|
|
176
|
+
model.building.position.set(offset[0], 0, offset[1]);
|
|
177
|
+
model.building.userData["geography"] = record;
|
|
178
|
+
model.building.userData["geographicDetail"] = { levels: h, source: "docs/six-north-michigan-reference.md" };
|
|
179
|
+
return model;
|
|
180
|
+
}
|