@a2f0/skyline 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/NOTICE.md +20 -0
  2. package/README.md +52 -2
  3. package/docs/package.md +222 -0
  4. package/docs/skyline-geography.md +503 -0
  5. package/lib/models/aon-center.d.ts +4 -0
  6. package/lib/models/aon-center.js +60 -0
  7. package/lib/models/aon-geographic.d.ts +9 -0
  8. package/lib/models/aon-geographic.js +35 -0
  9. package/lib/models/aon-tower.d.ts +23 -0
  10. package/lib/models/aon-tower.js +130 -0
  11. package/lib/models/blue-cross-geographic.d.ts +14 -0
  12. package/lib/models/blue-cross-geographic.js +248 -0
  13. package/lib/models/borg-warner-geographic.d.ts +20 -0
  14. package/lib/models/borg-warner-geographic.js +143 -0
  15. package/lib/models/buckingham-geographic.d.ts +10 -0
  16. package/lib/models/buckingham-geographic.js +121 -0
  17. package/lib/models/building-kit.d.ts +84 -0
  18. package/lib/models/building-kit.js +358 -0
  19. package/lib/models/celebrations.d.ts +39 -0
  20. package/lib/models/celebrations.js +44 -0
  21. package/lib/models/chicago-athletic-association-geographic.d.ts +32 -0
  22. package/lib/models/chicago-athletic-association-geographic.js +173 -0
  23. package/lib/models/crain-communications.d.ts +5 -0
  24. package/lib/models/crain-communications.js +75 -0
  25. package/lib/models/crain-geographic.d.ts +10 -0
  26. package/lib/models/crain-geographic.js +69 -0
  27. package/lib/models/crain-tower.d.ts +32 -0
  28. package/lib/models/crain-tower.js +378 -0
  29. package/lib/models/facade-grid.d.ts +18 -0
  30. package/lib/models/facade-grid.js +176 -0
  31. package/lib/models/gage-geographic.d.ts +19 -0
  32. package/lib/models/gage-geographic.js +154 -0
  33. package/lib/models/heritage-at-millennium-park.d.ts +3 -0
  34. package/lib/models/heritage-at-millennium-park.js +284 -0
  35. package/lib/models/heritage-geographic.d.ts +11 -0
  36. package/lib/models/heritage-geographic.js +208 -0
  37. package/lib/models/hyatt-west-tower-geographic.d.ts +26 -0
  38. package/lib/models/hyatt-west-tower-geographic.js +86 -0
  39. package/lib/models/keith-ascher-geographic.d.ts +29 -0
  40. package/lib/models/keith-ascher-geographic.js +110 -0
  41. package/lib/models/kemper-geographic.d.ts +8 -0
  42. package/lib/models/kemper-geographic.js +153 -0
  43. package/lib/models/kemper.d.ts +2 -0
  44. package/lib/models/kemper.js +78 -0
  45. package/lib/models/lake-view-geographic.d.ts +15 -0
  46. package/lib/models/lake-view-geographic.js +134 -0
  47. package/lib/models/maclean-center-geographic.d.ts +20 -0
  48. package/lib/models/maclean-center-geographic.js +162 -0
  49. package/lib/models/michigan-boulevard-geographic.d.ts +12 -0
  50. package/lib/models/michigan-boulevard-geographic.js +157 -0
  51. package/lib/models/michigan-plaza-south-geographic.d.ts +7 -0
  52. package/lib/models/michigan-plaza-south-geographic.js +84 -0
  53. package/lib/models/michigan-plaza-south.d.ts +2 -0
  54. package/lib/models/michigan-plaza-south.js +68 -0
  55. package/lib/models/millennium-park-plaza-geographic.d.ts +9 -0
  56. package/lib/models/millennium-park-plaza-geographic.js +103 -0
  57. package/lib/models/monroe-geographic.d.ts +19 -0
  58. package/lib/models/monroe-geographic.js +177 -0
  59. package/lib/models/north-michigan-180-geographic.d.ts +14 -0
  60. package/lib/models/north-michigan-180-geographic.js +117 -0
  61. package/lib/models/north-wabash-geographic.d.ts +14 -0
  62. package/lib/models/north-wabash-geographic.js +122 -0
  63. package/lib/models/on-the-park-geographic.d.ts +11 -0
  64. package/lib/models/on-the-park-geographic.js +245 -0
  65. package/lib/models/one-prudential-geographic.d.ts +12 -0
  66. package/lib/models/one-prudential-geographic.js +55 -0
  67. package/lib/models/one-prudential-plaza.d.ts +5 -0
  68. package/lib/models/one-prudential-plaza.js +72 -0
  69. package/lib/models/one-prudential-tower.d.ts +45 -0
  70. package/lib/models/one-prudential-tower.js +246 -0
  71. package/lib/models/peoples-gas-geographic.d.ts +23 -0
  72. package/lib/models/peoples-gas-geographic.js +250 -0
  73. package/lib/models/railway-exchange-geographic.d.ts +18 -0
  74. package/lib/models/railway-exchange-geographic.js +176 -0
  75. package/lib/models/river-plaza-geographic.d.ts +16 -0
  76. package/lib/models/river-plaza-geographic.js +104 -0
  77. package/lib/models/sheraton-grand-geographic.d.ts +24 -0
  78. package/lib/models/sheraton-grand-geographic.js +112 -0
  79. package/lib/models/six-north-michigan-geographic.d.ts +16 -0
  80. package/lib/models/six-north-michigan-geographic.js +180 -0
  81. package/lib/models/skyline-geography-data.d.ts +44 -0
  82. package/lib/models/skyline-geography-data.js +607 -0
  83. package/lib/models/skyline-geography.d.ts +23 -0
  84. package/lib/models/skyline-geography.js +322 -0
  85. package/lib/models/swissotel-geographic.d.ts +24 -0
  86. package/lib/models/swissotel-geographic.js +79 -0
  87. package/lib/models/three-illinois-center-geographic.d.ts +26 -0
  88. package/lib/models/three-illinois-center-geographic.js +86 -0
  89. package/lib/models/trump-geographic.d.ts +14 -0
  90. package/lib/models/trump-geographic.js +51 -0
  91. package/lib/models/trump-international-tower.d.ts +4 -0
  92. package/lib/models/trump-international-tower.js +84 -0
  93. package/lib/models/trump-tower.d.ts +43 -0
  94. package/lib/models/trump-tower.js +229 -0
  95. package/lib/models/two-illinois-center-geographic.d.ts +26 -0
  96. package/lib/models/two-illinois-center-geographic.js +85 -0
  97. package/lib/models/two-prudential-geographic.d.ts +25 -0
  98. package/lib/models/two-prudential-geographic.js +47 -0
  99. package/lib/models/two-prudential-plaza.d.ts +8 -0
  100. package/lib/models/two-prudential-plaza.js +55 -0
  101. package/lib/models/two-prudential-tower.d.ts +53 -0
  102. package/lib/models/two-prudential-tower.js +441 -0
  103. package/lib/models/university-club-geographic.d.ts +24 -0
  104. package/lib/models/university-club-geographic.js +217 -0
  105. package/lib/models/willoughby-tower-geographic.d.ts +17 -0
  106. package/lib/models/willoughby-tower-geographic.js +195 -0
  107. package/lib/models/window-illumination.d.ts +14 -0
  108. package/lib/models/window-illumination.js +64 -0
  109. package/lib/package-assets.d.ts +5 -0
  110. package/lib/package-assets.js +9 -0
  111. package/lib/skyline-package.d.ts +23 -0
  112. package/lib/skyline-package.js +25 -0
  113. package/lib/skyline-scene.d.ts +25 -0
  114. package/lib/skyline-scene.js +86 -0
  115. package/lib/study-types.d.ts +49 -0
  116. package/lib/study-types.js +1 -0
  117. package/lib/vendor/three-r186.d.ts +1 -0
  118. package/lib/vendor/three-r186.js +1 -0
  119. package/package.json +64 -4
  120. package/site/building-detail.html +54 -0
  121. package/site/building-detail.js +84 -0
  122. package/site/building-study.html +58 -0
  123. package/site/building-study.js +3 -0
  124. package/site/index.html +407 -0
  125. package/site/models/aon-center.js +60 -0
  126. package/site/models/aon-geographic.js +35 -0
  127. package/site/models/aon-tower.js +130 -0
  128. package/site/models/blue-cross-geographic.js +248 -0
  129. package/site/models/borg-warner-geographic.js +143 -0
  130. package/site/models/buckingham-geographic.js +121 -0
  131. package/site/models/building-kit.js +358 -0
  132. package/site/models/celebrations.js +44 -0
  133. package/site/models/chicago-athletic-association-geographic.js +173 -0
  134. package/site/models/crain-communications.js +75 -0
  135. package/site/models/crain-geographic.js +69 -0
  136. package/site/models/crain-reference.svg +10 -0
  137. package/site/models/crain-tower.js +378 -0
  138. package/site/models/facade-grid.js +176 -0
  139. package/site/models/gage-geographic.js +154 -0
  140. package/site/models/heritage-at-millennium-park.js +284 -0
  141. package/site/models/heritage-geographic.js +208 -0
  142. package/site/models/hyatt-west-tower-geographic.js +86 -0
  143. package/site/models/keith-ascher-geographic.js +110 -0
  144. package/site/models/kemper-geographic.js +153 -0
  145. package/site/models/kemper.js +78 -0
  146. package/site/models/lake-view-geographic.js +134 -0
  147. package/site/models/maclean-center-geographic.js +162 -0
  148. package/site/models/michigan-boulevard-geographic.js +157 -0
  149. package/site/models/michigan-plaza-south-geographic.js +84 -0
  150. package/site/models/michigan-plaza-south.js +68 -0
  151. package/site/models/millennium-park-plaza-geographic.js +103 -0
  152. package/site/models/monroe-geographic.js +177 -0
  153. package/site/models/north-michigan-180-geographic.js +117 -0
  154. package/site/models/north-wabash-geographic.js +122 -0
  155. package/site/models/on-the-park-geographic.js +245 -0
  156. package/site/models/one-prudential-geographic.js +55 -0
  157. package/site/models/one-prudential-plaza.js +72 -0
  158. package/site/models/one-prudential-tower.js +246 -0
  159. package/site/models/peoples-gas-geographic.js +250 -0
  160. package/site/models/railway-exchange-geographic.js +176 -0
  161. package/site/models/river-plaza-geographic.js +104 -0
  162. package/site/models/sheraton-grand-geographic.js +112 -0
  163. package/site/models/six-north-michigan-geographic.js +180 -0
  164. package/site/models/skyline-geography-data.js +607 -0
  165. package/site/models/skyline-geography.js +322 -0
  166. package/site/models/skyline-panorama.svg +3491 -0
  167. package/site/models/skyline-reference.svg +3090 -0
  168. package/site/models/swissotel-geographic.js +79 -0
  169. package/site/models/three-illinois-center-geographic.js +86 -0
  170. package/site/models/trump-geographic.js +51 -0
  171. package/site/models/trump-international-tower.js +84 -0
  172. package/site/models/trump-tower.js +229 -0
  173. package/site/models/two-illinois-center-geographic.js +85 -0
  174. package/site/models/two-prudential-geographic.js +47 -0
  175. package/site/models/two-prudential-plaza.js +55 -0
  176. package/site/models/two-prudential-tower.js +441 -0
  177. package/site/models/university-club-geographic.js +217 -0
  178. package/site/models/willoughby-tower-geographic.js +195 -0
  179. package/site/models/window-illumination.js +64 -0
  180. package/site/skyline-3d.html +92 -0
  181. package/site/skyline-3d.js +333 -0
  182. package/site/skyline-animated.svg +18795 -0
  183. package/site/skyline-comparison.js +97 -0
  184. package/site/skyline-original-fit.svg +18312 -0
  185. package/site/skyline-scene.js +86 -0
  186. package/site/skyline-study.html +83 -0
  187. package/site/skyline-study.js +109 -0
  188. package/site/skyline-webgl.html +667 -0
  189. package/site/stars.svg +176 -0
  190. package/site/study-loader.js +20 -0
  191. package/site/study-types.js +1 -0
  192. package/site/study-viewer.js +605 -0
  193. package/site/study.css +204 -0
  194. package/site/vendor/THREE-LICENSE.txt +21 -0
  195. package/site/vendor/three-r186.js +4228 -0
