@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,246 @@
|
|
|
1
|
+
import * as THREE from "../vendor/three-r186.js";
|
|
2
|
+
import { createBuilder, inside } from "./building-kit.js";
|
|
3
|
+
import { chainPier, chainsOf, gridBox, jointTrim, mitredBox, orient, paintedSlab, planOf, turnsAt } from "./facade-grid.js";
|
|
4
|
+
// Heights above the street. The penthouse top and the antenna tip are published, and the
|
|
5
|
+
// antenna's length is WGN's. The roof, the observatory band, the penthouse's louvers, the
|
|
6
|
+
// floor pitch, and the window rows are measured on the photograph the drawing was traced
|
|
7
|
+
// from, down from the published penthouse top. See docs/one-prudential-reference.md.
|
|
8
|
+
export const onePrudentialLevels = Object.freeze({
|
|
9
|
+
// The ground storey, then thirty-nine office floors, then the observatory: forty-one.
|
|
10
|
+
lobbyTop: 12.03,
|
|
11
|
+
pitch: 3.93,
|
|
12
|
+
floors: 41,
|
|
13
|
+
sill: 0.85,
|
|
14
|
+
head: 2.45,
|
|
15
|
+
sillCourse: 164.9,
|
|
16
|
+
bandFoot: 165.3,
|
|
17
|
+
copingFoot: 169,
|
|
18
|
+
roof: 169.5,
|
|
19
|
+
signBottom: 171,
|
|
20
|
+
signTop: 179.5,
|
|
21
|
+
louverBottom: 180.3,
|
|
22
|
+
penthouseTop: 183.2,
|
|
23
|
+
// The 311 ft tubular mast's top, where WGN's 73 ft antenna leaves its 12 ft socket.
|
|
24
|
+
mastTop: 259.4,
|
|
25
|
+
tip: 278,
|
|
26
|
+
bay: 2.35,
|
|
27
|
+
});
|
|
28
|
+
const h = onePrudentialLevels;
|
|
29
|
+
// A floor's level: the ground floor at the street, the office floors from the lobby's top,
|
|
30
|
+
// and the forty-first at the observatory band's foot.
|
|
31
|
+
export const floorLevel = (floor) => (floor === 1 ? 0 : h.lobbyTop + (floor - 2) * h.pitch);
|
|
32
|
+
// The facade module. Interior piers stand one bay apart, centred on the wall; the bays at
|
|
33
|
+
// each end take what is left, at least `endBay`, and the corner pier widens to fill it, so
|
|
34
|
+
// every window is the same width.
|
|
35
|
+
const endBay = 2.6, pierHalf = 0.55, pierDepth = 0.3, ribHalf = 0.5;
|
|
36
|
+
export function wallStations(length) {
|
|
37
|
+
const interior = Math.max(0, Math.floor((length - 2 * endBay) / h.bay));
|
|
38
|
+
const end = (length - interior * h.bay) / 2;
|
|
39
|
+
return { piers: Array.from({ length: interior + 1 }, (_, i) => end + i * h.bay), corner: end - h.bay + pierHalf };
|
|
40
|
+
}
|
|
41
|
+
// A wing's ribs: as many bays as the wall holds, centred on it.
|
|
42
|
+
export function wingRibs(length) {
|
|
43
|
+
const count = Math.floor(length / h.bay);
|
|
44
|
+
return Array.from({ length: count }, (_, i) => length / 2 + (i - (count - 1) / 2) * h.bay);
|
|
45
|
+
}
|
|
46
|
+
// The screen's fins: the drawing's forty-nine on the south face, one every 0.98 m from 2.2 m
|
|
47
|
+
// in; the other faces hold as many as fit, centred.
|
|
48
|
+
const finPitch = 0.98, finHalf = 0.15, finDepth = 0.2;
|
|
49
|
+
export function screenFins(length, front) {
|
|
50
|
+
if (front)
|
|
51
|
+
return Array.from({ length: 49 }, (_, i) => 2.2 + i * finPitch);
|
|
52
|
+
const count = Math.floor((length - 1) / finPitch) + 1;
|
|
53
|
+
return Array.from({ length: count }, (_, i) => length / 2 + (i - (count - 1) / 2) * finPitch);
|
|
54
|
+
}
|
|
55
|
+
// The sign, along the penthouse's south face: a board carrying the Rock of Gibraltar and
|
|
56
|
+
// the name, ten letters from 14 m to 42.8 m, their capital and ascenders taller than the rest.
|
|
57
|
+
const sign = { board: [4.6, 43.3], logo: [7.1, 13], rows: [172, 173.8, 177, 178.7], letters: 10, name: [14, 42.8], tall: new Set([0, 3, 6, 7, 9]) };
|
|
58
|
+
const mastFoot = 1.15, mastHead = 0.5, antennaHalf = 0.22;
|
|
59
|
+
const color = (hex) => new THREE.Color(hex);
|
|
60
|
+
const glassTones = [0x262626, 0x2c2c2c, 0x323232, 0x222222].map(color);
|
|
61
|
+
const litGlass = color(0x7c7c7c), dimGlass = color(0x4e4e4e);
|
|
62
|
+
const observatory = [0x3a3a3a, 0x444444, 0x404040].map(color), litObservatory = color(0x8a8a8a);
|
|
63
|
+
const aluminium = color(0x8e8e8e), darkSpandrel = color(0x242424), limestone = color(0xb0b0b0), roofing = color(0x4c4c4c);
|
|
64
|
+
const penthouseWall = color(0x9c9c9c), louverBacking = color(0x383838), board = color(0x2e2e2e), lettering = color(0xc4c4c4), logo = color(0xb0b0b0);
|
|
65
|
+
function paneColor(row, bay, wall) {
|
|
66
|
+
const hash = (Math.imul(row + 5, 0x9e3779b1) ^ Math.imul(bay + 23, 0x85ebca77) ^ Math.imul(wall + 13, 0xc2b2ae3d)) >>> 0;
|
|
67
|
+
const value = hash % 101;
|
|
68
|
+
if (value < 3)
|
|
69
|
+
return litGlass;
|
|
70
|
+
if (value < 6)
|
|
71
|
+
return dimGlass;
|
|
72
|
+
return glassTones[value % glassTones.length];
|
|
73
|
+
}
|
|
74
|
+
function wallRows(from, to) {
|
|
75
|
+
const rows = [{ lo: 0.4, hi: h.lobbyTop - 0.9, kind: "glass", floor: 1 }];
|
|
76
|
+
for (let floor = 2; floor < h.floors; floor += 1) {
|
|
77
|
+
const level = floorLevel(floor);
|
|
78
|
+
rows.push({ lo: rows.at(-1).hi, hi: level + h.sill, kind: "spandrel", floor }, { lo: level + h.sill, hi: level + h.head, kind: "glass", floor });
|
|
79
|
+
}
|
|
80
|
+
rows.push({ lo: rows.at(-1).hi, hi: h.bandFoot, kind: "spandrel", floor: h.floors - 1 });
|
|
81
|
+
rows.push({ lo: h.bandFoot, hi: h.copingFoot, kind: "band", floor: h.floors });
|
|
82
|
+
return rows.filter((row) => row.hi > from + 1e-6 && row.lo < to - 1e-6).map((row) => ({ ...row, lo: Math.max(row.lo, from), hi: Math.min(row.hi, to) }));
|
|
83
|
+
}
|
|
84
|
+
export function buildOnePrudentialTower(form) {
|
|
85
|
+
const kit = createBuilder(form.name, form.id);
|
|
86
|
+
const shell = kit.batch("One Prudential · shell", kit.material(0xffffff, { vertexColors: true }));
|
|
87
|
+
const glazing = kit.batch("One Prudential · windows and spandrels", kit.material(0xffffff, { vertexColors: true }));
|
|
88
|
+
const piers = kit.batch("One Prudential · limestone piers", kit.material(0xb4b4b4));
|
|
89
|
+
const bands = kit.batch("One Prudential · limestone courses", kit.material(0xc0c0c0));
|
|
90
|
+
const penthouse = kit.batch("One Prudential · penthouse and sign", kit.material(0xffffff, { vertexColors: true }));
|
|
91
|
+
const fins = kit.batch("One Prudential · screen louvers", kit.material(0x707070));
|
|
92
|
+
const mast = kit.batch("One Prudential · antenna mast", kit.material(0xb4b4b4));
|
|
93
|
+
const base = form.base;
|
|
94
|
+
const y = (real) => real - base;
|
|
95
|
+
const shown = (real) => real > base + 1e-6;
|
|
96
|
+
const tower = orient(form.tower), towerPlan = planOf(tower);
|
|
97
|
+
const wings = form.wings.filter(({ top }) => shown(top)).map(({ outline, top }) => ({ outline: orient(outline), plan: planOf(orient(outline)), top }));
|
|
98
|
+
// A wall is covered, up to a neighbour's roof, where a probe just outside it lands inside
|
|
99
|
+
// that neighbour: the tower covers its wings' inner walls, and the wings the tower's lower ones.
|
|
100
|
+
const parts = [{ outline: tower, top: h.roof }, ...wings.map(({ outline, top }) => ({ outline, top }))];
|
|
101
|
+
const coveredTo = (run, own) => Math.max(base, ...parts.filter(({ outline }) => outline !== own && inside(outline, run.at(run.length / 2, 0.3))).map(({ top }) => top));
|
|
102
|
+
const turns = turnsAt;
|
|
103
|
+
// The solids: walls, floors, and roofs, closed. Walls behind the facade are spandrel
|
|
104
|
+
// aluminium; a wall too short for a bay is plain limestone.
|
|
105
|
+
const solid = (outline, plan, top, paint) => {
|
|
106
|
+
plan.forEach((run) => {
|
|
107
|
+
const a = run.at(0), b = run.at(run.length), n = run.normal(0);
|
|
108
|
+
kit.quad(shell, [[a[0], 0, a[1]], [b[0], 0, b[1]], [b[0], y(top), b[1]], [a[0], y(top), a[1]]], [[n[0], 0, n[1]]], paint(run));
|
|
109
|
+
});
|
|
110
|
+
paintedSlab(kit, shell, outline, 0, false, roofing);
|
|
111
|
+
paintedSlab(kit, shell, outline, y(top), true, roofing);
|
|
112
|
+
};
|
|
113
|
+
// Piers cross a chain's joints in pieces, and cells stop short of them: see chainPier and
|
|
114
|
+
// jointTrim in facade-grid.ts.
|
|
115
|
+
const trim = jointTrim;
|
|
116
|
+
const pierOn = (plan, chain, froms, a, b, depth, top) => chainPier(kit, piers, plan, chain, froms, a, b, depth, top, y);
|
|
117
|
+
// A run's cells start just above its piers, so their undersides never share a plane.
|
|
118
|
+
const cellsFrom = (from) => from + 0.1;
|
|
119
|
+
// The tower's walls. Each chain of nearly collinear runs is one wall with one set of
|
|
120
|
+
// piers; a run's windows start above whatever wing covers it.
|
|
121
|
+
const towerChains = chainsOf(towerPlan);
|
|
122
|
+
const plainRuns = new Set(towerChains.filter(({ length }) => length < 2 * endBay).flatMap(({ runs }) => runs));
|
|
123
|
+
solid(tower, towerPlan, h.roof, (run) => (plainRuns.has(towerPlan.indexOf(run)) ? limestone : aluminium));
|
|
124
|
+
towerChains.forEach((chain, wall) => {
|
|
125
|
+
if (chain.length < 2 * endBay)
|
|
126
|
+
return;
|
|
127
|
+
const { piers: stations, corner } = wallStations(chain.length);
|
|
128
|
+
const columns = [corner - 0.2, ...stations, chain.length - corner + 0.2];
|
|
129
|
+
const froms = chain.runs.map((index) => coveredTo(towerPlan[index], tower));
|
|
130
|
+
chain.runs.forEach((index, k) => {
|
|
131
|
+
const run = towerPlan[index], start = chain.starts[k], end = start + run.length;
|
|
132
|
+
const rows = wallRows(cellsFrom(froms[k]), h.copingFoot);
|
|
133
|
+
// Windows and spandrels: one box on the run, a column per bay and a row per window,
|
|
134
|
+
// spandrel, and the observatory band.
|
|
135
|
+
const first = Math.max(columns[0], start + trim(towerPlan, chain, froms, k, k - 1)), last = Math.min(columns.at(-1), end - trim(towerPlan, chain, froms, k, k + 1));
|
|
136
|
+
const local = [first, ...columns.filter((s) => s > first + 1e-6 && s < last - 1e-6), last];
|
|
137
|
+
const offset = columns.filter((s) => s <= first + 1e-6).length - 1;
|
|
138
|
+
if (last - first < 0.05 || !rows.length)
|
|
139
|
+
return;
|
|
140
|
+
gridBox(kit, glazing, run, local.map((s) => s - start), [rows[0].lo, ...rows.map((row) => row.hi)], 0.02, 0.07, (column, r) => {
|
|
141
|
+
const row = rows[r], bay = offset + column;
|
|
142
|
+
if (row.kind === "spandrel")
|
|
143
|
+
return aluminium;
|
|
144
|
+
if (row.kind === "band")
|
|
145
|
+
return paneColor(row.floor, bay, wall).equals(litGlass) ? litObservatory : observatory[bay % observatory.length];
|
|
146
|
+
return paneColor(row.floor, bay, wall);
|
|
147
|
+
}, aluminium, y);
|
|
148
|
+
});
|
|
149
|
+
// The piers, from the street to the sill course, standing proud of the glass: one on
|
|
150
|
+
// every bay line, and a wider one at each corner.
|
|
151
|
+
for (const s of stations)
|
|
152
|
+
pierOn(towerPlan, chain, froms, s - pierHalf, s + pierHalf, pierDepth, h.sillCourse);
|
|
153
|
+
pierOn(towerPlan, chain, froms, 0, corner, pierDepth, h.sillCourse);
|
|
154
|
+
pierOn(towerPlan, chain, froms, chain.length - corner, chain.length, pierDepth, h.sillCourse);
|
|
155
|
+
});
|
|
156
|
+
// The limestone sill course under the observatory and the coping over it, round every run.
|
|
157
|
+
towerPlan.forEach((run, index) => {
|
|
158
|
+
const [before, after] = turns(towerPlan, index);
|
|
159
|
+
if (shown(h.bandFoot))
|
|
160
|
+
mitredBox(kit, bands, run, 0, run.length, before, after, pierDepth, Math.max(h.sillCourse, base), h.bandFoot, y);
|
|
161
|
+
if (shown(h.roof))
|
|
162
|
+
mitredBox(kit, bands, run, 0, run.length, before, after, 0.35, Math.max(h.copingFoot, base), h.roof, y);
|
|
163
|
+
});
|
|
164
|
+
// The wings: dark spandrels between limestone ribs on the same module, a window per bay
|
|
165
|
+
// on each floor, and a coping. Walls the tower covers carry nothing.
|
|
166
|
+
wings.forEach(({ outline, plan, top }, number) => {
|
|
167
|
+
solid(outline, plan, top, () => darkSpandrel);
|
|
168
|
+
// Where a wall meets the tower, its cells and coping stop clear of the tower's piers.
|
|
169
|
+
const coping = top - 0.5, clear = pierDepth + 0.05;
|
|
170
|
+
const open = plan.map((run) => coveredTo(run, outline) < coping);
|
|
171
|
+
chainsOf(plan).forEach((chain, wall) => {
|
|
172
|
+
const ribs = wingRibs(chain.length);
|
|
173
|
+
const froms = chain.runs.map((index) => (open[index] ? coveredTo(plan[index], outline) : top));
|
|
174
|
+
chain.runs.forEach((index, k) => {
|
|
175
|
+
const run = plan[index], start = chain.starts[k];
|
|
176
|
+
// The cells stop under the coping, clear of the ribs' tops.
|
|
177
|
+
const rows = wallRows(cellsFrom(froms[k]), coping - 0.05).filter((row) => row.kind !== "band");
|
|
178
|
+
if (!open[index] || !rows.length)
|
|
179
|
+
return;
|
|
180
|
+
// Cells stop short of a wall the tower covers, clear of the tower's piers in the corner.
|
|
181
|
+
const s0 = open[(index + plan.length - 1) % plan.length] ? trim(plan, chain, froms, k, k - 1) : clear;
|
|
182
|
+
const s1 = run.length - (open[(index + 1) % plan.length] ? trim(plan, chain, froms, k, k + 1) : clear);
|
|
183
|
+
const local = [s0, ...ribs.filter((s) => s > start + s0 + 1e-6 && s < start + s1 - 1e-6).map((s) => s - start), s1];
|
|
184
|
+
const offset = ribs.filter((s) => s <= start + 1e-6).length;
|
|
185
|
+
gridBox(kit, glazing, run, local, [rows[0].lo, ...rows.map((row) => row.hi)], 0.02, 0.07, (column, r) => (rows[r].kind === "glass" ? paneColor(rows[r].floor, offset + column, 40 + 10 * number + wall) : darkSpandrel), darkSpandrel, y);
|
|
186
|
+
});
|
|
187
|
+
for (const s of ribs)
|
|
188
|
+
pierOn(plan, chain, froms, s - ribHalf, s + ribHalf, pierDepth, coping);
|
|
189
|
+
});
|
|
190
|
+
// The coping, mitred between open walls. Against the tower it is mitred as if into a
|
|
191
|
+
// concave corner, its inner edge on the wall's own corners and its outer edge clear of
|
|
192
|
+
// the tower's piers.
|
|
193
|
+
plan.forEach((run, index) => {
|
|
194
|
+
if (!open[index])
|
|
195
|
+
return;
|
|
196
|
+
const [before, after] = turns(plan, index), quarter = Math.PI / 2;
|
|
197
|
+
mitredBox(kit, bands, run, 0, run.length, open[(index + plan.length - 1) % plan.length] ? before : quarter, open[(index + 1) % plan.length] ? after : quarter, 0.35, Math.max(coping, base), top, y);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
// The penthouse: a pale wall carrying the sign, under a dark screen of limestone-grey fins.
|
|
201
|
+
const { corner, along, length, depth } = form.penthouse;
|
|
202
|
+
const inward = [along[1], -along[0]];
|
|
203
|
+
const offsetBy = ([x, z], s, d) => [x + along[0] * s + inward[0] * d, z + along[1] * s + inward[1] * d];
|
|
204
|
+
const penthouseOutline = orient([corner, offsetBy(corner, length, 0), offsetBy(corner, length, depth), offsetBy(corner, 0, depth)]);
|
|
205
|
+
const penthousePlan = planOf(penthouseOutline);
|
|
206
|
+
penthousePlan.forEach((run) => {
|
|
207
|
+
const a = run.at(0), b = run.at(run.length), n = run.normal(0), normal = [[n[0], 0, n[1]]];
|
|
208
|
+
const p = (q, v) => [q[0], y(v), q[1]];
|
|
209
|
+
kit.quad(penthouse, [p(a, h.roof), p(b, h.roof), p(b, h.louverBottom), p(a, h.louverBottom)], normal, penthouseWall);
|
|
210
|
+
kit.quad(penthouse, [p(a, h.louverBottom), p(b, h.louverBottom), p(b, h.penthouseTop), p(a, h.penthouseTop)], normal, louverBacking);
|
|
211
|
+
const front = n[1] > 0.9;
|
|
212
|
+
for (const s of screenFins(run.length, front))
|
|
213
|
+
kit.box(fins, run.at(s), n, finHalf, 0, finDepth, y(h.louverBottom + 0.15), y(h.penthouseTop - 0.15));
|
|
214
|
+
if (front) {
|
|
215
|
+
const pitch = (sign.name[1] - sign.name[0]) / sign.letters;
|
|
216
|
+
const letters = Array.from({ length: sign.letters }, (_, i) => [sign.name[0] + i * pitch + 0.25, sign.name[0] + (i + 1) * pitch - 0.25]).flat();
|
|
217
|
+
const columns = [sign.board[0], sign.logo[0], sign.logo[1], ...letters, sign.board[1]];
|
|
218
|
+
// Rows: the board's foot, the logo, the letters, the tall letters, the board's head.
|
|
219
|
+
gridBox(kit, penthouse, run, columns, [h.signBottom, ...sign.rows, h.signTop], 0.02, 0.25, (column, row) => {
|
|
220
|
+
if (column === 1)
|
|
221
|
+
return row >= 1 && row <= 3 ? logo : board;
|
|
222
|
+
const letter = (column - 3) / 2;
|
|
223
|
+
if (column < 3 || !Number.isInteger(letter) || letter >= sign.letters)
|
|
224
|
+
return board;
|
|
225
|
+
return row === 2 || (row === 3 && sign.tall.has(letter)) ? lettering : board;
|
|
226
|
+
}, board, y);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
paintedSlab(kit, penthouse, penthouseOutline, y(h.roof), false, roofing);
|
|
230
|
+
paintedSlab(kit, penthouse, penthouseOutline, y(h.penthouseTop), true, roofing);
|
|
231
|
+
// The mast: an eight-sided steel tube tapering from the roof to its top, capped by a fan
|
|
232
|
+
// from its axis, and WGN's slim antenna on to the published tip.
|
|
233
|
+
const [mx, mz] = form.mast, sides = 8;
|
|
234
|
+
const ring = (radius, v) => Array.from({ length: sides }, (_, i) => { const a = (i + 0.5) * 2 * Math.PI / sides; return [mx + radius * Math.cos(a), y(v), mz - radius * Math.sin(a)]; });
|
|
235
|
+
const foot = ring(mastFoot, h.roof), head = ring(mastHead, h.mastTop);
|
|
236
|
+
// Each side leans in with the taper, so its normal tilts up by the apothem's fall per metre.
|
|
237
|
+
const lean = (mastFoot - mastHead) * Math.cos(Math.PI / sides) / (h.mastTop - h.roof);
|
|
238
|
+
for (let i = 0; i < sides; i += 1) {
|
|
239
|
+
const j = (i + 1) % sides, a = (i + 1) * 2 * Math.PI / sides, l = Math.hypot(1, lean), out = [Math.cos(a) / l, lean / l, -Math.sin(a) / l];
|
|
240
|
+
kit.quad(mast, [foot[i], foot[j], head[j], head[i]], [out]);
|
|
241
|
+
kit.triangle(mast, [[mx, y(h.mastTop), mz], head[i], head[j]], [[0, 1, 0], [0, 1, 0], [0, 1, 0]]);
|
|
242
|
+
kit.triangle(mast, [[mx, y(h.roof), mz], foot[j], foot[i]], [[0, -1, 0], [0, -1, 0], [0, -1, 0]]);
|
|
243
|
+
}
|
|
244
|
+
kit.box(mast, form.mast, [0, 1], antennaHalf, -antennaHalf, antennaHalf, y(h.mastTop), y(h.tip));
|
|
245
|
+
return kit.finish({ height: y(h.tip), outlines: [shell, bands], opacity: 0.18 });
|
|
246
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { BuildingModel, Plan } from "./building-kit.js";
|
|
2
|
+
import type { GeoBuilding } from "./skyline-geography-data.js";
|
|
3
|
+
export declare const peoplesGasLevels: Readonly<{
|
|
4
|
+
court: number;
|
|
5
|
+
inner: number;
|
|
6
|
+
colonnade: number;
|
|
7
|
+
entablature: number;
|
|
8
|
+
frieze: number;
|
|
9
|
+
cornice: number;
|
|
10
|
+
roof: number;
|
|
11
|
+
top: 82.9;
|
|
12
|
+
}>;
|
|
13
|
+
export declare const peoplesGasPalette: Readonly<{
|
|
14
|
+
glass: number[];
|
|
15
|
+
lit: 7237230;
|
|
16
|
+
dim: 3947580;
|
|
17
|
+
stone: 8158332;
|
|
18
|
+
granite: 5921370;
|
|
19
|
+
band: 9342606;
|
|
20
|
+
shadow: 2368548;
|
|
21
|
+
court: 10263708;
|
|
22
|
+
}>;
|
|
23
|
+
export declare function createPeoplesGasGeographicBuilding(record: GeoBuilding, projectPlan: (coordinates: [number, number][]) => Plan, offset: [number, number]): BuildingModel;
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import * as THREE from "../vendor/three-r186.js";
|
|
2
|
+
import { createBuilder, polygonOf } from "./building-kit.js";
|
|
3
|
+
import { chainSkin, chainsOf, orient, paintedSlab, planOf, turnsAt } from "./facade-grid.js";
|
|
4
|
+
// The Peoples Gas Building, 122 South Michigan Avenue (D. H. Burnham & Co., 1911): the
|
|
5
|
+
// geographic layout's model, on the mapped outline 145498710 and its parts. The original
|
|
6
|
+
// layout has no model of it; the drawing shows it between the Lake View Building and Adams
|
|
7
|
+
// Street, with the Borg-Warner Building before its Adams front.
|
|
8
|
+
//
|
|
9
|
+
// Twenty storeys rise on the Michigan and Adams fronts between wide corner piers:
|
|
10
|
+
// - two behind granite columns;
|
|
11
|
+
// - fourteen of paired windows, eleven bays to Michigan and nine to Adams;
|
|
12
|
+
// - three behind a colonnade under an entablature;
|
|
13
|
+
// - an attic under a frieze of pendant arches, a projecting cornice and its cresting.
|
|
14
|
+
// A light court opens over the ground storey and widens over the seventeenth floor. The top
|
|
15
|
+
// is the City's 272 ft. The drawn cresting reads 86 m, so heights read on the drawing are
|
|
16
|
+
// scaled to meet it; see docs/peoples-gas-reference.md. Units are meters; +x is east, +z is
|
|
17
|
+
// south.
|
|
18
|
+
const drawn = (height) => height * 82.9 / 86;
|
|
19
|
+
export const peoplesGasLevels = Object.freeze({
|
|
20
|
+
court: drawn(7.2), // the light court's floor, over the ground storey
|
|
21
|
+
inner: drawn(66.3), // the court's inner wings' roof, at the seventeenth floor
|
|
22
|
+
colonnade: drawn(67.47), // the colonnade's foot
|
|
23
|
+
entablature: drawn(76.8), // the colonnade's entablature
|
|
24
|
+
frieze: drawn(82.1), // the frieze under the cornice
|
|
25
|
+
cornice: drawn(84.3), // the projecting cornice
|
|
26
|
+
roof: drawn(84.8), // the cornice's top and the roof
|
|
27
|
+
top: 82.9, // the cresting's top, the published height
|
|
28
|
+
});
|
|
29
|
+
const h = peoplesGasLevels;
|
|
30
|
+
// The skins' colours: the window tones, and the terracotta, granite, bands, the frieze's
|
|
31
|
+
// shadow and the court's white brick, which no window shares.
|
|
32
|
+
export const peoplesGasPalette = Object.freeze({ glass: [0x2c2c2c, 0x303030, 0x343434, 0x383838], lit: 0x6e6e6e, dim: 0x3c3c3c, stone: 0x7c7c7c, granite: 0x5a5a5a, band: 0x8e8e8e, shadow: 0x242424, court: 0x9c9c9c });
|
|
33
|
+
const palette = peoplesGasPalette;
|
|
34
|
+
const color = (hex) => new THREE.Color(hex);
|
|
35
|
+
const glassTones = palette.glass.map(color);
|
|
36
|
+
const litGlass = color(palette.lit), dimGlass = color(palette.dim);
|
|
37
|
+
const stone = color(palette.stone), granite = color(palette.granite), band = color(palette.band), shadow = color(palette.shadow), courtBrick = color(palette.court);
|
|
38
|
+
const core = color(0x262626), party = color(0x646464), roofing = color(0x3c3c3c);
|
|
39
|
+
function paneColor(row, bay, wall) {
|
|
40
|
+
const hash = (Math.imul(row + 61, 0x9e3779b1) ^ Math.imul(bay + 89, 0x85ebca77) ^ Math.imul(wall + 73, 0xc2b2ae3d)) >>> 0;
|
|
41
|
+
const value = hash % 101;
|
|
42
|
+
if (value < 4)
|
|
43
|
+
return litGlass;
|
|
44
|
+
if (value < 8)
|
|
45
|
+
return dimGlass;
|
|
46
|
+
return glassTones[value % glassTones.length];
|
|
47
|
+
}
|
|
48
|
+
const rows = [{ lo: 0.25, hi: drawn(0.6), kind: "granite", floor: 1 }];
|
|
49
|
+
const push = (hi, kind, floor, more = {}) => rows.push({ lo: rows.at(-1).hi, hi: drawn(hi), kind, floor, ...more });
|
|
50
|
+
push(6.2, "shop", 1);
|
|
51
|
+
push(7.2, "granite", 2);
|
|
52
|
+
push(11.6, "shop", 2);
|
|
53
|
+
push(12.4, "granite", 2);
|
|
54
|
+
push(13.3, "belt", 2);
|
|
55
|
+
const sill = (n) => 63.36 - (16 - n) * 3.79;
|
|
56
|
+
for (let n = 3; n <= 16; n += 1) {
|
|
57
|
+
push(sill(n), "wall", n);
|
|
58
|
+
push(sill(n) + 2.24, "glass", n, { pier: true });
|
|
59
|
+
}
|
|
60
|
+
push(66.3, "wall", 16);
|
|
61
|
+
push(67.47, "wall", 17);
|
|
62
|
+
push(69.7, "rail", 17, { pier: true });
|
|
63
|
+
push(69.9, "rail", 17);
|
|
64
|
+
push(70.9, "open", 18);
|
|
65
|
+
push(73.2, "open", 18, { pier: true });
|
|
66
|
+
push(74.56, "open", 19);
|
|
67
|
+
push(76.8, "open", 19, { pier: true });
|
|
68
|
+
push(78.3, "band", 19);
|
|
69
|
+
push(78.9, "wall", 20);
|
|
70
|
+
push(81, "attic", 20);
|
|
71
|
+
push(81.3, "attic", 20, { reach: 0.45 });
|
|
72
|
+
push(81.55, "pendant", 20, { reach: 0.45 });
|
|
73
|
+
push(82.1, "pendant", 20, { reach: 0.9 });
|
|
74
|
+
push(84.3, "frieze", 20);
|
|
75
|
+
push(84.8, "parapet", 20);
|
|
76
|
+
export function createPeoplesGasGeographicBuilding(record, projectPlan, offset) {
|
|
77
|
+
const kit = createBuilder(record.name, record.id);
|
|
78
|
+
const shell = kit.batch("Peoples Gas Building · shell", kit.material(0xffffff, { vertexColors: true }));
|
|
79
|
+
const wall = kit.batch("Peoples Gas Building · terracotta and windows", kit.material(0xffffff, { vertexColors: true }));
|
|
80
|
+
const cornice = kit.batch("Peoples Gas Building · cornice", kit.material(0x8e8e8e));
|
|
81
|
+
const corners = (coordinates) => orient(polygonOf(projectPlan(coordinates)));
|
|
82
|
+
const lot = corners(record.footprint.coordinates);
|
|
83
|
+
// The mapped parts: the twenty-level ring, the north wing within it, the inner wings
|
|
84
|
+
// rising to the seventeenth floor, the court, and the notch in the north wall that opens
|
|
85
|
+
// it to the Lake View Building's light court.
|
|
86
|
+
const part = (way) => corners(record.parts.find((p) => p.way === way).coordinates);
|
|
87
|
+
const mapped = [1179833660, 1179833658, 1179833661, 1179833659, 1179842408].map(part);
|
|
88
|
+
// The lot's south-east corner, on Michigan and Adams, with the Adams front running west to
|
|
89
|
+
// the alley and the Michigan front north to the Lake View Building.
|
|
90
|
+
const corner = lot.reduce((best, p) => (p[0] + p[1] > best[0] + best[1] ? p : best));
|
|
91
|
+
const northEast = lot.reduce((best, p) => (p[0] - p[1] > best[0] - best[1] ? p : best));
|
|
92
|
+
const southWest = lot.reduce((best, p) => (p[1] - p[0] > best[1] - best[0] ? p : best));
|
|
93
|
+
const unit = (p) => { const d = Math.hypot(p[0] - corner[0], p[1] - corner[1]); return [(p[0] - corner[0]) / d, (p[1] - corner[1]) / d]; };
|
|
94
|
+
const west = unit(southWest), north = unit(northEast);
|
|
95
|
+
const at = (a, b) => [corner[0] + west[0] * a + north[0] * b, corner[1] + west[1] * a + north[1] * b];
|
|
96
|
+
// The inverse of `at`: a point's distances along the Adams and Michigan fronts, which
|
|
97
|
+
// meet a little off square.
|
|
98
|
+
const skew = west[0] * north[1] - west[1] * north[0];
|
|
99
|
+
const inLot = (p) => {
|
|
100
|
+
const [x, z] = [p[0] - corner[0], p[1] - corner[1]];
|
|
101
|
+
return [(x * north[1] - z * north[0]) / skew, (west[0] * z - west[1] * x) / skew];
|
|
102
|
+
};
|
|
103
|
+
const [adams, michigan] = [inLot(southWest)[0], inLot(northEast)[1]];
|
|
104
|
+
// Corners and edges by value, since parts share the outline's nodes.
|
|
105
|
+
const key = (p) => `${p[0].toFixed(3)},${p[1].toFixed(3)}`;
|
|
106
|
+
// Where a part's edge joins two of the outline's corners a few nodes apart, it follows
|
|
107
|
+
// the outline between them, so that the roof meets the walls corner to corner: the ring
|
|
108
|
+
// skips two of the Michigan front's nodes.
|
|
109
|
+
const index = new Map(lot.map((p, i) => [key(p), i]));
|
|
110
|
+
const follow = (polygon) => polygon.flatMap((a, i) => {
|
|
111
|
+
const [from, to] = [index.get(key(a)), index.get(key(polygon[(i + 1) % polygon.length]))], n = lot.length;
|
|
112
|
+
if (from === undefined || to === undefined)
|
|
113
|
+
return [a];
|
|
114
|
+
const [ahead, back] = [(to - from + n) % n, (from - to + n) % n];
|
|
115
|
+
const step = ahead > 1 && ahead <= 3 ? 1 : back > 1 && back <= 3 ? -1 : 0, between = step === 1 ? ahead - 1 : step === -1 ? back - 1 : 0;
|
|
116
|
+
return [a, ...Array.from({ length: between }, (_, k) => lot[(from + step * (k + 1) + n) % n])];
|
|
117
|
+
});
|
|
118
|
+
// And a part takes every other part's corner within 2 cm of one of its edges: the ring's
|
|
119
|
+
// edge along the court passes 1.1 cm from the court's corner.
|
|
120
|
+
const conform = (polygon, others) => polygon.flatMap((a, i) => {
|
|
121
|
+
const b = polygon[(i + 1) % polygon.length], length = Math.hypot(b[0] - a[0], b[1] - a[1]);
|
|
122
|
+
const on = others.flat().map((p) => ({ p, t: ((p[0] - a[0]) * (b[0] - a[0]) + (p[1] - a[1]) * (b[1] - a[1])) / length ** 2 }))
|
|
123
|
+
.filter(({ p, t }) => t > 1e-6 && t < 1 - 1e-6 && Math.abs((p[0] - a[0]) * (b[1] - a[1]) - (p[1] - a[1]) * (b[0] - a[0])) / length < 0.02);
|
|
124
|
+
return [a, ...[...new Map(on.map((x) => [key(x.p), x])).values()].sort((x, y) => x.t - y.t).map((x) => x.p)];
|
|
125
|
+
});
|
|
126
|
+
const followed = mapped.map(follow);
|
|
127
|
+
const [ring, northWing, innerWings, court, notch] = followed.map((polygon) => conform(polygon, followed.filter((other) => other !== polygon)));
|
|
128
|
+
const edges = (polygon) => new Set(polygon.flatMap((a, i) => { const b = polygon[(i + 1) % polygon.length]; return [`${key(a)}|${key(b)}`, `${key(b)}|${key(a)}`]; }));
|
|
129
|
+
const has = (set, run) => set.has(`${key(run.at(0))}|${key(run.at(run.length))}`);
|
|
130
|
+
// Two outlines sharing a run of edges, as one: the edges neither shares, end to end.
|
|
131
|
+
const merge = (p, q) => {
|
|
132
|
+
const [ep, eq] = [edges(p), edges(q)];
|
|
133
|
+
const kept = [...p.map((a, i) => [a, p[(i + 1) % p.length]]).filter(([a, b]) => !eq.has(`${key(a)}|${key(b)}`)),
|
|
134
|
+
...q.map((a, i) => [a, q[(i + 1) % q.length]]).filter(([a, b]) => !ep.has(`${key(a)}|${key(b)}`))];
|
|
135
|
+
const next = new Map(kept.map(([a, b]) => [key(a), b]));
|
|
136
|
+
const out = [kept[0][0]];
|
|
137
|
+
for (let p1 = kept[0][1]; key(p1) !== key(out[0]); p1 = next.get(key(p1)))
|
|
138
|
+
out.push(p1);
|
|
139
|
+
return orient(out);
|
|
140
|
+
};
|
|
141
|
+
// The court's walls face into it, so their outlines run the other way.
|
|
142
|
+
const inward = (polygon) => [...polygon].reverse();
|
|
143
|
+
const [lower, upper, opening] = [inward(court), inward(merge(court, innerWings)), inward(notch)];
|
|
144
|
+
const [lotEdges, wingEdges, lowerEdges] = [edges(lot), edges(innerWings), edges(court)];
|
|
145
|
+
const face = (run, lo, hi, paint) => {
|
|
146
|
+
const a = run.at(0), b = run.at(run.length), n = run.normal(0);
|
|
147
|
+
kit.quad(shell, [[a[0], lo, a[1]], [b[0], lo, b[1]], [b[0], hi, b[1]], [a[0], hi, a[1]]], [[n[0], 0, n[1]]], paint);
|
|
148
|
+
};
|
|
149
|
+
// The outer walls to the roof, the north wall, above the Lake View Building, in common
|
|
150
|
+
// brick, and open over the court's floor at the notch. Walls split where the ones they
|
|
151
|
+
// meet end, at the court's floor and the inner wings' roof, so every edge is shared.
|
|
152
|
+
const notchEdges = edges(notch);
|
|
153
|
+
planOf(lot).forEach((run) => {
|
|
154
|
+
const paint = run.normal(0)[1] < -0.9 ? party : core;
|
|
155
|
+
face(run, 0, h.court, paint);
|
|
156
|
+
if (!has(notchEdges, run))
|
|
157
|
+
face(run, h.court, h.roof, paint);
|
|
158
|
+
});
|
|
159
|
+
// The court's walls: the inner wings' to their roof, the rest to the roof, and over the
|
|
160
|
+
// inner wings the widened court's; the notch's but its open side.
|
|
161
|
+
planOf(lower).forEach((run) => {
|
|
162
|
+
face(run, h.court, h.inner, courtBrick);
|
|
163
|
+
if (!has(wingEdges, run))
|
|
164
|
+
face(run, h.inner, h.roof, courtBrick);
|
|
165
|
+
});
|
|
166
|
+
planOf(upper).forEach((run) => { if (!has(lowerEdges, run))
|
|
167
|
+
face(run, h.inner, h.roof, courtBrick); });
|
|
168
|
+
planOf(opening).forEach((run) => { if (!has(lotEdges, run))
|
|
169
|
+
face(run, h.court, h.roof, courtBrick); });
|
|
170
|
+
paintedSlab(kit, shell, lot, 0, false, roofing);
|
|
171
|
+
for (const roof of [ring, northWing])
|
|
172
|
+
paintedSlab(kit, shell, roof, h.roof, true, roofing);
|
|
173
|
+
paintedSlab(kit, shell, innerWings, h.inner, true, roofing);
|
|
174
|
+
for (const floor of [court, notch])
|
|
175
|
+
paintedSlab(kit, shell, floor, h.court, true, roofing);
|
|
176
|
+
// The cornice projects 90 cm along both fronts, to 2 cm over the roof, stopping 3 cm
|
|
177
|
+
// short of the north and alley walls. It starts 8 cm inside the lines between the fronts'
|
|
178
|
+
// corners, which the mapped walls stand at most 2 cm inside and 12.4 cm outside, so it
|
|
179
|
+
// always meets them. Its cresting: blocks 50 cm wide and 1 m apart near its edge.
|
|
180
|
+
const [out, lip] = [0.9, 0.02];
|
|
181
|
+
kit.prism(cornice, planOf(orient([at(-out, michigan - 0.03), at(-out, -out), at(adams - 0.03, -out), at(adams - 0.03, 0.08), at(0.08, 0.08), at(0.08, michigan - 0.03)])), [h.cornice, h.roof + lip]);
|
|
182
|
+
const [east, south] = [[-north[1], north[0]], [west[1], -west[0]]];
|
|
183
|
+
for (let b = 0.5; b < michigan - 0.5; b += 1)
|
|
184
|
+
kit.box(cornice, at(-0.7, b), east, 0.25, -0.15, 0.15, h.roof + lip, h.top);
|
|
185
|
+
for (let a = 0.5; a < adams - 0.5; a += 1)
|
|
186
|
+
kit.box(cornice, at(a, -0.7), south, 0.25, -0.15, 0.15, h.roof + lip, h.top);
|
|
187
|
+
const skin = (plan, keep, from, to, seed, openings, plain) => {
|
|
188
|
+
const span = rows.filter((row) => row.lo >= from - 1e-9 && row.hi <= to + 1e-9), heights = [span[0].lo, ...span.map((row) => row.hi)];
|
|
189
|
+
const paints = { wall: plain, granite, band, shadow };
|
|
190
|
+
chainsOf(plan).forEach((chain, index) => {
|
|
191
|
+
if (!chain.runs.every((run) => keep(plan[run])))
|
|
192
|
+
return;
|
|
193
|
+
const first = chain.runs[0], last = chain.runs.at(-1);
|
|
194
|
+
const s0 = turnsAt(plan, first)[0] > 0 ? 0.09 : 0.02, s1 = chain.length - (turnsAt(plan, last)[1] > 0 ? 0.09 : 0.02);
|
|
195
|
+
const [columns, cell] = openings(chain.length);
|
|
196
|
+
chainSkin(kit, wall, plan, chain, 0, chain.runs.length - 1, s0, s1, columns, heights, 0.02, 0.07, (bay, r) => {
|
|
197
|
+
const row = span[r], shows = cell((columns[bay - 1] + columns[bay]) / 2, row);
|
|
198
|
+
return shows === "glass" ? paneColor(row.floor, bay, seed * 8 + index) : paints[shows];
|
|
199
|
+
}, plain);
|
|
200
|
+
});
|
|
201
|
+
};
|
|
202
|
+
const near = (list, x) => Math.min(...list.map((c) => Math.abs(x - c)));
|
|
203
|
+
const columnsOf = (length, cuts) => [0, ...[...new Set(cuts.map((c) => Math.round(c * 1e4) / 1e4))].sort((p, q) => p - q), length];
|
|
204
|
+
// A street front: corner piers 6 m to the first column's middle, each with a window 1.7 m
|
|
205
|
+
// wide 2.6 m from its corner; `count` bays between, each column 1.6 m wide. A bay holds a
|
|
206
|
+
// pair of windows 1.2 m wide either side of a 38 cm mullion, the colonnade's opening, and
|
|
207
|
+
// an attic window 1.7 m wide; a pendant hangs over each column.
|
|
208
|
+
const front = (count) => (length) => {
|
|
209
|
+
const pitch = (length - 12) / count, lines = Array.from({ length: count + 1 }, (_, k) => 6 + k * pitch);
|
|
210
|
+
const centres = lines.slice(1).map((line) => line - pitch / 2), piers = [2.6, length - 2.6];
|
|
211
|
+
const cuts = [...lines.flatMap((c) => [-0.9, -0.8, -0.45, 0.45, 0.8, 0.9].map((d) => c + d)), ...centres.flatMap((c) => [-1.39, -0.85, -0.19, 0.19, 0.85, 1.39].map((d) => c + d)), ...piers.flatMap((c) => [c - 0.85, c + 0.85])];
|
|
212
|
+
return [columnsOf(length, cuts), (mid, row) => {
|
|
213
|
+
const inside = mid > lines[0] && mid < lines.at(-1), line = near(lines, mid), centre = near(centres, mid);
|
|
214
|
+
const pierWindow = near(piers, mid) < 0.85;
|
|
215
|
+
switch (row.kind) {
|
|
216
|
+
case "granite": return "granite";
|
|
217
|
+
case "shop": return inside && line > 0.8 ? "glass" : "granite";
|
|
218
|
+
case "belt":
|
|
219
|
+
case "band": return "band";
|
|
220
|
+
case "glass": return (inside && centre > 0.19 && centre < 1.39) || pierWindow ? "glass" : "wall";
|
|
221
|
+
case "rail": return row.pier && pierWindow ? "glass" : inside && line > 0.8 ? "band" : "wall";
|
|
222
|
+
case "open": return (row.pier && pierWindow) || (inside && line > 0.8) ? "glass" : "wall";
|
|
223
|
+
case "attic": return row.reach !== undefined && line < row.reach ? "shadow" : (inside && centre < 0.85) || pierWindow ? "glass" : "wall";
|
|
224
|
+
case "pendant": return line < row.reach ? "shadow" : "wall";
|
|
225
|
+
case "frieze": return "shadow";
|
|
226
|
+
default: return "wall";
|
|
227
|
+
}
|
|
228
|
+
}];
|
|
229
|
+
};
|
|
230
|
+
// The alley's and the court's windows, which the drawing does not show: 1.6 m wide, spread
|
|
231
|
+
// evenly about 3.2 m apart, one to each floor from the third.
|
|
232
|
+
const spread = (length) => {
|
|
233
|
+
const count = Math.max(1, Math.round(length / 3.2)), centres = Array.from({ length: count }, (_, k) => length * (k + 0.5) / count);
|
|
234
|
+
return [columnsOf(length, centres.flatMap((c) => [c - 0.8, c + 0.8])), (mid, row) => ((row.kind === "glass" || row.kind === "attic" || row.pier === true) && near(centres, mid) < 0.8 ? "glass" : "wall")];
|
|
235
|
+
};
|
|
236
|
+
const facing = (x, z) => (run) => run.normal(0)[0] * x + run.normal(0)[1] * z > 0.9;
|
|
237
|
+
// The fronts to the cornice, the alley to the roof; the north wall, shared with the Lake
|
|
238
|
+
// View Building, stays plain.
|
|
239
|
+
skin(planOf(lot), facing(1, 0), rows[0].lo, h.cornice, 0, front(11), stone);
|
|
240
|
+
skin(planOf(lot), facing(0, 1), rows[0].lo, h.cornice, 1, front(9), stone);
|
|
241
|
+
skin(planOf(lot), facing(-1, 0), rows[0].lo, h.roof, 2, spread, stone);
|
|
242
|
+
skin(planOf(lower), () => true, h.court, h.inner, 3, spread, courtBrick);
|
|
243
|
+
skin(planOf(upper), () => true, h.inner, h.roof, 4, spread, courtBrick);
|
|
244
|
+
skin(planOf(opening), (run) => !has(lotEdges, run), h.court, h.roof, 5, spread, courtBrick);
|
|
245
|
+
const model = kit.finish({ height: h.top, outlines: [shell, cornice], opacity: 0.16 });
|
|
246
|
+
model.building.position.set(offset[0], 0, offset[1]);
|
|
247
|
+
model.building.userData["geography"] = record;
|
|
248
|
+
model.building.userData["geographicDetail"] = { levels: h, source: "docs/peoples-gas-reference.md" };
|
|
249
|
+
return model;
|
|
250
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { BuildingModel, Plan } from "./building-kit.js";
|
|
2
|
+
import type { GeoBuilding } from "./skyline-geography-data.js";
|
|
3
|
+
export declare const railwayExchangeLevels: Readonly<{
|
|
4
|
+
belt: number;
|
|
5
|
+
frieze: number;
|
|
6
|
+
cornice: number;
|
|
7
|
+
eaves: 71.6;
|
|
8
|
+
top: 78.9;
|
|
9
|
+
}>;
|
|
10
|
+
export declare const railwayExchangePalette: Readonly<{
|
|
11
|
+
glass: number[];
|
|
12
|
+
lit: 7237230;
|
|
13
|
+
dim: 3947580;
|
|
14
|
+
terracotta: 8421504;
|
|
15
|
+
band: 9474192;
|
|
16
|
+
plain: 6447714;
|
|
17
|
+
}>;
|
|
18
|
+
export declare function createRailwayExchangeGeographicBuilding(record: GeoBuilding, projectPlan: (coordinates: [number, number][]) => Plan, offset: [number, number]): BuildingModel;
|