@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.
Files changed (195) hide show
  1. package/NOTICE.md +20 -0
  2. package/README.md +52 -2
  3. package/docs/package.md +222 -0
  4. package/docs/skyline-geography.md +503 -0
  5. package/lib/models/aon-center.d.ts +4 -0
  6. package/lib/models/aon-center.js +60 -0
  7. package/lib/models/aon-geographic.d.ts +9 -0
  8. package/lib/models/aon-geographic.js +35 -0
  9. package/lib/models/aon-tower.d.ts +23 -0
  10. package/lib/models/aon-tower.js +130 -0
  11. package/lib/models/blue-cross-geographic.d.ts +14 -0
  12. package/lib/models/blue-cross-geographic.js +248 -0
  13. package/lib/models/borg-warner-geographic.d.ts +20 -0
  14. package/lib/models/borg-warner-geographic.js +143 -0
  15. package/lib/models/buckingham-geographic.d.ts +10 -0
  16. package/lib/models/buckingham-geographic.js +121 -0
  17. package/lib/models/building-kit.d.ts +84 -0
  18. package/lib/models/building-kit.js +358 -0
  19. package/lib/models/celebrations.d.ts +39 -0
  20. package/lib/models/celebrations.js +44 -0
  21. package/lib/models/chicago-athletic-association-geographic.d.ts +32 -0
  22. package/lib/models/chicago-athletic-association-geographic.js +173 -0
  23. package/lib/models/crain-communications.d.ts +5 -0
  24. package/lib/models/crain-communications.js +75 -0
  25. package/lib/models/crain-geographic.d.ts +10 -0
  26. package/lib/models/crain-geographic.js +69 -0
  27. package/lib/models/crain-tower.d.ts +32 -0
  28. package/lib/models/crain-tower.js +378 -0
  29. package/lib/models/facade-grid.d.ts +18 -0
  30. package/lib/models/facade-grid.js +176 -0
  31. package/lib/models/gage-geographic.d.ts +19 -0
  32. package/lib/models/gage-geographic.js +154 -0
  33. package/lib/models/heritage-at-millennium-park.d.ts +3 -0
  34. package/lib/models/heritage-at-millennium-park.js +284 -0
  35. package/lib/models/heritage-geographic.d.ts +11 -0
  36. package/lib/models/heritage-geographic.js +208 -0
  37. package/lib/models/hyatt-west-tower-geographic.d.ts +26 -0
  38. package/lib/models/hyatt-west-tower-geographic.js +86 -0
  39. package/lib/models/keith-ascher-geographic.d.ts +29 -0
  40. package/lib/models/keith-ascher-geographic.js +110 -0
  41. package/lib/models/kemper-geographic.d.ts +8 -0
  42. package/lib/models/kemper-geographic.js +153 -0
  43. package/lib/models/kemper.d.ts +2 -0
  44. package/lib/models/kemper.js +78 -0
  45. package/lib/models/lake-view-geographic.d.ts +15 -0
  46. package/lib/models/lake-view-geographic.js +134 -0
  47. package/lib/models/maclean-center-geographic.d.ts +20 -0
  48. package/lib/models/maclean-center-geographic.js +162 -0
  49. package/lib/models/michigan-boulevard-geographic.d.ts +12 -0
  50. package/lib/models/michigan-boulevard-geographic.js +157 -0
  51. package/lib/models/michigan-plaza-south-geographic.d.ts +7 -0
  52. package/lib/models/michigan-plaza-south-geographic.js +84 -0
  53. package/lib/models/michigan-plaza-south.d.ts +2 -0
  54. package/lib/models/michigan-plaza-south.js +68 -0
  55. package/lib/models/millennium-park-plaza-geographic.d.ts +9 -0
  56. package/lib/models/millennium-park-plaza-geographic.js +103 -0
  57. package/lib/models/monroe-geographic.d.ts +19 -0
  58. package/lib/models/monroe-geographic.js +177 -0
  59. package/lib/models/north-michigan-180-geographic.d.ts +14 -0
  60. package/lib/models/north-michigan-180-geographic.js +117 -0
  61. package/lib/models/north-wabash-geographic.d.ts +14 -0
  62. package/lib/models/north-wabash-geographic.js +122 -0
  63. package/lib/models/on-the-park-geographic.d.ts +11 -0
  64. package/lib/models/on-the-park-geographic.js +245 -0
  65. package/lib/models/one-prudential-geographic.d.ts +12 -0
  66. package/lib/models/one-prudential-geographic.js +55 -0
  67. package/lib/models/one-prudential-plaza.d.ts +5 -0
  68. package/lib/models/one-prudential-plaza.js +72 -0
  69. package/lib/models/one-prudential-tower.d.ts +45 -0
  70. package/lib/models/one-prudential-tower.js +246 -0
  71. package/lib/models/peoples-gas-geographic.d.ts +23 -0
  72. package/lib/models/peoples-gas-geographic.js +250 -0
  73. package/lib/models/railway-exchange-geographic.d.ts +18 -0
  74. package/lib/models/railway-exchange-geographic.js +176 -0
  75. package/lib/models/river-plaza-geographic.d.ts +16 -0
  76. package/lib/models/river-plaza-geographic.js +104 -0
  77. package/lib/models/sheraton-grand-geographic.d.ts +24 -0
  78. package/lib/models/sheraton-grand-geographic.js +112 -0
  79. package/lib/models/six-north-michigan-geographic.d.ts +16 -0
  80. package/lib/models/six-north-michigan-geographic.js +180 -0
  81. package/lib/models/skyline-geography-data.d.ts +44 -0
  82. package/lib/models/skyline-geography-data.js +607 -0
  83. package/lib/models/skyline-geography.d.ts +23 -0
  84. package/lib/models/skyline-geography.js +322 -0
  85. package/lib/models/swissotel-geographic.d.ts +24 -0
  86. package/lib/models/swissotel-geographic.js +79 -0
  87. package/lib/models/three-illinois-center-geographic.d.ts +26 -0
  88. package/lib/models/three-illinois-center-geographic.js +86 -0
  89. package/lib/models/trump-geographic.d.ts +14 -0
  90. package/lib/models/trump-geographic.js +51 -0
  91. package/lib/models/trump-international-tower.d.ts +4 -0
  92. package/lib/models/trump-international-tower.js +84 -0
  93. package/lib/models/trump-tower.d.ts +43 -0
  94. package/lib/models/trump-tower.js +229 -0
  95. package/lib/models/two-illinois-center-geographic.d.ts +26 -0
  96. package/lib/models/two-illinois-center-geographic.js +85 -0
  97. package/lib/models/two-prudential-geographic.d.ts +25 -0
  98. package/lib/models/two-prudential-geographic.js +47 -0
  99. package/lib/models/two-prudential-plaza.d.ts +8 -0
  100. package/lib/models/two-prudential-plaza.js +55 -0
  101. package/lib/models/two-prudential-tower.d.ts +53 -0
  102. package/lib/models/two-prudential-tower.js +441 -0
  103. package/lib/models/university-club-geographic.d.ts +24 -0
  104. package/lib/models/university-club-geographic.js +217 -0
  105. package/lib/models/willoughby-tower-geographic.d.ts +17 -0
  106. package/lib/models/willoughby-tower-geographic.js +195 -0
  107. package/lib/models/window-illumination.d.ts +14 -0
  108. package/lib/models/window-illumination.js +64 -0
  109. package/lib/package-assets.d.ts +5 -0
  110. package/lib/package-assets.js +9 -0
  111. package/lib/skyline-package.d.ts +23 -0
  112. package/lib/skyline-package.js +25 -0
  113. package/lib/skyline-scene.d.ts +25 -0
  114. package/lib/skyline-scene.js +86 -0
  115. package/lib/study-types.d.ts +49 -0
  116. package/lib/study-types.js +1 -0
  117. package/lib/vendor/three-r186.d.ts +1 -0
  118. package/lib/vendor/three-r186.js +1 -0
  119. package/package.json +64 -4
  120. package/site/building-detail.html +54 -0
  121. package/site/building-detail.js +84 -0
  122. package/site/building-study.html +58 -0
  123. package/site/building-study.js +3 -0
  124. package/site/index.html +407 -0
  125. package/site/models/aon-center.js +60 -0
  126. package/site/models/aon-geographic.js +35 -0
  127. package/site/models/aon-tower.js +130 -0
  128. package/site/models/blue-cross-geographic.js +248 -0
  129. package/site/models/borg-warner-geographic.js +143 -0
  130. package/site/models/buckingham-geographic.js +121 -0
  131. package/site/models/building-kit.js +358 -0
  132. package/site/models/celebrations.js +44 -0
  133. package/site/models/chicago-athletic-association-geographic.js +173 -0
  134. package/site/models/crain-communications.js +75 -0
  135. package/site/models/crain-geographic.js +69 -0
  136. package/site/models/crain-reference.svg +10 -0
  137. package/site/models/crain-tower.js +378 -0
  138. package/site/models/facade-grid.js +176 -0
  139. package/site/models/gage-geographic.js +154 -0
  140. package/site/models/heritage-at-millennium-park.js +284 -0
  141. package/site/models/heritage-geographic.js +208 -0
  142. package/site/models/hyatt-west-tower-geographic.js +86 -0
  143. package/site/models/keith-ascher-geographic.js +110 -0
  144. package/site/models/kemper-geographic.js +153 -0
  145. package/site/models/kemper.js +78 -0
  146. package/site/models/lake-view-geographic.js +134 -0
  147. package/site/models/maclean-center-geographic.js +162 -0
  148. package/site/models/michigan-boulevard-geographic.js +157 -0
  149. package/site/models/michigan-plaza-south-geographic.js +84 -0
  150. package/site/models/michigan-plaza-south.js +68 -0
  151. package/site/models/millennium-park-plaza-geographic.js +103 -0
  152. package/site/models/monroe-geographic.js +177 -0
  153. package/site/models/north-michigan-180-geographic.js +117 -0
  154. package/site/models/north-wabash-geographic.js +122 -0
  155. package/site/models/on-the-park-geographic.js +245 -0
  156. package/site/models/one-prudential-geographic.js +55 -0
  157. package/site/models/one-prudential-plaza.js +72 -0
  158. package/site/models/one-prudential-tower.js +246 -0
  159. package/site/models/peoples-gas-geographic.js +250 -0
  160. package/site/models/railway-exchange-geographic.js +176 -0
  161. package/site/models/river-plaza-geographic.js +104 -0
  162. package/site/models/sheraton-grand-geographic.js +112 -0
  163. package/site/models/six-north-michigan-geographic.js +180 -0
  164. package/site/models/skyline-geography-data.js +607 -0
  165. package/site/models/skyline-geography.js +322 -0
  166. package/site/models/skyline-panorama.svg +3491 -0
  167. package/site/models/skyline-reference.svg +3090 -0
  168. package/site/models/swissotel-geographic.js +79 -0
  169. package/site/models/three-illinois-center-geographic.js +86 -0
  170. package/site/models/trump-geographic.js +51 -0
  171. package/site/models/trump-international-tower.js +84 -0
  172. package/site/models/trump-tower.js +229 -0
  173. package/site/models/two-illinois-center-geographic.js +85 -0
  174. package/site/models/two-prudential-geographic.js +47 -0
  175. package/site/models/two-prudential-plaza.js +55 -0
  176. package/site/models/two-prudential-tower.js +441 -0
  177. package/site/models/university-club-geographic.js +217 -0
  178. package/site/models/willoughby-tower-geographic.js +195 -0
  179. package/site/models/window-illumination.js +64 -0
  180. package/site/skyline-3d.html +92 -0
  181. package/site/skyline-3d.js +333 -0
  182. package/site/skyline-animated.svg +18795 -0
  183. package/site/skyline-comparison.js +97 -0
  184. package/site/skyline-original-fit.svg +18312 -0
  185. package/site/skyline-scene.js +86 -0
  186. package/site/skyline-study.html +83 -0
  187. package/site/skyline-study.js +109 -0
  188. package/site/skyline-webgl.html +667 -0
  189. package/site/stars.svg +176 -0
  190. package/site/study-loader.js +20 -0
  191. package/site/study-types.js +1 -0
  192. package/site/study-viewer.js +605 -0
  193. package/site/study.css +204 -0
  194. package/site/vendor/THREE-LICENSE.txt +21 -0
  195. package/site/vendor/three-r186.js +4228 -0
