@a2f0/skyline 0.0.0-stage → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE.md +20 -0
- package/README.md +52 -2
- package/docs/package.md +224 -0
- package/docs/skyline-geography.md +503 -0
- package/lib/models/aon-center.d.ts +4 -0
- package/lib/models/aon-center.js +60 -0
- package/lib/models/aon-geographic.d.ts +9 -0
- package/lib/models/aon-geographic.js +35 -0
- package/lib/models/aon-tower.d.ts +23 -0
- package/lib/models/aon-tower.js +130 -0
- package/lib/models/blue-cross-geographic.d.ts +14 -0
- package/lib/models/blue-cross-geographic.js +248 -0
- package/lib/models/borg-warner-geographic.d.ts +20 -0
- package/lib/models/borg-warner-geographic.js +143 -0
- package/lib/models/buckingham-geographic.d.ts +10 -0
- package/lib/models/buckingham-geographic.js +121 -0
- package/lib/models/building-kit.d.ts +84 -0
- package/lib/models/building-kit.js +358 -0
- package/lib/models/celebrations.d.ts +39 -0
- package/lib/models/celebrations.js +44 -0
- package/lib/models/chicago-athletic-association-geographic.d.ts +32 -0
- package/lib/models/chicago-athletic-association-geographic.js +173 -0
- package/lib/models/crain-communications.d.ts +5 -0
- package/lib/models/crain-communications.js +75 -0
- package/lib/models/crain-geographic.d.ts +10 -0
- package/lib/models/crain-geographic.js +69 -0
- package/lib/models/crain-tower.d.ts +32 -0
- package/lib/models/crain-tower.js +378 -0
- package/lib/models/facade-grid.d.ts +18 -0
- package/lib/models/facade-grid.js +176 -0
- package/lib/models/gage-geographic.d.ts +19 -0
- package/lib/models/gage-geographic.js +154 -0
- package/lib/models/heritage-at-millennium-park.d.ts +3 -0
- package/lib/models/heritage-at-millennium-park.js +284 -0
- package/lib/models/heritage-geographic.d.ts +11 -0
- package/lib/models/heritage-geographic.js +208 -0
- package/lib/models/hyatt-west-tower-geographic.d.ts +26 -0
- package/lib/models/hyatt-west-tower-geographic.js +86 -0
- package/lib/models/keith-ascher-geographic.d.ts +29 -0
- package/lib/models/keith-ascher-geographic.js +110 -0
- package/lib/models/kemper-geographic.d.ts +8 -0
- package/lib/models/kemper-geographic.js +153 -0
- package/lib/models/kemper.d.ts +2 -0
- package/lib/models/kemper.js +78 -0
- package/lib/models/lake-view-geographic.d.ts +15 -0
- package/lib/models/lake-view-geographic.js +134 -0
- package/lib/models/maclean-center-geographic.d.ts +20 -0
- package/lib/models/maclean-center-geographic.js +162 -0
- package/lib/models/michigan-boulevard-geographic.d.ts +12 -0
- package/lib/models/michigan-boulevard-geographic.js +157 -0
- package/lib/models/michigan-plaza-south-geographic.d.ts +7 -0
- package/lib/models/michigan-plaza-south-geographic.js +84 -0
- package/lib/models/michigan-plaza-south.d.ts +2 -0
- package/lib/models/michigan-plaza-south.js +68 -0
- package/lib/models/millennium-park-plaza-geographic.d.ts +9 -0
- package/lib/models/millennium-park-plaza-geographic.js +103 -0
- package/lib/models/monroe-geographic.d.ts +19 -0
- package/lib/models/monroe-geographic.js +177 -0
- package/lib/models/north-michigan-180-geographic.d.ts +14 -0
- package/lib/models/north-michigan-180-geographic.js +117 -0
- package/lib/models/north-wabash-geographic.d.ts +14 -0
- package/lib/models/north-wabash-geographic.js +122 -0
- package/lib/models/on-the-park-geographic.d.ts +11 -0
- package/lib/models/on-the-park-geographic.js +245 -0
- package/lib/models/one-prudential-geographic.d.ts +12 -0
- package/lib/models/one-prudential-geographic.js +55 -0
- package/lib/models/one-prudential-plaza.d.ts +5 -0
- package/lib/models/one-prudential-plaza.js +72 -0
- package/lib/models/one-prudential-tower.d.ts +45 -0
- package/lib/models/one-prudential-tower.js +246 -0
- package/lib/models/peoples-gas-geographic.d.ts +23 -0
- package/lib/models/peoples-gas-geographic.js +250 -0
- package/lib/models/railway-exchange-geographic.d.ts +18 -0
- package/lib/models/railway-exchange-geographic.js +176 -0
- package/lib/models/river-plaza-geographic.d.ts +16 -0
- package/lib/models/river-plaza-geographic.js +104 -0
- package/lib/models/sheraton-grand-geographic.d.ts +24 -0
- package/lib/models/sheraton-grand-geographic.js +112 -0
- package/lib/models/six-north-michigan-geographic.d.ts +16 -0
- package/lib/models/six-north-michigan-geographic.js +180 -0
- package/lib/models/skyline-geography-data.d.ts +44 -0
- package/lib/models/skyline-geography-data.js +607 -0
- package/lib/models/skyline-geography.d.ts +23 -0
- package/lib/models/skyline-geography.js +322 -0
- package/lib/models/swissotel-geographic.d.ts +24 -0
- package/lib/models/swissotel-geographic.js +79 -0
- package/lib/models/three-illinois-center-geographic.d.ts +26 -0
- package/lib/models/three-illinois-center-geographic.js +86 -0
- package/lib/models/trump-geographic.d.ts +14 -0
- package/lib/models/trump-geographic.js +51 -0
- package/lib/models/trump-international-tower.d.ts +4 -0
- package/lib/models/trump-international-tower.js +84 -0
- package/lib/models/trump-tower.d.ts +43 -0
- package/lib/models/trump-tower.js +229 -0
- package/lib/models/two-illinois-center-geographic.d.ts +26 -0
- package/lib/models/two-illinois-center-geographic.js +85 -0
- package/lib/models/two-prudential-geographic.d.ts +25 -0
- package/lib/models/two-prudential-geographic.js +47 -0
- package/lib/models/two-prudential-plaza.d.ts +8 -0
- package/lib/models/two-prudential-plaza.js +55 -0
- package/lib/models/two-prudential-tower.d.ts +53 -0
- package/lib/models/two-prudential-tower.js +441 -0
- package/lib/models/university-club-geographic.d.ts +24 -0
- package/lib/models/university-club-geographic.js +217 -0
- package/lib/models/willoughby-tower-geographic.d.ts +17 -0
- package/lib/models/willoughby-tower-geographic.js +195 -0
- package/lib/models/window-illumination.d.ts +14 -0
- package/lib/models/window-illumination.js +64 -0
- package/lib/package-assets.d.ts +5 -0
- package/lib/package-assets.js +9 -0
- package/lib/skyline-package.d.ts +23 -0
- package/lib/skyline-package.js +25 -0
- package/lib/skyline-scene.d.ts +25 -0
- package/lib/skyline-scene.js +86 -0
- package/lib/study-types.d.ts +49 -0
- package/lib/study-types.js +1 -0
- package/lib/vendor/three-r186.d.ts +1 -0
- package/lib/vendor/three-r186.js +1 -0
- package/package.json +64 -4
- package/site/building-detail.html +54 -0
- package/site/building-detail.js +84 -0
- package/site/building-study.html +58 -0
- package/site/building-study.js +3 -0
- package/site/index.html +407 -0
- package/site/models/aon-center.js +60 -0
- package/site/models/aon-geographic.js +35 -0
- package/site/models/aon-tower.js +130 -0
- package/site/models/blue-cross-geographic.js +248 -0
- package/site/models/borg-warner-geographic.js +143 -0
- package/site/models/buckingham-geographic.js +121 -0
- package/site/models/building-kit.js +358 -0
- package/site/models/celebrations.js +44 -0
- package/site/models/chicago-athletic-association-geographic.js +173 -0
- package/site/models/crain-communications.js +75 -0
- package/site/models/crain-geographic.js +69 -0
- package/site/models/crain-reference.svg +10 -0
- package/site/models/crain-tower.js +378 -0
- package/site/models/facade-grid.js +176 -0
- package/site/models/gage-geographic.js +154 -0
- package/site/models/heritage-at-millennium-park.js +284 -0
- package/site/models/heritage-geographic.js +208 -0
- package/site/models/hyatt-west-tower-geographic.js +86 -0
- package/site/models/keith-ascher-geographic.js +110 -0
- package/site/models/kemper-geographic.js +153 -0
- package/site/models/kemper.js +78 -0
- package/site/models/lake-view-geographic.js +134 -0
- package/site/models/maclean-center-geographic.js +162 -0
- package/site/models/michigan-boulevard-geographic.js +157 -0
- package/site/models/michigan-plaza-south-geographic.js +84 -0
- package/site/models/michigan-plaza-south.js +68 -0
- package/site/models/millennium-park-plaza-geographic.js +103 -0
- package/site/models/monroe-geographic.js +177 -0
- package/site/models/north-michigan-180-geographic.js +117 -0
- package/site/models/north-wabash-geographic.js +122 -0
- package/site/models/on-the-park-geographic.js +245 -0
- package/site/models/one-prudential-geographic.js +55 -0
- package/site/models/one-prudential-plaza.js +72 -0
- package/site/models/one-prudential-tower.js +246 -0
- package/site/models/peoples-gas-geographic.js +250 -0
- package/site/models/railway-exchange-geographic.js +176 -0
- package/site/models/river-plaza-geographic.js +104 -0
- package/site/models/sheraton-grand-geographic.js +112 -0
- package/site/models/six-north-michigan-geographic.js +180 -0
- package/site/models/skyline-geography-data.js +607 -0
- package/site/models/skyline-geography.js +322 -0
- package/site/models/skyline-panorama.svg +3491 -0
- package/site/models/skyline-reference.svg +3090 -0
- package/site/models/swissotel-geographic.js +79 -0
- package/site/models/three-illinois-center-geographic.js +86 -0
- package/site/models/trump-geographic.js +51 -0
- package/site/models/trump-international-tower.js +84 -0
- package/site/models/trump-tower.js +229 -0
- package/site/models/two-illinois-center-geographic.js +85 -0
- package/site/models/two-prudential-geographic.js +47 -0
- package/site/models/two-prudential-plaza.js +55 -0
- package/site/models/two-prudential-tower.js +441 -0
- package/site/models/university-club-geographic.js +217 -0
- package/site/models/willoughby-tower-geographic.js +195 -0
- package/site/models/window-illumination.js +64 -0
- package/site/skyline-3d.html +92 -0
- package/site/skyline-3d.js +333 -0
- package/site/skyline-animated.svg +18795 -0
- package/site/skyline-comparison.js +97 -0
- package/site/skyline-original-fit.svg +18312 -0
- package/site/skyline-scene.js +86 -0
- package/site/skyline-study.html +83 -0
- package/site/skyline-study.js +109 -0
- package/site/skyline-webgl.html +667 -0
- package/site/stars.svg +176 -0
- package/site/study-loader.js +20 -0
- package/site/study-types.js +1 -0
- package/site/study-viewer.js +605 -0
- package/site/study.css +204 -0
- package/site/vendor/THREE-LICENSE.txt +21 -0
- package/site/vendor/three-r186.js +4228 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import * as THREE from "../vendor/three-r186.js";
|
|
2
|
+
import { createBuilder, inside, polygonOf } from "./building-kit.js";
|
|
3
|
+
import { chainSkin, chainsOf, orient, paintedSlab, planOf } from "./facade-grid.js";
|
|
4
|
+
import { createWindowIllumination } from "./window-illumination.js";
|
|
5
|
+
// The Blue Cross and Blue Shield Tower, 300 East Randolph Street (Lohan Associates, 1997;
|
|
6
|
+
// Goettsch Partners' 24-storey addition, 2010): the geographic layout's model, on the
|
|
7
|
+
// mapped outline 95486960 and its parts 284779637 and 284779635. The original layout
|
|
8
|
+
// has no model of it; the drawing shows it right of Aon, outside the excerpt it is fitted
|
|
9
|
+
// to.
|
|
10
|
+
//
|
|
11
|
+
// A glass curtain wall on the 5 ft module wraps a central block, the mapped 227 m part,
|
|
12
|
+
// which stands forward of two end bays, the rest of the mapped 212 m part. Three bands of
|
|
13
|
+
// mechanical floors cross the block, their columns lit blue at night; at the middle band,
|
|
14
|
+
// the top of the 1997 tower, the end bays are open through. A screen carrying the company's
|
|
15
|
+
// emblems rises from the block's roof to the published 226.7 m. Heights are measured on
|
|
16
|
+
// the photograph down from that top; see docs/blue-cross-reference.md. Units are meters;
|
|
17
|
+
// +x is east, +z is south.
|
|
18
|
+
export const blueCrossLevels = Object.freeze({
|
|
19
|
+
lobbyTop: 10.26, // fourteen floors under the lower band; estimate
|
|
20
|
+
bands: [[65.7, 72.1], [117.1, 128.1], [170.1, 176.6]], // mechanical floors
|
|
21
|
+
occupied: 208.3, // the highest occupied floor, published
|
|
22
|
+
roof: 212.8, // the central block's glass top
|
|
23
|
+
wings: 212, // the end bays' roof, mapped
|
|
24
|
+
top: 226.7, // the screen's top, the published architectural height
|
|
25
|
+
module: 1.524, // 5 ft
|
|
26
|
+
// Office floors between the bands, each run divided evenly: the lobby to the lower band,
|
|
27
|
+
// then between the bands, and from the upper band to the top occupied floor.
|
|
28
|
+
floors: [14, 11, 11, 8],
|
|
29
|
+
});
|
|
30
|
+
const h = blueCrossLevels;
|
|
31
|
+
// Every office floor's level, from the lobby's top to the top occupied floor.
|
|
32
|
+
const zones = [
|
|
33
|
+
[h.lobbyTop, h.bands[0][0], h.floors[0]], [h.bands[0][1], h.bands[1][0], h.floors[1]],
|
|
34
|
+
[h.bands[1][1], h.bands[2][0], h.floors[2]], [h.bands[2][1], h.occupied, h.floors[3]],
|
|
35
|
+
];
|
|
36
|
+
// Center five-floor words in the three upper office zones, ordered from the top.
|
|
37
|
+
const messageTopFloors = zones.map(([, , count], zone) => h.floors.slice(0, zone).reduce((sum, floors) => sum + floors, 0)
|
|
38
|
+
+ Math.floor((count + 5) / 2) - 1).reverse().slice(0, 3);
|
|
39
|
+
export const blueCrossFloors = [...zones.flatMap(([from, to, count]) => Array.from({ length: count }, (_, i) => from + (to - from) * i / count)), h.occupied];
|
|
40
|
+
const spandrelBelow = 0.3, spandrelAbove = 0.9;
|
|
41
|
+
const color = (hex) => new THREE.Color(hex);
|
|
42
|
+
const glassTones = [0x414141, 0x464646, 0x3c3c3c, 0x4a4a4a].map(color);
|
|
43
|
+
const litGlass = color(0x8a8a8a), dimGlass = color(0x5c5c5c);
|
|
44
|
+
const spandrel = color(0x383838), recess = color(0x2a2a2a), screen = color(0x3e3e3e), lobby = color(0x333333), roofing = color(0x3a3a3a);
|
|
45
|
+
function paneColor(row, bay, wall) {
|
|
46
|
+
const hash = (Math.imul(row + 13, 0x9e3779b1) ^ Math.imul(bay + 37, 0x85ebca77) ^ Math.imul(wall + 23, 0xc2b2ae3d)) >>> 0;
|
|
47
|
+
const value = hash % 101;
|
|
48
|
+
if (value < 4)
|
|
49
|
+
return litGlass;
|
|
50
|
+
if (value < 8)
|
|
51
|
+
return dimGlass;
|
|
52
|
+
return glassTones[value % glassTones.length];
|
|
53
|
+
}
|
|
54
|
+
const rows = [];
|
|
55
|
+
{
|
|
56
|
+
const push = (lo, hi, kind, floor) => { if (hi - lo > 1e-6)
|
|
57
|
+
rows.push({ lo, hi, kind, floor }); };
|
|
58
|
+
let floor = 0;
|
|
59
|
+
zones.forEach(([from, to, count], zone) => {
|
|
60
|
+
for (let i = 0; i < count; i += 1, floor += 1) {
|
|
61
|
+
const level = from + (to - from) * i / count, next = from + (to - from) * (i + 1) / count;
|
|
62
|
+
push(rows.at(-1)?.hi ?? level, level + spandrelAbove, "spandrel", floor);
|
|
63
|
+
push(level + spandrelAbove, next - spandrelBelow, "glass", floor);
|
|
64
|
+
}
|
|
65
|
+
const band = h.bands[zone];
|
|
66
|
+
if (band)
|
|
67
|
+
push(rows.at(-1).hi, band[1], "band", -1);
|
|
68
|
+
});
|
|
69
|
+
// The top occupied floor, under a parapet that rises past the end bays' roof to the
|
|
70
|
+
// block's, and the screen above.
|
|
71
|
+
push(rows.at(-1).hi, h.occupied + spandrelAbove, "spandrel", floor);
|
|
72
|
+
push(h.occupied + spandrelAbove, h.wings - spandrelBelow, "glass", floor);
|
|
73
|
+
push(h.wings - spandrelBelow, h.wings, "spandrel", -1);
|
|
74
|
+
push(h.wings, h.roof, "spandrel", -1);
|
|
75
|
+
push(h.roof, h.top, "screen", -1);
|
|
76
|
+
}
|
|
77
|
+
const rowsBetween = (lo, hi) => rows.filter((row) => row.lo >= lo - 1e-6 && row.hi <= hi + 1e-6);
|
|
78
|
+
export function createBlueCrossGeographicBuilding(record, projectPlan, offset) {
|
|
79
|
+
const kit = createBuilder(record.name, record.id);
|
|
80
|
+
const shell = kit.batch("Blue Cross · shell", kit.material(0xffffff, { vertexColors: true }));
|
|
81
|
+
const wall = kit.batch("Blue Cross · glass, spandrels and bands", kit.material(0xffffff, { vertexColors: true }));
|
|
82
|
+
const mullions = kit.batch("Blue Cross · mullions", kit.material(0x2c2c2c));
|
|
83
|
+
const bars = kit.batch("Blue Cross · band columns and emblems", kit.material(0xb4b4b4));
|
|
84
|
+
const windows = [];
|
|
85
|
+
let windowColumns = 0;
|
|
86
|
+
const outline = (way) => orient(polygonOf(projectPlan(record.parts.find((part) => part.way === way).coordinates)));
|
|
87
|
+
const footprint = orient(polygonOf(projectPlan(record.footprint.coordinates)));
|
|
88
|
+
const block = outline(284779637), slab = outline(284779635);
|
|
89
|
+
// The end bays: the mapped 212 m part less the central block. The two parts share the
|
|
90
|
+
// corners where the block meets each bay, so the slab's outline splits at those corners
|
|
91
|
+
// into stretches inside the block and stretches outside it. Each outside stretch is a
|
|
92
|
+
// bay's outer wall, closed along the block's wall, whose vertices between the same two
|
|
93
|
+
// corners lie inside the slab.
|
|
94
|
+
const near = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1]) < 0.01;
|
|
95
|
+
const corners = slab.flatMap((p, i) => (block.some((q) => near(p, q)) ? [i] : []));
|
|
96
|
+
const bays = corners.flatMap((a, k) => {
|
|
97
|
+
const b = corners[(k + 1) % corners.length], path = [];
|
|
98
|
+
for (let i = a;; i = (i + 1) % slab.length) {
|
|
99
|
+
path.push(slab[i]);
|
|
100
|
+
if (i === b)
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
const [p, q] = [path[0], path[1]];
|
|
104
|
+
if (inside(block, [(p[0] + q[0]) / 2, (p[1] + q[1]) / 2]))
|
|
105
|
+
return [];
|
|
106
|
+
const [from, to] = [block.findIndex((v) => near(v, path.at(-1))), block.findIndex((v) => near(v, path[0]))];
|
|
107
|
+
const walk = (step) => { const way = []; for (let j = (from + step + block.length) % block.length; j !== to; j = (j + step + block.length) % block.length)
|
|
108
|
+
way.push(block[j]); return way; };
|
|
109
|
+
const back = [walk(1), walk(-1)].find((way) => way.every((v) => inside(slab, v)));
|
|
110
|
+
return [orient([...path, ...back])];
|
|
111
|
+
});
|
|
112
|
+
// A solid on an outline between two heights, its walls the shell's dark core.
|
|
113
|
+
const solid = (corners, lo, hi, paint) => {
|
|
114
|
+
planOf(corners).forEach((run) => {
|
|
115
|
+
const a = run.at(0), b = run.at(run.length), n = run.normal(0);
|
|
116
|
+
kit.quad(shell, [[a[0], lo, a[1]], [b[0], lo, b[1]], [b[0], hi, b[1]], [a[0], hi, a[1]]], [[n[0], 0, n[1]]], paint);
|
|
117
|
+
});
|
|
118
|
+
paintedSlab(kit, shell, corners, lo, false, roofing);
|
|
119
|
+
paintedSlab(kit, shell, corners, hi, true, roofing);
|
|
120
|
+
};
|
|
121
|
+
// The lobby fills the mapped outline to the first office floor.
|
|
122
|
+
solid(footprint, 0, h.lobbyTop, lobby);
|
|
123
|
+
solid(block, h.lobbyTop, h.top, spandrel);
|
|
124
|
+
const middle = h.bands[1];
|
|
125
|
+
for (const bay of bays) {
|
|
126
|
+
solid(bay, h.lobbyTop, middle[0], spandrel);
|
|
127
|
+
solid(bay, middle[1], h.wings, spandrel);
|
|
128
|
+
}
|
|
129
|
+
// A wall's skin between two heights, on the runs `keep` accepts: cells two modules wide on
|
|
130
|
+
// every row, and mullions one module apart on the office floors. Each unbroken stretch of
|
|
131
|
+
// kept runs along a chain is one box, held 2 cm clear of its ends.
|
|
132
|
+
const skin = (corners, lo, hi, seed, keep, lift = 0) => {
|
|
133
|
+
const plan = planOf(corners), cuts = rowsBetween(lo, hi);
|
|
134
|
+
const heights = [cuts[0].lo + lift, ...cuts.map((row) => row.hi)];
|
|
135
|
+
const top = Math.min(hi, h.roof) - 0.05;
|
|
136
|
+
chainsOf(plan).forEach((chain, index) => {
|
|
137
|
+
const count = Math.max(1, Math.round(chain.length / h.module));
|
|
138
|
+
const stations = Array.from({ length: count + 1 }, (_, i) => chain.length * i / count);
|
|
139
|
+
const messageFace = seed === 0 && plan[chain.runs[0]].normal(0)[1] > 0.9 && chain.length > 50;
|
|
140
|
+
if (messageFace)
|
|
141
|
+
windowColumns = count;
|
|
142
|
+
const kept = chain.runs.map((run) => keep(plan[run]));
|
|
143
|
+
for (let k0 = kept.indexOf(true); k0 >= 0;) {
|
|
144
|
+
let k1 = k0;
|
|
145
|
+
while (kept[k1 + 1])
|
|
146
|
+
k1 += 1;
|
|
147
|
+
const s0 = chain.starts[k0] + 0.02, s1 = chain.starts[k1] + plan[chain.runs[k1]].length - 0.02;
|
|
148
|
+
chainSkin(kit, wall, plan, chain, k0, k1, s0, s1, stations.filter((_, i) => messageFace || i % 2 === 0), heights, 0.02, 0.07, (cell, r) => {
|
|
149
|
+
const row = cuts[r];
|
|
150
|
+
if (row.kind === "glass") {
|
|
151
|
+
if (messageFace)
|
|
152
|
+
windows.push({ vertex: wall.positions.length / 3, column: cell - 1, floor: row.floor });
|
|
153
|
+
// Subdivide the message face at every mullion while retaining its
|
|
154
|
+
// original paired-pane colors when the lights are off.
|
|
155
|
+
return paneColor(row.floor, messageFace ? Math.ceil(cell / 2) : cell, seed * 8 + index);
|
|
156
|
+
}
|
|
157
|
+
if (row.kind === "band")
|
|
158
|
+
return recess;
|
|
159
|
+
if (row.kind === "screen")
|
|
160
|
+
return screen;
|
|
161
|
+
return spandrel;
|
|
162
|
+
}, spandrel);
|
|
163
|
+
// Mullions on each office run, whole on one run and held clear of its ends. Their
|
|
164
|
+
// backs stand a centimetre off the wall: on the block's side walls the middle band's
|
|
165
|
+
// mullions start just inside the end bays, whose closing walls lie in that plane and
|
|
166
|
+
// face the way the backs do.
|
|
167
|
+
for (const s of stations) {
|
|
168
|
+
const k = chain.starts.findIndex((from, i) => s < from + plan[chain.runs[i]].length + 1e-9);
|
|
169
|
+
if (k < k0 || k > k1)
|
|
170
|
+
continue;
|
|
171
|
+
const run = plan[chain.runs[k]], along = Math.min(Math.max(s - chain.starts[k], 0.3), run.length - 0.3);
|
|
172
|
+
if (run.length > 0.6 && top - lo > 1)
|
|
173
|
+
kit.box(mullions, run.at(along), run.normal(0), 0.05, 0.01, 0.16, heights[0] + 0.05, top);
|
|
174
|
+
}
|
|
175
|
+
k0 = kept.indexOf(true, k1 + 1);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
};
|
|
179
|
+
// Whether a run lies along an outline's edges, and whether an end bay stands against it.
|
|
180
|
+
const lies = (run, on) => [0, run.length / 2, run.length].every((s) => {
|
|
181
|
+
const p = run.at(s);
|
|
182
|
+
return on.some((q, i) => {
|
|
183
|
+
const r = on[(i + 1) % on.length], [dx, dz] = [r[0] - q[0], r[1] - q[1]], l = Math.hypot(dx, dz);
|
|
184
|
+
const t = Math.max(0, Math.min(1, ((p[0] - q[0]) * dx + (p[1] - q[1]) * dz) / (l * l)));
|
|
185
|
+
return Math.hypot(q[0] + dx * t - p[0], q[1] + dz * t - p[1]) < 0.05;
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
const covered = (run) => bays.some((bay) => inside(bay, run.at(run.length / 2, 0.5)));
|
|
189
|
+
// The end bays cover the block's side walls except through the middle band and above
|
|
190
|
+
// their roof, so its skin there keeps to those heights. The bays' own skins leave off the
|
|
191
|
+
// runs they share with the block. `lift` raises the block's foot off the lobby's roof:
|
|
192
|
+
// where the block meets a bay their skins cross, and their feet would share that plane.
|
|
193
|
+
skin(block, h.lobbyTop, h.top, 0, (run) => !covered(run), 0.05);
|
|
194
|
+
skin(block, middle[0] - spandrelBelow, middle[1], 0, covered);
|
|
195
|
+
skin(block, h.wings, h.top, 0, covered);
|
|
196
|
+
bays.forEach((bay, i) => {
|
|
197
|
+
skin(bay, h.lobbyTop, middle[0], i + 1, (run) => !lies(run, block));
|
|
198
|
+
skin(bay, middle[1], h.wings, i + 1, (run) => !lies(run, block));
|
|
199
|
+
});
|
|
200
|
+
// An emblem: a plate standing 7 to 30 cm off the screen, cut to an outline given across and
|
|
201
|
+
// up from its centre. Its faces fan from the centre, which sees the whole outline, and its
|
|
202
|
+
// sides follow the outline's edges.
|
|
203
|
+
const emblem = (centre, t, n, y, shape) => {
|
|
204
|
+
const point = ([u, v], depth) => [centre[0] + t[0] * u + n[0] * depth, y + v, centre[1] + t[1] * u + n[1] * depth];
|
|
205
|
+
const out = [n[0], 0, n[1]], back = [-n[0], 0, -n[1]];
|
|
206
|
+
shape.forEach((p, i) => {
|
|
207
|
+
const q = shape[(i + 1) % shape.length];
|
|
208
|
+
kit.triangle(bars, [point([0, 0], 0.3), point(p, 0.3), point(q, 0.3)], [out, out, out]);
|
|
209
|
+
kit.triangle(bars, [point([0, 0], 0.07), point(q, 0.07), point(p, 0.07)], [back, back, back]);
|
|
210
|
+
// The edge's side faces away from the centre.
|
|
211
|
+
const sign = Math.sign((q[1] - p[1]) * (p[0] + q[0]) - (q[0] - p[0]) * (p[1] + q[1])), length = Math.hypot(q[0] - p[0], q[1] - p[1]);
|
|
212
|
+
const [across, up] = [sign * (q[1] - p[1]) / length, -sign * (q[0] - p[0]) / length];
|
|
213
|
+
kit.quad(bars, [point(p, 0.07), point(q, 0.07), point(q, 0.3), point(p, 0.3)], [[t[0] * across, up, t[1] * across]]);
|
|
214
|
+
});
|
|
215
|
+
};
|
|
216
|
+
// The company's emblems, 4.2 m tall: a cross, its arms a third of its span, and a shield,
|
|
217
|
+
// square-shouldered and drawn to a point.
|
|
218
|
+
const cross = [[0.7, 2.1], [0.7, 0.7], [2.1, 0.7], [2.1, -0.7], [0.7, -0.7], [0.7, -2.1],
|
|
219
|
+
[-0.7, -2.1], [-0.7, -0.7], [-2.1, -0.7], [-2.1, 0.7], [-0.7, 0.7], [-0.7, 2.1]];
|
|
220
|
+
const shield = [[-1.8, 2.1], [1.8, 2.1], [1.8, 0.3], [1.6, -0.7], [1.0, -1.5], [0, -2.1], [-1.0, -1.5], [-1.6, -0.7], [-1.8, 0.3]];
|
|
221
|
+
// The block's columns, exposed in each band and in the screen on its long faces: eight
|
|
222
|
+
// across each, one to a structural bay. The emblems stand in the screen's first two
|
|
223
|
+
// bays at the south face's west end, the cross west of the shield.
|
|
224
|
+
const faces = chainsOf(planOf(block)).filter(({ length }) => length > 50);
|
|
225
|
+
faces.forEach((chain) => {
|
|
226
|
+
const plan = planOf(block), run = plan[chain.runs[0]], last = plan[chain.runs.at(-1)];
|
|
227
|
+
const a = run.at(0), b = last.at(last.length), n = run.normal(0), length = Math.hypot(b[0] - a[0], b[1] - a[1]);
|
|
228
|
+
const t = [(b[0] - a[0]) / length, (b[1] - a[1]) / length];
|
|
229
|
+
const at = (s) => [a[0] + t[0] * s, a[1] + t[1] * s];
|
|
230
|
+
const pitch = length / 8;
|
|
231
|
+
for (let i = 0; i < 8; i += 1) {
|
|
232
|
+
for (const [lo, hi] of [...h.bands, [h.roof + 0.4, h.top - 0.3]])
|
|
233
|
+
kit.box(bars, at((i + 0.5) * pitch), n, 0.45, 0.07, 0.4, lo + 0.1, hi - 0.1);
|
|
234
|
+
}
|
|
235
|
+
// The south face runs west to east; the emblems stand in its first two bays.
|
|
236
|
+
if (n[1] > 0.9)
|
|
237
|
+
[cross, shield].forEach((shape, i) => emblem(at((i + 1) * pitch), t, n, h.roof + 6.7, shape));
|
|
238
|
+
});
|
|
239
|
+
const model = kit.finish({ height: h.top, outlines: [shell, mullions], opacity: 0.16 });
|
|
240
|
+
const glass = model.building.getObjectByName(wall.name);
|
|
241
|
+
if (!(glass instanceof THREE.Mesh) || !(glass.material instanceof THREE.MeshToonMaterial))
|
|
242
|
+
throw new Error("Blue Cross window lighting requires its glass mesh and toon material");
|
|
243
|
+
model.illumination = createWindowIllumination(glass.geometry, glass.material, windows, windowColumns, messageTopFloors);
|
|
244
|
+
model.building.position.set(offset[0], 0, offset[1]);
|
|
245
|
+
model.building.userData["geography"] = record;
|
|
246
|
+
model.building.userData["geographicDetail"] = { levels: h, source: "docs/blue-cross-reference.md" };
|
|
247
|
+
return model;
|
|
248
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
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 Borg-Warner Building, 200 South Michigan Avenue (A. Epstein & Sons with William
|
|
5
|
+
// Lescaze, 1958): the geographic layout's model, on the mapped outline 124873918. The
|
|
6
|
+
// original layout has no model of it; the drawing shows it across Adams from Peoples Gas.
|
|
7
|
+
//
|
|
8
|
+
// A curtain wall of aluminium mullions and dark spandrels wraps the Michigan, Adams and
|
|
9
|
+
// south fronts between clad corner columns, under a deep fascia. On the roof, an office
|
|
10
|
+
// penthouse stands flush with the Michigan and south fronts, and a taller block behind it.
|
|
11
|
+
// The roof and the penthouse are the City's 240 and 258 ft, and the block the Skyscraper
|
|
12
|
+
// Center's 83.5 m: each is the published height nearest its drawn reading. The drawn
|
|
13
|
+
// front's top reads 76 m, so heights read on the drawing are scaled to meet the roof; see
|
|
14
|
+
// docs/borg-warner-reference.md. Units are meters; +x is east, +z is south.
|
|
15
|
+
const drawn = (height) => height * 73.2 / 76;
|
|
16
|
+
export const borgWarnerLevels = Object.freeze({
|
|
17
|
+
neighbour: 31.1, // Symphony Center's roof, the City's 102 ft, over the south front's plain wall
|
|
18
|
+
fascia: drawn(72.9), // the fascia over the top floor's windows
|
|
19
|
+
roof: 73.2, // the roof, the City's 240 ft
|
|
20
|
+
penthouse: 78.6, // the office penthouse's top, the City's 258 ft
|
|
21
|
+
top: 83.5, // the block behind it, the Skyscraper Center's height
|
|
22
|
+
});
|
|
23
|
+
const h = borgWarnerLevels;
|
|
24
|
+
// The skins' colours: the window tones, and the spandrels, mullions, corner columns, the
|
|
25
|
+
// penthouse's cap and the plain walls, which no window shares.
|
|
26
|
+
export const borgWarnerPalette = Object.freeze({ glass: [0x3a3a3a, 0x3e3e3e, 0x424242, 0x464646], lit: 0x7a7a7a, dim: 0x4a4a4a, spandrel: 0x2d2d2d, mullion: 0x5e5e5e, corner: 0x525252, cap: 0x6a6a6a, plain: 0x585858 });
|
|
27
|
+
const palette = borgWarnerPalette;
|
|
28
|
+
const color = (hex) => new THREE.Color(hex);
|
|
29
|
+
const glassTones = palette.glass.map(color);
|
|
30
|
+
const litGlass = color(palette.lit), dimGlass = color(palette.dim);
|
|
31
|
+
const spandrel = color(palette.spandrel), mullion = color(palette.mullion), cornerColumn = color(palette.corner), cap = color(palette.cap), plain = color(palette.plain);
|
|
32
|
+
const core = color(0x262626), roofing = color(0x3c3c3c);
|
|
33
|
+
function paneColor(row, bay, wall) {
|
|
34
|
+
const hash = (Math.imul(row + 67, 0x9e3779b1) ^ Math.imul(bay + 97, 0x85ebca77) ^ Math.imul(wall + 79, 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 = [{ lo: 0.25, hi: drawn(0.5), kind: "curb", floor: 1 }];
|
|
43
|
+
const push = (hi, kind, floor) => rows.push({ lo: rows.at(-1).hi, hi, kind, floor });
|
|
44
|
+
const spandrelTop = (k) => 70.6 - k * 3.21;
|
|
45
|
+
push(drawn(spandrelTop(20) - 0.98), "glass", 1);
|
|
46
|
+
for (let k = 20; k >= 0; k -= 1) {
|
|
47
|
+
push(drawn(spandrelTop(k)), "spandrel", 22 - k);
|
|
48
|
+
push(k > 0 ? drawn(spandrelTop(k - 1) - 0.98) : h.fascia, "glass", 22 - k);
|
|
49
|
+
}
|
|
50
|
+
push(h.roof, "fascia", 22);
|
|
51
|
+
// The row that Symphony Center's roof crosses, split there.
|
|
52
|
+
const crossed = rows.findIndex((row) => row.lo < h.neighbour && row.hi > h.neighbour);
|
|
53
|
+
rows.splice(crossed, 1, { ...rows[crossed], hi: h.neighbour }, { ...rows[crossed], lo: h.neighbour });
|
|
54
|
+
// The penthouse's rows: a curb, its windows, and a light cap, as drawn over the fascia.
|
|
55
|
+
const penthouseRows = [
|
|
56
|
+
{ lo: h.roof, hi: h.roof + 0.3, kind: "curb", floor: 23 },
|
|
57
|
+
{ lo: h.roof + 0.3, hi: h.penthouse - 0.6, kind: "glass", floor: 23 },
|
|
58
|
+
{ lo: h.penthouse - 0.6, hi: h.penthouse, kind: "fascia", floor: 23 },
|
|
59
|
+
];
|
|
60
|
+
export function createBorgWarnerGeographicBuilding(record, projectPlan, offset) {
|
|
61
|
+
const kit = createBuilder(record.name, record.id);
|
|
62
|
+
const shell = kit.batch("Borg-Warner Building · shell", kit.material(0xffffff, { vertexColors: true }));
|
|
63
|
+
const wall = kit.batch("Borg-Warner Building · curtain wall", kit.material(0xffffff, { vertexColors: true }));
|
|
64
|
+
const block = kit.batch("Borg-Warner Building · rooftop block", kit.material(0x565656));
|
|
65
|
+
const roofTop = kit.batch("Borg-Warner Building · penthouse", kit.material(0x3c3c3c));
|
|
66
|
+
const lot = orient(polygonOf(projectPlan(record.footprint.coordinates)));
|
|
67
|
+
// The lot's south-east corner, with its south wall running west along Symphony Center
|
|
68
|
+
// and its Michigan front north to Adams.
|
|
69
|
+
const corner = lot.reduce((best, p) => (p[0] + p[1] > best[0] + best[1] ? p : best));
|
|
70
|
+
const northEast = lot.reduce((best, p) => (p[0] - p[1] > best[0] - best[1] ? p : best));
|
|
71
|
+
const southWest = lot.reduce((best, p) => (p[1] - p[0] > best[1] - best[0] ? p : best));
|
|
72
|
+
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]; };
|
|
73
|
+
const west = unit(southWest), north = unit(northEast);
|
|
74
|
+
const at = (a, b) => [corner[0] + west[0] * a + north[0] * b, corner[1] + west[1] * a + north[1] * b];
|
|
75
|
+
const rect = (a0, a1, b0, b1) => orient([at(a0, b0), at(a1, b0), at(a1, b1), at(a0, b1)]);
|
|
76
|
+
planOf(lot).forEach((run) => {
|
|
77
|
+
const a = run.at(0), b = run.at(run.length), n = run.normal(0);
|
|
78
|
+
kit.quad(shell, [[a[0], 0, a[1]], [b[0], 0, b[1]], [b[0], h.roof, b[1]], [a[0], h.roof, a[1]]], [[n[0], 0, n[1]]], core);
|
|
79
|
+
});
|
|
80
|
+
paintedSlab(kit, shell, lot, 0, false, roofing);
|
|
81
|
+
paintedSlab(kit, shell, lot, h.roof, true, roofing);
|
|
82
|
+
// On the roof: the office penthouse, 39 m along the south front and 22 m up Michigan, as
|
|
83
|
+
// the drawn penthouse meets the projected fronts. It stands 5 cm inside the south front
|
|
84
|
+
// and 12 cm inside the line between Michigan's corners, which the mapped wall stands up to
|
|
85
|
+
// 9.5 cm inside. Behind it, the taller block stands 14 to 27.4 m from Michigan and 1 to
|
|
86
|
+
// 28.3 m from the south front, as its drawn corners project, sunk 10 cm into the roof so
|
|
87
|
+
// that its floor lies clear of the penthouse's. Both footprints are estimates.
|
|
88
|
+
const penthouse = rect(0.12, 39, 0.05, 22);
|
|
89
|
+
kit.prism(roofTop, planOf(penthouse), [h.roof, h.penthouse]);
|
|
90
|
+
kit.prism(block, planOf(rect(14, 27.4, 1, 28.3)), [h.roof - 0.1, h.top]);
|
|
91
|
+
const paints = { spandrel, mullion, corner: cornerColumn, cap, plain };
|
|
92
|
+
const skin = (plan, keep, span, seed, openings) => {
|
|
93
|
+
const heights = [span[0].lo, ...span.map((row) => row.hi)];
|
|
94
|
+
chainsOf(plan).forEach((chain, index) => {
|
|
95
|
+
if (!chain.runs.every((run) => keep(plan[run])))
|
|
96
|
+
return;
|
|
97
|
+
const first = chain.runs[0], last = chain.runs.at(-1);
|
|
98
|
+
const s0 = turnsAt(plan, first)[0] > 0 ? 0.09 : 0.02, s1 = chain.length - (turnsAt(plan, last)[1] > 0 ? 0.09 : 0.02);
|
|
99
|
+
const [columns, cell] = openings(chain.length);
|
|
100
|
+
chainSkin(kit, wall, plan, chain, 0, chain.runs.length - 1, s0, s1, columns, heights, 0.02, 0.07, (bay, r) => {
|
|
101
|
+
const row = span[r], shows = cell((columns[bay - 1] + columns[bay]) / 2, row);
|
|
102
|
+
return shows === "glass" ? paneColor(row.floor, bay, seed * 8 + index) : paints[shows];
|
|
103
|
+
}, plain);
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
const near = (list, x) => Math.min(...list.map((c) => Math.abs(x - c)));
|
|
107
|
+
// The curtain wall: corner columns 1.2 m wide at each end, and mullions 12 cm wide about
|
|
108
|
+
// 1.33 m apart between them, as drawn: 22 bays on Michigan. Below `plainUnder` the wall
|
|
109
|
+
// is a neighbour's, plain.
|
|
110
|
+
const curtain = (plainUnder = 0) => (length) => {
|
|
111
|
+
const count = Math.max(1, Math.round((length - 2.4) / 1.33)), pitch = (length - 2.4) / count;
|
|
112
|
+
const mullions = Array.from({ length: count - 1 }, (_, k) => 1.2 + (k + 1) * pitch);
|
|
113
|
+
const cuts = [1.2, length - 1.2, ...mullions.flatMap((m) => [m - 0.06, m + 0.06])];
|
|
114
|
+
return [[0, ...cuts.sort((p, q) => p - q), length], (mid, row) => {
|
|
115
|
+
if (row.hi <= plainUnder + 1e-6)
|
|
116
|
+
return "plain";
|
|
117
|
+
if (mid < 1.2 || mid > length - 1.2)
|
|
118
|
+
return "corner";
|
|
119
|
+
if (near(mullions, mid) < 0.06)
|
|
120
|
+
return "mullion";
|
|
121
|
+
return row.kind === "glass" ? "glass" : row.kind === "fascia" ? (row.floor === 23 ? "cap" : "spandrel") : "spandrel";
|
|
122
|
+
}];
|
|
123
|
+
};
|
|
124
|
+
// The alley's windows, which the drawing does not show: 1.6 m wide, spread evenly about
|
|
125
|
+
// 3.2 m apart, one to each floor above the lobby.
|
|
126
|
+
const alley = (length) => {
|
|
127
|
+
const count = Math.max(1, Math.round(length / 3.2)), centres = Array.from({ length: count }, (_, k) => length * (k + 0.5) / count);
|
|
128
|
+
return [[0, ...centres.flatMap((c) => [c - 0.8, c + 0.8]), length], (mid, row) => (row.kind === "glass" && row.floor > 1 && near(centres, mid) < 0.8 ? "glass" : "plain")];
|
|
129
|
+
};
|
|
130
|
+
const facing = (x, z) => (run) => run.normal(0)[0] * x + run.normal(0)[1] * z > 0.9;
|
|
131
|
+
// Michigan and Adams carry the curtain wall to the roof; the south front above Symphony
|
|
132
|
+
// Center's 102 ft roof, plain below; the alley has windows.
|
|
133
|
+
skin(planOf(lot), (run) => facing(1, 0)(run) || facing(0, -1)(run), rows, 0, curtain());
|
|
134
|
+
skin(planOf(lot), facing(0, 1), rows, 1, curtain(h.neighbour));
|
|
135
|
+
skin(planOf(lot), facing(-1, 0), rows, 2, alley);
|
|
136
|
+
// The penthouse's glass between the same mullions, under its cap.
|
|
137
|
+
skin(planOf(penthouse), () => true, penthouseRows, 3, curtain());
|
|
138
|
+
const model = kit.finish({ height: h.top, outlines: [shell, roofTop, block], opacity: 0.16 });
|
|
139
|
+
model.building.position.set(offset[0], 0, offset[1]);
|
|
140
|
+
model.building.userData["geography"] = record;
|
|
141
|
+
model.building.userData["geographicDetail"] = { levels: h, source: "docs/borg-warner-reference.md" };
|
|
142
|
+
return model;
|
|
143
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import * as THREE from "../vendor/three-r186.js";
|
|
2
|
+
import { createBuilder, polygonOf } from "./building-kit.js";
|
|
3
|
+
import { chainSkin, chainsOf, orient, paintedSlab, planOf, turnAt, turnsAt } from "./facade-grid.js";
|
|
4
|
+
// The Buckingham, 360 East Randolph Street (Fujikawa Johnson & Associates, 1982): the
|
|
5
|
+
// geographic layout's model, on the mapped outline 95486940 and its rooftop part 284790189.
|
|
6
|
+
// The original layout has no model of it; the drawing shows it right of 340 on the Park,
|
|
7
|
+
// outside the excerpt it is fitted to.
|
|
8
|
+
//
|
|
9
|
+
// A concrete frame of five bays a face, a band at every floor over bronze ribbon windows. On
|
|
10
|
+
// the south, east and north faces the end bays stand forward of the middle three, and two
|
|
11
|
+
// corners are notched for stacked balconies. The top floor's tall openings sit under a cap
|
|
12
|
+
// at the mapped 119 m, and the rooftop enclosure rises to the published 121.9 m. Floor levels
|
|
13
|
+
// are measured on the drawing down from the mapped roof; see docs/buckingham-reference.md.
|
|
14
|
+
// Units are meters; +x is east, +z is south.
|
|
15
|
+
export const buckinghamLevels = Object.freeze({
|
|
16
|
+
lowestFloor: 18.94, // floor 7, the drawing's lowest floor line
|
|
17
|
+
pitch: 2.5456, // the drawn floor lines, 36 floors apart from floor 7's to floor 43's
|
|
18
|
+
capFoot: 117.4, // the top floor's openings' head, under the cap
|
|
19
|
+
roof: 119, // the cap's top, mapped
|
|
20
|
+
top: 121.9, // the rooftop enclosure's top, the published architectural height
|
|
21
|
+
});
|
|
22
|
+
const h = buckinghamLevels;
|
|
23
|
+
// Each floor's level, from the second, over the lobby, to the 44th.
|
|
24
|
+
const floor = (n) => h.lowestFloor + (n - 7) * h.pitch;
|
|
25
|
+
const color = (hex) => new THREE.Color(hex);
|
|
26
|
+
const glassTones = [0x303030, 0x343434, 0x383838, 0x3c3c3c].map(color);
|
|
27
|
+
const litGlass = color(0x6e6e6e), dimGlass = color(0x464646);
|
|
28
|
+
const concrete = color(0x4c4c4c), lobby = color(0x3a3a3a), core = color(0x262626), roofing = color(0x3a3a3a), enclosure = color(0x2e2e2e);
|
|
29
|
+
function paneColor(row, bay, wall) {
|
|
30
|
+
const hash = (Math.imul(row + 19, 0x9e3779b1) ^ Math.imul(bay + 43, 0x85ebca77) ^ Math.imul(wall + 31, 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 rows = [{ lo: 0.25, hi: floor(2) - 0.3, kind: "lobby", floor: 1 }];
|
|
39
|
+
for (let n = 2; n <= 43; n += 1) {
|
|
40
|
+
rows.push({ lo: floor(n) - 0.3, hi: floor(n) + 0.55, kind: "band", floor: n });
|
|
41
|
+
rows.push({ lo: floor(n) + 0.55, hi: floor(n + 1) - 0.3, kind: "glass", floor: n });
|
|
42
|
+
}
|
|
43
|
+
rows.push({ lo: floor(44) - 0.3, hi: floor(44) + 1.4, kind: "band", floor: 44 });
|
|
44
|
+
rows.push({ lo: floor(44) + 1.4, hi: h.capFoot, kind: "glass", floor: 44 });
|
|
45
|
+
rows.push({ lo: h.capFoot, hi: h.roof, kind: "band", floor: 45 });
|
|
46
|
+
const heights = [rows[0].lo, ...rows.map((row) => row.hi)];
|
|
47
|
+
export function createBuckinghamGeographicBuilding(record, projectPlan, offset) {
|
|
48
|
+
const kit = createBuilder(record.name, record.id);
|
|
49
|
+
const shell = kit.batch("Buckingham · shell", kit.material(0xffffff, { vertexColors: true }));
|
|
50
|
+
const wall = kit.batch("Buckingham · frame and glass", kit.material(0xffffff, { vertexColors: true }));
|
|
51
|
+
const piers = kit.batch("Buckingham · piers", kit.material(0x565656));
|
|
52
|
+
const balconies = kit.batch("Buckingham · corner balconies", kit.material(0x5a5a5a));
|
|
53
|
+
const outline = (way) => orient(polygonOf(projectPlan(record.parts.find((part) => part.way === way).coordinates)));
|
|
54
|
+
const tower = outline(95486940), roofRoom = outline(284790189);
|
|
55
|
+
// A solid on an outline between two heights.
|
|
56
|
+
const solid = (corners, lo, hi, paint) => {
|
|
57
|
+
planOf(corners).forEach((run) => {
|
|
58
|
+
const a = run.at(0), b = run.at(run.length), n = run.normal(0);
|
|
59
|
+
kit.quad(shell, [[a[0], lo, a[1]], [b[0], lo, b[1]], [b[0], hi, b[1]], [a[0], hi, a[1]]], [[n[0], 0, n[1]]], paint);
|
|
60
|
+
});
|
|
61
|
+
paintedSlab(kit, shell, corners, lo, false, roofing);
|
|
62
|
+
paintedSlab(kit, shell, corners, hi, true, roofing);
|
|
63
|
+
};
|
|
64
|
+
solid(tower, 0, h.roof, core);
|
|
65
|
+
solid(roofRoom, h.roof, h.top, enclosure);
|
|
66
|
+
// Every wall's skin, one box a chain, held 2 cm clear of its ends, or 9 cm, past its own
|
|
67
|
+
// depth, where the outline turns in and the next wall's skin would otherwise cross it. A
|
|
68
|
+
// face's cells are a quarter bay wide, with its corner piers painted in its end cells.
|
|
69
|
+
const plan = planOf(tower), chains = chainsOf(plan);
|
|
70
|
+
chains.forEach((chain, index) => {
|
|
71
|
+
const first = chain.runs[0], last = chain.runs.at(-1);
|
|
72
|
+
const s0 = turnsAt(plan, first)[0] > 0 ? 0.09 : 0.02, s1 = chain.length - (turnsAt(plan, last)[1] > 0 ? 0.09 : 0.02);
|
|
73
|
+
const count = Math.max(1, Math.round((chain.length - 1.2) / 1.6));
|
|
74
|
+
const columns = chain.length > 4 ? [0, 0.6, ...Array.from({ length: count - 1 }, (_, i) => 0.6 + (chain.length - 1.2) * (i + 1) / count), chain.length - 0.6, chain.length] : [0, chain.length];
|
|
75
|
+
chainSkin(kit, wall, plan, chain, 0, chain.runs.length - 1, s0, s1, columns, heights, 0.02, 0.07, (bay, r) => {
|
|
76
|
+
const row = rows[r], pier = chain.length > 4 && (bay <= 1 || bay >= columns.length - 1);
|
|
77
|
+
if (row.kind === "lobby")
|
|
78
|
+
return lobby;
|
|
79
|
+
if (row.kind === "band" || pier)
|
|
80
|
+
return concrete;
|
|
81
|
+
return paneColor(row.floor, bay, index);
|
|
82
|
+
}, concrete);
|
|
83
|
+
// Piers stand proud between a wall's bays, about 6.4 m wide: three across a face's
|
|
84
|
+
// set-back middle and five across the flat west face; an end bay has none.
|
|
85
|
+
const bays = Math.round(chain.length / 6.5);
|
|
86
|
+
for (let k = 1; k < bays; k += 1) {
|
|
87
|
+
const s = chain.length * k / bays, j = chain.starts.findLastIndex((start) => start <= s), run = plan[chain.runs[j]];
|
|
88
|
+
kit.box(piers, run.at(s - chain.starts[j]), run.normal(0), 0.3, 0.05, 0.25, 0.3, h.roof - 0.1);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
// The notched corners: two short walls turning in between two faces. A balcony fills each
|
|
92
|
+
// notch on every floor to the 43rd, a slab held clear of the walls' skins under an L of
|
|
93
|
+
// railing along its open sides.
|
|
94
|
+
chains.forEach((chain, i) => {
|
|
95
|
+
const next = chains[(i + 1) % chains.length];
|
|
96
|
+
if (chain.length > 3 || next.length > 3 || turnAt(plan[chain.runs.at(-1)], plan[next.runs[0]]) <= 0)
|
|
97
|
+
return;
|
|
98
|
+
const a = plan[chain.runs[0]], b = plan[next.runs[0]];
|
|
99
|
+
const [p0, p1, p2] = [a.at(0), a.at(a.length), b.at(b.length)];
|
|
100
|
+
const [na, nb] = [a.normal(0), b.normal(0)];
|
|
101
|
+
const q = [p0[0] + p2[0] - p1[0], p0[1] + p2[1] - p1[1]];
|
|
102
|
+
const off = (p, n, d) => [p[0] + n[0] * d, p[1] + n[1] * d];
|
|
103
|
+
const a0 = off(p0, na, 0.07), b0 = off(p2, nb, 0.07), inner = off(off(p1, na, 0.07), nb, 0.07);
|
|
104
|
+
// Railing: the slab's open sides, 5 cm deep, turned in toward the notch's inner corner.
|
|
105
|
+
const inward = (from, to) => {
|
|
106
|
+
const [dx, dz] = [to[0] - from[0], to[1] - from[1]], l = Math.hypot(dx, dz), n = [-dz / l, dx / l];
|
|
107
|
+
return (inner[0] - from[0]) * n[0] + (inner[1] - from[1]) * n[1] > 0 ? n : [-n[0], -n[1]];
|
|
108
|
+
};
|
|
109
|
+
const [na0, nb0] = [inward(a0, q), inward(q, b0)];
|
|
110
|
+
const rail = [a0, q, b0, off(b0, nb0, 0.05), off(off(q, na0, 0.05), nb0, 0.05), off(a0, na0, 0.05)];
|
|
111
|
+
for (let n = 2; n <= 43; n += 1) {
|
|
112
|
+
kit.prism(balconies, planOf(orient([inner, a0, q, b0])), [floor(n) - 0.12, floor(n) + 0.12]);
|
|
113
|
+
kit.prism(balconies, planOf(orient(rail)), [floor(n) + 0.12, floor(n) + 1.0]);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
const model = kit.finish({ height: h.top, outlines: [shell, piers], opacity: 0.16 });
|
|
117
|
+
model.building.position.set(offset[0], 0, offset[1]);
|
|
118
|
+
model.building.userData["geography"] = record;
|
|
119
|
+
model.building.userData["geographicDetail"] = { levels: h, source: "docs/buckingham-reference.md" };
|
|
120
|
+
return model;
|
|
121
|
+
}
|