@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,250 @@
1
+ import * as THREE from "../vendor/three-r186.js";
2
+ import { createBuilder, polygonOf } from "./building-kit.js";
3
+ import { chainSkin, chainsOf, orient, paintedSlab, planOf, turnsAt } from "./facade-grid.js";
4
+ // The Peoples Gas Building, 122 South Michigan Avenue (D. H. Burnham & Co., 1911): the
5
+ // geographic layout's model, on the mapped outline 145498710 and its parts. The original
6
+ // layout has no model of it; the drawing shows it between the Lake View Building and Adams
7
+ // Street, with the Borg-Warner Building before its Adams front.
8
+ //
9
+ // Twenty storeys rise on the Michigan and Adams fronts between wide corner piers:
10
+ // - two behind granite columns;
11
+ // - fourteen of paired windows, eleven bays to Michigan and nine to Adams;
12
+ // - three behind a colonnade under an entablature;
13
+ // - an attic under a frieze of pendant arches, a projecting cornice and its cresting.
14
+ // A light court opens over the ground storey and widens over the seventeenth floor. The top
15
+ // is the City's 272 ft. The drawn cresting reads 86 m, so heights read on the drawing are
16
+ // scaled to meet it; see docs/peoples-gas-reference.md. Units are meters; +x is east, +z is
17
+ // south.
18
+ const drawn = (height) => height * 82.9 / 86;
19
+ export const peoplesGasLevels = Object.freeze({
20
+ court: drawn(7.2), // the light court's floor, over the ground storey
21
+ inner: drawn(66.3), // the court's inner wings' roof, at the seventeenth floor
22
+ colonnade: drawn(67.47), // the colonnade's foot
23
+ entablature: drawn(76.8), // the colonnade's entablature
24
+ frieze: drawn(82.1), // the frieze under the cornice
25
+ cornice: drawn(84.3), // the projecting cornice
26
+ roof: drawn(84.8), // the cornice's top and the roof
27
+ top: 82.9, // the cresting's top, the published height
28
+ });
29
+ const h = peoplesGasLevels;
30
+ // The skins' colours: the window tones, and the terracotta, granite, bands, the frieze's
31
+ // shadow and the court's white brick, which no window shares.
32
+ export const peoplesGasPalette = Object.freeze({ glass: [0x2c2c2c, 0x303030, 0x343434, 0x383838], lit: 0x6e6e6e, dim: 0x3c3c3c, stone: 0x7c7c7c, granite: 0x5a5a5a, band: 0x8e8e8e, shadow: 0x242424, court: 0x9c9c9c });
33
+ const palette = peoplesGasPalette;
34
+ const color = (hex) => new THREE.Color(hex);
35
+ const glassTones = palette.glass.map(color);
36
+ const litGlass = color(palette.lit), dimGlass = color(palette.dim);
37
+ const stone = color(palette.stone), granite = color(palette.granite), band = color(palette.band), shadow = color(palette.shadow), courtBrick = color(palette.court);
38
+ const core = color(0x262626), party = color(0x646464), roofing = color(0x3c3c3c);
39
+ function paneColor(row, bay, wall) {
40
+ const hash = (Math.imul(row + 61, 0x9e3779b1) ^ Math.imul(bay + 89, 0x85ebca77) ^ Math.imul(wall + 73, 0xc2b2ae3d)) >>> 0;
41
+ const value = hash % 101;
42
+ if (value < 4)
43
+ return litGlass;
44
+ if (value < 8)
45
+ return dimGlass;
46
+ return glassTones[value % glassTones.length];
47
+ }
48
+ const rows = [{ lo: 0.25, hi: drawn(0.6), kind: "granite", floor: 1 }];
49
+ const push = (hi, kind, floor, more = {}) => rows.push({ lo: rows.at(-1).hi, hi: drawn(hi), kind, floor, ...more });
50
+ push(6.2, "shop", 1);
51
+ push(7.2, "granite", 2);
52
+ push(11.6, "shop", 2);
53
+ push(12.4, "granite", 2);
54
+ push(13.3, "belt", 2);
55
+ const sill = (n) => 63.36 - (16 - n) * 3.79;
56
+ for (let n = 3; n <= 16; n += 1) {
57
+ push(sill(n), "wall", n);
58
+ push(sill(n) + 2.24, "glass", n, { pier: true });
59
+ }
60
+ push(66.3, "wall", 16);
61
+ push(67.47, "wall", 17);
62
+ push(69.7, "rail", 17, { pier: true });
63
+ push(69.9, "rail", 17);
64
+ push(70.9, "open", 18);
65
+ push(73.2, "open", 18, { pier: true });
66
+ push(74.56, "open", 19);
67
+ push(76.8, "open", 19, { pier: true });
68
+ push(78.3, "band", 19);
69
+ push(78.9, "wall", 20);
70
+ push(81, "attic", 20);
71
+ push(81.3, "attic", 20, { reach: 0.45 });
72
+ push(81.55, "pendant", 20, { reach: 0.45 });
73
+ push(82.1, "pendant", 20, { reach: 0.9 });
74
+ push(84.3, "frieze", 20);
75
+ push(84.8, "parapet", 20);
76
+ export function createPeoplesGasGeographicBuilding(record, projectPlan, offset) {
77
+ const kit = createBuilder(record.name, record.id);
78
+ const shell = kit.batch("Peoples Gas Building · shell", kit.material(0xffffff, { vertexColors: true }));
79
+ const wall = kit.batch("Peoples Gas Building · terracotta and windows", kit.material(0xffffff, { vertexColors: true }));
80
+ const cornice = kit.batch("Peoples Gas Building · cornice", kit.material(0x8e8e8e));
81
+ const corners = (coordinates) => orient(polygonOf(projectPlan(coordinates)));
82
+ const lot = corners(record.footprint.coordinates);
83
+ // The mapped parts: the twenty-level ring, the north wing within it, the inner wings
84
+ // rising to the seventeenth floor, the court, and the notch in the north wall that opens
85
+ // it to the Lake View Building's light court.
86
+ const part = (way) => corners(record.parts.find((p) => p.way === way).coordinates);
87
+ const mapped = [1179833660, 1179833658, 1179833661, 1179833659, 1179842408].map(part);
88
+ // The lot's south-east corner, on Michigan and Adams, with the Adams front running west to
89
+ // the alley and the Michigan front north to the Lake View Building.
90
+ const corner = lot.reduce((best, p) => (p[0] + p[1] > best[0] + best[1] ? p : best));
91
+ const northEast = lot.reduce((best, p) => (p[0] - p[1] > best[0] - best[1] ? p : best));
92
+ const southWest = lot.reduce((best, p) => (p[1] - p[0] > best[1] - best[0] ? p : best));
93
+ const unit = (p) => { const d = Math.hypot(p[0] - corner[0], p[1] - corner[1]); return [(p[0] - corner[0]) / d, (p[1] - corner[1]) / d]; };
94
+ const west = unit(southWest), north = unit(northEast);
95
+ const at = (a, b) => [corner[0] + west[0] * a + north[0] * b, corner[1] + west[1] * a + north[1] * b];
96
+ // The inverse of `at`: a point's distances along the Adams and Michigan fronts, which
97
+ // meet a little off square.
98
+ const skew = west[0] * north[1] - west[1] * north[0];
99
+ const inLot = (p) => {
100
+ const [x, z] = [p[0] - corner[0], p[1] - corner[1]];
101
+ return [(x * north[1] - z * north[0]) / skew, (west[0] * z - west[1] * x) / skew];
102
+ };
103
+ const [adams, michigan] = [inLot(southWest)[0], inLot(northEast)[1]];
104
+ // Corners and edges by value, since parts share the outline's nodes.
105
+ const key = (p) => `${p[0].toFixed(3)},${p[1].toFixed(3)}`;
106
+ // Where a part's edge joins two of the outline's corners a few nodes apart, it follows
107
+ // the outline between them, so that the roof meets the walls corner to corner: the ring
108
+ // skips two of the Michigan front's nodes.
109
+ const index = new Map(lot.map((p, i) => [key(p), i]));
110
+ const follow = (polygon) => polygon.flatMap((a, i) => {
111
+ const [from, to] = [index.get(key(a)), index.get(key(polygon[(i + 1) % polygon.length]))], n = lot.length;
112
+ if (from === undefined || to === undefined)
113
+ return [a];
114
+ const [ahead, back] = [(to - from + n) % n, (from - to + n) % n];
115
+ const step = ahead > 1 && ahead <= 3 ? 1 : back > 1 && back <= 3 ? -1 : 0, between = step === 1 ? ahead - 1 : step === -1 ? back - 1 : 0;
116
+ return [a, ...Array.from({ length: between }, (_, k) => lot[(from + step * (k + 1) + n) % n])];
117
+ });
118
+ // And a part takes every other part's corner within 2 cm of one of its edges: the ring's
119
+ // edge along the court passes 1.1 cm from the court's corner.
120
+ const conform = (polygon, others) => polygon.flatMap((a, i) => {
121
+ const b = polygon[(i + 1) % polygon.length], length = Math.hypot(b[0] - a[0], b[1] - a[1]);
122
+ const on = others.flat().map((p) => ({ p, t: ((p[0] - a[0]) * (b[0] - a[0]) + (p[1] - a[1]) * (b[1] - a[1])) / length ** 2 }))
123
+ .filter(({ p, t }) => t > 1e-6 && t < 1 - 1e-6 && Math.abs((p[0] - a[0]) * (b[1] - a[1]) - (p[1] - a[1]) * (b[0] - a[0])) / length < 0.02);
124
+ return [a, ...[...new Map(on.map((x) => [key(x.p), x])).values()].sort((x, y) => x.t - y.t).map((x) => x.p)];
125
+ });
126
+ const followed = mapped.map(follow);
127
+ const [ring, northWing, innerWings, court, notch] = followed.map((polygon) => conform(polygon, followed.filter((other) => other !== polygon)));
128
+ const edges = (polygon) => new Set(polygon.flatMap((a, i) => { const b = polygon[(i + 1) % polygon.length]; return [`${key(a)}|${key(b)}`, `${key(b)}|${key(a)}`]; }));
129
+ const has = (set, run) => set.has(`${key(run.at(0))}|${key(run.at(run.length))}`);
130
+ // Two outlines sharing a run of edges, as one: the edges neither shares, end to end.
131
+ const merge = (p, q) => {
132
+ const [ep, eq] = [edges(p), edges(q)];
133
+ const kept = [...p.map((a, i) => [a, p[(i + 1) % p.length]]).filter(([a, b]) => !eq.has(`${key(a)}|${key(b)}`)),
134
+ ...q.map((a, i) => [a, q[(i + 1) % q.length]]).filter(([a, b]) => !ep.has(`${key(a)}|${key(b)}`))];
135
+ const next = new Map(kept.map(([a, b]) => [key(a), b]));
136
+ const out = [kept[0][0]];
137
+ for (let p1 = kept[0][1]; key(p1) !== key(out[0]); p1 = next.get(key(p1)))
138
+ out.push(p1);
139
+ return orient(out);
140
+ };
141
+ // The court's walls face into it, so their outlines run the other way.
142
+ const inward = (polygon) => [...polygon].reverse();
143
+ const [lower, upper, opening] = [inward(court), inward(merge(court, innerWings)), inward(notch)];
144
+ const [lotEdges, wingEdges, lowerEdges] = [edges(lot), edges(innerWings), edges(court)];
145
+ const face = (run, lo, hi, paint) => {
146
+ const a = run.at(0), b = run.at(run.length), n = run.normal(0);
147
+ kit.quad(shell, [[a[0], lo, a[1]], [b[0], lo, b[1]], [b[0], hi, b[1]], [a[0], hi, a[1]]], [[n[0], 0, n[1]]], paint);
148
+ };
149
+ // The outer walls to the roof, the north wall, above the Lake View Building, in common
150
+ // brick, and open over the court's floor at the notch. Walls split where the ones they
151
+ // meet end, at the court's floor and the inner wings' roof, so every edge is shared.
152
+ const notchEdges = edges(notch);
153
+ planOf(lot).forEach((run) => {
154
+ const paint = run.normal(0)[1] < -0.9 ? party : core;
155
+ face(run, 0, h.court, paint);
156
+ if (!has(notchEdges, run))
157
+ face(run, h.court, h.roof, paint);
158
+ });
159
+ // The court's walls: the inner wings' to their roof, the rest to the roof, and over the
160
+ // inner wings the widened court's; the notch's but its open side.
161
+ planOf(lower).forEach((run) => {
162
+ face(run, h.court, h.inner, courtBrick);
163
+ if (!has(wingEdges, run))
164
+ face(run, h.inner, h.roof, courtBrick);
165
+ });
166
+ planOf(upper).forEach((run) => { if (!has(lowerEdges, run))
167
+ face(run, h.inner, h.roof, courtBrick); });
168
+ planOf(opening).forEach((run) => { if (!has(lotEdges, run))
169
+ face(run, h.court, h.roof, courtBrick); });
170
+ paintedSlab(kit, shell, lot, 0, false, roofing);
171
+ for (const roof of [ring, northWing])
172
+ paintedSlab(kit, shell, roof, h.roof, true, roofing);
173
+ paintedSlab(kit, shell, innerWings, h.inner, true, roofing);
174
+ for (const floor of [court, notch])
175
+ paintedSlab(kit, shell, floor, h.court, true, roofing);
176
+ // The cornice projects 90 cm along both fronts, to 2 cm over the roof, stopping 3 cm
177
+ // short of the north and alley walls. It starts 8 cm inside the lines between the fronts'
178
+ // corners, which the mapped walls stand at most 2 cm inside and 12.4 cm outside, so it
179
+ // always meets them. Its cresting: blocks 50 cm wide and 1 m apart near its edge.
180
+ const [out, lip] = [0.9, 0.02];
181
+ kit.prism(cornice, planOf(orient([at(-out, michigan - 0.03), at(-out, -out), at(adams - 0.03, -out), at(adams - 0.03, 0.08), at(0.08, 0.08), at(0.08, michigan - 0.03)])), [h.cornice, h.roof + lip]);
182
+ const [east, south] = [[-north[1], north[0]], [west[1], -west[0]]];
183
+ for (let b = 0.5; b < michigan - 0.5; b += 1)
184
+ kit.box(cornice, at(-0.7, b), east, 0.25, -0.15, 0.15, h.roof + lip, h.top);
185
+ for (let a = 0.5; a < adams - 0.5; a += 1)
186
+ kit.box(cornice, at(a, -0.7), south, 0.25, -0.15, 0.15, h.roof + lip, h.top);
187
+ const skin = (plan, keep, from, to, seed, openings, plain) => {
188
+ const span = rows.filter((row) => row.lo >= from - 1e-9 && row.hi <= to + 1e-9), heights = [span[0].lo, ...span.map((row) => row.hi)];
189
+ const paints = { wall: plain, granite, band, shadow };
190
+ chainsOf(plan).forEach((chain, index) => {
191
+ if (!chain.runs.every((run) => keep(plan[run])))
192
+ return;
193
+ const first = chain.runs[0], last = chain.runs.at(-1);
194
+ const s0 = turnsAt(plan, first)[0] > 0 ? 0.09 : 0.02, s1 = chain.length - (turnsAt(plan, last)[1] > 0 ? 0.09 : 0.02);
195
+ const [columns, cell] = openings(chain.length);
196
+ chainSkin(kit, wall, plan, chain, 0, chain.runs.length - 1, s0, s1, columns, heights, 0.02, 0.07, (bay, r) => {
197
+ const row = span[r], shows = cell((columns[bay - 1] + columns[bay]) / 2, row);
198
+ return shows === "glass" ? paneColor(row.floor, bay, seed * 8 + index) : paints[shows];
199
+ }, plain);
200
+ });
201
+ };
202
+ const near = (list, x) => Math.min(...list.map((c) => Math.abs(x - c)));
203
+ const columnsOf = (length, cuts) => [0, ...[...new Set(cuts.map((c) => Math.round(c * 1e4) / 1e4))].sort((p, q) => p - q), length];
204
+ // A street front: corner piers 6 m to the first column's middle, each with a window 1.7 m
205
+ // wide 2.6 m from its corner; `count` bays between, each column 1.6 m wide. A bay holds a
206
+ // pair of windows 1.2 m wide either side of a 38 cm mullion, the colonnade's opening, and
207
+ // an attic window 1.7 m wide; a pendant hangs over each column.
208
+ const front = (count) => (length) => {
209
+ const pitch = (length - 12) / count, lines = Array.from({ length: count + 1 }, (_, k) => 6 + k * pitch);
210
+ const centres = lines.slice(1).map((line) => line - pitch / 2), piers = [2.6, length - 2.6];
211
+ const cuts = [...lines.flatMap((c) => [-0.9, -0.8, -0.45, 0.45, 0.8, 0.9].map((d) => c + d)), ...centres.flatMap((c) => [-1.39, -0.85, -0.19, 0.19, 0.85, 1.39].map((d) => c + d)), ...piers.flatMap((c) => [c - 0.85, c + 0.85])];
212
+ return [columnsOf(length, cuts), (mid, row) => {
213
+ const inside = mid > lines[0] && mid < lines.at(-1), line = near(lines, mid), centre = near(centres, mid);
214
+ const pierWindow = near(piers, mid) < 0.85;
215
+ switch (row.kind) {
216
+ case "granite": return "granite";
217
+ case "shop": return inside && line > 0.8 ? "glass" : "granite";
218
+ case "belt":
219
+ case "band": return "band";
220
+ case "glass": return (inside && centre > 0.19 && centre < 1.39) || pierWindow ? "glass" : "wall";
221
+ case "rail": return row.pier && pierWindow ? "glass" : inside && line > 0.8 ? "band" : "wall";
222
+ case "open": return (row.pier && pierWindow) || (inside && line > 0.8) ? "glass" : "wall";
223
+ case "attic": return row.reach !== undefined && line < row.reach ? "shadow" : (inside && centre < 0.85) || pierWindow ? "glass" : "wall";
224
+ case "pendant": return line < row.reach ? "shadow" : "wall";
225
+ case "frieze": return "shadow";
226
+ default: return "wall";
227
+ }
228
+ }];
229
+ };
230
+ // The alley's and the court's windows, which the drawing does not show: 1.6 m wide, spread
231
+ // evenly about 3.2 m apart, one to each floor from the third.
232
+ const spread = (length) => {
233
+ const count = Math.max(1, Math.round(length / 3.2)), centres = Array.from({ length: count }, (_, k) => length * (k + 0.5) / count);
234
+ return [columnsOf(length, centres.flatMap((c) => [c - 0.8, c + 0.8])), (mid, row) => ((row.kind === "glass" || row.kind === "attic" || row.pier === true) && near(centres, mid) < 0.8 ? "glass" : "wall")];
235
+ };
236
+ const facing = (x, z) => (run) => run.normal(0)[0] * x + run.normal(0)[1] * z > 0.9;
237
+ // The fronts to the cornice, the alley to the roof; the north wall, shared with the Lake
238
+ // View Building, stays plain.
239
+ skin(planOf(lot), facing(1, 0), rows[0].lo, h.cornice, 0, front(11), stone);
240
+ skin(planOf(lot), facing(0, 1), rows[0].lo, h.cornice, 1, front(9), stone);
241
+ skin(planOf(lot), facing(-1, 0), rows[0].lo, h.roof, 2, spread, stone);
242
+ skin(planOf(lower), () => true, h.court, h.inner, 3, spread, courtBrick);
243
+ skin(planOf(upper), () => true, h.inner, h.roof, 4, spread, courtBrick);
244
+ skin(planOf(opening), (run) => !has(lotEdges, run), h.court, h.roof, 5, spread, courtBrick);
245
+ const model = kit.finish({ height: h.top, outlines: [shell, cornice], opacity: 0.16 });
246
+ model.building.position.set(offset[0], 0, offset[1]);
247
+ model.building.userData["geography"] = record;
248
+ model.building.userData["geographicDetail"] = { levels: h, source: "docs/peoples-gas-reference.md" };
249
+ return model;
250
+ }
@@ -0,0 +1,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,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,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
+ }