@a2f0/skyline 0.0.0-stage → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/NOTICE.md +20 -0
  2. package/README.md +52 -2
  3. package/docs/package.md +224 -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,176 @@
1
+ import * as THREE from "../vendor/three-r186.js";
2
+ import { createBuilder, polygonOf } from "./building-kit.js";
3
+ import { chainSkin, chainsOf, orient, paintedSlab, planOf, turnsAt } from "./facade-grid.js";
4
+ // The Railway Exchange Building, 224 South Michigan Avenue (Frederick P. Dinkelberg of D. H.
5
+ // Burnham & Co., 1904): the geographic layout's model, on the mapped outline 124873931. The
6
+ // original layout has no model of it; the drawing shows it at Michigan and Jackson, left of
7
+ // the Borg-Warner Building.
8
+ //
9
+ // White terracotta fronts on Michigan and Jackson rise seventeen storeys in eleven bays of
10
+ // paired windows, with a belt under the fourteenth floor. Round windows fill the frieze on
11
+ // the seventeenth, under a projecting cornice and a hipped copper roof. The cornice is the
12
+ // City's 235 ft and the roof's top the Skyscraper Center's 259 ft: each is the published
13
+ // height nearest its drawn reading. The drawn cornice reads 73.7 m, so heights read on the
14
+ // drawing are scaled to meet it; see docs/railway-exchange-reference.md. Units are meters;
15
+ // +x is east, +z is south.
16
+ const drawn = (height) => height * 71.6 / 73.7;
17
+ export const railwayExchangeLevels = Object.freeze({
18
+ belt: drawn(54.6), // the belt under the fourteenth floor
19
+ frieze: drawn(67.4), // the frieze and its round windows
20
+ cornice: drawn(71.8), // the projecting cornice
21
+ eaves: 71.6, // the cornice's top and the roof's foot, the City's 235 ft
22
+ top: 78.9, // the roof's top, the Skyscraper Center's 259 ft
23
+ });
24
+ const h = railwayExchangeLevels;
25
+ // The skins' colours: the window tones, and the terracotta, the belt and the plain walls,
26
+ // which no window shares.
27
+ export const railwayExchangePalette = Object.freeze({ glass: [0x2c2c2c, 0x303030, 0x343434, 0x383838], lit: 0x6e6e6e, dim: 0x3c3c3c, terracotta: 0x808080, band: 0x909090, plain: 0x626262 });
28
+ const palette = railwayExchangePalette;
29
+ const color = (hex) => new THREE.Color(hex);
30
+ const glassTones = palette.glass.map(color);
31
+ const litGlass = color(palette.lit), dimGlass = color(palette.dim);
32
+ const terracotta = color(palette.terracotta), band = color(palette.band), plain = color(palette.plain);
33
+ const core = color(0x262626), roofing = color(0x3c3c3c), copper = color(0x9c9c9c);
34
+ function paneColor(row, bay, wall) {
35
+ const hash = (Math.imul(row + 71, 0x9e3779b1) ^ Math.imul(bay + 101, 0x85ebca77) ^ Math.imul(wall + 83, 0xc2b2ae3d)) >>> 0;
36
+ const value = hash % 101;
37
+ if (value < 4)
38
+ return litGlass;
39
+ if (value < 8)
40
+ return dimGlass;
41
+ return glassTones[value % glassTones.length];
42
+ }
43
+ const rows = [{ lo: 0.25, hi: drawn(0.6), kind: "base", floor: 1 }];
44
+ const push = (hi, kind, floor, dy) => rows.push({ lo: rows.at(-1).hi, hi: drawn(hi), kind, floor, ...(dy === undefined ? {} : { dy }) });
45
+ push(7.4, "shop", 1);
46
+ const sill = (n) => (n <= 13 ? 51.53 - (13 - n) * 3.843 : 55.8 + (n - 14) * 3.85);
47
+ for (let n = 2; n <= 16; n += 1) {
48
+ if (n === 14) {
49
+ push(54.6, "wall", 13);
50
+ push(55.4, "band", 13);
51
+ }
52
+ push(sill(n), "wall", n);
53
+ push(sill(n) + 2.6, "glass", n);
54
+ }
55
+ push(67.4, "wall", 16);
56
+ push(67.8, "frieze", 17);
57
+ for (const [lo, hi] of [[67.8, 68.3], [68.3, 68.8], [68.8, 69.3], [69.3, 69.8], [69.8, 70.3]])
58
+ push(hi, "oculus", 17, Math.abs((lo + hi) / 2 - 69.05));
59
+ push(71.8, "frieze", 17);
60
+ // Over the cornice's foot, the plain walls' last row to the eaves.
61
+ rows.push({ lo: rows.at(-1).hi, hi: h.eaves, kind: "frieze", floor: 17 });
62
+ export function createRailwayExchangeGeographicBuilding(record, projectPlan, offset) {
63
+ const kit = createBuilder(record.name, record.id);
64
+ const shell = kit.batch("Railway Exchange Building · shell", kit.material(0xffffff, { vertexColors: true }));
65
+ const wall = kit.batch("Railway Exchange Building · terracotta and windows", kit.material(0xffffff, { vertexColors: true }));
66
+ const cornice = kit.batch("Railway Exchange Building · cornice", kit.material(0x8a8a8a));
67
+ const lot = orient(polygonOf(projectPlan(record.footprint.coordinates)));
68
+ // The lot's south-east corner, on Michigan and Jackson, with the Jackson front running
69
+ // west and the Michigan front north to Symphony Center.
70
+ const corner = lot.reduce((best, p) => (p[0] + p[1] > best[0] + best[1] ? p : best));
71
+ const northEast = lot.reduce((best, p) => (p[0] - p[1] > best[0] - best[1] ? p : best));
72
+ const southWest = lot.reduce((best, p) => (p[1] - p[0] > best[1] - best[0] ? p : best));
73
+ const northWest = lot.reduce((best, p) => (-p[0] - p[1] > -best[0] - best[1] ? p : best));
74
+ const unit = (p) => { const d = Math.hypot(p[0] - corner[0], p[1] - corner[1]); return [(p[0] - corner[0]) / d, (p[1] - corner[1]) / d]; };
75
+ const west = unit(southWest), north = unit(northEast);
76
+ const at = (a, b) => [corner[0] + west[0] * a + north[0] * b, corner[1] + west[1] * a + north[1] * b];
77
+ const skew = west[0] * north[1] - west[1] * north[0];
78
+ const inLot = (p) => {
79
+ const [x, z] = [p[0] - corner[0], p[1] - corner[1]];
80
+ return [(x * north[1] - z * north[0]) / skew, (west[0] * z - west[1] * x) / skew];
81
+ };
82
+ const [jackson, michigan] = [inLot(southWest)[0], inLot(northEast)[1]];
83
+ const lift = ([x, z], y) => [x, y, z];
84
+ // The walls to the roof's foot.
85
+ planOf(lot).forEach((run) => {
86
+ const a = run.at(0), b = run.at(run.length), n = run.normal(0);
87
+ kit.quad(shell, [[a[0], 0, a[1]], [b[0], 0, b[1]], [b[0], h.eaves, b[1]], [a[0], h.eaves, a[1]]], [[n[0], 0, n[1]]], core);
88
+ });
89
+ paintedSlab(kit, shell, lot, 0, false, roofing);
90
+ paintedSlab(kit, shell, lot, h.eaves, true, roofing);
91
+ // The roof: a closed solid on the lot's four corners, 10 cm inside the lines between them,
92
+ // which the Jackson wall stands up to 6.7 cm inside, so that no face lies in the walls'.
93
+ // It rises from a foot 10 cm inside the block, up the eaves, to a flat top 10 m in from
94
+ // each side over the light well, as the photograph shows; the drawing simplifies it to a
95
+ // pyramid. Its slopes are in two triangles each, since the lot is out of square.
96
+ const northWestIn = inLot(northWest);
97
+ const ring = (inset) => orient([at(inset, inset), at(jackson - inset, inset), at(northWestIn[0] - inset, northWestIn[1] - inset), at(inset, michigan - inset)]);
98
+ const [eaves, crown] = [ring(0.1), ring(10)], foot = h.eaves - 0.1;
99
+ planOf(eaves).forEach((run) => {
100
+ const p = run.at(0), q = run.at(run.length), n = run.normal(0);
101
+ kit.quad(shell, [lift(p, foot), lift(q, foot), lift(q, h.eaves), lift(p, h.eaves)], [[n[0], 0, n[1]]], copper);
102
+ });
103
+ paintedSlab(kit, shell, eaves, foot, false, roofing);
104
+ paintedSlab(kit, shell, crown, h.top, true, copper);
105
+ // Each slope's outward normal, from its points and the roof's middle.
106
+ const middle = lift(at(jackson / 2, michigan / 2), (h.eaves + h.top) / 2);
107
+ const face = (points) => {
108
+ const [a, b, c] = points;
109
+ const n = new THREE.Vector3(b[0] - a[0], b[1] - a[1], b[2] - a[2]).cross(new THREE.Vector3(c[0] - a[0], c[1] - a[1], c[2] - a[2])).normalize();
110
+ const mid = [0, 1, 2].map((k) => (a[k] + b[k] + c[k]) / 3);
111
+ if (n.x * (mid[0] - middle[0]) + n.y * (mid[1] - middle[1]) + n.z * (mid[2] - middle[2]) < 0)
112
+ n.negate();
113
+ const normal = [n.x, n.y, n.z];
114
+ kit.triangle(shell, points, [normal, normal, normal], copper);
115
+ };
116
+ eaves.forEach((p, i) => {
117
+ const q = eaves[(i + 1) % 4], [r, s] = [crown[i], crown[(i + 1) % 4]];
118
+ face([lift(p, h.eaves), lift(q, h.eaves), lift(s, h.top)]);
119
+ face([lift(p, h.eaves), lift(s, h.top), lift(r, h.top)]);
120
+ });
121
+ // The cornice projects 90 cm along both fronts, from 12 cm inside the lines between their
122
+ // corners to 2 cm over the eaves, stopping 3 cm short of the north and alley walls.
123
+ kit.prism(cornice, planOf(orient([at(-0.9, michigan - 0.03), at(-0.9, -0.9), at(jackson - 0.03, -0.9), at(jackson - 0.03, 0.12), at(0.12, 0.12), at(0.12, michigan - 0.03)])), [h.cornice, h.eaves + 0.02]);
124
+ const paints = { wall: terracotta, band, plain };
125
+ const skin = (keep, span, seed, openings, sides) => {
126
+ const plan = planOf(lot), heights = [span[0].lo, ...span.map((row) => row.hi)];
127
+ chainsOf(plan).forEach((chain, index) => {
128
+ if (!chain.runs.every((run) => keep(plan[run])))
129
+ return;
130
+ const first = chain.runs[0], last = chain.runs.at(-1);
131
+ const s0 = turnsAt(plan, first)[0] > 0 ? 0.09 : 0.02, s1 = chain.length - (turnsAt(plan, last)[1] > 0 ? 0.09 : 0.02);
132
+ const [columns, cell] = openings(chain.length);
133
+ chainSkin(kit, wall, plan, chain, 0, chain.runs.length - 1, s0, s1, columns, heights, 0.02, 0.07, (bay, r) => {
134
+ const row = span[r], shows = cell((columns[bay - 1] + columns[bay]) / 2, row);
135
+ return shows === "glass" ? paneColor(row.floor, bay, seed * 8 + index) : paints[shows];
136
+ }, sides);
137
+ });
138
+ };
139
+ const near = (list, x) => Math.min(...list.map((c) => Math.abs(x - c)));
140
+ // A street front: eleven bays 4.4 m apart about its middle, each a pair of windows 1.44 m
141
+ // wide either side of a 47 cm mullion and a round window 2.6 m across over it, as drawn.
142
+ const front = (length) => {
143
+ const centres = Array.from({ length: 11 }, (_, k) => length / 2 + (k - 5) * 4.4);
144
+ const cuts = centres.flatMap((c) => [-1.675, -1.3, -0.9, -0.6, -0.235, 0.235, 0.6, 0.9, 1.3, 1.675].map((d) => c + d));
145
+ return [[0, ...cuts, length], (mid, row) => {
146
+ const dx = near(centres, mid);
147
+ if (row.kind === "band")
148
+ return "band";
149
+ if (row.kind === "shop")
150
+ return dx < 1.675 ? "glass" : "wall";
151
+ if (row.kind === "glass")
152
+ return dx > 0.235 && dx < 1.675 ? "glass" : "wall";
153
+ if (row.kind === "oculus")
154
+ return dx * dx + row.dy * row.dy <= 1.3 * 1.3 ? "glass" : "wall";
155
+ return "wall";
156
+ }];
157
+ };
158
+ // The alley's windows, which the drawing does not show: 1.6 m wide, spread evenly about
159
+ // 3.2 m apart, one to each floor above the ground floor, the seventeenth's as tall as the
160
+ // round windows' band.
161
+ const alley = (length) => {
162
+ const count = Math.max(1, Math.round(length / 3.2)), centres = Array.from({ length: count }, (_, k) => length * (k + 0.5) / count);
163
+ return [[0, ...centres.flatMap((c) => [c - 0.8, c + 0.8]), length], (mid, row) => ((row.kind === "glass" || row.kind === "oculus") && near(centres, mid) < 0.8 ? "glass" : "plain")];
164
+ };
165
+ const facing = (x, z) => (run) => run.normal(0)[0] * x + run.normal(0)[1] * z > 0.9;
166
+ // Michigan and Jackson carry the fronts; the alley has windows; the north wall, shared
167
+ // with Symphony Center, stays plain.
168
+ skin((run) => facing(1, 0)(run) || facing(0, 1)(run), rows.slice(0, -1), 0, front, terracotta);
169
+ skin(facing(-1, 0), rows, 1, alley, plain);
170
+ skin(facing(0, -1), rows, 2, (length) => [[0, length], () => "plain"], plain);
171
+ const model = kit.finish({ height: h.top, outlines: [shell, cornice], opacity: 0.16 });
172
+ model.building.position.set(offset[0], 0, offset[1]);
173
+ model.building.userData["geography"] = record;
174
+ model.building.userData["geographicDetail"] = { levels: h, source: "docs/railway-exchange-reference.md" };
175
+ return model;
176
+ }
@@ -0,0 +1,16 @@
1
+ import type { BuildingModel, Plan } from "./building-kit.js";
2
+ import type { GeoBuilding } from "./skyline-geography-data.js";
3
+ export declare const riverPlazaLevels: Readonly<{
4
+ podium: 8;
5
+ floors: 56;
6
+ roof: 156;
7
+ box: 159.7;
8
+ bay: 1.6;
9
+ }>;
10
+ export declare const riverPlazaPalette: Readonly<{
11
+ glass: number[];
12
+ lit: 9079434;
13
+ dim: 5263440;
14
+ concrete: 10132122;
15
+ }>;
16
+ export declare function createRiverPlazaGeographicBuilding(record: GeoBuilding, projectPlan: (coordinates: [number, number][]) => Plan, offset: [number, number]): BuildingModel;
@@ -0,0 +1,104 @@
1
+ import * as THREE from "../vendor/three-r186.js";
2
+ import { createBuilder, polygonOf } from "./building-kit.js";
3
+ import { chainSkin, chainsOf, orient, paintedSlab, planOf, turnsAt } from "./facade-grid.js";
4
+ // River Plaza, 405 North Wabash Avenue (Ezra Gordon–Jack M. Levin and Associates, 1977): the
5
+ // geographic layout's model, on the mapped outline 285867424 and its parts. The original
6
+ // layout has no model of it. From the photograph's camera its top stands behind Two Illinois
7
+ // Center, in the gap between Two Prudential Plaza and Aon Center that the drawing's
8
+ // `office-west-of-aon` group paints, the only mapped building on that sightline tall enough
9
+ // to reach the drawn top.
10
+ //
11
+ // A white concrete slab of 56 storeys with notched corners: a grid of exposed frame and
12
+ // punched windows over a low podium on the river side, to a flat roof at OpenStreetMap's 156 m
13
+ // under a box to the published 159.7 m. See docs/river-plaza-two-illinois-center-reference.md. Units are
14
+ // meters; +x is east, +z is south.
15
+ export const riverPlazaLevels = Object.freeze({
16
+ podium: 8, // the podium's roof, OpenStreetMap's
17
+ floors: 56,
18
+ roof: 156, // the slab's roof, OpenStreetMap's
19
+ box: 159.7, // the rooftop box's top, the published height and tip, the Skyscraper Center's 524 ft
20
+ bay: 1.6, // the frame's bays, an estimate from photographs
21
+ });
22
+ const h = riverPlazaLevels;
23
+ const pitch = h.roof / h.floors;
24
+ // The skins' colours: the window tones, and the concrete, which no window shares.
25
+ export const riverPlazaPalette = Object.freeze({ glass: [0x2c2c2c, 0x303030, 0x343434, 0x282828], lit: 0x8a8a8a, dim: 0x505050, concrete: 0x9a9a9a });
26
+ const palette = riverPlazaPalette;
27
+ const color = (hex) => new THREE.Color(hex);
28
+ const glassTones = palette.glass.map(color);
29
+ const litGlass = color(palette.lit), dimGlass = color(palette.dim), concrete = color(palette.concrete);
30
+ const core = color(0x6a6a6a), roofing = color(0x8a8a8a);
31
+ function paneColor(row, bay, wall) {
32
+ const hash = (Math.imul(row + 19, 0x9e3779b1) ^ Math.imul(bay + 37, 0x85ebca77) ^ Math.imul(wall + 29, 0xc2b2ae3d)) >>> 0;
33
+ const value = hash % 101;
34
+ if (value < 12)
35
+ return litGlass;
36
+ if (value < 18)
37
+ return dimGlass;
38
+ return glassTones[value % glassTones.length];
39
+ }
40
+ export function createRiverPlazaGeographicBuilding(record, projectPlan, offset) {
41
+ const kit = createBuilder(record.name, record.id);
42
+ const shell = kit.batch("River Plaza · shell", kit.material(0xffffff, { vertexColors: true }));
43
+ const wall = kit.batch("River Plaza · concrete and windows", kit.material(0xffffff, { vertexColors: true }));
44
+ const box = kit.batch("River Plaza · rooftop box", kit.material(0x9a9a9a));
45
+ const [tower, podium, top] = record.parts.map((part) => orient(polygonOf(projectPlan(part.coordinates))));
46
+ // The slab and the podium, each a closed shell on its mapped part.
47
+ for (const [plan, height] of [[tower, h.roof], [podium, h.podium]]) {
48
+ planOf(plan).forEach((run) => {
49
+ const a = run.at(0), b = run.at(run.length), n = run.normal(0);
50
+ kit.quad(shell, [[a[0], 0, a[1]], [b[0], 0, b[1]], [b[0], height, b[1]], [a[0], height, a[1]]], [[n[0], 0, n[1]]], core);
51
+ });
52
+ paintedSlab(kit, shell, plan, 0, false, roofing);
53
+ paintedSlab(kit, shell, plan, height, true, roofing);
54
+ }
55
+ // The box on the roof, on its mapped part.
56
+ kit.prism(box, planOf(top), [h.roof, h.box]);
57
+ // The slab's skin, one box to a run of a chain, held 2 cm clear of the chain's ends or 9 cm
58
+ // where the outline turns in: on each floor, a window 1.1 m wide and 1.5 m tall in each
59
+ // 1.6 m bay of the frame, from the chain's middle out. A run the podium stands against, its
60
+ // two ends on the podium's outline, starts over the podium's roof; a chain whose runs differ
61
+ // is skinned in parts, which meet at their joint.
62
+ const plan = planOf(tower);
63
+ const edges = planOf(podium);
64
+ const onEdge = (p) => edges.some((edge) => {
65
+ const a = edge.at(0), b = edge.at(edge.length), t = Math.max(0, Math.min(1, ((p[0] - a[0]) * (b[0] - a[0]) + (p[1] - a[1]) * (b[1] - a[1])) / (edge.length * edge.length)));
66
+ return Math.hypot(p[0] - a[0] - t * (b[0] - a[0]), p[1] - a[1] - t * (b[1] - a[1])) < 0.05;
67
+ });
68
+ const covered = (run) => onEdge(run.at(0)) && onEdge(run.at(run.length));
69
+ chainsOf(plan).forEach((chain, index) => {
70
+ const first = chain.runs[0], last = chain.runs.at(-1);
71
+ const s0 = turnsAt(plan, first)[0] > 0 ? 0.09 : 0.02, s1 = chain.length - (turnsAt(plan, last)[1] > 0 ? 0.09 : 0.02);
72
+ const count = Math.floor(chain.length / 2 / h.bay);
73
+ const centres = Array.from({ length: 2 * count }, (_, k) => chain.length / 2 + (k - count + 0.5) * h.bay);
74
+ // The chain's parts, each a span of runs the podium covers alike.
75
+ const parts = [];
76
+ chain.runs.forEach((run, k) => { const part = parts.at(-1); if (part && covered(plan[run]) === covered(plan[chain.runs[part[0]]]))
77
+ part[1] = k;
78
+ else
79
+ parts.push([k, k]); });
80
+ for (const [k0, k1] of parts) {
81
+ const base = covered(plan[chain.runs[k0]]) ? h.podium : 0.25;
82
+ const from = k0 === 0 ? s0 : chain.starts[k0], to = k1 === chain.runs.length - 1 ? s1 : chain.starts[k1] + plan[chain.runs[k1]].length;
83
+ const heights = [base];
84
+ for (let n = Math.ceil(base / pitch); n < h.floors; n += 1) {
85
+ const sill = n * pitch + 0.75;
86
+ if (sill > base + 0.2)
87
+ heights.push(sill, sill + 1.5);
88
+ }
89
+ heights.push(h.roof);
90
+ const cuts = centres.flatMap((c) => [c - 0.55, c + 0.55]).filter((s) => s > from + 0.01 && s < to - 0.01);
91
+ const columns = [from, ...cuts, to];
92
+ chainSkin(kit, wall, plan, chain, k0, k1, from, to, columns, heights, 0.02, 0.07, (bay, r) => {
93
+ const middle = (columns[bay - 1] + columns[bay]) / 2;
94
+ const glazed = r % 2 === 1 && centres.some((c) => Math.abs(middle - c) < 0.55);
95
+ return glazed ? paneColor(r, bay, index) : concrete;
96
+ }, concrete);
97
+ }
98
+ });
99
+ const model = kit.finish({ height: h.box, outlines: [shell, box], opacity: 0.16 });
100
+ model.building.position.set(offset[0], 0, offset[1]);
101
+ model.building.userData["geography"] = record;
102
+ model.building.userData["geographicDetail"] = { levels: h, source: "docs/river-plaza-two-illinois-center-reference.md" };
103
+ return model;
104
+ }
@@ -0,0 +1,24 @@
1
+ import type { BuildingModel, Plan, Vec2 } from "./building-kit.js";
2
+ import type { GeoBuilding } from "./skyline-geography-data.js";
3
+ export declare const sheratonLevels: Readonly<{
4
+ pitch: 2.67;
5
+ topFloor: 97;
6
+ corner: 100.1;
7
+ arms: number;
8
+ drum: 12.2;
9
+ top: 112.3;
10
+ }>;
11
+ export declare const sheratonDrums: readonly {
12
+ centre: Vec2;
13
+ radius: number;
14
+ base: number;
15
+ }[];
16
+ export declare const sheratonPalette: Readonly<{
17
+ glass: number[];
18
+ lit: 9079434;
19
+ dim: 5263440;
20
+ precast: 8684676;
21
+ drum: 11842740;
22
+ fin: 5921370;
23
+ }>;
24
+ export declare function createSheratonGrandGeographicBuilding(record: GeoBuilding, projectPlan: (coordinates: [number, number][]) => Plan, offset: [number, number]): BuildingModel;
@@ -0,0 +1,112 @@
1
+ import * as THREE from "../vendor/three-r186.js";
2
+ import { createBuilder, polygonOf } from "./building-kit.js";
3
+ import { chainSkin, chainsOf, orient, paintedSlab, planOf, turnsAt } from "./facade-grid.js";
4
+ // The Sheraton Grand Chicago Riverwalk, 301 East North Water Street (Solomon Cordwell Buenz,
5
+ // 1992): the geographic layout's model, on the mapped tower 592122464 and its corner part
6
+ // 1269924307. The original layout has no model of it. The drawing's `buckingham-east` group,
7
+ // right of The Buckingham, is its corner: a lit crown over a tower of windows.
8
+ //
9
+ // A cream precast tower on an L, its arms ending in round towers, of punched windows at the
10
+ // published 2.67 m floor-to-floor. The corner's round tower rises two floors over the arms,
11
+ // and each round end carries an open drum of maroon fins, floodlit at night. Heights stand on
12
+ // the Skyscraper Center's datum, its top floor at 97 m and the corner drum's top at 112.3 m;
13
+ // its 112.8 m tip, 50 cm higher, is not modelled. See docs/sheraton-grand-reference.md.
14
+ // Units are meters; +x is east, +z is south.
15
+ export const sheratonLevels = Object.freeze({
16
+ pitch: 2.67, // Emporis's floor-to-floor, 8.75 ft
17
+ topFloor: 97, // the Skyscraper Center's occupied height, the corner's top floor
18
+ corner: 100.1, // the corner's roof, a floor-to-roof of 3.1 m over it as Emporis gives it
19
+ arms: 100.1 - 2 * 2.67, // the arms' roof, two floors lower, at the mapped arms' 32 levels
20
+ drum: 12.2, // the drums' height, Emporis's architectural height over its main roof
21
+ top: 112.3, // the corner drum's top, the Skyscraper Center's architectural height
22
+ });
23
+ const h = sheratonLevels;
24
+ // The round ends the drums stand on, circles fitted to the mapped parts' arcs within 11 cm:
25
+ // the corner, part 1269924307, and the east and north arms' ends, parts 1269924306 and
26
+ // 1269924308. Centres are east and north metres from Crain's centre.
27
+ export const sheratonDrums = Object.freeze([
28
+ { centre: [407.44, 465.95], radius: 7.97, base: h.corner },
29
+ { centre: [439.52, 462.06], radius: 7.37, base: h.arms },
30
+ { centre: [409.75, 498.32], radius: 7.39, base: h.arms },
31
+ ]);
32
+ // The skins' colours: the window tones, the precast, and the drums' cream and maroon fins,
33
+ // which no window shares.
34
+ export const sheratonPalette = Object.freeze({ glass: [0x2a2a2a, 0x2e2e2e, 0x323232, 0x262626], lit: 0x8a8a8a, dim: 0x505050, precast: 0x848484, drum: 0xb4b4b4, fin: 0x5a5a5a });
35
+ const palette = sheratonPalette;
36
+ const color = (hex) => new THREE.Color(hex);
37
+ const glassTones = palette.glass.map(color);
38
+ const litGlass = color(palette.lit), dimGlass = color(palette.dim), precast = color(palette.precast), drumFace = color(palette.drum), fin = color(palette.fin);
39
+ const core = color(0x585858), roofing = color(0x686868);
40
+ function paneColor(row, bay, wall) {
41
+ const hash = (Math.imul(row + 17, 0x9e3779b1) ^ Math.imul(bay + 89, 0x85ebca77) ^ Math.imul(wall + 37, 0xc2b2ae3d)) >>> 0;
42
+ const value = hash % 101;
43
+ if (value < 14)
44
+ return litGlass;
45
+ if (value < 20)
46
+ return dimGlass;
47
+ return glassTones[value % glassTones.length];
48
+ }
49
+ // A wall's rows from `base` to `top`: a window 1.5 m tall 80 cm over each floor line, the
50
+ // floors counted down from the top floor at the published pitch, from the last floor line
51
+ // under the base, whose window may clear it.
52
+ function rowsFor(base, top) {
53
+ const heights = [base];
54
+ for (let level = h.topFloor - Math.ceil((h.topFloor - base) / h.pitch) * h.pitch; level < top; level += h.pitch) {
55
+ if (level + 0.8 > base + 0.2 && level + 2.3 < top - 0.2)
56
+ heights.push(level + 0.8, level + 2.3);
57
+ }
58
+ heights.push(top);
59
+ return heights;
60
+ }
61
+ export function createSheratonGrandGeographicBuilding(record, projectPlan, offset) {
62
+ const kit = createBuilder(record.name, record.id);
63
+ const shell = kit.batch("Sheraton Grand · shell", kit.material(0xffffff, { vertexColors: true }));
64
+ const wall = kit.batch("Sheraton Grand · precast and windows", kit.material(0xffffff, { vertexColors: true }));
65
+ const drums = kit.batch("Sheraton Grand · drums", kit.material(0xffffff, { vertexColors: true }));
66
+ const tower = orient(polygonOf(projectPlan(record.parts[0].coordinates)));
67
+ const circle = (centre, radius, sides = 24) => orient(Array.from({ length: sides }, (_, k) => {
68
+ const angle = (k / sides) * Math.PI * 2;
69
+ return [centre[0] + radius * Math.cos(angle), -centre[1] + radius * Math.sin(angle)];
70
+ }));
71
+ const cornerPlan = orient(polygonOf(projectPlan(record.parts[1].coordinates)));
72
+ // A closed solid of walls and slabs on an outline, painted by its runs.
73
+ const solid = (target, outline, lo, hi, paint) => {
74
+ planOf(outline).forEach((run, k) => {
75
+ const a = run.at(0), b = run.at(run.length), n = run.normal(0);
76
+ kit.quad(target, [[a[0], lo, a[1]], [b[0], lo, b[1]], [b[0], hi, b[1]], [a[0], hi, a[1]]], [[n[0], 0, n[1]]], paint(k));
77
+ });
78
+ paintedSlab(kit, target, outline, lo, false, roofing);
79
+ paintedSlab(kit, target, outline, hi, true, roofing);
80
+ };
81
+ // The tower to the arms' roof, and the corner's round tower, on its mapped part, two floors
82
+ // over it.
83
+ solid(shell, tower, 0, h.arms, () => core);
84
+ solid(shell, cornerPlan, h.arms, h.corner, () => core);
85
+ // Each drum: cream, a maroon fin every other facet, on its round end.
86
+ for (const { centre, radius, base } of sheratonDrums)
87
+ solid(drums, circle(centre, radius), base, base + h.drum, (k) => (k % 2 ? fin : drumFace));
88
+ // The skins, one box to a chain of runs turning less than 30°, so the round ends carry one:
89
+ // a window 1.4 m wide in each 2.4 m bay, set out from the chain's middle, on every floor.
90
+ const skin = (outline, base, top, seed) => {
91
+ const plan = planOf(outline), heights = rowsFor(base, top);
92
+ chainsOf(plan, 30 * Math.PI / 180).forEach((chain, index) => {
93
+ const first = chain.runs[0], last = chain.runs.at(-1);
94
+ const s0 = turnsAt(plan, first)[0] > 0 ? 0.09 : 0.02, s1 = chain.length - (turnsAt(plan, last)[1] > 0 ? 0.09 : 0.02);
95
+ const count = Math.floor((chain.length - 1.4) / 2 / 2.4);
96
+ const centres = Array.from({ length: 2 * count + 1 }, (_, k) => chain.length / 2 + (k - count) * 2.4);
97
+ const cuts = centres.flatMap((c) => [c - 0.7, c + 0.7]).filter((s) => s > s0 + 0.01 && s < s1 - 0.01);
98
+ const columns = [s0, ...cuts, s1];
99
+ chainSkin(kit, wall, plan, chain, 0, chain.runs.length - 1, s0, s1, columns, heights, 0.02, 0.07, (bay, r) => {
100
+ const middle = (columns[bay - 1] + columns[bay]) / 2;
101
+ return r % 2 === 1 && centres.some((c) => Math.abs(middle - c) < 0.7) ? paneColor(r, bay, seed * 16 + index) : precast;
102
+ }, precast);
103
+ });
104
+ };
105
+ skin(tower, 0.25, h.arms, 0);
106
+ skin(cornerPlan, h.arms, h.corner, 1);
107
+ const model = kit.finish({ height: h.top, outlines: [shell, drums], opacity: 0.16 });
108
+ model.building.position.set(offset[0], 0, offset[1]);
109
+ model.building.userData["geography"] = record;
110
+ model.building.userData["geographicDetail"] = { levels: h, source: "docs/sheraton-grand-reference.md" };
111
+ return model;
112
+ }
@@ -0,0 +1,16 @@
1
+ import type { BuildingModel, Plan } from "./building-kit.js";
2
+ import type { GeoBuilding } from "./skyline-geography-data.js";
3
+ export declare const sixNorthLevels: Readonly<{
4
+ block: 62.1;
5
+ cornice: 63.6;
6
+ band: 65;
7
+ bandTop: 66.9;
8
+ arches: 75.6;
9
+ towerCornice: 78;
10
+ stage: 84.5;
11
+ top: 86;
12
+ towerSouth: 7.5;
13
+ towerNorth: 20;
14
+ towerDepth: 12;
15
+ }>;
16
+ export declare function createSixNorthMichiganGeographicBuilding(record: GeoBuilding, projectPlan: (coordinates: [number, number][]) => Plan, offset: [number, number]): BuildingModel;
@@ -0,0 +1,180 @@
1
+ import * as THREE from "../vendor/three-r186.js";
2
+ import { createBuilder, polygonOf } from "./building-kit.js";
3
+ import { chainSkin, chainsOf, orient, paintedSlab, planOf, turnsAt } from "./facade-grid.js";
4
+ // Six North Michigan, the Montgomery Ward Building (Schmidt, Garden & Martin, 1899; four
5
+ // floors added by Holabird & Roche, 1923): the geographic layout's model, on the mapped
6
+ // outline 126982631. The original layout has no model of it; the drawing shows it right of
7
+ // Willoughby Tower, outside the excerpt it is fitted to.
8
+ //
9
+ // A sixteen-storey block, 27.5 m on Michigan by 49.6 m on Madison, under a projecting
10
+ // cornice. The tower, built in the middle of the Michigan front rather than on the corner
11
+ // first planned, rises flush with it: a floor above the block, a band, an arched stage of
12
+ // three tall windows to a face under a cornice, and a top stage of panels under a cap at
13
+ // 86 m, where the pyramid and statue came off in 1947. Heights are measured on the drawing
14
+ // down from that top; see docs/six-north-michigan-reference.md. Units are meters; +x is
15
+ // east, +z is south.
16
+ export const sixNorthLevels = Object.freeze({
17
+ block: 62.1, // the block's walls, under its cornice
18
+ cornice: 63.6, // the block's cornice and roof
19
+ band: 65, // the tower's band, over the floor above the block
20
+ bandTop: 66.9,
21
+ arches: 75.6, // the arched stage, under the tower's cornice
22
+ towerCornice: 78, // the tower's cornice, under the top stage
23
+ stage: 84.5, // the top stage, under its cap
24
+ top: 86, // the cap, the published height
25
+ towerSouth: 7.5, // the tower's south wall, north of Madison
26
+ towerNorth: 20, // its north wall
27
+ towerDepth: 12, // west of the Michigan front
28
+ });
29
+ const h = sixNorthLevels;
30
+ // Each floor's level: a 6 m ground floor, then 3.74 m floors to the block's cornice.
31
+ const floor = (n) => (n < 2 ? 0 : 6 + (n - 2) * 3.74);
32
+ const color = (hex) => new THREE.Color(hex);
33
+ const glassTones = [0x4c4c4c, 0x505050, 0x545454, 0x585858].map(color);
34
+ const litGlass = color(0x8a8a8a), dimGlass = color(0x5e5e5e);
35
+ const brick = color(0x6e6e6e), panel = color(0x9c9c9c), core = color(0x2e2e2e), roofing = color(0x484848);
36
+ function paneColor(row, bay, wall) {
37
+ const hash = (Math.imul(row + 31, 0x9e3779b1) ^ Math.imul(bay + 57, 0x85ebca77) ^ Math.imul(wall + 43, 0xc2b2ae3d)) >>> 0;
38
+ const value = hash % 101;
39
+ if (value < 4)
40
+ return litGlass;
41
+ if (value < 8)
42
+ return dimGlass;
43
+ return glassTones[value % glassTones.length];
44
+ }
45
+ const blockRows = [
46
+ { lo: 0.25, hi: 1, kind: "brick", floor: 1 },
47
+ { lo: 1, hi: 5.2, kind: "glass", floor: 1 },
48
+ { lo: 5.2, hi: floor(2) - 0.35, kind: "brick", floor: 1 },
49
+ ];
50
+ for (let n = 2; n <= 16; n += 1) {
51
+ blockRows.push({ lo: floor(n) - 0.35, hi: floor(n) + 1.25, kind: "brick", floor: n });
52
+ blockRows.push({ lo: floor(n) + 1.25, hi: floor(n + 1) - 0.35, kind: "glass", floor: n });
53
+ }
54
+ blockRows.push({ lo: floor(17) - 0.35, hi: h.block, kind: "brick", floor: 17 });
55
+ // The tower's rows over the block: the seventeenth floor's windows under the band; then the
56
+ // arched stage's small windows over the band and its tall windows, their heads narrowing
57
+ // for the last half-metre; then the top stage's panel.
58
+ const towerRows = [
59
+ { lo: h.block, hi: 62.95, kind: "brick", floor: 17 },
60
+ { lo: 62.95, hi: 64.77, kind: "floor17", floor: 17 },
61
+ { lo: 64.77, hi: h.band, kind: "brick", floor: 17 },
62
+ { lo: h.bandTop, hi: 68.2, kind: "small", floor: 18 },
63
+ { lo: 68.2, hi: 70.7, kind: "brick", floor: 18 },
64
+ { lo: 70.7, hi: 73.7, kind: "arch", floor: 19 },
65
+ { lo: 73.7, hi: 74.2, kind: "head", floor: 19 },
66
+ { lo: 74.2, hi: h.arches, kind: "brick", floor: 20 },
67
+ { lo: h.towerCornice, hi: 83.2, kind: "panel", floor: 21 },
68
+ { lo: 83.2, hi: h.stage, kind: "brick", floor: 21 },
69
+ ];
70
+ const rowsBetween = (rows, lo, hi) => rows
71
+ .map((row) => ({ ...row, lo: Math.max(row.lo, lo), hi: Math.min(row.hi, hi) }))
72
+ .filter((row) => row.hi - row.lo > 0.05);
73
+ export function createSixNorthMichiganGeographicBuilding(record, projectPlan, offset) {
74
+ const kit = createBuilder(record.name, record.id);
75
+ const shell = kit.batch("Six North Michigan · shell", kit.material(0xffffff, { vertexColors: true }));
76
+ const wall = kit.batch("Six North Michigan · brick and windows", kit.material(0xffffff, { vertexColors: true }));
77
+ const cornices = kit.batch("Six North Michigan · cornices", kit.material(0x747474));
78
+ const lot = orient(polygonOf(projectPlan(record.footprint.coordinates)));
79
+ // The lot's south-east corner, and its Madison and Michigan fronts running from it.
80
+ const corner = lot.reduce((best, p) => (p[0] + p[1] > best[0] + best[1] ? p : best));
81
+ const neighbours = [lot[(lot.indexOf(corner) + 1) % lot.length], lot[(lot.indexOf(corner) + lot.length - 1) % lot.length]];
82
+ const unit = (p) => { const d = Math.hypot(p[0] - corner[0], p[1] - corner[1]); return [(p[0] - corner[0]) / d, (p[1] - corner[1]) / d]; };
83
+ const [west, north] = neighbours[0][0] < neighbours[1][0] ? [unit(neighbours[0]), unit(neighbours[1])] : [unit(neighbours[1]), unit(neighbours[0])];
84
+ const at = (a, b) => [corner[0] + west[0] * a + north[0] * b, corner[1] + west[1] * a + north[1] * b];
85
+ // The inverse of `at`: a point's distances along Madison and Michigan, whose fronts meet
86
+ // a little off square.
87
+ const skew = west[0] * north[1] - west[1] * north[0];
88
+ const inLot = (p) => {
89
+ const [x, z] = [p[0] - corner[0], p[1] - corner[1]];
90
+ return [(x * north[1] - z * north[0]) / skew, (west[0] * z - west[1] * x) / skew];
91
+ };
92
+ const rect = (a0, a1, b0, b1) => orient([at(a0, b0), at(a1, b0), at(a1, b1), at(a0, b1)]);
93
+ const [depth, front] = [Math.max(...lot.map((p) => inLot(p)[0])), Math.max(...lot.map((p) => inLot(p)[1]))];
94
+ const tower = (grow) => rect(-grow, h.towerDepth + grow, h.towerSouth - grow, h.towerNorth + grow);
95
+ // A solid on an outline between two heights.
96
+ const solid = (corners, lo, hi) => {
97
+ planOf(corners).forEach((run) => {
98
+ const a = run.at(0), b = run.at(run.length), n = run.normal(0);
99
+ kit.quad(shell, [[a[0], lo, a[1]], [b[0], lo, b[1]], [b[0], hi, b[1]], [a[0], hi, a[1]]], [[n[0], 0, n[1]]], core);
100
+ });
101
+ paintedSlab(kit, shell, corners, lo, false, roofing);
102
+ paintedSlab(kit, shell, corners, hi, true, roofing);
103
+ };
104
+ solid(lot, 0, h.block);
105
+ solid(tower(0), h.block, h.arches);
106
+ solid(tower(-0.8), h.towerCornice, h.stage);
107
+ // The block's cornice overhangs its walls by a metre, stopping either side of the tower,
108
+ // which rises through it, 2 cm clear of its walls so that the two undersides do not share
109
+ // an edge; the tower's band, cornice and cap are its outline grown or shrunk.
110
+ const [notchNorth, notchSouth, notchBack] = [h.towerNorth + 0.02, h.towerSouth - 0.02, h.towerDepth + 0.02];
111
+ const corniceOutline = orient([at(-1, -1), at(depth + 1, -1), at(depth + 1, front + 1), at(-1, front + 1), at(-1, notchNorth), at(notchBack, notchNorth), at(notchBack, notchSouth), at(-1, notchSouth)]);
112
+ kit.prism(cornices, planOf(corniceOutline), [h.block, h.cornice]);
113
+ kit.prism(cornices, planOf(tower(0.5)), [h.band, h.bandTop]);
114
+ kit.prism(cornices, planOf(tower(1.2)), [h.arches, h.towerCornice]);
115
+ kit.prism(cornices, planOf(tower(-0.3)), [h.stage, h.top]);
116
+ // A wall's skin, one box to a chain the `keep` test accepts, held 2 cm clear of its ends
117
+ // or 9 cm where the outline turns in. `openings` lays a wall's columns out across it,
118
+ // from its start to its end, and says how far each reaches into a window: 0 is brick; a
119
+ // floor's window takes any other; the tower's tall windows 1 and over, its seventeenth
120
+ // floor's 2, its small windows 3, and their heads and the top stage's panel 4. Floors
121
+ // below `fromFloor` stay brick.
122
+ const glassFrom = { brick: 99, glass: 1, floor17: 2, small: 3, arch: 1, head: 4, panel: 4 };
123
+ const skin = (corners, rows, lo, hi, seed, keep, openings, fromFloor = 1) => {
124
+ const plan = planOf(corners), cut = rowsBetween(rows, lo, hi), heights = [cut[0].lo, ...cut.map((row) => row.hi)];
125
+ chainsOf(plan).forEach((chain, index) => {
126
+ if (!chain.runs.every((run) => keep(plan[run])))
127
+ return;
128
+ const first = chain.runs[0], last = chain.runs.at(-1);
129
+ const s0 = turnsAt(plan, first)[0] > 0 ? 0.09 : 0.02, s1 = chain.length - (turnsAt(plan, last)[1] > 0 ? 0.09 : 0.02);
130
+ const [columns, reach] = openings(chain.length, plan[first].at(0), plan[last].at(plan[last].length));
131
+ chainSkin(kit, wall, plan, chain, 0, chain.runs.length - 1, s0, s1, columns, heights, 0.02, 0.07, (bay, r) => {
132
+ const row = cut[r], into = reach(bay);
133
+ if (row.kind === "panel")
134
+ return into >= 4 ? panel : brick;
135
+ if (row.floor < fromFloor)
136
+ return brick;
137
+ return into >= glassFrom[row.kind] ? paneColor(row.floor, bay, seed * 8 + index) : brick;
138
+ }, brick);
139
+ });
140
+ };
141
+ // Windows centred at `centres`, `width` wide, along a wall from its start.
142
+ const windows = (length, centres, width) => {
143
+ const inside = centres.filter((c) => c - width / 2 > 0.1 && c + width / 2 < length - 0.1);
144
+ return [[0, ...inside.flatMap((c) => [c - width / 2, c + width / 2]), length], (bay) => (bay % 2 === 0 && bay <= 2 * inside.length ? 1 : 0)];
145
+ };
146
+ // A position along a wall from its start, given in lot coordinates across it.
147
+ const along = (length, start, end, target, axis) => length * (target - inLot(start)[axis]) / (inLot(end)[axis] - inLot(start)[axis]);
148
+ const michigan = (run) => run.normal(0)[0] > 0.9, party = (run) => run.normal(0)[1] < -0.9;
149
+ // The Michigan front: three windows to each wing either side of the tower, and three to
150
+ // the tower between them, 3 m apart on the centres of the windows above.
151
+ const michiganFront = (length, start, end) => windows(length, [1.6, 3.75, 5.9, 10.75, 13.75, 16.75, 21.6, 23.75, 25.9].map((b) => along(length, start, end, b, 1)), 1.3);
152
+ // Madison, the alley and the north wall: a window every 2.75 m. The north wall is shared
153
+ // with 20 North Michigan, eight storeys high, and stays blank below its ninth floor.
154
+ const punched = (length) => { const bays = Math.round(length / 2.75); return windows(length, Array.from({ length: bays }, (_, i) => length * (i + 0.5) / bays), 1.1); };
155
+ skin(lot, blockRows, 0.25, h.block, 0, michigan, michiganFront);
156
+ skin(lot, blockRows, 0.25, h.block, 1, (run) => !michigan(run) && !party(run), punched);
157
+ skin(lot, blockRows, 0.25, h.block, 6, party, punched, 9);
158
+ // The tower's faces: three windows to a face, centred 3 m apart. A tall window is 2 m
159
+ // wide, its head the middle metre; the seventeenth floor's are 1.3 m and the small ones
160
+ // 1.1 m, all on the same centres.
161
+ const towerFace = (length) => {
162
+ const steps = [-1, -0.65, -0.55, -0.5, 0.5, 0.55, 0.65, 1];
163
+ const columns = [0, ...[-3, 0, 3].flatMap((d) => steps.map((s) => length / 2 + d + s)), length];
164
+ // Bays count from one: a pier, then 1, 2, 3, 4, 3, 2, 1 across each window.
165
+ return [columns, (bay) => (bay > 24 || (bay - 1) % 8 === 0 ? 0 : [0, 1, 2, 3, 4, 3, 2, 1][(bay - 1) % 8])];
166
+ };
167
+ // The top stage's panel, 7.2 m wide in the middle of each face.
168
+ const panelled = (length) => [[0, length / 2 - 3.6, length / 2 + 3.6, length], (bay) => (bay === 2 ? 4 : 0)];
169
+ // Over the block, the tower's Michigan face shows from the block's walls; the cornice
170
+ // hides its other faces' foot.
171
+ skin(tower(0), towerRows, h.block, h.band, 2, michigan, towerFace);
172
+ skin(tower(0), towerRows, h.cornice, h.band, 3, (run) => !michigan(run), towerFace);
173
+ skin(tower(0), towerRows, h.bandTop, h.arches, 4, () => true, towerFace);
174
+ skin(tower(-0.8), towerRows, h.towerCornice, h.stage, 5, () => true, panelled);
175
+ const model = kit.finish({ height: h.top, outlines: [shell, cornices], opacity: 0.16 });
176
+ model.building.position.set(offset[0], 0, offset[1]);
177
+ model.building.userData["geography"] = record;
178
+ model.building.userData["geographicDetail"] = { levels: h, source: "docs/six-north-michigan-reference.md" };
179
+ return model;
180
+ }