@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,23 @@
1
+ import type { BuildingModel, Vec2 } from "./building-kit.js";
2
+ export interface AonForm {
3
+ name: string;
4
+ id: string;
5
+ outline: Vec2[];
6
+ enclosure: Vec2[];
7
+ mast: Vec2;
8
+ base: number;
9
+ }
10
+ export declare const aonLevels: Readonly<{
11
+ roof: 340;
12
+ capBottom: 338.5;
13
+ lobbyTop: 11.9;
14
+ pitch: 3.87;
15
+ sill: 0.8;
16
+ head: 3.1;
17
+ enclosureTop: 346.3;
18
+ tip: 362.5;
19
+ bay: 3.048;
20
+ baysPerFace: 15;
21
+ faceLength: 20;
22
+ }>;
23
+ export declare function buildAonTower(form: AonForm): BuildingModel;
@@ -0,0 +1,130 @@
1
+ import * as THREE from "../vendor/three-r186.js";
2
+ import { createBuilder, line } from "./building-kit.js";
3
+ import { chainsOf, gridBox, mitredBox, orient, paintedSlab, planOf, turnAt } from "./facade-grid.js";
4
+ // Heights above the street. The floor pitch and the cap are measured on the photograph the
5
+ // drawing was traced from. The lobby takes what is left below eighty office floors and the
6
+ // two mechanical floors over them, which with the lobby make the published 83; daylight
7
+ // photographs show the slots unchanged over the mechanical floors, so the glass keeps the
8
+ // office floors' pitch up to the cap.
9
+ export const aonLevels = Object.freeze({
10
+ roof: 340,
11
+ capBottom: 338.5,
12
+ lobbyTop: 11.9,
13
+ pitch: 3.87,
14
+ sill: 0.8,
15
+ head: 3.1,
16
+ enclosureTop: 346.3,
17
+ tip: 362.5,
18
+ bay: 3.048, // nominal clean-plan module; mapped spacing follows each face's chord
19
+ baysPerFace: 15, // tenant portal's Suite 1300 plan: sixteen piers, fifteen openings
20
+ faceLength: 20,
21
+ });
22
+ const h = aonLevels;
23
+ const color = (hex) => new THREE.Color(hex);
24
+ const glassTones = [0x262626, 0x2c2c2c, 0x323232, 0x222222].map(color);
25
+ const litGlass = color(0x7c7c7c), dimGlass = color(0x4e4e4e);
26
+ const spandrel = color(0x1b1b1b), granite = color(0xb2b2b2), core = color(0x202020);
27
+ const slat = color(0x9c9c9c), slatGap = color(0x444444), roofing = color(0x4c4c4c);
28
+ function paneColor(row, bay, face) {
29
+ const hash = (Math.imul(row + 17, 0x9e3779b1) ^ Math.imul(bay + 31, 0x85ebca77) ^ Math.imul(face + 7, 0xc2b2ae3d)) >>> 0;
30
+ const value = hash % 101;
31
+ if (value < 3)
32
+ return litGlass;
33
+ if (value < 6)
34
+ return dimGlass;
35
+ return glassTones[value % glassTones.length];
36
+ }
37
+ export function buildAonTower(form) {
38
+ const kit = createBuilder(form.name, form.id);
39
+ const shell = kit.batch("Aon · tube shell", kit.material(0xffffff, { vertexColors: true }));
40
+ const glazing = kit.batch("Aon · window ribbons", kit.material(0xffffff, { vertexColors: true }));
41
+ const piers = kit.batch("Aon · granite piers", kit.material(0xbababa));
42
+ const cap = kit.batch("Aon · granite cap", kit.material(0xc4c4c4));
43
+ const enclosure = kit.batch("Aon · rooftop enclosure", kit.material(0xffffff, { vertexColors: true }));
44
+ const mast = kit.batch("Aon · antenna mast", kit.material(0x6a6a6a));
45
+ const base = form.base;
46
+ const y = (real) => real - base;
47
+ const outline = orient(form.outline), plan = planOf(outline);
48
+ // A traced outline can split one face into nearly collinear pieces, which chainsOf joins
49
+ // within 5°: a chain at least `faceLength` long is a main face, whose facade stands on its
50
+ // chord, and every other run belongs to a notched corner.
51
+ const faces = chainsOf(plan).map(({ runs }) => ({ chord: line(plan[runs[0]].at(0), plan[runs.at(-1)].at(plan[runs.at(-1)].length)), runs })).filter(({ chord }) => chord.length >= h.faceLength);
52
+ const onFace = new Set(faces.flatMap(({ runs }) => runs));
53
+ const grid = (target, run, columns, rows, back, front, paint, sides) => gridBox(kit, target, run, columns, rows, back, front, paint, sides, y);
54
+ // The shaft: dark spandrel walls on the faces, granite on the notched corners, a floor,
55
+ // and the flat roof.
56
+ plan.forEach((run, index) => {
57
+ const a = run.at(0), b = run.at(run.length), n = run.normal(0);
58
+ kit.quad(shell, [[a[0], 0, a[1]], [b[0], 0, b[1]], [b[0], y(h.roof), b[1]], [a[0], y(h.roof), a[1]]], [[n[0], 0, n[1]]], onFace.has(index) ? core : granite);
59
+ });
60
+ paintedSlab(kit, shell, outline, 0, false, roofing);
61
+ paintedSlab(kit, shell, outline, y(h.roof), true, roofing);
62
+ // The source plan fixes fifteen openings per face, independent of an OSM trace's
63
+ // corner depth. Distribute them over each face's chord; mapped spacing remains an
64
+ // approximation (docs/aon-reference.md, FID-AON-001). End piers stay on their face.
65
+ const faceBays = (run) => {
66
+ const count = h.baysPerFace, bay = run.length / count;
67
+ return Array.from({ length: count + 1 }, (_, i) => i * bay);
68
+ };
69
+ const pierHalf = 0.65, pierPoint = 0.7;
70
+ const floors = [];
71
+ for (let level = h.lobbyTop; level + h.pitch <= h.capBottom + 1e-6; level += h.pitch)
72
+ floors.push(level);
73
+ faces.forEach(({ chord: run }, face) => {
74
+ const stations = faceBays(run);
75
+ // End piers are inset from the corner. Stop glass at their centres so it
76
+ // cannot peek past the taper of the V and create two extra narrow slots.
77
+ const glassStations = [pierHalf, ...stations.slice(1, -1), run.length - pierHalf];
78
+ // The glass: one ribbon per floor across the face, cut into panes at the columns, which
79
+ // stand in front of the joints. The lobby is a single tall storey of glass.
80
+ const visible = (low) => low >= base;
81
+ if (visible(0.4))
82
+ grid(glazing, run, glassStations, [0.4, h.lobbyTop - 0.9], 0.02, 0.07, (bay) => paneColor(0, bay, face), spandrel);
83
+ floors.forEach((level, row) => {
84
+ if (!visible(level + h.sill))
85
+ return;
86
+ grid(glazing, run, glassStations, [level + h.sill, level + h.head], 0.02, 0.07, (bay) => paneColor(row + 1, bay, face), spandrel);
87
+ });
88
+ // The V-shaped columns: granite prisms from just above grade to the cap, their points
89
+ // outward. They start 0.3 m up so their relief leaves the street outline unchanged.
90
+ for (const station of stations) {
91
+ const s = Math.min(Math.max(station, pierHalf), run.length - pierHalf);
92
+ const [left, right, point] = [run.at(s - pierHalf, 0), run.at(s + pierHalf, 0), run.at(s, pierPoint)];
93
+ const low = y(Math.max(base, 0.3)), high = y(h.capBottom);
94
+ const up = [0, 1, 0], down = [0, -1, 0];
95
+ const p = (q, v) => [q[0], v, q[1]];
96
+ // Outward is to the right of a and then b, as it is for every run of the plan.
97
+ const faceOf = (a, b) => { const d = [b[0] - a[0], b[1] - a[1]], l = Math.hypot(...d); return [-d[1] / l, 0, d[0] / l]; };
98
+ kit.quad(piers, [p(left, low), p(point, low), p(point, high), p(left, high)], [faceOf(left, point)]);
99
+ kit.quad(piers, [p(point, low), p(right, low), p(right, high), p(point, high)], [faceOf(point, right)]);
100
+ kit.quad(piers, [p(right, low), p(left, low), p(left, high), p(right, high)], [[-run.normal(0)[0], 0, -run.normal(0)[1]]]);
101
+ kit.triangle(piers, [p(left, high), p(point, high), p(right, high)], [up, up, up]);
102
+ kit.triangle(piers, [p(left, low), p(right, low), p(point, low)], [down, down, down]);
103
+ }
104
+ });
105
+ // The granite cap: a fascia under the roof edge on every run, standing clear of the
106
+ // columns' points and mitred on the bisector of each corner.
107
+ const capWidth = pierPoint + 0.2;
108
+ if (h.roof > base)
109
+ plan.forEach((run, index) => {
110
+ mitredBox(kit, cap, run, 0, run.length, turnAt(plan[(index + plan.length - 1) % plan.length], run), turnAt(run, plan[(index + 1) % plan.length]), capWidth, h.capBottom, h.roof, y);
111
+ });
112
+ // The rooftop enclosure: louvered walls from the roof to the published top, and a lid.
113
+ const enclosureOutline = orient(form.enclosure);
114
+ planOf(enclosureOutline).forEach((run) => {
115
+ const rows = [];
116
+ for (let v = h.roof; v < h.enclosureTop - 1e-6; v += 0.6)
117
+ rows.push(v);
118
+ rows.push(h.enclosureTop);
119
+ const a = run.at(0), b = run.at(run.length), n = run.normal(0);
120
+ // Its walls are cells of blades and gaps, so the enclosure closes on its own lid and floor.
121
+ const at = (q, v) => [q[0], y(v), q[1]];
122
+ for (let r = 0; r + 1 < rows.length; r += 1)
123
+ kit.quad(enclosure, [at(a, rows[r]), at(b, rows[r]), at(b, rows[r + 1]), at(a, rows[r + 1])], [[n[0], 0, n[1]]], r % 2 === 0 ? slat : slatGap);
124
+ });
125
+ paintedSlab(kit, enclosure, enclosureOutline, y(h.roof), false, roofing);
126
+ paintedSlab(kit, enclosure, enclosureOutline, y(h.enclosureTop), true, roofing);
127
+ // The antenna: a slim mast from the enclosure to the published tip.
128
+ kit.box(mast, form.mast, [0, 1], 0.3, -0.3, 0.3, y(h.enclosureTop), y(h.tip));
129
+ return kit.finish({ height: y(h.tip), outlines: [shell, cap], opacity: 0.18 });
130
+ }
@@ -0,0 +1,14 @@
1
+ import type { BuildingModel, Plan } from "./building-kit.js";
2
+ import type { GeoBuilding } from "./skyline-geography-data.js";
3
+ export declare const blueCrossLevels: Readonly<{
4
+ lobbyTop: 10.26;
5
+ bands: readonly [readonly [65.7, 72.1], readonly [117.1, 128.1], readonly [170.1, 176.6]];
6
+ occupied: 208.3;
7
+ roof: 212.8;
8
+ wings: 212;
9
+ top: 226.7;
10
+ module: 1.524;
11
+ floors: readonly [14, 11, 11, 8];
12
+ }>;
13
+ export declare const blueCrossFloors: number[];
14
+ export declare function createBlueCrossGeographicBuilding(record: GeoBuilding, projectPlan: (coordinates: [number, number][]) => Plan, offset: [number, number]): BuildingModel;
@@ -0,0 +1,248 @@
1
+ import * as THREE from "../vendor/three-r186.js";
2
+ import { createBuilder, inside, polygonOf } from "./building-kit.js";
3
+ import { chainSkin, chainsOf, orient, paintedSlab, planOf } from "./facade-grid.js";
4
+ import { createWindowIllumination } from "./window-illumination.js";
5
+ // The Blue Cross and Blue Shield Tower, 300 East Randolph Street (Lohan Associates, 1997;
6
+ // Goettsch Partners' 24-storey addition, 2010): the geographic layout's model, on the
7
+ // mapped outline 95486960 and its parts 284779637 and 284779635. The original layout
8
+ // has no model of it; the drawing shows it right of Aon, outside the excerpt it is fitted
9
+ // to.
10
+ //
11
+ // A glass curtain wall on the 5 ft module wraps a central block, the mapped 227 m part,
12
+ // which stands forward of two end bays, the rest of the mapped 212 m part. Three bands of
13
+ // mechanical floors cross the block, their columns lit blue at night; at the middle band,
14
+ // the top of the 1997 tower, the end bays are open through. A screen carrying the company's
15
+ // emblems rises from the block's roof to the published 226.7 m. Heights are measured on
16
+ // the photograph down from that top; see docs/blue-cross-reference.md. Units are meters;
17
+ // +x is east, +z is south.
18
+ export const blueCrossLevels = Object.freeze({
19
+ lobbyTop: 10.26, // fourteen floors under the lower band; estimate
20
+ bands: [[65.7, 72.1], [117.1, 128.1], [170.1, 176.6]], // mechanical floors
21
+ occupied: 208.3, // the highest occupied floor, published
22
+ roof: 212.8, // the central block's glass top
23
+ wings: 212, // the end bays' roof, mapped
24
+ top: 226.7, // the screen's top, the published architectural height
25
+ module: 1.524, // 5 ft
26
+ // Office floors between the bands, each run divided evenly: the lobby to the lower band,
27
+ // then between the bands, and from the upper band to the top occupied floor.
28
+ floors: [14, 11, 11, 8],
29
+ });
30
+ const h = blueCrossLevels;
31
+ // Every office floor's level, from the lobby's top to the top occupied floor.
32
+ const zones = [
33
+ [h.lobbyTop, h.bands[0][0], h.floors[0]], [h.bands[0][1], h.bands[1][0], h.floors[1]],
34
+ [h.bands[1][1], h.bands[2][0], h.floors[2]], [h.bands[2][1], h.occupied, h.floors[3]],
35
+ ];
36
+ // Center five-floor words in the three upper office zones, ordered from the top.
37
+ const messageTopFloors = zones.map(([, , count], zone) => h.floors.slice(0, zone).reduce((sum, floors) => sum + floors, 0)
38
+ + Math.floor((count + 5) / 2) - 1).reverse().slice(0, 3);
39
+ export const blueCrossFloors = [...zones.flatMap(([from, to, count]) => Array.from({ length: count }, (_, i) => from + (to - from) * i / count)), h.occupied];
40
+ const spandrelBelow = 0.3, spandrelAbove = 0.9;
41
+ const color = (hex) => new THREE.Color(hex);
42
+ const glassTones = [0x414141, 0x464646, 0x3c3c3c, 0x4a4a4a].map(color);
43
+ const litGlass = color(0x8a8a8a), dimGlass = color(0x5c5c5c);
44
+ const spandrel = color(0x383838), recess = color(0x2a2a2a), screen = color(0x3e3e3e), lobby = color(0x333333), roofing = color(0x3a3a3a);
45
+ function paneColor(row, bay, wall) {
46
+ const hash = (Math.imul(row + 13, 0x9e3779b1) ^ Math.imul(bay + 37, 0x85ebca77) ^ Math.imul(wall + 23, 0xc2b2ae3d)) >>> 0;
47
+ const value = hash % 101;
48
+ if (value < 4)
49
+ return litGlass;
50
+ if (value < 8)
51
+ return dimGlass;
52
+ return glassTones[value % glassTones.length];
53
+ }
54
+ const rows = [];
55
+ {
56
+ const push = (lo, hi, kind, floor) => { if (hi - lo > 1e-6)
57
+ rows.push({ lo, hi, kind, floor }); };
58
+ let floor = 0;
59
+ zones.forEach(([from, to, count], zone) => {
60
+ for (let i = 0; i < count; i += 1, floor += 1) {
61
+ const level = from + (to - from) * i / count, next = from + (to - from) * (i + 1) / count;
62
+ push(rows.at(-1)?.hi ?? level, level + spandrelAbove, "spandrel", floor);
63
+ push(level + spandrelAbove, next - spandrelBelow, "glass", floor);
64
+ }
65
+ const band = h.bands[zone];
66
+ if (band)
67
+ push(rows.at(-1).hi, band[1], "band", -1);
68
+ });
69
+ // The top occupied floor, under a parapet that rises past the end bays' roof to the
70
+ // block's, and the screen above.
71
+ push(rows.at(-1).hi, h.occupied + spandrelAbove, "spandrel", floor);
72
+ push(h.occupied + spandrelAbove, h.wings - spandrelBelow, "glass", floor);
73
+ push(h.wings - spandrelBelow, h.wings, "spandrel", -1);
74
+ push(h.wings, h.roof, "spandrel", -1);
75
+ push(h.roof, h.top, "screen", -1);
76
+ }
77
+ const rowsBetween = (lo, hi) => rows.filter((row) => row.lo >= lo - 1e-6 && row.hi <= hi + 1e-6);
78
+ export function createBlueCrossGeographicBuilding(record, projectPlan, offset) {
79
+ const kit = createBuilder(record.name, record.id);
80
+ const shell = kit.batch("Blue Cross · shell", kit.material(0xffffff, { vertexColors: true }));
81
+ const wall = kit.batch("Blue Cross · glass, spandrels and bands", kit.material(0xffffff, { vertexColors: true }));
82
+ const mullions = kit.batch("Blue Cross · mullions", kit.material(0x2c2c2c));
83
+ const bars = kit.batch("Blue Cross · band columns and emblems", kit.material(0xb4b4b4));
84
+ const windows = [];
85
+ let windowColumns = 0;
86
+ const outline = (way) => orient(polygonOf(projectPlan(record.parts.find((part) => part.way === way).coordinates)));
87
+ const footprint = orient(polygonOf(projectPlan(record.footprint.coordinates)));
88
+ const block = outline(284779637), slab = outline(284779635);
89
+ // The end bays: the mapped 212 m part less the central block. The two parts share the
90
+ // corners where the block meets each bay, so the slab's outline splits at those corners
91
+ // into stretches inside the block and stretches outside it. Each outside stretch is a
92
+ // bay's outer wall, closed along the block's wall, whose vertices between the same two
93
+ // corners lie inside the slab.
94
+ const near = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1]) < 0.01;
95
+ const corners = slab.flatMap((p, i) => (block.some((q) => near(p, q)) ? [i] : []));
96
+ const bays = corners.flatMap((a, k) => {
97
+ const b = corners[(k + 1) % corners.length], path = [];
98
+ for (let i = a;; i = (i + 1) % slab.length) {
99
+ path.push(slab[i]);
100
+ if (i === b)
101
+ break;
102
+ }
103
+ const [p, q] = [path[0], path[1]];
104
+ if (inside(block, [(p[0] + q[0]) / 2, (p[1] + q[1]) / 2]))
105
+ return [];
106
+ const [from, to] = [block.findIndex((v) => near(v, path.at(-1))), block.findIndex((v) => near(v, path[0]))];
107
+ const walk = (step) => { const way = []; for (let j = (from + step + block.length) % block.length; j !== to; j = (j + step + block.length) % block.length)
108
+ way.push(block[j]); return way; };
109
+ const back = [walk(1), walk(-1)].find((way) => way.every((v) => inside(slab, v)));
110
+ return [orient([...path, ...back])];
111
+ });
112
+ // A solid on an outline between two heights, its walls the shell's dark core.
113
+ const solid = (corners, lo, hi, paint) => {
114
+ planOf(corners).forEach((run) => {
115
+ const a = run.at(0), b = run.at(run.length), n = run.normal(0);
116
+ 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);
117
+ });
118
+ paintedSlab(kit, shell, corners, lo, false, roofing);
119
+ paintedSlab(kit, shell, corners, hi, true, roofing);
120
+ };
121
+ // The lobby fills the mapped outline to the first office floor.
122
+ solid(footprint, 0, h.lobbyTop, lobby);
123
+ solid(block, h.lobbyTop, h.top, spandrel);
124
+ const middle = h.bands[1];
125
+ for (const bay of bays) {
126
+ solid(bay, h.lobbyTop, middle[0], spandrel);
127
+ solid(bay, middle[1], h.wings, spandrel);
128
+ }
129
+ // A wall's skin between two heights, on the runs `keep` accepts: cells two modules wide on
130
+ // every row, and mullions one module apart on the office floors. Each unbroken stretch of
131
+ // kept runs along a chain is one box, held 2 cm clear of its ends.
132
+ const skin = (corners, lo, hi, seed, keep, lift = 0) => {
133
+ const plan = planOf(corners), cuts = rowsBetween(lo, hi);
134
+ const heights = [cuts[0].lo + lift, ...cuts.map((row) => row.hi)];
135
+ const top = Math.min(hi, h.roof) - 0.05;
136
+ chainsOf(plan).forEach((chain, index) => {
137
+ const count = Math.max(1, Math.round(chain.length / h.module));
138
+ const stations = Array.from({ length: count + 1 }, (_, i) => chain.length * i / count);
139
+ const messageFace = seed === 0 && plan[chain.runs[0]].normal(0)[1] > 0.9 && chain.length > 50;
140
+ if (messageFace)
141
+ windowColumns = count;
142
+ const kept = chain.runs.map((run) => keep(plan[run]));
143
+ for (let k0 = kept.indexOf(true); k0 >= 0;) {
144
+ let k1 = k0;
145
+ while (kept[k1 + 1])
146
+ k1 += 1;
147
+ const s0 = chain.starts[k0] + 0.02, s1 = chain.starts[k1] + plan[chain.runs[k1]].length - 0.02;
148
+ chainSkin(kit, wall, plan, chain, k0, k1, s0, s1, stations.filter((_, i) => messageFace || i % 2 === 0), heights, 0.02, 0.07, (cell, r) => {
149
+ const row = cuts[r];
150
+ if (row.kind === "glass") {
151
+ if (messageFace)
152
+ windows.push({ vertex: wall.positions.length / 3, column: cell - 1, floor: row.floor });
153
+ // Subdivide the message face at every mullion while retaining its
154
+ // original paired-pane colors when the lights are off.
155
+ return paneColor(row.floor, messageFace ? Math.ceil(cell / 2) : cell, seed * 8 + index);
156
+ }
157
+ if (row.kind === "band")
158
+ return recess;
159
+ if (row.kind === "screen")
160
+ return screen;
161
+ return spandrel;
162
+ }, spandrel);
163
+ // Mullions on each office run, whole on one run and held clear of its ends. Their
164
+ // backs stand a centimetre off the wall: on the block's side walls the middle band's
165
+ // mullions start just inside the end bays, whose closing walls lie in that plane and
166
+ // face the way the backs do.
167
+ for (const s of stations) {
168
+ const k = chain.starts.findIndex((from, i) => s < from + plan[chain.runs[i]].length + 1e-9);
169
+ if (k < k0 || k > k1)
170
+ continue;
171
+ const run = plan[chain.runs[k]], along = Math.min(Math.max(s - chain.starts[k], 0.3), run.length - 0.3);
172
+ if (run.length > 0.6 && top - lo > 1)
173
+ kit.box(mullions, run.at(along), run.normal(0), 0.05, 0.01, 0.16, heights[0] + 0.05, top);
174
+ }
175
+ k0 = kept.indexOf(true, k1 + 1);
176
+ }
177
+ });
178
+ };
179
+ // Whether a run lies along an outline's edges, and whether an end bay stands against it.
180
+ const lies = (run, on) => [0, run.length / 2, run.length].every((s) => {
181
+ const p = run.at(s);
182
+ return on.some((q, i) => {
183
+ const r = on[(i + 1) % on.length], [dx, dz] = [r[0] - q[0], r[1] - q[1]], l = Math.hypot(dx, dz);
184
+ const t = Math.max(0, Math.min(1, ((p[0] - q[0]) * dx + (p[1] - q[1]) * dz) / (l * l)));
185
+ return Math.hypot(q[0] + dx * t - p[0], q[1] + dz * t - p[1]) < 0.05;
186
+ });
187
+ });
188
+ const covered = (run) => bays.some((bay) => inside(bay, run.at(run.length / 2, 0.5)));
189
+ // The end bays cover the block's side walls except through the middle band and above
190
+ // their roof, so its skin there keeps to those heights. The bays' own skins leave off the
191
+ // runs they share with the block. `lift` raises the block's foot off the lobby's roof:
192
+ // where the block meets a bay their skins cross, and their feet would share that plane.
193
+ skin(block, h.lobbyTop, h.top, 0, (run) => !covered(run), 0.05);
194
+ skin(block, middle[0] - spandrelBelow, middle[1], 0, covered);
195
+ skin(block, h.wings, h.top, 0, covered);
196
+ bays.forEach((bay, i) => {
197
+ skin(bay, h.lobbyTop, middle[0], i + 1, (run) => !lies(run, block));
198
+ skin(bay, middle[1], h.wings, i + 1, (run) => !lies(run, block));
199
+ });
200
+ // An emblem: a plate standing 7 to 30 cm off the screen, cut to an outline given across and
201
+ // up from its centre. Its faces fan from the centre, which sees the whole outline, and its
202
+ // sides follow the outline's edges.
203
+ const emblem = (centre, t, n, y, shape) => {
204
+ const point = ([u, v], depth) => [centre[0] + t[0] * u + n[0] * depth, y + v, centre[1] + t[1] * u + n[1] * depth];
205
+ const out = [n[0], 0, n[1]], back = [-n[0], 0, -n[1]];
206
+ shape.forEach((p, i) => {
207
+ const q = shape[(i + 1) % shape.length];
208
+ kit.triangle(bars, [point([0, 0], 0.3), point(p, 0.3), point(q, 0.3)], [out, out, out]);
209
+ kit.triangle(bars, [point([0, 0], 0.07), point(q, 0.07), point(p, 0.07)], [back, back, back]);
210
+ // The edge's side faces away from the centre.
211
+ const sign = Math.sign((q[1] - p[1]) * (p[0] + q[0]) - (q[0] - p[0]) * (p[1] + q[1])), length = Math.hypot(q[0] - p[0], q[1] - p[1]);
212
+ const [across, up] = [sign * (q[1] - p[1]) / length, -sign * (q[0] - p[0]) / length];
213
+ kit.quad(bars, [point(p, 0.07), point(q, 0.07), point(q, 0.3), point(p, 0.3)], [[t[0] * across, up, t[1] * across]]);
214
+ });
215
+ };
216
+ // The company's emblems, 4.2 m tall: a cross, its arms a third of its span, and a shield,
217
+ // square-shouldered and drawn to a point.
218
+ const cross = [[0.7, 2.1], [0.7, 0.7], [2.1, 0.7], [2.1, -0.7], [0.7, -0.7], [0.7, -2.1],
219
+ [-0.7, -2.1], [-0.7, -0.7], [-2.1, -0.7], [-2.1, 0.7], [-0.7, 0.7], [-0.7, 2.1]];
220
+ const shield = [[-1.8, 2.1], [1.8, 2.1], [1.8, 0.3], [1.6, -0.7], [1.0, -1.5], [0, -2.1], [-1.0, -1.5], [-1.6, -0.7], [-1.8, 0.3]];
221
+ // The block's columns, exposed in each band and in the screen on its long faces: eight
222
+ // across each, one to a structural bay. The emblems stand in the screen's first two
223
+ // bays at the south face's west end, the cross west of the shield.
224
+ const faces = chainsOf(planOf(block)).filter(({ length }) => length > 50);
225
+ faces.forEach((chain) => {
226
+ const plan = planOf(block), run = plan[chain.runs[0]], last = plan[chain.runs.at(-1)];
227
+ const a = run.at(0), b = last.at(last.length), n = run.normal(0), length = Math.hypot(b[0] - a[0], b[1] - a[1]);
228
+ const t = [(b[0] - a[0]) / length, (b[1] - a[1]) / length];
229
+ const at = (s) => [a[0] + t[0] * s, a[1] + t[1] * s];
230
+ const pitch = length / 8;
231
+ for (let i = 0; i < 8; i += 1) {
232
+ for (const [lo, hi] of [...h.bands, [h.roof + 0.4, h.top - 0.3]])
233
+ kit.box(bars, at((i + 0.5) * pitch), n, 0.45, 0.07, 0.4, lo + 0.1, hi - 0.1);
234
+ }
235
+ // The south face runs west to east; the emblems stand in its first two bays.
236
+ if (n[1] > 0.9)
237
+ [cross, shield].forEach((shape, i) => emblem(at((i + 1) * pitch), t, n, h.roof + 6.7, shape));
238
+ });
239
+ const model = kit.finish({ height: h.top, outlines: [shell, mullions], opacity: 0.16 });
240
+ const glass = model.building.getObjectByName(wall.name);
241
+ if (!(glass instanceof THREE.Mesh) || !(glass.material instanceof THREE.MeshToonMaterial))
242
+ throw new Error("Blue Cross window lighting requires its glass mesh and toon material");
243
+ model.illumination = createWindowIllumination(glass.geometry, glass.material, windows, windowColumns, messageTopFloors);
244
+ model.building.position.set(offset[0], 0, offset[1]);
245
+ model.building.userData["geography"] = record;
246
+ model.building.userData["geographicDetail"] = { levels: h, source: "docs/blue-cross-reference.md" };
247
+ return model;
248
+ }
@@ -0,0 +1,20 @@
1
+ import type { BuildingModel, Plan } from "./building-kit.js";
2
+ import type { GeoBuilding } from "./skyline-geography-data.js";
3
+ export declare const borgWarnerLevels: Readonly<{
4
+ neighbour: 31.1;
5
+ fascia: number;
6
+ roof: 73.2;
7
+ penthouse: 78.6;
8
+ top: 83.5;
9
+ }>;
10
+ export declare const borgWarnerPalette: Readonly<{
11
+ glass: number[];
12
+ lit: 8026746;
13
+ dim: 4868682;
14
+ spandrel: 2960685;
15
+ mullion: 6184542;
16
+ corner: 5395026;
17
+ cap: 6974058;
18
+ plain: 5789784;
19
+ }>;
20
+ export declare function createBorgWarnerGeographicBuilding(record: GeoBuilding, projectPlan: (coordinates: [number, number][]) => Plan, offset: [number, number]): BuildingModel;
@@ -0,0 +1,143 @@
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 Borg-Warner Building, 200 South Michigan Avenue (A. Epstein & Sons with William
5
+ // Lescaze, 1958): the geographic layout's model, on the mapped outline 124873918. The
6
+ // original layout has no model of it; the drawing shows it across Adams from Peoples Gas.
7
+ //
8
+ // A curtain wall of aluminium mullions and dark spandrels wraps the Michigan, Adams and
9
+ // south fronts between clad corner columns, under a deep fascia. On the roof, an office
10
+ // penthouse stands flush with the Michigan and south fronts, and a taller block behind it.
11
+ // The roof and the penthouse are the City's 240 and 258 ft, and the block the Skyscraper
12
+ // Center's 83.5 m: each is the published height nearest its drawn reading. The drawn
13
+ // front's top reads 76 m, so heights read on the drawing are scaled to meet the roof; see
14
+ // docs/borg-warner-reference.md. Units are meters; +x is east, +z is south.
15
+ const drawn = (height) => height * 73.2 / 76;
16
+ export const borgWarnerLevels = Object.freeze({
17
+ neighbour: 31.1, // Symphony Center's roof, the City's 102 ft, over the south front's plain wall
18
+ fascia: drawn(72.9), // the fascia over the top floor's windows
19
+ roof: 73.2, // the roof, the City's 240 ft
20
+ penthouse: 78.6, // the office penthouse's top, the City's 258 ft
21
+ top: 83.5, // the block behind it, the Skyscraper Center's height
22
+ });
23
+ const h = borgWarnerLevels;
24
+ // The skins' colours: the window tones, and the spandrels, mullions, corner columns, the
25
+ // penthouse's cap and the plain walls, which no window shares.
26
+ export const borgWarnerPalette = Object.freeze({ glass: [0x3a3a3a, 0x3e3e3e, 0x424242, 0x464646], lit: 0x7a7a7a, dim: 0x4a4a4a, spandrel: 0x2d2d2d, mullion: 0x5e5e5e, corner: 0x525252, cap: 0x6a6a6a, plain: 0x585858 });
27
+ const palette = borgWarnerPalette;
28
+ const color = (hex) => new THREE.Color(hex);
29
+ const glassTones = palette.glass.map(color);
30
+ const litGlass = color(palette.lit), dimGlass = color(palette.dim);
31
+ const spandrel = color(palette.spandrel), mullion = color(palette.mullion), cornerColumn = color(palette.corner), cap = color(palette.cap), plain = color(palette.plain);
32
+ const core = color(0x262626), roofing = color(0x3c3c3c);
33
+ function paneColor(row, bay, wall) {
34
+ const hash = (Math.imul(row + 67, 0x9e3779b1) ^ Math.imul(bay + 97, 0x85ebca77) ^ Math.imul(wall + 79, 0xc2b2ae3d)) >>> 0;
35
+ const value = hash % 101;
36
+ if (value < 4)
37
+ return litGlass;
38
+ if (value < 8)
39
+ return dimGlass;
40
+ return glassTones[value % glassTones.length];
41
+ }
42
+ const rows = [{ lo: 0.25, hi: drawn(0.5), kind: "curb", floor: 1 }];
43
+ const push = (hi, kind, floor) => rows.push({ lo: rows.at(-1).hi, hi, kind, floor });
44
+ const spandrelTop = (k) => 70.6 - k * 3.21;
45
+ push(drawn(spandrelTop(20) - 0.98), "glass", 1);
46
+ for (let k = 20; k >= 0; k -= 1) {
47
+ push(drawn(spandrelTop(k)), "spandrel", 22 - k);
48
+ push(k > 0 ? drawn(spandrelTop(k - 1) - 0.98) : h.fascia, "glass", 22 - k);
49
+ }
50
+ push(h.roof, "fascia", 22);
51
+ // The row that Symphony Center's roof crosses, split there.
52
+ const crossed = rows.findIndex((row) => row.lo < h.neighbour && row.hi > h.neighbour);
53
+ rows.splice(crossed, 1, { ...rows[crossed], hi: h.neighbour }, { ...rows[crossed], lo: h.neighbour });
54
+ // The penthouse's rows: a curb, its windows, and a light cap, as drawn over the fascia.
55
+ const penthouseRows = [
56
+ { lo: h.roof, hi: h.roof + 0.3, kind: "curb", floor: 23 },
57
+ { lo: h.roof + 0.3, hi: h.penthouse - 0.6, kind: "glass", floor: 23 },
58
+ { lo: h.penthouse - 0.6, hi: h.penthouse, kind: "fascia", floor: 23 },
59
+ ];
60
+ export function createBorgWarnerGeographicBuilding(record, projectPlan, offset) {
61
+ const kit = createBuilder(record.name, record.id);
62
+ const shell = kit.batch("Borg-Warner Building · shell", kit.material(0xffffff, { vertexColors: true }));
63
+ const wall = kit.batch("Borg-Warner Building · curtain wall", kit.material(0xffffff, { vertexColors: true }));
64
+ const block = kit.batch("Borg-Warner Building · rooftop block", kit.material(0x565656));
65
+ const roofTop = kit.batch("Borg-Warner Building · penthouse", kit.material(0x3c3c3c));
66
+ const lot = orient(polygonOf(projectPlan(record.footprint.coordinates)));
67
+ // The lot's south-east corner, with its south wall running west along Symphony Center
68
+ // and its Michigan front north to Adams.
69
+ const corner = lot.reduce((best, p) => (p[0] + p[1] > best[0] + best[1] ? p : best));
70
+ const northEast = lot.reduce((best, p) => (p[0] - p[1] > best[0] - best[1] ? p : best));
71
+ const southWest = lot.reduce((best, p) => (p[1] - p[0] > best[1] - best[0] ? p : best));
72
+ 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]; };
73
+ const west = unit(southWest), north = unit(northEast);
74
+ const at = (a, b) => [corner[0] + west[0] * a + north[0] * b, corner[1] + west[1] * a + north[1] * b];
75
+ const rect = (a0, a1, b0, b1) => orient([at(a0, b0), at(a1, b0), at(a1, b1), at(a0, b1)]);
76
+ planOf(lot).forEach((run) => {
77
+ const a = run.at(0), b = run.at(run.length), n = run.normal(0);
78
+ 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);
79
+ });
80
+ paintedSlab(kit, shell, lot, 0, false, roofing);
81
+ paintedSlab(kit, shell, lot, h.roof, true, roofing);
82
+ // On the roof: the office penthouse, 39 m along the south front and 22 m up Michigan, as
83
+ // the drawn penthouse meets the projected fronts. It stands 5 cm inside the south front
84
+ // and 12 cm inside the line between Michigan's corners, which the mapped wall stands up to
85
+ // 9.5 cm inside. Behind it, the taller block stands 14 to 27.4 m from Michigan and 1 to
86
+ // 28.3 m from the south front, as its drawn corners project, sunk 10 cm into the roof so
87
+ // that its floor lies clear of the penthouse's. Both footprints are estimates.
88
+ const penthouse = rect(0.12, 39, 0.05, 22);
89
+ kit.prism(roofTop, planOf(penthouse), [h.roof, h.penthouse]);
90
+ kit.prism(block, planOf(rect(14, 27.4, 1, 28.3)), [h.roof - 0.1, h.top]);
91
+ const paints = { spandrel, mullion, corner: cornerColumn, cap, plain };
92
+ const skin = (plan, keep, span, seed, openings) => {
93
+ const heights = [span[0].lo, ...span.map((row) => row.hi)];
94
+ chainsOf(plan).forEach((chain, index) => {
95
+ if (!chain.runs.every((run) => keep(plan[run])))
96
+ return;
97
+ const first = chain.runs[0], last = chain.runs.at(-1);
98
+ const s0 = turnsAt(plan, first)[0] > 0 ? 0.09 : 0.02, s1 = chain.length - (turnsAt(plan, last)[1] > 0 ? 0.09 : 0.02);
99
+ const [columns, cell] = openings(chain.length);
100
+ chainSkin(kit, wall, plan, chain, 0, chain.runs.length - 1, s0, s1, columns, heights, 0.02, 0.07, (bay, r) => {
101
+ const row = span[r], shows = cell((columns[bay - 1] + columns[bay]) / 2, row);
102
+ return shows === "glass" ? paneColor(row.floor, bay, seed * 8 + index) : paints[shows];
103
+ }, plain);
104
+ });
105
+ };
106
+ const near = (list, x) => Math.min(...list.map((c) => Math.abs(x - c)));
107
+ // The curtain wall: corner columns 1.2 m wide at each end, and mullions 12 cm wide about
108
+ // 1.33 m apart between them, as drawn: 22 bays on Michigan. Below `plainUnder` the wall
109
+ // is a neighbour's, plain.
110
+ const curtain = (plainUnder = 0) => (length) => {
111
+ const count = Math.max(1, Math.round((length - 2.4) / 1.33)), pitch = (length - 2.4) / count;
112
+ const mullions = Array.from({ length: count - 1 }, (_, k) => 1.2 + (k + 1) * pitch);
113
+ const cuts = [1.2, length - 1.2, ...mullions.flatMap((m) => [m - 0.06, m + 0.06])];
114
+ return [[0, ...cuts.sort((p, q) => p - q), length], (mid, row) => {
115
+ if (row.hi <= plainUnder + 1e-6)
116
+ return "plain";
117
+ if (mid < 1.2 || mid > length - 1.2)
118
+ return "corner";
119
+ if (near(mullions, mid) < 0.06)
120
+ return "mullion";
121
+ return row.kind === "glass" ? "glass" : row.kind === "fascia" ? (row.floor === 23 ? "cap" : "spandrel") : "spandrel";
122
+ }];
123
+ };
124
+ // The alley's windows, which the drawing does not show: 1.6 m wide, spread evenly about
125
+ // 3.2 m apart, one to each floor above the lobby.
126
+ const alley = (length) => {
127
+ const count = Math.max(1, Math.round(length / 3.2)), centres = Array.from({ length: count }, (_, k) => length * (k + 0.5) / count);
128
+ return [[0, ...centres.flatMap((c) => [c - 0.8, c + 0.8]), length], (mid, row) => (row.kind === "glass" && row.floor > 1 && near(centres, mid) < 0.8 ? "glass" : "plain")];
129
+ };
130
+ const facing = (x, z) => (run) => run.normal(0)[0] * x + run.normal(0)[1] * z > 0.9;
131
+ // Michigan and Adams carry the curtain wall to the roof; the south front above Symphony
132
+ // Center's 102 ft roof, plain below; the alley has windows.
133
+ skin(planOf(lot), (run) => facing(1, 0)(run) || facing(0, -1)(run), rows, 0, curtain());
134
+ skin(planOf(lot), facing(0, 1), rows, 1, curtain(h.neighbour));
135
+ skin(planOf(lot), facing(-1, 0), rows, 2, alley);
136
+ // The penthouse's glass between the same mullions, under its cap.
137
+ skin(planOf(penthouse), () => true, penthouseRows, 3, curtain());
138
+ const model = kit.finish({ height: h.top, outlines: [shell, roofTop, block], opacity: 0.16 });
139
+ model.building.position.set(offset[0], 0, offset[1]);
140
+ model.building.userData["geography"] = record;
141
+ model.building.userData["geographicDetail"] = { levels: h, source: "docs/borg-warner-reference.md" };
142
+ return model;
143
+ }
@@ -0,0 +1,10 @@
1
+ import type { BuildingModel, Plan } from "./building-kit.js";
2
+ import type { GeoBuilding } from "./skyline-geography-data.js";
3
+ export declare const buckinghamLevels: Readonly<{
4
+ lowestFloor: 18.94;
5
+ pitch: 2.5456;
6
+ capFoot: 117.4;
7
+ roof: 119;
8
+ top: 121.9;
9
+ }>;
10
+ export declare function createBuckinghamGeographicBuilding(record: GeoBuilding, projectPlan: (coordinates: [number, number][]) => Plan, offset: [number, number]): BuildingModel;