@@ -0,0 +1,208 @@
1
+ import { createBuilder, inside, line, polygonOf, rectangle } from "./building-kit.js";
2
+ // Meters, with east/+x and south/+z. Design elevations from PD 787, sheets
3
+ // 63741/63743; occupied/tip heights from CTBUH. See the reference audit for
4
+ // the distinction between the 2001 design drawings and the completed building.
5
+ export const heritageGeographicLevels = Object.freeze({
6
+ podium: (107 + 8 / 12) * 0.3048,
7
+ lowerRoof: (293 + 8 / 12) * 0.3048,
8
+ occupied: 176.8,
9
+ mainRoof: (594 + 6 / 12) * 0.3048,
10
+ tip: 192.4,
11
+ });
12
+ export function heritageFloorHeight(floor) {
13
+ const h = heritageGeographicLevels;
14
+ return floor <= 28
15
+ ? h.podium + (floor - 9) * (h.lowerRoof - h.podium) / 19
16
+ : h.lowerRoof + (floor - 28) * (h.occupied - h.lowerRoof) / 29;
17
+ }
18
+ // A separate geographic factory leaves the original illustration fit intact.
19
+ // Reuses its construction vocabulary: inset panes, raised mullions, six-floor
20
+ // bands, projecting crown fins, and a louvered rooftop mechanical screen.
21
+ export function createHeritageGeographicBuilding(record, projectPlan, offset) {
22
+ const h = heritageGeographicLevels;
23
+ const kit = createBuilder(record.name, record.id);
24
+ // Match the original artwork's neutral palette, using the same toon-lighting
25
+ // compensation as heritage-at-millennium-park.js for glass, trim and precast.
26
+ const shell = kit.batch("Heritage · tower and lower wing", kit.material(0x363636));
27
+ const base = kit.batch("Heritage · mapped ground footprint", kit.material(0x787878));
28
+ const stone = kit.batch("Heritage · limestone frame", kit.material(0x8c8c8c));
29
+ const trim = kit.batch("Heritage · bronze mullions and slab edges", kit.material(0x141414));
30
+ const roof = kit.batch("Heritage · roof terraces", kit.material(0x363636));
31
+ const mechanical = kit.batch("Heritage · mechanical penthouses", kit.material(0xa4a4a4));
32
+ const glass = [0x444444, 0x474747, 0x4a4a4a, 0x3a3a3a, 0x7a7a7a].map((color, i) => kit.batch(`Heritage · glazing ${i + 1}`, kit.material(color)));
33
+ const masonry = [0x8c8c8c, 0x787878, 0x919191, 0x808080].map((color, i) => kit.batch(`Heritage · Wabash facade ${i + 1}`, kit.material(color)));
34
+ const pane = (floor, bay, side = 0) => {
35
+ const hash = (floor * 131 + bay * 37 + side * 59 + 11) % 97;
36
+ return glass[hash < 5 ? 4 : hash < 8 ? 3 : hash % 3];
37
+ };
38
+ const ground = projectPlan(record.footprint.coordinates);
39
+ const tower = projectPlan(record.parts.find((p) => p.way === 686199648).coordinates);
40
+ const wing = projectPlan(record.parts.find((p) => p.way === 686199649).coordinates);
41
+ // The lower east wall is concave in the floor plans, unlike the straight
42
+ // chord in OSM. Subdivide that chord inward, retaining its mapped endpoints.
43
+ // The 2.1 m inset is a drawing-derived estimate, not a survey measurement.
44
+ const eastWing = wing.findIndex((run) => run.length > 40 && run.normal(0)[0] > 0.9);
45
+ if (eastWing >= 0) {
46
+ const run = wing[eastWing], steps = 16;
47
+ const points = Array.from({ length: steps + 1 }, (_, i) => run.at(run.length * i / steps, -2.1 * Math.sin(Math.PI * i / steps)));
48
+ wing.splice(eastWing, 1, ...points.slice(1).map((p, i) => line(points[i], p)));
49
+ }
50
+ const towerPolygon = polygonOf(tower), wingPolygon = polygonOf(wing);
51
+ const crownBase = heritageFloorHeight(53);
52
+ // A full podium supports both wings. No guessed podium ratio is used.
53
+ kit.prism(base, ground, [0, h.podium]);
54
+ kit.prism(shell, tower, [h.podium, crownBase]);
55
+ kit.prism(shell, wing, [h.podium, h.lowerRoof - 0.65]);
56
+ kit.prism(roof, wing, [h.lowerRoof - 0.65, h.lowerRoof]);
57
+ const center = towerPolygon.reduce((sum, p) => [sum[0] + p[0] / towerPolygon.length, sum[1] + p[1] / towerPolygon.length], [0, 0]);
58
+ const inset = towerPolygon.map((p) => {
59
+ const length = Math.hypot(p[0] - center[0], p[1] - center[1]);
60
+ return p.map((n, i) => n + (center[i] - n) * 1.1 / length);
61
+ });
62
+ const crown = inset.map((p, i) => line(p, inset[(i + 1) % inset.length]));
63
+ kit.prism(shell, crown, [crownBase, h.mainRoof - 0.75]);
64
+ kit.prism(stone, tower, [h.mainRoof - 0.75, h.mainRoof]);
65
+ // Every pane and piece of trim is a closed shallow solid. Offsets separate
66
+ // visible surfaces from their backing wall, avoiding coincident faces.
67
+ const strip = (batch, run, from, to, y0, y1, back, front) => {
68
+ // Stop solid trim short of polygon corners. Adjacent faces can turn
69
+ // inward; their projecting returns must not overlap at the same elevation.
70
+ const clearance = Math.max(Math.abs(back), Math.abs(front)) + 0.025;
71
+ from = Math.max(from, clearance);
72
+ to = Math.min(to, run.length - clearance);
73
+ if (to - from < 0.03 || y1 <= y0)
74
+ return;
75
+ kit.box(batch, run.at((from + to) / 2), run.normal(0), (to - from) / 2, back, front, y0, y1);
76
+ };
77
+ const archedPane = (batch, run, s, halfWidth, bottom, top) => {
78
+ const spring = top - 0.6;
79
+ const profile = [[-halfWidth, bottom], [halfWidth, bottom], ...Array.from({ length: 13 }, (_, i) => {
80
+ const angle = Math.PI * i / 12;
81
+ return [Math.cos(angle) * halfWidth, spring + Math.sin(angle) * 0.6];
82
+ })];
83
+ const vertex = ([side, y], depth) => { const [x, z] = run.at(s + side, depth); return [x, y, z]; };
84
+ const n = run.normal(0), normal = [n[0], 0, n[1]];
85
+ for (const [depth, sign] of [[0.10, -1], [0.14, 1]]) {
86
+ const faceNormal = normal.map((v) => v * sign);
87
+ for (let i = 1; i < profile.length - 1; i += 1)
88
+ kit.triangle(batch, [profile[0], profile[i], profile[i + 1]].map((p) => vertex(p, depth)), [faceNormal, faceNormal, faceNormal]);
89
+ }
90
+ profile.forEach((a, i) => {
91
+ const b = profile[(i + 1) % profile.length], dx = b[0] - a[0], dy = b[1] - a[1], length = Math.hypot(dx, dy);
92
+ kit.quad(batch, [vertex(a, 0.10), vertex(b, 0.10), vertex(b, 0.14), vertex(a, 0.14)], [[n[1] * dy / length, -dx / length, -n[0] * dy / length]]);
93
+ });
94
+ };
95
+ const exposed = (run, y, other, top) => y >= top || !inside(other, run.at(run.length / 2, 0.12));
96
+ function facade(runs, firstFloor, lastFloor, ceiling, other = [], otherTop = 0) {
97
+ runs.forEach((run, side) => {
98
+ // Small polygon facets on the curved east wing each make one bay;
99
+ // other faces use approximately 3 m apartment/window modules.
100
+ const bays = Math.max(1, Math.round(run.length / 3.15));
101
+ const width = run.length / bays;
102
+ for (let floor = firstFloor; floor <= lastFloor; floor += 1) {
103
+ const bottom = heritageFloorHeight(floor);
104
+ const top = Math.min(ceiling, floor === 57 ? h.mainRoof - 0.8 : heritageFloorHeight(floor + 1));
105
+ if (!exposed(run, bottom, other, otherTop))
106
+ continue;
107
+ for (let bay = 0; bay < bays; bay += 1) {
108
+ const from = bay * width + 0.20, to = (bay + 1) * width - 0.20;
109
+ strip(pane(floor, bay, side), run, from, to, bottom + 0.28, top - 0.24, 0.015, 0.055);
110
+ strip(trim, run, (from + to) / 2 - 0.035, (from + to) / 2 + 0.035, bottom + 0.28, top - 0.24, 0.06, 0.10);
111
+ }
112
+ const major = (floor - 9) % 6 === 0;
113
+ strip(major ? stone : trim, run, 0.015, run.length - 0.015, bottom + 0.03, bottom + (major ? 0.42 : 0.19), 0.07, major ? 0.28 : 0.13);
114
+ }
115
+ // Split uprights at the adjacent wing's roof, so hidden partitions do
116
+ // not create lines across its terrace or show through its exterior.
117
+ const bottom = exposed(run, heritageFloorHeight(firstFloor), other, otherTop) ? heritageFloorHeight(firstFloor) : otherTop;
118
+ for (let i = 0; i <= bays; i += 1) {
119
+ const s = Math.max(0.18, Math.min(run.length - 0.18, i * width));
120
+ strip(stone, run, s - 0.10, s + 0.10, bottom + 0.02, ceiling - 0.02, 0.14, 0.34);
121
+ }
122
+ });
123
+ }
124
+ facade(tower, 9, 52, crownBase, wingPolygon, h.lowerRoof);
125
+ facade(wing, 9, 27, h.lowerRoof - 0.65, towerPolygon, h.mainRoof);
126
+ facade(crown, 53, 57, h.mainRoof - 0.75);
127
+ // Tall pale fins and open reveals around the inset upper-floor glazing.
128
+ tower.forEach((run) => {
129
+ const bays = Math.max(1, Math.round(run.length / 3.15));
130
+ for (let i = 0; i < bays; i += 1) {
131
+ const s = (i + 0.5) * run.length / bays;
132
+ strip(stone, run, s - 0.28, s + 0.28, crownBase - 0.01, h.mainRoof - 0.76, -1.25, 0.40);
133
+ }
134
+ });
135
+ // The lower wing repeats the taller crown piers over its top three floors.
136
+ wing.forEach((run) => {
137
+ if (!exposed(run, h.lowerRoof - 1, towerPolygon, h.mainRoof))
138
+ return;
139
+ strip(stone, run, 0.02, run.length - 0.02, h.lowerRoof - 0.60, h.lowerRoof - 0.03, 0.02, 0.6);
140
+ const count = Math.max(1, Math.round(run.length / 3.15));
141
+ for (let i = 0; i < count; i += 1) {
142
+ const s = (i + 0.5) * run.length / count;
143
+ strip(stone, run, s - 0.24, s + 0.24, heritageFloorHeight(25), h.lowerRoof - 0.66, 0.35, 0.59);
144
+ }
145
+ });
146
+ // Mechanical volumes and a north/south screen set back from the outer rim.
147
+ // Their plan dimensions are estimated from the elevations and contractor photo.
148
+ kit.prism(mechanical, rectangle(-63, -51, 59, 86), [h.mainRoof, 189.8]);
149
+ const screen = line([-48.5, 91], [-48.5, 56]);
150
+ for (let s = 0.25; s < screen.length; s += 1.35)
151
+ strip(stone, screen, s, s + 0.16, h.mainRoof + 0.04, h.tip, -0.18, 0.18);
152
+ for (let y = h.mainRoof + 0.25; y < h.tip - 0.2; y += 0.55)
153
+ strip(mechanical, screen, 0.15, screen.length - 0.15, y, y + 0.28, -0.14, 0.14);
154
+ // Balcony stacks occupy the stepped north and south returns visible in the
155
+ // upper-floor plan. Slabs and rails are inset along each selected return.
156
+ for (const run of tower.filter((r) => r.length > 4.5 && r.length < 7.5 && Math.abs(r.normal(0)[1]) > 0.9)) {
157
+ for (let floor = 10; floor <= 52; floor += 1) {
158
+ const y = heritageFloorHeight(floor);
159
+ if (!exposed(run, y, wingPolygon, h.lowerRoof))
160
+ continue;
161
+ strip(stone, run, 0.32, run.length - 0.32, y, y + 0.17, -0.1, 1.05);
162
+ strip(trim, run, 0.32, run.length - 0.32, y + 1.02, y + 1.10, 0.95, 1.03);
163
+ for (let s = 0.4; s < run.length - 0.3; s += 0.55)
164
+ strip(trim, run, s, s + 0.045, y + 0.17, y + 1.02, 0.96, 1.01);
165
+ }
166
+ }
167
+ // Podium: retail at grade, parking grilles, taller upper openings, and the
168
+ // four preserved Wabash facades. Historic bay proportions are approximated
169
+ // from sheet 63746, not copied from the modern tower's glass grid.
170
+ ground.forEach((run) => {
171
+ if (run.length < 2)
172
+ return;
173
+ const west = run.normal(0)[0] < -0.9;
174
+ const bays = Math.max(1, Math.round(run.length / (west ? 3.4 : 4.0)));
175
+ const width = run.length / bays;
176
+ for (let bay = 0; bay < bays; bay += 1) {
177
+ const s = (bay + 0.5) * width, historic = west && bay >= 4 && bay < bays - 4;
178
+ const block = Math.min(3, Math.floor((bay - 4) / Math.max(1, (bays - 8) / 4)));
179
+ if (historic)
180
+ strip(masonry[block], run, bay * width + 0.04, (bay + 1) * width - 0.04, 0.3, 24.2 + (block % 2) * 0.5, 0.02, 0.09);
181
+ for (let floor = 0; floor < 6; floor += 1) {
182
+ const y = floor === 0 ? 0.65 : 5.3 + (floor - 1) * 3.7;
183
+ const top = y + (floor === 0 ? 3.95 : 2.75);
184
+ if (historic && block !== 1 && floor > 0)
185
+ archedPane(pane(floor, bay), run, s, width * 0.31, y, top);
186
+ else
187
+ strip(pane(floor, bay), run, s - width * 0.31, s + width * 0.31, y, top, 0.10, 0.14);
188
+ if (historic) {
189
+ strip(stone, run, s - width * 0.34, s + width * 0.34, top + 0.03, top + 0.19, 0.15, 0.26);
190
+ strip(stone, run, s - width * 0.34, s + width * 0.34, y - 0.15, y - 0.02, 0.15, 0.25);
191
+ }
192
+ else if (floor > 0) {
193
+ for (let y2 = y + 0.3; y2 < top; y2 += 0.38)
194
+ strip(trim, run, s - width * 0.31, s + width * 0.31, y2, y2 + 0.09, 0.15, 0.20);
195
+ }
196
+ }
197
+ strip(glass[0], run, s - width * 0.34, s + width * 0.34, 26.1, h.podium - 0.8, 0.025, 0.055);
198
+ strip(stone, run, s - 0.12, s + 0.12, 25.8, h.podium - 0.7, 0.06, 0.26);
199
+ }
200
+ strip(stone, run, 0.03, run.length - 0.03, 24.9, 25.55, 0.27, 0.44);
201
+ strip(stone, run, 0.03, run.length - 0.03, h.podium - 0.55, h.podium - 0.03, 0.06, 0.33);
202
+ });
203
+ const model = kit.finish({ height: h.tip, outlines: [base, shell, mechanical], opacity: 0.16 });
204
+ model.building.position.set(offset[0], 0, offset[1]);
205
+ model.building.userData["geography"] = record;
206
+ model.building.userData["geographicDetail"] = { levels: h, highestOccupiedFloor: 57, source: "docs/heritage-geographic-reference.md" };
207
+ return model;
208
+ }
@@ -0,0 +1,86 @@
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 Hyatt Regency Chicago's West Tower, 151 East Wacker Drive (A. Epstein and Sons, 1974):
5
+ // the geographic layout's model, on its mapped part 235920252. The original layout has no
6
+ // model of it. From the photograph's camera it stands between Aon Center and the Blue Cross
7
+ // and Blue Shield Tower, where the drawing's Blue Cross group paints a dark brown sliver.
8
+ //
9
+ // A brick slab of 33 storeys: each face carries narrow window slots, continuous from the
10
+ // lobby to the top two floors, on a 3.5 m module between solid corners, and a plain band
11
+ // over them to a flat roof at the published 111.3 m. See docs/hyatt-west-tower-reference.md.
12
+ // Units are meters; +x is east, +z is south.
13
+ export const hyattWestLevels = Object.freeze({
14
+ floors: 33,
15
+ roof: 111.3, // the published height, the Skyscraper Center's 365 ft
16
+ band: 31, // the last floor with windows; the two above are the plain band
17
+ module: 3.5, // the slots' spacing, measured on the photograph and the drawing
18
+ slot: 1, // each slot's width
19
+ corner: 2, // the solid brick at least left at each corner
20
+ });
21
+ const h = hyattWestLevels;
22
+ const pitch = h.roof / h.floors;
23
+ // The skins' colours: the window tones, and the brick and the slots' spandrels, which no window
24
+ // shares.
25
+ export const hyattWestPalette = Object.freeze({ glass: [0x202020, 0x242424, 0x282828, 0x1d1d1d], lit: 0x6d6d6d, dim: 0x4a4a4a, brick: 0x3b3b3b, spandrel: 0x2c2c2c });
26
+ const palette = hyattWestPalette;
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), brick = color(palette.brick), spandrel = color(palette.spandrel);
30
+ const core = color(0x282828), roofing = color(0x333333);
31
+ function paneColor(row, bay, wall) {
32
+ const hash = (Math.imul(row + 43, 0x9e3779b1) ^ Math.imul(bay + 19, 0x85ebca77) ^ Math.imul(wall + 7, 0xc2b2ae3d)) >>> 0;
33
+ const value = hash % 101;
34
+ if (value < 10)
35
+ return litGlass;
36
+ if (value < 16)
37
+ return dimGlass;
38
+ return glassTones[value % glassTones.length];
39
+ }
40
+ export const hyattWestRows = [{ lo: 0.25, hi: pitch - 0.3, kind: "glass", floor: 1 }];
41
+ for (let n = 2; n <= h.band; n += 1) {
42
+ const floor = (n - 1) * pitch;
43
+ hyattWestRows.push({ lo: hyattWestRows.at(-1).hi, hi: floor + 0.8, kind: "spandrel", floor: n });
44
+ hyattWestRows.push({ lo: floor + 0.8, hi: floor + 2.9, kind: "glass", floor: n });
45
+ }
46
+ hyattWestRows.push({ lo: hyattWestRows.at(-1).hi, hi: h.roof, kind: "band", floor: h.band + 1 });
47
+ const rows = hyattWestRows;
48
+ export function createHyattWestTowerGeographicBuilding(record, projectPlan, offset) {
49
+ const kit = createBuilder(record.name, record.id);
50
+ const shell = kit.batch("Hyatt Regency West Tower · shell", kit.material(0xffffff, { vertexColors: true }));
51
+ const wall = kit.batch("Hyatt Regency West Tower · brick and windows", kit.material(0xffffff, { vertexColors: true }));
52
+ const lot = orient(polygonOf(projectPlan(record.parts[0].coordinates)));
53
+ planOf(lot).forEach((run) => {
54
+ const a = run.at(0), b = run.at(run.length), n = run.normal(0);
55
+ kit.quad(shell, [[a[0], 0, a[1]], [b[0], 0, b[1]], [b[0], h.roof, b[1]], [a[0], h.roof, a[1]]], [[n[0], 0, n[1]]], core);
56
+ });
57
+ paintedSlab(kit, shell, lot, 0, false, roofing);
58
+ paintedSlab(kit, shell, lot, h.roof, true, roofing);
59
+ // Every face's skin, one box to a chain, held 2 cm clear of its ends or 9 cm where the
60
+ // outline turns in: slots on the module, set out from the chain's middle, as many as leave
61
+ // the corners solid; the lobby's glass runs the chain's length.
62
+ const plan = planOf(lot), heights = [rows[0].lo, ...rows.map((row) => row.hi)];
63
+ chainsOf(plan).forEach((chain, index) => {
64
+ const first = chain.runs[0], last = chain.runs.at(-1);
65
+ const s0 = turnsAt(plan, first)[0] > 0 ? 0.09 : 0.02, s1 = chain.length - (turnsAt(plan, last)[1] > 0 ? 0.09 : 0.02);
66
+ const count = Math.max(0, Math.floor((chain.length - 2 * h.corner - h.slot) / h.module) + 1);
67
+ const centres = Array.from({ length: count }, (_, k) => chain.length / 2 + (k - (count - 1) / 2) * h.module);
68
+ const cuts = centres.flatMap((c) => [c - h.slot / 2, c + h.slot / 2]).filter((s) => s > s0 + 0.01 && s < s1 - 0.01);
69
+ const columns = [s0, ...cuts, s1];
70
+ chainSkin(kit, wall, plan, chain, 0, chain.runs.length - 1, s0, s1, columns, heights, 0.02, 0.07, (bay, r) => {
71
+ const row = rows[r], middle = (columns[bay - 1] + columns[bay]) / 2;
72
+ if (row.kind === "band")
73
+ return brick;
74
+ if (row.floor === 1)
75
+ return paneColor(row.floor, bay, index);
76
+ if (!centres.some((c) => Math.abs(middle - c) < h.slot / 2))
77
+ return brick;
78
+ return row.kind === "glass" ? paneColor(row.floor, bay, index) : spandrel;
79
+ }, brick);
80
+ });
81
+ const model = kit.finish({ height: h.roof, outlines: [shell], opacity: 0.16 });
82
+ model.building.position.set(offset[0], 0, offset[1]);
83
+ model.building.userData["geography"] = record;
84
+ model.building.userData["geographicDetail"] = { levels: h, source: "docs/hyatt-west-tower-reference.md" };
85
+ return model;
86
+ }
@@ -0,0 +1,110 @@
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
+ export const keithSpec = { name: "Edson Keith Building", top: 30.78, drawnTop: 35.69, drawnWindow: [30.15, 33.17], bays: [[1.67, 5.36], [7.24, 12.09], [13.44, 17.34]], seed: 11 };
5
+ export const ascherSpec = { name: "Theodore Ascher Building", top: 30.78, drawnTop: 35.48, drawnWindow: [29.97, 32.97], bays: [[1.85, 5.7], [7.9, 11.75]], seed: 17 };
6
+ // Each building's levels: the ground floor's shopfronts under a sign band, and six floors
7
+ // from the second up to the seventh, whose drawn window, scaled, sets their pitch; its sill
8
+ // stands 80 cm over its floor.
9
+ export function gageGroupLevels(spec) {
10
+ const drawn = (height) => height * spec.top / spec.drawnTop;
11
+ const [sill, head] = spec.drawnWindow.map(drawn);
12
+ const second = 5.5, seventh = sill - 0.8;
13
+ return Object.freeze({ shopfront: 4.3, second, pitch: (seventh - second) / 5, sill: 0.8, window: head - sill, top: spec.top });
14
+ }
15
+ // The skins' colours: the window tones, and the brick and the band, which no window shares.
16
+ export const gageGroupPalette = Object.freeze({ glass: [0x262626, 0x2a2a2a, 0x2e2e2e, 0x323232], lit: 0x6e6e6e, dim: 0x3a3a3a, brick: 0x464646, band: 0x525252 });
17
+ const palette = gageGroupPalette;
18
+ const color = (hex) => new THREE.Color(hex);
19
+ const glassTones = palette.glass.map(color);
20
+ const litGlass = color(palette.lit), dimGlass = color(palette.dim);
21
+ const brick = color(palette.brick), band = color(palette.band);
22
+ const core = color(0x262626), roofing = color(0x3a3a3a);
23
+ function paneColor(row, bay, wall) {
24
+ const hash = (Math.imul(row + 29, 0x9e3779b1) ^ Math.imul(bay + 71, 0x85ebca77) ^ Math.imul(wall + 59, 0xc2b2ae3d)) >>> 0;
25
+ const value = hash % 101;
26
+ if (value < 4)
27
+ return litGlass;
28
+ if (value < 8)
29
+ return dimGlass;
30
+ return glassTones[value % glassTones.length];
31
+ }
32
+ function createGageGroupBuilding(spec, record, projectPlan, offset) {
33
+ const h = gageGroupLevels(spec);
34
+ const floor = (n) => (n < 2 ? 0 : h.second + (n - 2) * h.pitch);
35
+ const rows = [
36
+ { lo: 0.25, hi: 0.9, kind: "wall", floor: 1 },
37
+ { lo: 0.9, hi: h.shopfront, kind: "glass", floor: 1 },
38
+ { lo: h.shopfront, hi: h.second, kind: "band", floor: 1 },
39
+ ];
40
+ for (let n = 2; n <= 7; n += 1) {
41
+ rows.push({ lo: rows.at(-1).hi, hi: floor(n) + h.sill, kind: "wall", floor: n });
42
+ rows.push({ lo: floor(n) + h.sill, hi: floor(n) + h.sill + h.window, kind: "glass", floor: n });
43
+ }
44
+ rows.push({ lo: rows.at(-1).hi, hi: h.top, kind: "wall", floor: 8 });
45
+ const kit = createBuilder(record.name, record.id);
46
+ const shell = kit.batch(`${spec.name} · shell`, kit.material(0xffffff, { vertexColors: true }));
47
+ const wall = kit.batch(`${spec.name} · brick and windows`, kit.material(0xffffff, { vertexColors: true }));
48
+ const lot = orient(polygonOf(projectPlan(record.footprint.coordinates)));
49
+ // The lot's south-east corner, where its Michigan front starts north.
50
+ const corner = lot.reduce((best, p) => (p[0] + p[1] > best[0] + best[1] ? p : best));
51
+ const northEast = lot.reduce((best, p) => (p[0] - p[1] > best[0] - best[1] ? p : best));
52
+ const north = [northEast[0] - corner[0], northEast[1] - corner[1]];
53
+ planOf(lot).forEach((run) => {
54
+ const a = run.at(0), b = run.at(run.length), n = run.normal(0);
55
+ kit.quad(shell, [[a[0], 0, a[1]], [b[0], 0, b[1]], [b[0], h.top, b[1]], [a[0], h.top, a[1]]], [[n[0], 0, n[1]]], core);
56
+ });
57
+ paintedSlab(kit, shell, lot, 0, false, roofing);
58
+ paintedSlab(kit, shell, lot, h.top, true, roofing);
59
+ // A wall's skin, one box to a chain the `keep` test accepts, held 2 cm clear of its ends
60
+ // or 9 cm where the outline turns in. `openings` lays a wall's columns out across it, from
61
+ // its start, and says from a cell's middle and floor whether it holds a window. Only the
62
+ // front carries the sign band.
63
+ const skin = (seed, keep, openings) => {
64
+ const plan = planOf(lot), heights = [rows[0].lo, ...rows.map((row) => row.hi)];
65
+ chainsOf(plan).forEach((chain, index) => {
66
+ if (!chain.runs.every((run) => keep(plan[run])))
67
+ return;
68
+ const first = chain.runs[0], last = chain.runs.at(-1);
69
+ const s0 = turnsAt(plan, first)[0] > 0 ? 0.09 : 0.02, s1 = chain.length - (turnsAt(plan, last)[1] > 0 ? 0.09 : 0.02);
70
+ const [columns, glazed] = openings(chain.length, plan[first].at(0));
71
+ chainSkin(kit, wall, plan, chain, 0, chain.runs.length - 1, s0, s1, columns, heights, 0.02, 0.07, (bay, r) => {
72
+ const row = rows[r];
73
+ if (row.kind === "band")
74
+ return seed === 0 ? band : brick;
75
+ return row.kind === "glass" && glazed((columns[bay - 1] + columns[bay]) / 2, row.floor) ? paneColor(row.floor, bay, seed * 8 + index) : brick;
76
+ }, brick);
77
+ });
78
+ };
79
+ const facing = (x, z) => (run) => run.normal(0)[0] * x + run.normal(0)[1] * z > 0.9;
80
+ // The Michigan front, as drawn from its south corner: each bay's Chicago window, a sash
81
+ // 80 cm wide either side of its fixed pane, split from it by 12 cm mullions painted in the
82
+ // window's plane. The front's chain runs from whichever corner the outline's winding gives
83
+ // it.
84
+ const michigan = (length, start) => {
85
+ const fromSouth = Math.hypot(start[0] - corner[0], start[1] - corner[1]) < Math.hypot(north[0], north[1]) / 2;
86
+ const at = (s) => (fromSouth ? s : length - s);
87
+ const spans = spec.bays.map(([a, b]) => [at(a), at(b)].sort((p, q) => p - q));
88
+ const mullions = spans.flatMap(([a, b]) => [a + 0.8, b - 0.8]);
89
+ const cuts = [...spans.flat(), ...mullions.flatMap((m) => [m - 0.06, m + 0.06])].filter((s) => s > 0.1 && s < length - 0.1);
90
+ return [[0, ...[...new Set(cuts)].sort((p, q) => p - q), length], (middle) => spans.some(([a, b]) => middle > a && middle < b) && !mullions.some((m) => Math.abs(middle - m) < 0.06)];
91
+ };
92
+ // The alley: windows 1.6 m wide spread evenly about 3.2 m apart on every floor above the
93
+ // ground floor.
94
+ const alley = (length) => {
95
+ const count = Math.max(1, Math.round(length / 3.2)), centres = Array.from({ length: count }, (_, i) => length * (i + 0.5) / count);
96
+ return [[0, ...centres.flatMap((c) => [c - 0.8, c + 0.8]), length], (middle, floor) => floor >= 2 && centres.some((c) => Math.abs(middle - c) < 0.8)];
97
+ };
98
+ skin(0, facing(1, 0), michigan);
99
+ skin(1, facing(-1, 0), alley);
100
+ // The party walls keep the bare shell. The Gage, each other and the University Club cover
101
+ // them, and a skin would stand into the neighbour's lot, its top level with the other
102
+ // building's roof.
103
+ const model = kit.finish({ height: h.top, outlines: [shell], opacity: 0.16 });
104
+ model.building.position.set(offset[0], 0, offset[1]);
105
+ model.building.userData["geography"] = record;
106
+ model.building.userData["geographicDetail"] = { levels: h, source: "docs/keith-ascher-reference.md" };
107
+ return model;
108
+ }
109
+ export const createKeithGeographicBuilding = (record, projectPlan, offset) => createGageGroupBuilding(keithSpec, record, projectPlan, offset);
110
+ export const createAscherGeographicBuilding = (record, projectPlan, offset) => createGageGroupBuilding(ascherSpec, record, projectPlan, offset);
@@ -0,0 +1,153 @@
1
+ import { createBuilder, inside, line, polygonOf } from "./building-kit.js";
2
+ // Kemper Building at 1 East Wacker Drive: the geographic layout's detailed
3
+ // model. The mapped ground outline and tower part keep their OpenStreetMap
4
+ // outlines; the 159 m top is OSM's height tag (522 ft in the 1962 record).
5
+ // The marble shell, window bays, raised mullions, dark crown band with light
6
+ // fins, and projecting cap reuse the fitted model's vocabulary at meter scale.
7
+ // Window-row, bay, and crown spacings are estimates; see
8
+ // docs/kemper-geographic-reference.md. Units are meters; +x is east, +z south.
9
+ export const kemperGeographicLevels = Object.freeze({
10
+ podium: 7.8, // OSM way 685494067: two floors at the tower's average floor height
11
+ roof: 159, // OSM height tag on way 64389514
12
+ floors: 41,
13
+ });
14
+ const h = kemperGeographicLevels;
15
+ // Floors 1-2 are the podium; the tower pitch follows the published 41 floors.
16
+ const pitch = (h.roof - h.podium) / (h.floors - 2);
17
+ const floorHeight = (floor) => h.podium + (floor - 3) * pitch;
18
+ // The crown band spans the 40th and 41st floors up to the roof: dark glazing
19
+ // behind light fins, capped by a projecting parapet.
20
+ const crownBase = floorHeight(40);
21
+ const shaftTop = h.roof - 0.5;
22
+ export function createKemperGeographicBuilding(record, projectPlan, offset) {
23
+ const kit = createBuilder(record.name, record.id);
24
+ // The neutral gray palette of the fitted model: marble shell, dark glazing
25
+ // with scattered lit units, pale mullions and fins.
26
+ const marble = kit.batch("Kemper · marble shell", kit.material(0x929292));
27
+ const mullions = kit.batch("Kemper · mullions and bands", kit.material(0x9b9b9b));
28
+ const crownGlass = kit.batch("Kemper · crown glazing", kit.material(0x1c1c1c));
29
+ const crownFins = kit.batch("Kemper · crown fins", kit.material(0xa3a3a3));
30
+ const cap = kit.batch("Kemper · roof cap", kit.material(0xa8a8a8));
31
+ const lit = kit.batch("Kemper · lit glazing", kit.material(0x6a6a6a));
32
+ const dim = kit.batch("Kemper · dim glazing", kit.material(0x505050));
33
+ const tones = [0x2e2e2e, 0x323232, 0x363636, 0x2a2a2a].map((color, i) => kit.batch(`Kemper · glazing ${i + 1}`, kit.material(color)));
34
+ const paneTone = (row, bay, side) => {
35
+ const hash = (Math.imul(row + 11, 0x9e3779b1) ^ Math.imul(bay + 31, 0x85ebca77) ^ Math.imul(side + 7, 0xc2b2ae3d)) >>> 0;
36
+ const value = hash % 97;
37
+ if (value < 3)
38
+ return lit;
39
+ if (value < 6)
40
+ return dim;
41
+ return tones[value % tones.length];
42
+ };
43
+ const ground = projectPlan(record.footprint.coordinates);
44
+ const tower = projectPlan(record.parts.find((p) => p.way === 685494066).coordinates);
45
+ const towerPolygon = polygonOf(tower);
46
+ // The mapped volumes: the full ground outline supports the tower, and the
47
+ // tower part rises from the podium, so no coincident exterior walls overlap.
48
+ kit.prism(marble, ground, [0, h.podium]);
49
+ kit.prism(marble, tower, [h.podium, shaftTop]);
50
+ // A closed shallow solid on a run. Offsets separate the visible surfaces from
51
+ // their backing wall, and the ends stop short of polygon corners so adjacent
52
+ // walls' projecting returns cannot overlap at the same elevation.
53
+ const strip = (batch, run, from, to, y0, y1, back, front) => {
54
+ const clearance = Math.max(Math.abs(back), Math.abs(front)) + 0.025;
55
+ from = Math.max(from, clearance);
56
+ to = Math.min(to, run.length - clearance);
57
+ if (to - from < 0.03 || y1 <= y0)
58
+ return;
59
+ kit.box(batch, run.at((from + to) / 2), run.normal(0), (to - from) / 2, back, front, y0, y1);
60
+ };
61
+ // Tower facade: one pane per bay per row between full-height mullions, with
62
+ // a spandrel at each floor line. Mullions stand proudest of the tower detail.
63
+ tower.forEach((run, side) => {
64
+ const bays = Math.max(1, Math.round(run.length / 3.1));
65
+ const width = run.length / bays;
66
+ for (let floor = 3; floor <= 39; floor += 1) {
67
+ const bottom = floorHeight(floor), top = floorHeight(floor + 1);
68
+ for (let bay = 0; bay < bays; bay += 1) {
69
+ strip(paneTone(floor, bay, side), run, bay * width + 0.16, (bay + 1) * width - 0.16, bottom + 0.26, top - 0.24, 0.02, 0.07);
70
+ }
71
+ strip(mullions, run, 0.015, run.length - 0.015, top - 0.15, top + 0.05, 0.03, 0.12);
72
+ }
73
+ for (let i = 0; i <= bays; i += 1) {
74
+ const s = Math.max(0.18, Math.min(run.length - 0.18, i * width));
75
+ strip(mullions, run, s - 0.06, s + 0.06, h.podium + 0.02, crownBase - 0.05, 0.05, 0.16);
76
+ }
77
+ strip(mullions, run, 0.015, run.length - 0.015, h.podium - 0.05, h.podium + 0.35, 0.06, 0.18);
78
+ });
79
+ // The crown: dark glazing between light fins over the top two floors, then
80
+ // a projecting closed cap at the 159 m roof. The mapped outline carries
81
+ // sub-meter tracing jogs on its north and west edges; the prisms keep them,
82
+ // but a closed band cannot turn their concave corners, so the cap ring drops
83
+ // vertices within 1.2 m of the chord between their neighbours and verifies
84
+ // that every remaining corner is convex: a future part refresh could add a
85
+ // jog the tolerance keeps, which must fail here with a clear message rather
86
+ // than as a band error at page load.
87
+ tower.forEach((run) => {
88
+ strip(crownGlass, run, 0.12, run.length - 0.12, crownBase + 0.05, shaftTop - 0.25, 0.01, 0.05);
89
+ const count = Math.max(1, Math.round(run.length / 1.05));
90
+ for (let i = 0; i < count; i += 1) {
91
+ const s = (i + 0.5) * run.length / count;
92
+ strip(crownFins, run, s - 0.13, s + 0.13, crownBase - 0.05, shaftTop - 0.2, 0.04, 0.28);
93
+ }
94
+ });
95
+ const simplified = tower.map((run) => run.at(0));
96
+ for (let pass = 0; pass < 8; pass += 1) {
97
+ let changed = false;
98
+ for (let i = 0; i < simplified.length; i += 1) {
99
+ const a = simplified[(i - 1 + simplified.length) % simplified.length];
100
+ const b = simplified[(i + 1) % simplified.length];
101
+ const chord = Math.hypot(b[0] - a[0], b[1] - a[1]);
102
+ const p = simplified[i];
103
+ if (chord < 1e-9 || (Math.abs((b[0] - a[0]) * (a[1] - p[1]) - (a[0] - p[0]) * (b[1] - a[1])) / chord < 1.2 && simplified.length > 4)) {
104
+ simplified.splice(i, 1);
105
+ changed = true;
106
+ break;
107
+ }
108
+ }
109
+ if (!changed)
110
+ break;
111
+ }
112
+ for (let i = 0; i < simplified.length; i += 1) {
113
+ const a = simplified[(i - 1 + simplified.length) % simplified.length];
114
+ const b = simplified[i];
115
+ const c = simplified[(i + 1) % simplified.length];
116
+ const turn = ((b[0] - a[0]) * (c[1] - b[1]) - (b[1] - a[1]) * (c[0] - b[0]))
117
+ / (Math.hypot(b[0] - a[0], b[1] - a[1]) * Math.hypot(c[0] - b[0], c[1] - b[1]));
118
+ if (turn > 1e-6)
119
+ throw new Error("Kemper's mapped cap ring still turns a concave corner after simplification.");
120
+ }
121
+ const capRuns = simplified.map((p, i) => line(p, simplified[(i + 1) % simplified.length]));
122
+ kit.band(cap, capRuns, shaftTop, h.roof, 0.4, { closed: true });
123
+ // Podium: storefront glazing at grade, its mullions, a spandrel head, the
124
+ // second floor's windows, and a projecting cap. Cap pieces under the tower
125
+ // are skipped so the cap does not run through the tower's walls.
126
+ ground.forEach((run, side) => {
127
+ if (run.length < 2)
128
+ return;
129
+ const bays = Math.max(1, Math.round(run.length / 3.0));
130
+ const width = run.length / bays;
131
+ for (let bay = 0; bay < bays; bay += 1) {
132
+ strip(paneTone(1, bay, side), run, bay * width + 0.18, (bay + 1) * width - 0.18, 0.9, 4.4, 0.02, 0.07);
133
+ strip(paneTone(2, bay, side + 9), run, bay * width + 0.18, (bay + 1) * width - 0.18, 5.1, 7.15, 0.02, 0.07);
134
+ }
135
+ for (let i = 0; i <= bays; i += 1) {
136
+ const s = Math.max(0.18, Math.min(run.length - 0.18, i * width));
137
+ strip(mullions, run, s - 0.06, s + 0.06, 0.85, 7.2, 0.04, 0.13);
138
+ }
139
+ strip(mullions, run, 0.015, run.length - 0.015, 4.45, 4.95, 0.03, 0.15);
140
+ const count = Math.max(1, Math.ceil(run.length / 1.5));
141
+ for (let index = 0; index < count; index += 1) {
142
+ const from = index * run.length / count, to = (index + 1) * run.length / count;
143
+ if (inside(towerPolygon, run.at((from + to) / 2, 0.3)))
144
+ continue;
145
+ strip(mullions, run, from, to, 7.35, h.podium, 0.02, 0.3);
146
+ }
147
+ });
148
+ const model = kit.finish({ height: h.roof, outlines: [marble, cap], opacity: 0.16 });
149
+ model.building.position.set(offset[0], 0, offset[1]);
150
+ model.building.userData["geography"] = record;
151
+ model.building.userData["geographicDetail"] = { levels: h, floors: 41, source: "docs/kemper-geographic-reference.md" };
152
+ return model;
153
+ }
@@ -0,0 +1,78 @@
1
+ import * as THREE from "../vendor/three-r186.js";
2
+ // Proportions follow the existing SVG, in the Crain model's scene units.
3
+ // This is a closed, reusable mesh; the footprint and unseen faces are inferred.
4
+ export function createKemperBuilding() {
5
+ const building = new THREE.Group();
6
+ building.name = "Kemper Building";
7
+ building.userData["buildingId"] = "building-kemper";
8
+ const width = 31;
9
+ const depth = 53.5;
10
+ const height = 141;
11
+ const gradient = new THREE.DataTexture(new Uint8Array([70, 135, 200, 255]), 4, 1, THREE.RedFormat);
12
+ gradient.minFilter = gradient.magFilter = THREE.NearestFilter;
13
+ gradient.needsUpdate = true;
14
+ const material = (color) => new THREE.MeshToonMaterial({ color, gradientMap: gradient });
15
+ const stone = material(0x929292);
16
+ const glass = material(0x303030);
17
+ const ribs = material(0x9b9b9b);
18
+ const crown = material(0x383838);
19
+ const materials = [stone, glass, ribs, crown];
20
+ let triangleCount = 0;
21
+ function box(name, size, position, surface) {
22
+ const mesh = new THREE.Mesh(new THREE.BoxGeometry(...size), surface);
23
+ mesh.name = name;
24
+ mesh.position.set(...position);
25
+ mesh.castShadow = mesh.receiveShadow = true;
26
+ building.add(mesh);
27
+ triangleCount += 12;
28
+ return mesh;
29
+ }
30
+ const shell = box("closed tower shell", [width, height - 3, depth], [0, (height - 3) / 2, 0], stone);
31
+ box("recessed crown", [width + 0.1, 2.1, depth + 0.1], [0, height - 1.95, 0], crown);
32
+ box("roof cap", [width + 1.1, 0.9, depth + 1.1], [0, height - 0.45, 0], ribs);
33
+ const windowPositions = [], ribPositions = [];
34
+ function quad(target, a, b, c, d) {
35
+ for (const point of [a, b, c, a, c, d])
36
+ target.push(...point);
37
+ }
38
+ const corners = [[-width / 2, depth / 2], [width / 2, depth / 2], [width / 2, -depth / 2], [-width / 2, -depth / 2]];
39
+ for (let face = 0; face < 4; face += 1) {
40
+ const a = corners[face], b = corners[(face + 1) % 4];
41
+ const length = Math.hypot(b[0] - a[0], b[1] - a[1]);
42
+ const normal = [(a[1] - b[1]) / length, (b[0] - a[0]) / length];
43
+ const point = (t, y, offset) => [a[0] + (b[0] - a[0]) * t + normal[0] * offset, y, a[1] + (b[1] - a[1]) * t + normal[1] * offset];
44
+ const strip = (target, left, right, bottom, top, offset) => quad(target, point(left, bottom, offset), point(right, bottom, offset), point(right, top, offset), point(left, top, offset));
45
+ for (let bay = 0; bay < 14; bay += 1) {
46
+ // The SVG's left face has a broad blank panel between these window bays.
47
+ if (face !== 0 || bay === 0 || bay >= 8) {
48
+ const left = (bay + 0.18) / 14, right = (bay + 0.73) / 14;
49
+ strip(windowPositions, left, right, 5, height - 3.1, 0.025);
50
+ // Raised mullions have front and side faces, so orbiting reveals depth.
51
+ const fin = 0.16 / length;
52
+ strip(ribPositions, left - fin, left, 5, height - 3.1, 0.27);
53
+ quad(ribPositions, point(left, 5, 0.025), point(left, height - 3.1, 0.025), point(left, height - 3.1, 0.27), point(left, 5, 0.27));
54
+ strip(ribPositions, right, right + fin, 5, height - 3.1, 0.27);
55
+ quad(ribPositions, point(right, 5, 0.27), point(right, height - 3.1, 0.27), point(right, height - 3.1, 0.025), point(right, 5, 0.025));
56
+ }
57
+ strip(ribPositions, (bay + 0.3) / 14, (bay + 0.38) / 14, height - 3, height - 0.9, 0.12);
58
+ }
59
+ }
60
+ for (const [name, positions, surface] of [["vertical window bays", windowPositions, glass], ["raised mullions and crown slots", ribPositions, ribs]]) {
61
+ const geometry = new THREE.BufferGeometry();
62
+ geometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3));
63
+ geometry.computeVertexNormals();
64
+ const mesh = new THREE.Mesh(geometry, surface);
65
+ mesh.name = name;
66
+ mesh.castShadow = mesh.receiveShadow = true;
67
+ building.add(mesh);
68
+ triangleCount += positions.length / 9;
69
+ }
70
+ const edges = new THREE.LineSegments(new THREE.EdgesGeometry(shell.geometry), new THREE.LineBasicMaterial({ color: 0xcccccc, transparent: true, opacity: 0.3 }));
71
+ edges.position.copy(shell.position);
72
+ building.add(edges);
73
+ return {
74
+ building, height, triangleCount,
75
+ setHighlighted(highlighted) { materials.forEach((surface) => surface.emissive.setHex(highlighted ? 0x222222 : 0)); },
76
+ setWireframe(enabled) { materials.forEach((surface) => { surface.wireframe = enabled; }); edges.visible = !enabled; },
77
+ };
78
+ }