@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,134 @@
|
|
|
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 Lake View Building, 116 South Michigan Avenue (Jenney, Mundie & Jensen; its top five
|
|
5
|
+
// storeys added by them in 1912): the geographic layout's model, on the mapped outline
|
|
6
|
+
// 145498711. The original layout has no model of it; the drawing shows it between Peoples
|
|
7
|
+
// Gas and the MacLean Center.
|
|
8
|
+
//
|
|
9
|
+
// A narrow stone front, three windows to a floor, rises seventeen storeys: arched windows
|
|
10
|
+
// on the sixteenth, small attic windows on the seventeenth, and a cornice band at the top.
|
|
11
|
+
// The top is the Skyscraper Center's 73.2 m. The drawn front's top reads 71.9 m, so heights
|
|
12
|
+
// read on the drawing are scaled to meet it; see docs/lake-view-reference.md. Units are
|
|
13
|
+
// meters; +x is east, +z is south.
|
|
14
|
+
const drawn = (height) => height * 73.2 / 71.9;
|
|
15
|
+
export const lakeViewLevels = Object.freeze({
|
|
16
|
+
attic: drawn(67.9), // the seventeenth floor, with its small windows
|
|
17
|
+
cornice: drawn(70.8), // the cornice band
|
|
18
|
+
top: 73.2, // the published top
|
|
19
|
+
});
|
|
20
|
+
const h = lakeViewLevels;
|
|
21
|
+
// The skins' colours: the window tones, and the stone and its cornice, which no window
|
|
22
|
+
// shares.
|
|
23
|
+
export const lakeViewPalette = Object.freeze({ glass: [0x2c2c2c, 0x303030, 0x343434, 0x383838], lit: 0x6e6e6e, dim: 0x3c3c3c, stone: 0x767676, band: 0x888888 });
|
|
24
|
+
const palette = lakeViewPalette;
|
|
25
|
+
const color = (hex) => new THREE.Color(hex);
|
|
26
|
+
const glassTones = palette.glass.map(color);
|
|
27
|
+
const litGlass = color(palette.lit), dimGlass = color(palette.dim);
|
|
28
|
+
const stone = color(palette.stone), band = color(palette.band), core = color(0x262626), roofing = color(0x3c3c3c);
|
|
29
|
+
function paneColor(row, bay, wall) {
|
|
30
|
+
const hash = (Math.imul(row + 59, 0x9e3779b1) ^ Math.imul(bay + 83, 0x85ebca77) ^ Math.imul(wall + 71, 0xc2b2ae3d)) >>> 0;
|
|
31
|
+
const value = hash % 101;
|
|
32
|
+
if (value < 4)
|
|
33
|
+
return litGlass;
|
|
34
|
+
if (value < 8)
|
|
35
|
+
return dimGlass;
|
|
36
|
+
return glassTones[value % glassTones.length];
|
|
37
|
+
}
|
|
38
|
+
const base = (n) => 37.1 + (n - 9) * 3.85;
|
|
39
|
+
const rows = [
|
|
40
|
+
{ lo: 0.25, hi: drawn(1), kind: "wall", floor: 1 },
|
|
41
|
+
{ lo: drawn(1), hi: drawn(9), kind: "glass", floor: 1 },
|
|
42
|
+
];
|
|
43
|
+
const push = (hi, kind, floor) => rows.push({ lo: rows.at(-1).hi, hi, kind, floor });
|
|
44
|
+
for (let n = 2; n <= 15; n += 1) {
|
|
45
|
+
push(drawn(base(n) + 0.6), "wall", n);
|
|
46
|
+
push(drawn(base(n) + 3.1), "glass", n);
|
|
47
|
+
}
|
|
48
|
+
push(drawn(64.5), "wall", 16);
|
|
49
|
+
push(drawn(65.9), "glass", 16);
|
|
50
|
+
push(drawn(66.5), "head", 16);
|
|
51
|
+
push(drawn(68.6), "wall", 17);
|
|
52
|
+
push(drawn(69.6), "glass", 17);
|
|
53
|
+
push(h.cornice, "wall", 17);
|
|
54
|
+
push(h.top, "band", 17);
|
|
55
|
+
export function createLakeViewGeographicBuilding(record, projectPlan, offset) {
|
|
56
|
+
const kit = createBuilder(record.name, record.id);
|
|
57
|
+
const shell = kit.batch("Lake View Building · shell", kit.material(0xffffff, { vertexColors: true }));
|
|
58
|
+
const wall = kit.batch("Lake View Building · stone and windows", kit.material(0xffffff, { vertexColors: true }));
|
|
59
|
+
const lot = orient(polygonOf(projectPlan(record.footprint.coordinates)));
|
|
60
|
+
// The lot's south-east corner, with its south wall running west and its Michigan front
|
|
61
|
+
// north to the MacLean Center.
|
|
62
|
+
const corner = lot.reduce((best, p) => (p[0] + p[1] > best[0] + best[1] ? p : best));
|
|
63
|
+
const northEast = lot.reduce((best, p) => (p[0] - p[1] > best[0] - best[1] ? p : best));
|
|
64
|
+
const neighbours = [lot[(lot.indexOf(corner) + 1) % lot.length], lot[(lot.indexOf(corner) + lot.length - 1) % lot.length]];
|
|
65
|
+
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]; };
|
|
66
|
+
const west = unit(neighbours[0][0] < neighbours[1][0] ? neighbours[0] : neighbours[1]), north = unit(northEast);
|
|
67
|
+
// A point's distances along the south wall and the Michigan front, which meet a little
|
|
68
|
+
// off square.
|
|
69
|
+
const skew = west[0] * north[1] - west[1] * north[0];
|
|
70
|
+
const inLot = (p) => {
|
|
71
|
+
const [x, z] = [p[0] - corner[0], p[1] - corner[1]];
|
|
72
|
+
return [(x * north[1] - z * north[0]) / skew, (west[0] * z - west[1] * x) / skew];
|
|
73
|
+
};
|
|
74
|
+
const front = inLot(northEast)[1];
|
|
75
|
+
planOf(lot).forEach((run) => {
|
|
76
|
+
const a = run.at(0), b = run.at(run.length), n = run.normal(0);
|
|
77
|
+
kit.quad(shell, [[a[0], 0, a[1]], [b[0], 0, b[1]], [b[0], h.top, b[1]], [a[0], h.top, a[1]]], [[n[0], 0, n[1]]], core);
|
|
78
|
+
});
|
|
79
|
+
paintedSlab(kit, shell, lot, 0, false, roofing);
|
|
80
|
+
paintedSlab(kit, shell, lot, h.top, true, roofing);
|
|
81
|
+
// A wall's skin, one box to a chain the `keep` test accepts, held 2 cm clear of its ends
|
|
82
|
+
// or 9 cm where the outline turns in. `openings` lays a wall's columns out across it and
|
|
83
|
+
// says which cross a window's side (1) or its middle (2), which alone continues into an
|
|
84
|
+
// arched head. Only a `banded` wall carries the cornice band.
|
|
85
|
+
const skin = (seed, keep, openings, banded = false) => {
|
|
86
|
+
const plan = planOf(lot), heights = [rows[0].lo, ...rows.map((row) => row.hi)];
|
|
87
|
+
chainsOf(plan).forEach((chain, index) => {
|
|
88
|
+
if (!chain.runs.every((run) => keep(plan[run])))
|
|
89
|
+
return;
|
|
90
|
+
const first = chain.runs[0], last = chain.runs.at(-1);
|
|
91
|
+
const s0 = turnsAt(plan, first)[0] > 0 ? 0.09 : 0.02, s1 = chain.length - (turnsAt(plan, last)[1] > 0 ? 0.09 : 0.02);
|
|
92
|
+
const [columns, crossing] = openings(chain.length, plan[first].at(0), plan[last].at(plan[last].length));
|
|
93
|
+
chainSkin(kit, wall, plan, chain, 0, chain.runs.length - 1, s0, s1, columns, heights, 0.02, 0.07, (bay, r) => {
|
|
94
|
+
const row = rows[r], into = crossing(bay);
|
|
95
|
+
if (row.kind === "band")
|
|
96
|
+
return banded ? band : stone;
|
|
97
|
+
return (row.kind === "glass" && into >= 1) || (row.kind === "head" && into >= 2) ? paneColor(row.floor, bay, seed * 8 + index) : stone;
|
|
98
|
+
}, stone);
|
|
99
|
+
});
|
|
100
|
+
};
|
|
101
|
+
// Windows `width` wide at `centres`, positions along a wall from its start, each split
|
|
102
|
+
// into sides and a middle half.
|
|
103
|
+
const windows = (length, centres, width) => {
|
|
104
|
+
const inside = centres.filter((c) => c - width / 2 > 0.1 && c + width / 2 < length - 0.1).sort((p, q) => p - q);
|
|
105
|
+
const columns = [0, ...inside.flatMap((c) => [c - width / 2, c - width / 4, c + width / 4, c + width / 2]), length];
|
|
106
|
+
// Bays count from one: a pier, then side, middle, side for each window, then the last pier.
|
|
107
|
+
return [columns, (bay) => { const k = (bay - 1) % 4; return bay > 4 * inside.length || k === 0 ? 0 : k === 2 ? 2 : 1; }];
|
|
108
|
+
};
|
|
109
|
+
// The Michigan front's three windows, 2.5 m wide and 3.5 m apart about its middle, as
|
|
110
|
+
// drawn; the alley's, which the drawing does not show, 1.6 m wide and spread evenly about
|
|
111
|
+
// 3 m apart.
|
|
112
|
+
const michigan = (length, start, end) => {
|
|
113
|
+
const [b0, b1] = [inLot(start)[1], inLot(end)[1]], along = (b) => length * (b - b0) / (b1 - b0);
|
|
114
|
+
return windows(length, [-3.5, 0, 3.5].map((d) => along(front / 2 + d)), 2.5);
|
|
115
|
+
};
|
|
116
|
+
const alley = (length) => {
|
|
117
|
+
const count = Math.max(1, Math.round(length / 3.2)), [columns, crossing] = windows(length, Array.from({ length: count }, (_, k) => length * (k + 0.5) / count), 1.6);
|
|
118
|
+
return [columns, (bay) => Math.min(1, crossing(bay))];
|
|
119
|
+
};
|
|
120
|
+
// The Michigan front and the alley by their facing and their place in the lot; the side
|
|
121
|
+
// walls, shared with the taller MacLean Center and Peoples Gas, and the south light
|
|
122
|
+
// court's walls stay plain. The cornice band runs only along Michigan.
|
|
123
|
+
const facing = (x, z) => (run) => run.normal(0)[0] * x + run.normal(0)[1] * z > 0.9;
|
|
124
|
+
const isMichigan = (run) => facing(1, 0)(run) && inLot(run.at(run.length / 2))[0] < 1;
|
|
125
|
+
const isAlley = (run) => facing(-1, 0)(run) && inLot(run.at(run.length / 2))[0] > 40;
|
|
126
|
+
skin(0, isMichigan, michigan, true);
|
|
127
|
+
skin(1, isAlley, alley);
|
|
128
|
+
skin(2, (run) => !isMichigan(run) && !isAlley(run), (length) => [[0, length], () => 0]);
|
|
129
|
+
const model = kit.finish({ height: h.top, outlines: [shell], opacity: 0.16 });
|
|
130
|
+
model.building.position.set(offset[0], 0, offset[1]);
|
|
131
|
+
model.building.userData["geography"] = record;
|
|
132
|
+
model.building.userData["geographicDetail"] = { levels: h, source: "docs/lake-view-reference.md" };
|
|
133
|
+
return model;
|
|
134
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
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 MacLean Center, 112 South Michigan Avenue, built as the Illinois Athletic Club
|
|
5
|
+
// (Barnett, Haynes & Barnett, 1908) and raised six floors by Swann & Weiskopf in 1985: the
|
|
6
|
+
// geographic layout's model, on the mapped outline 145498712. The original layout has no
|
|
7
|
+
// model of it; the drawing shows it between the Lake View and Monroe Buildings.
|
|
8
|
+
//
|
|
9
|
+
// A narrow Michigan front, a narrow window and three wide ones to a floor and another
|
|
10
|
+
// narrow one, each section at its drawn widths, rises twelve storeys to the old club's
|
|
11
|
+
// projecting cornice and a frieze of round windows; the addition carries six more floors,
|
|
12
|
+
// arched on the top one, to a pierced parapet. The parapet's top is the Skyscraper Center's
|
|
13
|
+
// 77.4 m. The drawn parapet reads 73.8 m, so heights read on the drawing are scaled to meet
|
|
14
|
+
// it; see docs/maclean-center-reference.md. Units are meters; +x is east, +z is south.
|
|
15
|
+
const drawn = (height) => height * 77.4 / 73.8;
|
|
16
|
+
export const macleanLevels = Object.freeze({
|
|
17
|
+
band: drawn(40.9), // the old club's bands under its cornice
|
|
18
|
+
cornice: drawn(43.8), // the projecting cornice
|
|
19
|
+
frieze: drawn(45.6), // the frieze of round windows
|
|
20
|
+
addition: drawn(48.5), // the 1985 floors, over the frieze
|
|
21
|
+
openings: drawn(70.9), // the parapet's openings
|
|
22
|
+
parapet: drawn(72.6), // its top band
|
|
23
|
+
top: 77.4, // the parapet's top, the published height
|
|
24
|
+
});
|
|
25
|
+
const h = macleanLevels;
|
|
26
|
+
// The skins' colours: the window tones, and the stone, bands and the parapet's shadowed
|
|
27
|
+
// openings, which no window shares.
|
|
28
|
+
export const macleanPalette = Object.freeze({ glass: [0x2c2c2c, 0x303030, 0x343434, 0x383838], lit: 0x6e6e6e, dim: 0x3c3c3c, stone: 0x7e7e7e, band: 0x8e8e8e, shadow: 0x242424 });
|
|
29
|
+
const palette = macleanPalette;
|
|
30
|
+
const color = (hex) => new THREE.Color(hex);
|
|
31
|
+
const glassTones = palette.glass.map(color);
|
|
32
|
+
const litGlass = color(palette.lit), dimGlass = color(palette.dim);
|
|
33
|
+
const stone = color(palette.stone), band = color(palette.band), shadow = color(palette.shadow);
|
|
34
|
+
const core = color(0x262626), roofing = color(0x3c3c3c);
|
|
35
|
+
function paneColor(row, bay, wall) {
|
|
36
|
+
const hash = (Math.imul(row + 53, 0x9e3779b1) ^ Math.imul(bay + 79, 0x85ebca77) ^ Math.imul(wall + 67, 0xc2b2ae3d)) >>> 0;
|
|
37
|
+
const value = hash % 101;
|
|
38
|
+
if (value < 4)
|
|
39
|
+
return litGlass;
|
|
40
|
+
if (value < 8)
|
|
41
|
+
return dimGlass;
|
|
42
|
+
return glassTones[value % glassTones.length];
|
|
43
|
+
}
|
|
44
|
+
const rows = [
|
|
45
|
+
{ lo: 0.25, hi: drawn(1), kind: "wall", floor: 1 },
|
|
46
|
+
{ lo: drawn(1), hi: drawn(4.5), kind: "glass", floor: 1 },
|
|
47
|
+
];
|
|
48
|
+
const push = (hi, kind, floor, dy) => rows.push({ lo: rows.at(-1).hi, hi, kind, floor, ...(dy === undefined ? {} : { dy }) });
|
|
49
|
+
for (let n = 2; n <= 12; n += 1) {
|
|
50
|
+
const base = 5.1 + (n - 2) * 3.2;
|
|
51
|
+
push(drawn(base + 0.6), "wall", n);
|
|
52
|
+
push(drawn(base + 3.1), "glass", n);
|
|
53
|
+
}
|
|
54
|
+
push(h.band, "wall", 12);
|
|
55
|
+
push(h.frieze, "band", 12);
|
|
56
|
+
push(drawn(46.5), "wall", 12);
|
|
57
|
+
// The round windows, 90 cm across and centred at 46.95 m as drawn, in five slices, each
|
|
58
|
+
// carrying its middle's drawn height from the centre.
|
|
59
|
+
for (const [lo, hi] of [[46.5, 46.65], [46.65, 46.85], [46.85, 47.05], [47.05, 47.25], [47.25, 47.4]])
|
|
60
|
+
push(drawn(hi), "oculus", 12, Math.abs((lo + hi) / 2 - 46.95));
|
|
61
|
+
push(h.addition, "wall", 12);
|
|
62
|
+
for (const [floor, lo, hi] of [[13, 49.06, 51.48], [14, 52.17, 54.72], [15, 55.98, 58.54], [16, 59.67, 62.21], [17, 63.37, 65.92], [18, 66.7, 68.23]]) {
|
|
63
|
+
push(drawn(lo), "wall", floor);
|
|
64
|
+
push(drawn(hi), "glass", floor);
|
|
65
|
+
}
|
|
66
|
+
push(drawn(68.83), "head", 18);
|
|
67
|
+
push(h.openings, "wall", 18);
|
|
68
|
+
rows.push({ lo: h.openings, hi: h.parapet, kind: "opening", floor: 18 });
|
|
69
|
+
rows.push({ lo: h.parapet, hi: h.top, kind: "wall", floor: 18 });
|
|
70
|
+
export function createMacleanCenterGeographicBuilding(record, projectPlan, offset) {
|
|
71
|
+
const kit = createBuilder(record.name, record.id);
|
|
72
|
+
const shell = kit.batch("MacLean Center · shell", kit.material(0xffffff, { vertexColors: true }));
|
|
73
|
+
const wall = kit.batch("MacLean Center · stone and windows", kit.material(0xffffff, { vertexColors: true }));
|
|
74
|
+
const cornice = kit.batch("MacLean Center · cornice", kit.material(0x8a8a8a));
|
|
75
|
+
const lot = orient(polygonOf(projectPlan(record.footprint.coordinates)));
|
|
76
|
+
// The lot's south-east corner, with its south wall running west and its Michigan front
|
|
77
|
+
// north to the Monroe Building.
|
|
78
|
+
const corner = lot.reduce((best, p) => (p[0] + p[1] > best[0] + best[1] ? p : best));
|
|
79
|
+
const northEast = lot.reduce((best, p) => (p[0] - p[1] > best[0] - best[1] ? p : best));
|
|
80
|
+
const neighbours = [lot[(lot.indexOf(corner) + 1) % lot.length], lot[(lot.indexOf(corner) + lot.length - 1) % lot.length]];
|
|
81
|
+
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]; };
|
|
82
|
+
const west = unit(neighbours[0][0] < neighbours[1][0] ? neighbours[0] : neighbours[1]), north = unit(northEast);
|
|
83
|
+
const at = (a, b) => [corner[0] + west[0] * a + north[0] * b, corner[1] + west[1] * a + north[1] * b];
|
|
84
|
+
// The inverse of `at`: a point's distances along the south wall and the Michigan front,
|
|
85
|
+
// which meet a little off square.
|
|
86
|
+
const skew = west[0] * north[1] - west[1] * north[0];
|
|
87
|
+
const inLot = (p) => {
|
|
88
|
+
const [x, z] = [p[0] - corner[0], p[1] - corner[1]];
|
|
89
|
+
return [(x * north[1] - z * north[0]) / skew, (west[0] * z - west[1] * x) / skew];
|
|
90
|
+
};
|
|
91
|
+
const front = inLot(northEast)[1];
|
|
92
|
+
const rect = (a0, a1, b0, b1) => orient([at(a0, b0), at(a1, b0), at(a1, b1), at(a0, b1)]);
|
|
93
|
+
planOf(lot).forEach((run) => {
|
|
94
|
+
const a = run.at(0), b = run.at(run.length), n = run.normal(0);
|
|
95
|
+
kit.quad(shell, [[a[0], 0, a[1]], [b[0], 0, b[1]], [b[0], h.top, b[1]], [a[0], h.top, a[1]]], [[n[0], 0, n[1]]], core);
|
|
96
|
+
});
|
|
97
|
+
paintedSlab(kit, shell, lot, 0, false, roofing);
|
|
98
|
+
paintedSlab(kit, shell, lot, h.top, true, roofing);
|
|
99
|
+
// The old club's cornice projects 60 cm along the Michigan front, 3 cm short of the
|
|
100
|
+
// neighbours' walls, past the skin's ends 2 cm in, and 5 cm into its own.
|
|
101
|
+
kit.prism(cornice, planOf(rect(-0.6, 0.05, 0.03, front - 0.03)), [h.cornice, h.frieze]);
|
|
102
|
+
const skin = (seed, keep, openings, plain = false) => {
|
|
103
|
+
const plan = planOf(lot), heights = [rows[0].lo, ...rows.map((row) => row.hi)];
|
|
104
|
+
chainsOf(plan).forEach((chain, index) => {
|
|
105
|
+
if (!chain.runs.every((run) => keep(plan[run])))
|
|
106
|
+
return;
|
|
107
|
+
const first = chain.runs[0], last = chain.runs.at(-1);
|
|
108
|
+
const s0 = turnsAt(plan, first)[0] > 0 ? 0.09 : 0.02, s1 = chain.length - (turnsAt(plan, last)[1] > 0 ? 0.09 : 0.02);
|
|
109
|
+
const [columns, cell] = openings(chain.length, plan[first].at(0), plan[last].at(plan[last].length));
|
|
110
|
+
chainSkin(kit, wall, plan, chain, 0, chain.runs.length - 1, s0, s1, columns, heights, 0.02, 0.07, (bay, r) => {
|
|
111
|
+
const row = rows[r];
|
|
112
|
+
if (row.kind === "band")
|
|
113
|
+
return plain ? stone : band;
|
|
114
|
+
const shows = cell(bay, row);
|
|
115
|
+
return shows === "glass" ? paneColor(row.floor, bay, seed * 8 + index) : shows === "shadow" ? shadow : stone;
|
|
116
|
+
}, stone);
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
// The Michigan front's five columns, narrow windows 8.4 m either side of the middle and
|
|
120
|
+
// wide ones at the middle and 4.35 m either side, with each section's widths as drawn: the
|
|
121
|
+
// old club's windows 1.4 and 2.36 m wide, the addition's 2 and 3.15 m, and the parapet's
|
|
122
|
+
// openings 2.9 m. Only the wide columns have arched heads, their middle half, and round
|
|
123
|
+
// windows over them.
|
|
124
|
+
const michigan = (length, start, end) => {
|
|
125
|
+
const [b0, b1] = [inLot(start)[1], inLot(end)[1]], along = (b) => length * (b - b0) / (b1 - b0), middle = front / 2;
|
|
126
|
+
const windows = [[-8.4, false], [-4.35, true], [0, true], [4.35, true], [8.4, false]].map(([d, wide]) => ({ c: along(middle + d), wide }));
|
|
127
|
+
const halves = (wide) => (wide ? [0.25, 0.42, 0.45, 0.79, 1.18, 1.45, 1.575] : [0.7, 1, 1.45]);
|
|
128
|
+
const cuts = windows.flatMap(({ c, wide }) => halves(wide).flatMap((half) => [c - half, c + half]));
|
|
129
|
+
const columns = [0, ...[...new Set(cuts)].sort((p, q) => p - q), length];
|
|
130
|
+
return [columns, (bay, row) => {
|
|
131
|
+
const middleOf = (columns[bay - 1] + columns[bay]) / 2, window = windows.find(({ c, wide }) => Math.abs(middleOf - c) < (wide ? 1.575 : 1.45));
|
|
132
|
+
if (!window)
|
|
133
|
+
return "wall";
|
|
134
|
+
const dx = Math.abs(middleOf - window.c), wide = window.wide, old = row.floor <= 12;
|
|
135
|
+
if (row.kind === "glass")
|
|
136
|
+
return dx < (wide ? (old ? 1.18 : 1.575) : (old ? 0.7 : 1)) ? "glass" : "wall";
|
|
137
|
+
if (row.kind === "head")
|
|
138
|
+
return wide && dx < 0.79 ? "glass" : "wall";
|
|
139
|
+
if (row.kind === "oculus")
|
|
140
|
+
return wide && dx * dx + row.dy * row.dy <= 0.45 * 0.45 ? "glass" : "wall";
|
|
141
|
+
if (row.kind === "opening")
|
|
142
|
+
return dx < 1.45 ? "shadow" : "wall";
|
|
143
|
+
return "wall";
|
|
144
|
+
}];
|
|
145
|
+
};
|
|
146
|
+
// The alley's windows, 1.6 m wide every 3.2 m.
|
|
147
|
+
const alley = (length) => {
|
|
148
|
+
const count = Math.max(1, Math.round(length / 3.2)), centres = Array.from({ length: count }, (_, k) => length * (k + 0.5) / count);
|
|
149
|
+
return [[0, ...centres.flatMap((c) => [c - 0.8, c + 0.8]), length], (bay, row) => (row.kind === "glass" && bay % 2 === 0 && bay <= 2 * count ? "glass" : "wall")];
|
|
150
|
+
};
|
|
151
|
+
const facing = (x, z) => (run) => run.normal(0)[0] * x + run.normal(0)[1] * z > 0.9;
|
|
152
|
+
// The bands run only along Michigan; the side walls are shared with the Lake View and
|
|
153
|
+
// Monroe Buildings and stay plain.
|
|
154
|
+
skin(0, facing(1, 0), michigan);
|
|
155
|
+
skin(1, facing(-1, 0), alley, true);
|
|
156
|
+
skin(2, (run) => !facing(1, 0)(run) && !facing(-1, 0)(run), (length) => [[0, length], () => "wall"], true);
|
|
157
|
+
const model = kit.finish({ height: h.top, outlines: [shell, cornice], opacity: 0.16 });
|
|
158
|
+
model.building.position.set(offset[0], 0, offset[1]);
|
|
159
|
+
model.building.userData["geography"] = record;
|
|
160
|
+
model.building.userData["geographicDetail"] = { levels: h, source: "docs/maclean-center-reference.md" };
|
|
161
|
+
return model;
|
|
162
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
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 Michigan Boulevard Building, 30 North Michigan Avenue (Jarvis Hunt, 1914, fifteen
|
|
5
|
+
// storeys; six added by Hunt in 1923): the geographic layout's model, on the mapped outline
|
|
6
|
+
// 126982630. The original layout has no model of it; the drawing shows its Michigan front
|
|
7
|
+
// and, above 20 North Michigan, its south wall, right of Six North Michigan's tower.
|
|
8
|
+
//
|
|
9
|
+
// A terracotta street front of five bays, a single window and four pairs to a floor, over
|
|
10
|
+
// twenty-one storeys, with a belt course over the thirteenth and an attic of panels under a
|
|
11
|
+
// parapet at the published 82 m. The terracotta turns one bay onto the south wall; beyond
|
|
12
|
+
// it the wall is common brick, shared with 20 North Michigan to its eighth floor and blank
|
|
13
|
+
// to the eighteenth. Heights are measured on the drawing down from that top; see
|
|
14
|
+
// docs/michigan-boulevard-reference.md. Units are meters; +x is east, +z is south.
|
|
15
|
+
export const michiganBoulevardLevels = Object.freeze({
|
|
16
|
+
belt: 49, // the belt course over the thirteenth floor
|
|
17
|
+
beltTop: 50.2,
|
|
18
|
+
attic: 78.16, // the attic, over the twenty-first floor's windows
|
|
19
|
+
top: 82, // the parapet, the published height
|
|
20
|
+
endParapet: 83.3, // the raised parapet over the front's south bay
|
|
21
|
+
pedestals: 82.6, // the pedestals over the front's piers
|
|
22
|
+
returnBay: 5.6, // the terracotta's return onto the south wall
|
|
23
|
+
});
|
|
24
|
+
const h = michiganBoulevardLevels;
|
|
25
|
+
// Each floor's level: a 4.8 m ground floor, 3.7 m floors to the thirteenth, which the belt
|
|
26
|
+
// course makes 4.7 m, and 3.58 m floors above.
|
|
27
|
+
const floor = (n) => (n < 2 ? 0 : n <= 13 ? 4.8 + (n - 2) * 3.7 : 50.2 + (n - 14) * 3.58);
|
|
28
|
+
const color = (hex) => new THREE.Color(hex);
|
|
29
|
+
const glassTones = [0x262626, 0x2a2a2a, 0x2e2e2e, 0x323232].map(color);
|
|
30
|
+
const litGlass = color(0x6e6e6e), dimGlass = color(0x3a3a3a);
|
|
31
|
+
const terracotta = color(0x444444), returnBay = color(0x585858), brick = color(0x404040), belt = color(0x505050), panel = color(0x363636);
|
|
32
|
+
const core = color(0x262626), roofing = color(0x3a3a3a);
|
|
33
|
+
function paneColor(row, bay, wall) {
|
|
34
|
+
const hash = (Math.imul(row + 37, 0x9e3779b1) ^ Math.imul(bay + 61, 0x85ebca77) ^ Math.imul(wall + 47, 0xc2b2ae3d)) >>> 0;
|
|
35
|
+
const value = hash % 101;
|
|
36
|
+
if (value < 4)
|
|
37
|
+
return litGlass;
|
|
38
|
+
if (value < 8)
|
|
39
|
+
return dimGlass;
|
|
40
|
+
return glassTones[value % glassTones.length];
|
|
41
|
+
}
|
|
42
|
+
const rows = [
|
|
43
|
+
{ lo: 0.25, hi: 0.9, kind: "wall", floor: 1 },
|
|
44
|
+
{ lo: 0.9, hi: 4.2, kind: "glass", floor: 1 },
|
|
45
|
+
];
|
|
46
|
+
for (let n = 2; n <= 21; n += 1) {
|
|
47
|
+
const below = rows.at(-1).hi;
|
|
48
|
+
if (n === 14) {
|
|
49
|
+
rows.push({ lo: below, hi: h.belt, kind: "wall", floor: n });
|
|
50
|
+
rows.push({ lo: h.belt, hi: h.beltTop, kind: "belt", floor: n });
|
|
51
|
+
rows.push({ lo: h.beltTop, hi: floor(n) + 0.9, kind: "wall", floor: n });
|
|
52
|
+
}
|
|
53
|
+
else
|
|
54
|
+
rows.push({ lo: below, hi: floor(n) + 0.9, kind: "wall", floor: n });
|
|
55
|
+
rows.push({ lo: floor(n) + 0.9, hi: floor(n) + 2.9, kind: "glass", floor: n });
|
|
56
|
+
}
|
|
57
|
+
rows.push({ lo: h.attic, hi: 79.2, kind: "attic", floor: 22 });
|
|
58
|
+
rows.push({ lo: 79.2, hi: 80.9, kind: "panel", floor: 22 });
|
|
59
|
+
rows.push({ lo: 80.9, hi: h.top, kind: "attic", floor: 22 });
|
|
60
|
+
export function createMichiganBoulevardGeographicBuilding(record, projectPlan, offset) {
|
|
61
|
+
const kit = createBuilder(record.name, record.id);
|
|
62
|
+
const shell = kit.batch("Michigan Boulevard Building · shell", kit.material(0xffffff, { vertexColors: true }));
|
|
63
|
+
const wall = kit.batch("Michigan Boulevard Building · terracotta and windows", kit.material(0xffffff, { vertexColors: true }));
|
|
64
|
+
const parapets = kit.batch("Michigan Boulevard Building · parapet", kit.material(0x4c4c4c));
|
|
65
|
+
const lot = orient(polygonOf(projectPlan(record.footprint.coordinates)));
|
|
66
|
+
// The lot's south-east corner, with its south wall running west and its Michigan front
|
|
67
|
+
// north to the Washington Street corner.
|
|
68
|
+
const corner = lot.reduce((best, p) => (p[0] + p[1] > best[0] + best[1] ? p : best));
|
|
69
|
+
const northEast = lot.reduce((best, p) => (p[0] - p[1] > best[0] - best[1] ? p : best));
|
|
70
|
+
const neighbours = [lot[(lot.indexOf(corner) + 1) % lot.length], lot[(lot.indexOf(corner) + lot.length - 1) % lot.length]];
|
|
71
|
+
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]; };
|
|
72
|
+
const west = unit(neighbours[0][0] < neighbours[1][0] ? neighbours[0] : neighbours[1]), north = unit(northEast);
|
|
73
|
+
const at = (a, b) => [corner[0] + west[0] * a + north[0] * b, corner[1] + west[1] * a + north[1] * b];
|
|
74
|
+
// The inverse of `at`: a point's distances along the south wall and the Michigan front,
|
|
75
|
+
// which meet a little off square.
|
|
76
|
+
const skew = west[0] * north[1] - west[1] * north[0];
|
|
77
|
+
const inLot = (p) => {
|
|
78
|
+
const [x, z] = [p[0] - corner[0], p[1] - corner[1]];
|
|
79
|
+
return [(x * north[1] - z * north[0]) / skew, (west[0] * z - west[1] * x) / skew];
|
|
80
|
+
};
|
|
81
|
+
const front = inLot(northEast)[1];
|
|
82
|
+
const rect = (a0, a1, b0, b1) => orient([at(a0, b0), at(a1, b0), at(a1, b1), at(a0, b1)]);
|
|
83
|
+
planOf(lot).forEach((run) => {
|
|
84
|
+
const a = run.at(0), b = run.at(run.length), n = run.normal(0);
|
|
85
|
+
kit.quad(shell, [[a[0], 0, a[1]], [b[0], 0, b[1]], [b[0], h.top, b[1]], [a[0], h.top, a[1]]], [[n[0], 0, n[1]]], core);
|
|
86
|
+
});
|
|
87
|
+
paintedSlab(kit, shell, lot, 0, false, roofing);
|
|
88
|
+
paintedSlab(kit, shell, lot, h.top, true, roofing);
|
|
89
|
+
// Over the front's south bay the parapet rises, and pedestals stand over the piers between
|
|
90
|
+
// the pairs and at the Washington corner, 5 cm inside the walls' planes.
|
|
91
|
+
kit.prism(parapets, planOf(rect(0.05, 0.65, 0.05, 6.2)), [h.top, h.endParapet]);
|
|
92
|
+
for (const b of [12.4, 18.5, 24.6, front - 0.45])
|
|
93
|
+
kit.prism(parapets, planOf(rect(0.05, 0.65, b - 0.4, b + 0.4)), [h.top, h.pedestals]);
|
|
94
|
+
const skin = (seed, keep, openings) => {
|
|
95
|
+
const plan = planOf(lot), heights = [rows[0].lo, ...rows.map((row) => row.hi)];
|
|
96
|
+
chainsOf(plan).forEach((chain, index) => {
|
|
97
|
+
if (!chain.runs.every((run) => keep(plan[run])))
|
|
98
|
+
return;
|
|
99
|
+
const first = chain.runs[0], last = chain.runs.at(-1);
|
|
100
|
+
const s0 = turnsAt(plan, first)[0] > 0 ? 0.09 : 0.02, s1 = chain.length - (turnsAt(plan, last)[1] > 0 ? 0.09 : 0.02);
|
|
101
|
+
const [columns, column] = openings(chain.length, plan[first].at(0), plan[last].at(plan[last].length));
|
|
102
|
+
chainSkin(kit, wall, plan, chain, 0, chain.runs.length - 1, s0, s1, columns, heights, 0.02, 0.07, (bay, r) => {
|
|
103
|
+
const row = rows[r], { face, window, from, panel: paneled } = column(bay);
|
|
104
|
+
if (row.kind === "belt")
|
|
105
|
+
return face === brick ? brick : belt;
|
|
106
|
+
if (row.kind === "panel")
|
|
107
|
+
return paneled ? panel : face;
|
|
108
|
+
return row.kind === "glass" && window && row.floor >= from ? paneColor(row.floor, bay, seed * 8 + index) : face;
|
|
109
|
+
}, terracotta);
|
|
110
|
+
});
|
|
111
|
+
};
|
|
112
|
+
// A position along a wall from its start, given in lot coordinates across it, and back.
|
|
113
|
+
const along = (length, start, end, axis) => {
|
|
114
|
+
const [p, q] = [inLot(start)[axis], inLot(end)[axis]];
|
|
115
|
+
return [(target) => length * (target - p) / (q - p), (s) => p + (s / length) * (q - p)];
|
|
116
|
+
};
|
|
117
|
+
// Windows `width` wide centred at `centres`, in lot coordinates along `axis`, with the
|
|
118
|
+
// walls also split at `breaks`; `column` says, in the same coordinates, what else each
|
|
119
|
+
// column carries.
|
|
120
|
+
const windows = (length, start, end, axis, centres, width, column, breaks = []) => {
|
|
121
|
+
const [to, from] = along(length, start, end, axis);
|
|
122
|
+
const inside = centres.map(to).filter((c) => c - width / 2 > 0.1 && c + width / 2 < length - 0.1);
|
|
123
|
+
const cuts = [...inside.flatMap((c) => [c - width / 2, c + width / 2]), ...breaks.map(to).filter((s) => s > 0.1 && s < length - 0.1)];
|
|
124
|
+
const columns = [0, ...[...new Set(cuts)].sort((p, q) => p - q), length];
|
|
125
|
+
// Bays count from one, each between two columns; a bay holds a window when its middle
|
|
126
|
+
// lies inside one.
|
|
127
|
+
return [columns, (bay) => {
|
|
128
|
+
const middle = (columns[bay - 1] + columns[bay]) / 2;
|
|
129
|
+
return { ...column(from(middle)), window: inside.some((c) => Math.abs(middle - c) < width / 2) };
|
|
130
|
+
}];
|
|
131
|
+
};
|
|
132
|
+
const pairs = (centres) => centres.flatMap((c) => [c - 1, c + 1]);
|
|
133
|
+
const facing = (x, z) => (run) => run.normal(0)[0] * x + run.normal(0)[1] * z > 0.9;
|
|
134
|
+
const [isMichigan, isSouth, isWashington] = [facing(1, 0), facing(0, 1), facing(0, -1)];
|
|
135
|
+
// The Michigan front: a single window in its south bay and a pair in each of the other
|
|
136
|
+
// four, 2 m apart; the attic's panel spans each bay's windows.
|
|
137
|
+
const frontBays = [[3.1, 0.725], ...[9.3, 15.45, 21.55, 27.75].map((c) => [c, 1.725])];
|
|
138
|
+
const michigan = (length, start, end) => windows(length, start, end, 1, [3.1, ...pairs([9.3, 15.45, 21.55, 27.75])], 1.45, (b) => ({ face: terracotta, from: 1, panel: frontBays.some(([c, half]) => Math.abs(b - c) < half) }));
|
|
139
|
+
// The south wall: the terracotta's return bay with a pair of windows over 20 North
|
|
140
|
+
// Michigan, then brick with a pair every 5.1 m on the top four floors, as drawn.
|
|
141
|
+
const southWall = (length, start, end) => windows(length, start, end, 0, pairs([2.5, 8, 13.2, 18.3, 23.4, 28.5, 33.6, 38.7, 43.8]), 1.1, (a) => (a < h.returnBay ? { face: returnBay, from: 9, panel: false } : { face: brick, from: 18, panel: false }), [h.returnBay]);
|
|
142
|
+
// Washington Street's terracotta front and the brick alley: a pair every 6.1 m.
|
|
143
|
+
const rear = (face) => (length, start, end) => {
|
|
144
|
+
const axis = Math.abs(inLot(end)[0] - inLot(start)[0]) > Math.abs(inLot(end)[1] - inLot(start)[1]) ? 0 : 1, [, from] = along(length, start, end, axis);
|
|
145
|
+
const count = Math.max(1, Math.round(length / 6.1));
|
|
146
|
+
return windows(length, start, end, axis, pairs(Array.from({ length: count }, (_, i) => from(length * (i + 0.5) / count))), 1.1, () => ({ face, from: 1, panel: false }));
|
|
147
|
+
};
|
|
148
|
+
skin(0, isMichigan, michigan);
|
|
149
|
+
skin(1, isSouth, southWall);
|
|
150
|
+
skin(2, isWashington, rear(terracotta));
|
|
151
|
+
skin(3, (run) => !isMichigan(run) && !isSouth(run) && !isWashington(run), rear(brick));
|
|
152
|
+
const model = kit.finish({ height: h.endParapet, outlines: [shell, parapets], opacity: 0.16 });
|
|
153
|
+
model.building.position.set(offset[0], 0, offset[1]);
|
|
154
|
+
model.building.userData["geography"] = record;
|
|
155
|
+
model.building.userData["geographicDetail"] = { levels: h, source: "docs/michigan-boulevard-reference.md" };
|
|
156
|
+
return model;
|
|
157
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { createBuilder } from "./building-kit.js";
|
|
2
|
+
// Michigan Plaza South at 205 North Michigan Avenue: the geographic layout's
|
|
3
|
+
// detailed model. The mapped ground outline stays exact; the 168.6 m top is
|
|
4
|
+
// the published architectural height. The dark curtain-wall grid, recessed
|
|
5
|
+
// panes with scattered lit units, and pale mullions reuse the fitted model's
|
|
6
|
+
// vocabulary at meter scale. Window-row and bay spacings are estimates; see
|
|
7
|
+
// docs/michigan-plaza-south-geographic-reference.md. Units are meters; +x is
|
|
8
|
+
// east, +z is south.
|
|
9
|
+
export const michiganPlazaSouthGeographicLevels = Object.freeze({
|
|
10
|
+
roof: 168.6, // published architectural height
|
|
11
|
+
floors: 44,
|
|
12
|
+
});
|
|
13
|
+
const h = michiganPlazaSouthGeographicLevels;
|
|
14
|
+
const shaftTop = h.roof - 0.8;
|
|
15
|
+
const pitch = shaftTop / h.floors;
|
|
16
|
+
export function createMichiganPlazaSouthGeographicBuilding(record, projectPlan, offset) {
|
|
17
|
+
const kit = createBuilder(record.name, record.id);
|
|
18
|
+
// The neutral palette of the fitted model: a mid-gray frame grid over dark
|
|
19
|
+
// glazing with scattered lit panes.
|
|
20
|
+
const shell = kit.batch("Michigan Plaza South · mapped tower shell", kit.material(0x555555));
|
|
21
|
+
const mullions = kit.batch("Michigan Plaza South · mullions and bands", kit.material(0x7a7a7a));
|
|
22
|
+
const lit = kit.batch("Michigan Plaza South · lit glazing", kit.material(0x5a5a5a));
|
|
23
|
+
const dim = kit.batch("Michigan Plaza South · dim glazing", kit.material(0x464646));
|
|
24
|
+
const tones = [0x2a2a2a, 0x2e2e2e, 0x333333, 0x262626].map((color, i) => kit.batch(`Michigan Plaza South · glazing ${i + 1}`, kit.material(color)));
|
|
25
|
+
const paneTone = (row, bay, side) => {
|
|
26
|
+
const hash = (Math.imul(row + 5, 0x9e3779b1) ^ Math.imul(bay + 17, 0x85ebca77) ^ Math.imul(side + 3, 0xc2b2ae3d)) >>> 0;
|
|
27
|
+
const value = hash % 97;
|
|
28
|
+
if (value < 3)
|
|
29
|
+
return lit;
|
|
30
|
+
if (value < 6)
|
|
31
|
+
return dim;
|
|
32
|
+
return tones[value % tones.length];
|
|
33
|
+
};
|
|
34
|
+
const ground = projectPlan(record.footprint.coordinates);
|
|
35
|
+
// The mapped volume: the outline itself is the tower, with a roof parapet.
|
|
36
|
+
kit.prism(shell, ground, [0, shaftTop]);
|
|
37
|
+
// A closed shallow solid on a run. Offsets separate the visible surfaces from
|
|
38
|
+
// their backing wall, and the ends stop short of polygon corners so adjacent
|
|
39
|
+
// walls' projecting returns cannot overlap at the same elevation.
|
|
40
|
+
const strip = (batch, run, from, to, y0, y1, back, front) => {
|
|
41
|
+
const clearance = Math.max(Math.abs(back), Math.abs(front)) + 0.025;
|
|
42
|
+
from = Math.max(from, clearance);
|
|
43
|
+
to = Math.min(to, run.length - clearance);
|
|
44
|
+
if (to - from < 0.03 || y1 <= y0)
|
|
45
|
+
return;
|
|
46
|
+
kit.box(batch, run.at((from + to) / 2), run.normal(0), (to - from) / 2, back, front, y0, y1);
|
|
47
|
+
};
|
|
48
|
+
// The dense Miesian grid: one pane per bay per row between full-height
|
|
49
|
+
// mullions, with a pale base course at grade. Mullions stand proudest.
|
|
50
|
+
ground.forEach((run, side) => {
|
|
51
|
+
if (run.length < 2)
|
|
52
|
+
return;
|
|
53
|
+
const bays = Math.max(1, Math.round(run.length / 3.1));
|
|
54
|
+
const width = run.length / bays;
|
|
55
|
+
for (let row = 1; row <= h.floors; row += 1) {
|
|
56
|
+
const bottom = Math.max((row - 1) * pitch, 0.85), top = row * pitch;
|
|
57
|
+
for (let bay = 0; bay < bays; bay += 1) {
|
|
58
|
+
strip(paneTone(row, bay, side), run, bay * width + 0.18, (bay + 1) * width - 0.18, bottom + 0.28, top - 0.24, 0.03, 0.07);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
for (let i = 0; i <= bays; i += 1) {
|
|
62
|
+
const s = Math.max(0.18, Math.min(run.length - 0.18, i * width));
|
|
63
|
+
strip(mullions, run, s - 0.06, s + 0.06, 0.25, shaftTop - 0.05, 0.05, 0.16);
|
|
64
|
+
}
|
|
65
|
+
strip(mullions, run, 0.015, run.length - 0.015, 0.3, 0.75, 0.06, 0.18);
|
|
66
|
+
});
|
|
67
|
+
// The roof parapet wraps the mapped outline at the published 168.6 m top.
|
|
68
|
+
// The outline's notch has concave corners, which a closed band cannot turn,
|
|
69
|
+
// so the parapet is per-segment boxes that stop short of every corner.
|
|
70
|
+
ground.forEach((run) => {
|
|
71
|
+
if (run.length < 2)
|
|
72
|
+
return;
|
|
73
|
+
const count = Math.max(1, Math.ceil(run.length / 1.5));
|
|
74
|
+
for (let index = 0; index < count; index += 1) {
|
|
75
|
+
const from = index * run.length / count, to = (index + 1) * run.length / count;
|
|
76
|
+
strip(mullions, run, from, to, shaftTop, h.roof, 0.05, 0.35);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
const model = kit.finish({ height: h.roof, outlines: [shell, mullions], opacity: 0.16 });
|
|
80
|
+
model.building.position.set(offset[0], 0, offset[1]);
|
|
81
|
+
model.building.userData["geography"] = record;
|
|
82
|
+
model.building.userData["geographicDetail"] = { levels: h, floors: 44, source: "docs/michigan-plaza-south-geographic-reference.md" };
|
|
83
|
+
return model;
|
|
84
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import * as THREE from "../vendor/three-r186.js";
|
|
2
|
+
// The roof outline and visible window spacing follow the source SVG. The
|
|
3
|
+
// footprint, hidden faces, and lower floors obscured in the drawing are inferred.
|
|
4
|
+
export function createMichiganPlazaSouthBuilding() {
|
|
5
|
+
const building = new THREE.Group();
|
|
6
|
+
building.name = "Michigan Plaza South";
|
|
7
|
+
building.userData["buildingId"] = "building-michigan-plaza-south-tower";
|
|
8
|
+
const width = 46.7, depth = 46.7, height = 180;
|
|
9
|
+
const gradient = new THREE.DataTexture(new Uint8Array([90, 145, 205, 255]), 4, 1, THREE.RedFormat);
|
|
10
|
+
gradient.minFilter = gradient.magFilter = THREE.NearestFilter;
|
|
11
|
+
gradient.needsUpdate = true;
|
|
12
|
+
const material = (color) => new THREE.MeshToonMaterial({ color, gradientMap: gradient });
|
|
13
|
+
const frame = material(0x555555);
|
|
14
|
+
const glass = material(0x323232);
|
|
15
|
+
const recess = material(0x252525);
|
|
16
|
+
const materials = [frame, glass, recess];
|
|
17
|
+
let triangleCount = 0;
|
|
18
|
+
function box(name, size, position, surface) {
|
|
19
|
+
const mesh = new THREE.Mesh(new THREE.BoxGeometry(...size), surface);
|
|
20
|
+
mesh.name = name;
|
|
21
|
+
mesh.position.set(...position);
|
|
22
|
+
mesh.castShadow = mesh.receiveShadow = true;
|
|
23
|
+
building.add(mesh);
|
|
24
|
+
triangleCount += 12;
|
|
25
|
+
}
|
|
26
|
+
box("closed tower shell", [width, height, depth], [0, height / 2, 0], frame);
|
|
27
|
+
box("inset flat roof", [width - 1, 0.15, depth - 1], [0, height + 0.025, 0], recess);
|
|
28
|
+
const windows = [], reveals = [];
|
|
29
|
+
const quad = (positions, a, b, c, d) => {
|
|
30
|
+
for (const point of [a, b, c, a, c, d])
|
|
31
|
+
positions.push(...point);
|
|
32
|
+
};
|
|
33
|
+
const corners = [[-width / 2, depth / 2], [width / 2, depth / 2], [width / 2, -depth / 2], [-width / 2, -depth / 2]];
|
|
34
|
+
for (let face = 0; face < 4; face += 1) {
|
|
35
|
+
const a = corners[face], b = corners[(face + 1) % 4];
|
|
36
|
+
const length = Math.hypot(b[0] - a[0], b[1] - a[1]);
|
|
37
|
+
const normal = [(a[1] - b[1]) / length, (b[0] - a[0]) / length];
|
|
38
|
+
const point = (t, y, offset) => [a[0] + (b[0] - a[0]) * t + normal[0] * offset, y, a[1] + (b[1] - a[1]) * t + normal[1] * offset];
|
|
39
|
+
// Match the 24- and 23-column grids drawn on the two visible SVG faces.
|
|
40
|
+
const columns = face % 2 === 0 ? 24 : 23;
|
|
41
|
+
for (let row = 0; row < 40; row += 1) {
|
|
42
|
+
const top = height - 13 - row * 4.1;
|
|
43
|
+
const bottom = top - 2.7;
|
|
44
|
+
for (let column = 0; column < columns; column += 1) {
|
|
45
|
+
const left = (column + 0.22) / columns, right = (column + 0.88) / columns;
|
|
46
|
+
// A dark surround and offset pane separate each opening without
|
|
47
|
+
// allocating thousands of individual meshes or external textures.
|
|
48
|
+
quad(reveals, point(left - 0.002, bottom - 0.08, 0.025), point(right + 0.002, bottom - 0.08, 0.025), point(right + 0.002, top + 0.08, 0.025), point(left - 0.002, top + 0.08, 0.025));
|
|
49
|
+
quad(windows, point(left, bottom, 0.055), point(right, bottom, 0.055), point(right, top, 0.055), point(left, top, 0.055));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
for (const [name, positions, surface] of [["window surrounds", reveals, recess], ["window grid", windows, glass]]) {
|
|
54
|
+
const geometry = new THREE.BufferGeometry();
|
|
55
|
+
geometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3));
|
|
56
|
+
geometry.computeVertexNormals();
|
|
57
|
+
const mesh = new THREE.Mesh(geometry, surface);
|
|
58
|
+
mesh.name = name;
|
|
59
|
+
mesh.castShadow = mesh.receiveShadow = true;
|
|
60
|
+
building.add(mesh);
|
|
61
|
+
triangleCount += positions.length / 9;
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
building, height, triangleCount,
|
|
65
|
+
setHighlighted(highlighted) { materials.forEach((surface) => surface.emissive.setHex(highlighted ? 0x222222 : 0)); },
|
|
66
|
+
setWireframe(enabled) { materials.forEach((surface) => { surface.wireframe = enabled; }); },
|
|
67
|
+
};
|
|
68
|
+
}
|