@@ -0,0 +1,245 @@
1
+ import * as THREE from "../vendor/three-r186.js";
2
+ import { arc, createBuilder, inside, polygonOf } from "./building-kit.js";
3
+ import { chainSkin, chainsOf, mitredBox, orient, paintedSlab, planOf, turnsAt } from "./facade-grid.js";
4
+ // 340 on the Park, 340 East Randolph Street (Solomon Cordwell Buenz, 2007): the geographic
5
+ // layout's model, on the mapped outline 95486949 and its parts 284789056 and 284789058. The
6
+ // original layout has no model of it; the drawing shows it right of the Blue Cross and Blue
7
+ // Shield Tower, outside the excerpt it is fitted to.
8
+ //
9
+ // A glass tower on a straight south face, a diagonal south-east face and a north face
10
+ // curved to keep The Buckingham's views. A white concrete frame stands on the south face:
11
+ // piers, a beam every fifth floor, and a parapet band over four penthouse floors, with a
12
+ // ladder of cantilevered balconies inside its west pier and recessed balconies inside its
13
+ // east one. The two-and-a-half-storey winter garden fills the bay over the one deeper
14
+ // beam. A glazed block fills the foot of the diagonal face, up to the top of floor 16's
15
+ // beam. Floors are counted one by one from the lobby. Heights are measured on the drawing
16
+ // down from the published 204.9 m top; see docs/340-on-the-park-reference.md. Units are
17
+ // meters; +x is east, +z is south.
18
+ export const onTheParkLevels = Object.freeze({
19
+ lowestBeam: 21.35, // floor 6's beam, the drawing's lowest
20
+ topBeam: 184.2, // floor 61's, under the penthouses
21
+ band: 198.6, // the parapet band's foot, over the fourth penthouse floor
22
+ roof: 203.8, // the band's top and the roof deck
23
+ top: 204.9, // the glass guard's top, the published architectural height
24
+ beam: 1.45, // a beam's depth; the winter garden's is 2.2 m
25
+ });
26
+ const h = onTheParkLevels;
27
+ const pitch = (h.topBeam - h.lowestBeam) / 55, penthouse = (h.band - h.topBeam) / 4;
28
+ // Each floor's level, from the second, over the lobby, to a 65th at the band's foot.
29
+ const floor = (n) => (n <= 61 ? h.lowestBeam + (n - 6) * pitch : h.topBeam + (n - 61) * penthouse);
30
+ const beamFloors = Array.from({ length: 12 }, (_, k) => 6 + 5 * k), garden = 26;
31
+ // Floors whose west bay, the windows' column and the ladder, holds recessed balconies: the
32
+ // two over the winter garden's tall row and the penthouses.
33
+ const westRecessed = (n) => n === garden + 3 || n === garden + 4 || n >= 61;
34
+ const beamSpan = (n) => { const half = (n === garden ? 2.2 : h.beam) / 2; return [floor(n) - half, floor(n) + half]; };
35
+ // The corner block's roof, level with the top of floor 16's beam.
36
+ const podiumTop = beamSpan(16)[1];
37
+ const color = (hex) => new THREE.Color(hex);
38
+ const glassTones = [0x2c2c2c, 0x303030, 0x343434, 0x383838].map(color);
39
+ const litGlass = color(0x7a7a7a), dimGlass = color(0x4a4a4a);
40
+ const slab = color(0x2a2a2a), recess = color(0x242424), lobby = color(0x3a3a3a), plant = color(0x303030);
41
+ const concrete = color(0xb4b4b4), core = color(0x262626), roofing = color(0x3a3a3a);
42
+ function paneColor(row, bay, wall) {
43
+ const hash = (Math.imul(row + 17, 0x9e3779b1) ^ Math.imul(bay + 41, 0x85ebca77) ^ Math.imul(wall + 29, 0xc2b2ae3d)) >>> 0;
44
+ const value = hash % 101;
45
+ if (value < 4)
46
+ return litGlass;
47
+ if (value < 8)
48
+ return dimGlass;
49
+ return glassTones[value % glassTones.length];
50
+ }
51
+ function rowsTo(top, { gardenRow = false, foot = 0.25 } = {}) {
52
+ const rows = [];
53
+ const push = (lo, hi, kind, n) => {
54
+ const [a, b] = [Math.max(lo, rows.at(-1)?.hi ?? foot), Math.min(hi, top)];
55
+ if (b - a > 1e-6)
56
+ rows.push({ lo: a, hi: b, kind, floor: n });
57
+ };
58
+ push(foot, floor(2) - 0.3, "lobby", 1);
59
+ for (let n = 2; n <= 64; n += 1) {
60
+ if (gardenRow && n > garden && n < garden + 3)
61
+ continue;
62
+ push(floor(n) - 0.3, floor(n) + 0.3, "slab", n);
63
+ push(floor(n) + 0.3, floor(gardenRow && n === garden ? n + 3 : n + 1) - 0.3, "glass", n);
64
+ }
65
+ push(floor(65) - 0.3, floor(65) + 0.3, "slab", 65);
66
+ push(floor(65) + 0.3, top, "plant", 65);
67
+ return rows;
68
+ }
69
+ export function createOnTheParkGeographicBuilding(record, projectPlan, offset) {
70
+ const kit = createBuilder(record.name, record.id);
71
+ const shell = kit.batch("340 on the Park · shell", kit.material(0xffffff, { vertexColors: true }));
72
+ const wall = kit.batch("340 on the Park · curtain wall", kit.material(0xffffff, { vertexColors: true }));
73
+ const frame = kit.batch("340 on the Park · concrete frame", kit.material(0xb4b4b4));
74
+ const rails = kit.batch("340 on the Park · railings and guards", kit.material(0x7c7c7c));
75
+ const outline = (way) => orient(polygonOf(projectPlan(record.parts.find((part) => part.way === way).coordinates)));
76
+ const tower = outline(284789056), podium = outline(284789058);
77
+ // A solid on an outline between two heights, its walls the shell's dark core.
78
+ const solid = (corners, lo, hi) => {
79
+ planOf(corners).forEach((run) => {
80
+ const a = run.at(0), b = run.at(run.length), n = run.normal(0);
81
+ kit.quad(shell, [[a[0], lo, a[1]], [b[0], lo, b[1]], [b[0], hi, b[1]], [a[0], hi, a[1]]], [[n[0], 0, n[1]]], core);
82
+ });
83
+ paintedSlab(kit, shell, corners, lo, false, roofing);
84
+ paintedSlab(kit, shell, corners, hi, true, roofing);
85
+ };
86
+ solid(tower, 0, h.roof);
87
+ solid(podium, 0, podiumTop);
88
+ // Whether a run lies along an outline's edges, and whether the corner block stands
89
+ // against it.
90
+ const lies = (run, on) => [0, run.length / 2, run.length].every((s) => {
91
+ const p = run.at(s);
92
+ return on.some((q, i) => {
93
+ const r = on[(i + 1) % on.length], [dx, dz] = [r[0] - q[0], r[1] - q[1]], l = Math.hypot(dx, dz);
94
+ const t = Math.max(0, Math.min(1, ((p[0] - q[0]) * dx + (p[1] - q[1]) * dz) / (l * l)));
95
+ return Math.hypot(q[0] + dx * t - p[0], q[1] + dz * t - p[1]) < 0.05;
96
+ });
97
+ });
98
+ const covered = (run) => inside(podium, run.at(run.length / 2, 0.5));
99
+ // A cell is painted by its row and by where its middle stands along the wall: `recessed`
100
+ // marks glass in a balcony's shadow and `white` white wall.
101
+ const paint = (cuts, face, recessed = (_s, _row) => false, white = (_s, _row) => false) => (s, r) => {
102
+ const row = cuts[r];
103
+ if (row.kind === "lobby")
104
+ return lobby;
105
+ if (row.kind === "plant")
106
+ return plant;
107
+ if (row.kind === "slab")
108
+ return white(s, row) ? concrete : slab;
109
+ return recessed(s, row) ? recess : white(s, row) ? concrete : paneColor(row.floor, Math.round(s / 3), face);
110
+ };
111
+ // A wall's skin, on the runs `keep` accepts: cells about two 5 ft modules wide on every
112
+ // row. Each unbroken stretch of kept runs along a chain is one box, held 2 cm clear of its
113
+ // ends, or 9 cm, past its own depth, where the outline turns in and the next wall's skin
114
+ // would otherwise cross it.
115
+ const skin = (corners, cuts, seed, keep) => {
116
+ const plan = planOf(corners), heights = [cuts[0].lo, ...cuts.map((row) => row.hi)];
117
+ chainsOf(plan).forEach((chain, index) => {
118
+ const count = Math.max(1, Math.round(chain.length / 3.048));
119
+ const columns = Array.from({ length: count + 1 }, (_, i) => chain.length * i / count);
120
+ const kept = chain.runs.map((run) => keep(plan[run]));
121
+ for (let k0 = kept.indexOf(true); k0 >= 0;) {
122
+ let k1 = k0;
123
+ while (kept[k1 + 1])
124
+ k1 += 1;
125
+ const clear = (k, end) => (turnsAt(plan, chain.runs[k])[end] > 0 && (end ? k === chain.runs.length - 1 : k === 0) ? 0.09 : 0.02);
126
+ const s0 = chain.starts[k0] + clear(k0, 0), s1 = chain.starts[k1] + plan[chain.runs[k1]].length - clear(k1, 1);
127
+ const colour = paint(cuts, seed * 8 + index);
128
+ chainSkin(kit, wall, plan, chain, k0, k1, s0, s1, columns, heights, 0.02, 0.07, (bay, r) => colour(((columns[bay - 1] ?? s0) + (columns[bay] ?? s1)) / 2, r), slab);
129
+ k0 = kept.indexOf(true, k1 + 1);
130
+ }
131
+ });
132
+ };
133
+ // The south face and its frame, laid out from the photograph, west to east: a 1.92 m pier,
134
+ // a column of windows, the balcony ladder, a 2.24 m pier, the glass field, the recessed
135
+ // balconies and a 2.24 m pier at the face's east end.
136
+ const facesSouth = (run) => run.normal(0)[1] > 0.99 && run.length > 40;
137
+ const plan = planOf(tower), chains = chainsOf(plan);
138
+ const southChain = chains.find((chain) => chain.runs.length === 1 && facesSouth(plan[chain.runs[0]]));
139
+ const south = plan[southChain.runs[0]], L = south.length, n = south.normal(0);
140
+ const westFirst = south.at(0)[0] < south.at(L)[0], scale = L / 43.2;
141
+ const along = (s) => (westFirst ? s * scale : L - s * scale);
142
+ const at = (s0, s1) => south.at((along(s0) + along(s1)) / 2);
143
+ const half = (s0, s1) => Math.abs(along(s1) - along(s0)) / 2;
144
+ const between = (s, a, b) => { const x = westFirst ? s / scale : (L - s) / scale; return x >= a - 1e-6 && x < b - 1e-6; };
145
+ const piers = [[0, 1.92], [8.0, 10.24], [40.96, 43.2]];
146
+ const [ladder, field, balconies] = [[4.8, 8.0], [10.24, 34.24], [34.24, 40.96]];
147
+ // The south face's skin in three boxes that meet under the inner and east piers, running
148
+ // on behind the band to the roof so the piers' tops never share its top's plane. The
149
+ // middle one carries the winter garden's tall row; the column of windows is punched in
150
+ // white wall, with 25 cm jambs either side, and the balconies' glass stands in shadow. Over
151
+ // the winter garden and on the penthouse floors the windows give way to balconies too.
152
+ const southRows = rowsTo(h.roof), gardenRows = rowsTo(h.roof, { gardenRow: true });
153
+ const southColumns = [0, 1.92, 2.17, 4.55, 4.8, 6.4, 8.0, 9.12, 10.24, ...Array.from({ length: 8 }, (_, i) => 13.24 + 3 * i), 35.92, 37.6, 39.28, 40.96, 42.08, 43.2].map(along).sort((a, b) => a - b);
154
+ const sections = [[0, 9.12, southRows], [9.12, 42.08, gardenRows], [42.08, 43.2, southRows]];
155
+ sections.forEach(([a, b, cuts], i) => {
156
+ const [s0, s1] = [Math.min(along(a), along(b)), Math.max(along(a), along(b))];
157
+ const colour = paint(cuts, i, (s, row) => between(s, ...ladder) || between(s, ...balconies) || (westRecessed(row.floor) && between(s, 1.92, 4.8)), (s, row) => (row.kind === "slab" ? between(s, 1.92, 4.8) : !westRecessed(row.floor) && (between(s, 1.92, 2.17) || between(s, 4.55, 4.8))));
158
+ const inset = (s, end) => (s < 0.01 || s > L - 0.01 ? (end ? -0.02 : 0.02) : end ? -0.01 : 0.01);
159
+ chainSkin(kit, wall, plan, southChain, 0, 0, s0 + inset(s0, false), s1 + inset(s1, true), southColumns, [cuts[0].lo, ...cuts.map((row) => row.hi)], 0.02, 0.07, (bay, r) => colour(((southColumns[bay - 1] ?? s0) + (southColumns[bay] ?? s1)) / 2, r), slab);
160
+ });
161
+ // The frame: piers from above grade to the band, a beam every fifth floor between the
162
+ // inner and east piers, deeper under the winter garden, and the parapet band over all.
163
+ for (const [a, b] of piers)
164
+ kit.box(frame, at(a, b), n, half(a, b), 0.05, 0.6, 0.3, h.band);
165
+ for (const f of beamFloors) {
166
+ const [lo, hi] = beamSpan(f);
167
+ kit.box(frame, at(8.3, 42.9), n, half(8.3, 42.9), 0.07, 0.45, lo, hi);
168
+ }
169
+ kit.box(frame, at(0, 43.2), n, half(0, 43.2) - 0.001, 0.07, 0.65, h.band, h.roof);
170
+ // The ladder's cantilevered balconies, a slab and a glass railing on each floor to the
171
+ // 60th. Over the winter garden and on the penthouse floors, balconies recessed behind the
172
+ // frame's plane cross the windows' column and the ladder instead.
173
+ for (let f = 2; f <= 64; f += 1) {
174
+ if (!westRecessed(f)) {
175
+ kit.box(frame, at(...ladder), n, half(...ladder), 0.07, 1.5, floor(f) - 0.15, floor(f) + 0.15);
176
+ kit.box(rails, at(...ladder), n, half(...ladder) - 0.05, 1.37, 1.45, floor(f) + 0.15, floor(f) + 1.05);
177
+ continue;
178
+ }
179
+ kit.box(frame, at(1.92, ladder[1]), n, half(1.92, ladder[1]), 0.07, 0.3, floor(f) - 0.15, floor(f) + 0.15);
180
+ kit.box(rails, at(1.92, ladder[1]), n, half(1.92, ladder[1]), 0.22, 0.28, floor(f) + 0.15, floor(f) + 1.05);
181
+ }
182
+ // Railings on the frame's plane: the recessed balconies on every floor but the winter
183
+ // garden's, and across the glass field on the garden's floors and the penthouses'. A
184
+ // railing stands on its floor's slab edge, or 2 cm over the beam where one crosses, clear
185
+ // of the feet of the columns and posts it passes.
186
+ for (let f = 2; f <= 64; f += 1) {
187
+ if (f > garden && f < garden + 3)
188
+ continue;
189
+ const wide = f === garden || f === garden + 3 || f === garden + 4 || f >= 61;
190
+ const [a, b] = wide ? [field[0], balconies[1]] : balconies;
191
+ const base = beamFloors.includes(f) ? beamSpan(f)[1] + 0.02 : floor(f) + 0.15;
192
+ if (!beamFloors.includes(f))
193
+ kit.box(frame, at(a, b), n, half(a, b), 0.07, 0.3, floor(f) - 0.15, floor(f) + 0.15);
194
+ kit.box(rails, at(a, b), n, half(a, b), 0.22, 0.28, base, base + 0.9);
195
+ }
196
+ // Three round columns through the winter garden's bay, and square posts at the same
197
+ // stations through the penthouses.
198
+ for (const s of [17.3, 25.6, 33.9]) {
199
+ const c = south.at(along(s), 0.62);
200
+ kit.prism(frame, [arc(c, 0.45, 0, 2 * Math.PI, false, Math.PI / 6)], [beamSpan(garden)[1], beamSpan(garden + 5)[0]]);
201
+ kit.box(frame, south.at(along(s)), n, 0.3, 0.05, 0.6, beamSpan(61)[1], h.band);
202
+ }
203
+ // The other walls: glass to the roof, the diagonal face's foot left to the corner block,
204
+ // which carries its own from a little higher so the two skins' feet never share a plane.
205
+ const plain = rowsTo(h.roof);
206
+ skin(tower, plain, 1, (run) => !facesSouth(run) && !covered(run) && !lies(run, podium));
207
+ skin(tower, plain.filter((row) => row.hi > podiumTop + 1e-6).map((row, i) => (i === 0 ? { ...row, lo: podiumTop } : row)), 2, covered);
208
+ skin(podium, rowsTo(podiumTop, { foot: 0.3 }), 3, (run) => !lies(run, tower));
209
+ // Fins at the beam floors and the top penthouse floor cross the diagonal face's last
210
+ // 8.6 m, where the drawing puts them, up to a white pier at the east tip.
211
+ const diagonal = chains.find((chain) => chain.runs.length === 2 && plan[chain.runs[0]].normal(0)[0] > 0.6);
212
+ const tipFirst = !covered(plan[diagonal.runs[0]]);
213
+ const [finFrom, finTo] = tipFirst ? [0, 8.6] : [diagonal.length - 8.6, diagonal.length];
214
+ diagonal.runs.forEach((index, k) => {
215
+ const run = plan[index], start = diagonal.starts[k];
216
+ const [s0, s1] = [Math.max(finFrom - start, 0), Math.min(finTo - start, run.length)];
217
+ if (s1 - s0 < 0.1)
218
+ return;
219
+ const [before, after] = turnsAt(plan, index);
220
+ for (const f of [...beamFloors, 64]) {
221
+ const [lo, hi] = [floor(f) - 0.35, floor(f) + 0.35];
222
+ if (covered(run) && lo < podiumTop)
223
+ continue;
224
+ mitredBox(kit, frame, run, s0, s1, s0 < 1e-6 ? before : undefined, s1 > run.length - 1e-6 ? after : undefined, 0.3, lo, hi);
225
+ }
226
+ });
227
+ const tip = plan[diagonal.runs[tipFirst ? 0 : 1]];
228
+ kit.box(frame, tip.at(tipFirst ? 0.45 : tip.length - 0.45), tip.normal(0), 0.45, 0.05, 0.4, 0.3, h.roof - 0.1);
229
+ // Glass guards round the roof and the corner block's terrace. The terrace's stop 30 cm
230
+ // short of the tower, whose diagonal face meets the block's walls at acute corners and
231
+ // whose skin reaches just past them.
232
+ plan.forEach((run, i) => { const [before, after] = turnsAt(plan, i); mitredBox(kit, rails, run, 0, run.length, before, after, 0.06, h.roof, h.top); });
233
+ const block = planOf(podium);
234
+ block.forEach((run, i) => {
235
+ if (lies(run, tower))
236
+ return;
237
+ const [before, after] = turnsAt(block, i), ends = [lies(block[(i + block.length - 1) % block.length], tower), lies(block[(i + 1) % block.length], tower)];
238
+ mitredBox(kit, rails, run, ends[0] ? 0.3 : 0, run.length - (ends[1] ? 0.3 : 0), ends[0] ? undefined : before, ends[1] ? undefined : after, 0.06, podiumTop, podiumTop + 1.1);
239
+ });
240
+ const model = kit.finish({ height: h.top, outlines: [shell, frame], opacity: 0.16 });
241
+ model.building.position.set(offset[0], 0, offset[1]);
242
+ model.building.userData["geography"] = record;
243
+ model.building.userData["geographicDetail"] = { levels: h, source: "docs/340-on-the-park-reference.md" };
244
+ return model;
245
+ }
@@ -0,0 +1,55 @@
1
+ import { polygonOf } from "./building-kit.js";
2
+ import { chainsOf, orient, planOf } from "./facade-grid.js";
3
+ import { buildOnePrudentialTower, onePrudentialLevels } from "./one-prudential-tower.js";
4
+ // One Prudential Plaza: the geographic layout's model, on the mapped tower part 685493609,
5
+ // the east wing 685493610 wrapped round its east end, the west wing 685493612 round its
6
+ // west and north, and the mast at the mapped antenna part 685493614's centre. The tower is
7
+ // the shared generator in one-prudential-tower.ts, the same one the original layout's copy
8
+ // uses. The map gives the wings levels but no heights: the east wing's roof is measured
9
+ // on the photograph, and the west wing, which the photograph cannot see, keeps its three
10
+ // levels' estimate. The penthouse stands on the mapped south face where the photograph
11
+ // shows it. See docs/one-prudential-reference.md. Units are meters; +x is east, +z is south.
12
+ export const onePrudentialGeographicLevels = Object.freeze({
13
+ roof: onePrudentialLevels.roof,
14
+ penthouseTop: onePrudentialLevels.penthouseTop, // published architectural height
15
+ mastTop: onePrudentialLevels.mastTop,
16
+ tip: onePrudentialLevels.tip, // published antenna tip
17
+ floors: onePrudentialLevels.floors,
18
+ eastWing: 56.4, // part 685493610: measured on the photograph; the map gives ten levels
19
+ westWing: 13.4, // part 685493612: three levels at 183.2/41, estimated
20
+ });
21
+ const h = onePrudentialGeographicLevels;
22
+ export function createOnePrudentialGeographicBuilding(record, projectPlan, offset) {
23
+ const outline = (way) => polygonOf(projectPlan(record.parts.find((p) => p.way === way).coordinates));
24
+ const tower = orient(outline(685493609));
25
+ // The south face is the tower's wall facing most nearly south; the penthouse stands on
26
+ // its chord, 2.3 m in from its west end.
27
+ const plan = planOf(tower);
28
+ const south = chainsOf(plan).map(({ runs }) => [plan[runs[0]].at(0), plan[runs.at(-1)].at(plan[runs.at(-1)].length)])
29
+ .reduce((best, chord) => (chord[1][0] - chord[0][0] > best[1][0] - best[0][0] ? chord : best));
30
+ const length = Math.hypot(south[1][0] - south[0][0], south[1][1] - south[0][1]);
31
+ const along = [(south[1][0] - south[0][0]) / length, (south[1][1] - south[0][1]) / length];
32
+ const corner = [south[0][0] + along[0] * 2.3, south[0][1] + along[1] * 2.3];
33
+ // The mast at the mapped antenna's area centroid.
34
+ const ring = outline(685493614);
35
+ let twice = 0, cx = 0, cz = 0;
36
+ ring.forEach(([x, z], i) => {
37
+ const [x2, z2] = ring[(i + 1) % ring.length], cross = x * z2 - x2 * z;
38
+ twice += cross;
39
+ cx += (x + x2) * cross;
40
+ cz += (z + z2) * cross;
41
+ });
42
+ const model = buildOnePrudentialTower({
43
+ name: record.name,
44
+ id: record.id,
45
+ tower,
46
+ wings: [{ outline: outline(685493610), top: h.eastWing }, { outline: outline(685493612), top: h.westWing }],
47
+ penthouse: { corner, along, length: 50.1, depth: 12 },
48
+ mast: [cx / (3 * twice), cz / (3 * twice)],
49
+ base: 0,
50
+ });
51
+ model.building.position.set(offset[0], 0, offset[1]);
52
+ model.building.userData["geography"] = record;
53
+ model.building.userData["geographicDetail"] = { levels: h, floors: h.floors, source: "docs/one-prudential-reference.md" };
54
+ return model;
55
+ }
@@ -0,0 +1,72 @@
1
+ import { buildOnePrudentialTower, floorLevel, onePrudentialLevels, screenFins, wallStations, wingRibs } from "./one-prudential-tower.js";
2
+ // One Prudential Plaza on a clean version of its plan, in real meters, for the skyline
3
+ // study's original layout, which places this copy in skyline-study.ts. one-prudential-tower.ts
4
+ // builds it, and the geographic model on the mapped outlines.
5
+ //
6
+ // The plan is the mapped one squared to the slab's grid, keeping each mapped wall's length:
7
+ // a 71.6 x 22.2 m slab, stepped out 2.9 m along the north of its west end, with a 29 x
8
+ // 15.5 m annex behind it, and the east wing wrapped round its east end, 38.2 m on Randolph
9
+ // and 60.4 m on Stetson. The west wing is lower than the drawing's datum, so this copy has
10
+ // none. See docs/one-prudential-reference.md. Units are meters; +x is east and +z is south.
11
+ const half = 71.6 / 2, south = 22.2 / 2, north = -south;
12
+ // The tower's walls break where the wing meets them, as the mapped outline's do, so each
13
+ // piece starts its windows above whatever covers it.
14
+ const wingWest = 26, wingBack = 11.5;
15
+ const tower = [[-half, south], [wingWest, south], [half, south], [half, north], [wingBack, north], [7, north], [7, -26.6], [-22, -26.6], [-22, north], [-38.7, north], [-38.7, 4.1], [-half, 4.1]];
16
+ const wingSouth = south + 15, wingEast = wingWest + 38.2, wingNorth = wingSouth - 60.4;
17
+ const eastWing = [[half, south], [wingWest, south], [wingWest, wingSouth], [wingEast, wingSouth], [wingEast, wingNorth], [wingBack, wingNorth], [wingBack, north], [half, north]];
18
+ // The east wing's roof, measured on the photograph down from the tower's. The map gives the
19
+ // wing ten levels and no height; its windows keep the tower's floors.
20
+ export const onePrudentialWingTop = 56.4;
21
+ // The penthouse starts 2.3 m in from the west end and runs 50.1 m along the south face,
22
+ // 12 m deep; the mast stands 52.6 m along and 14.9 m behind that face, as mapped.
23
+ const penthouse = { corner: [-half + 2.3, south], along: [1, 0], length: 50.1, depth: 12 };
24
+ const mast = [-half + 52.6, south - 14.9];
25
+ // The skyline drawing shows the slab from the photograph's treeline up, so its platform
26
+ // datum crosses the tower above the street: fitted with the placement in skyline-study.ts,
27
+ // which scales, turns, and places this copy. Its y = 0 is that datum.
28
+ export const onePrudentialSkylineBase = 40;
29
+ export function createOnePrudentialPlazaBuilding() {
30
+ return buildOnePrudentialTower({ name: "One Prudential Plaza", id: "building-one-prudential-plaza", tower, wings: [{ outline: eastWing, top: onePrudentialWingTop }], penthouse, mast, base: onePrudentialSkylineBase });
31
+ }
32
+ // Features the skyline test projects against the drawing, in this copy's coordinates.
33
+ const h = onePrudentialLevels;
34
+ const at = ([x, z], real) => [x, real - onePrudentialSkylineBase, z];
35
+ const southWall = wallStations(2 * half), eastWall = wallStations(2 * south);
36
+ const [ribsSouth, ribsEast] = [wingRibs(wingEast - wingWest), wingRibs(wingSouth - wingNorth)];
37
+ const windowCentre = (floor) => floorLevel(floor) + (h.sill + h.head) / 2;
38
+ const penthouseEast = [penthouse.corner[0] + penthouse.length, south];
39
+ export const onePrudentialFeatures = {
40
+ // The roof's corners and the observatory band's foot, on the walls' corner edges.
41
+ roofLeft: at([-half, south], h.roof),
42
+ roofNear: at([half, south], h.roof),
43
+ roofRight: at([half, north], h.roof),
44
+ bandFootLeft: at([-half, south], h.bandFoot),
45
+ bandFootNear: at([half, south], h.bandFoot),
46
+ // The penthouse's south face: the sign wall's top under the screen, and the screen's top.
47
+ penthouseTopWest: at(penthouse.corner, h.louverBottom),
48
+ penthouseTopEast: at(penthouseEast, h.louverBottom),
49
+ screenTopWest: at(penthouse.corner, h.penthouseTop),
50
+ screenTopEast: at(penthouseEast, h.penthouseTop),
51
+ // The drawing stops the mast at the tubular mast's top; WGN's slim antenna goes on above.
52
+ mastTip: at(mast, h.mastTop),
53
+ // A window on the south face, clear of every neighbour, for hover.
54
+ southFacade: at([-half + southWall.piers[12] + h.bay / 2, south + 0.07], windowCentre(30)),
55
+ wingSouthWest: at([wingWest, wingSouth], onePrudentialWingTop),
56
+ wingCorner: at([wingEast, wingSouth], onePrudentialWingTop),
57
+ wingEastEnd: at([wingEast, wingNorth], onePrudentialWingTop),
58
+ // What reaches furthest right on the wing: its coping's outer north-east corner, top and
59
+ // bottom, and its wall's foot there.
60
+ wingRibEdge: [at([wingEast + 0.35, wingNorth - 0.35], onePrudentialWingTop), at([wingEast + 0.35, wingNorth - 0.35], onePrudentialWingTop - 0.5), [wingEast, 0, wingNorth]],
61
+ // The interior piers, halfway out to their faces at mid-height, and the screen's fins and
62
+ // the wing's ribs likewise, so a sight line meets each before reaching its point.
63
+ southPiers: southWall.piers.map((s) => at([-half + s, south + 0.15], 120)),
64
+ eastPiers: eastWall.piers.map((s) => at([half + 0.15, south - s], 120)),
65
+ screenLouvers: screenFins(penthouse.length, true).map((s) => at([penthouse.corner[0] + s, south + 0.1], (h.louverBottom + h.penthouseTop) / 2)),
66
+ southRibs: ribsSouth.map((s) => at([wingWest + s, wingSouth + 0.15], 50)),
67
+ // The drawing shows twenty-four of the east wall's twenty-five; the last falls past its end.
68
+ eastRibs: ribsEast.slice(0, 24).map((s) => at([wingEast + 0.15, wingSouth - s], 50)),
69
+ // Window centres in the south face's fourth bay on six floors, the drawing's first row
70
+ // and every sixth below it.
71
+ floorRows: [40, 34, 28, 22, 16, 10].map((floor) => at([-half + southWall.piers[2] + h.bay / 2, south + 0.07], windowCentre(floor))),
72
+ };
@@ -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
+ }