@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,69 @@
1
+ import { polygonOf } from "./building-kit.js";
2
+ import { buildCrainTower, crainFloors, crainMainRoof } from "./crain-tower.js";
3
+ // Crain Communications Building at 150 North Michigan Avenue: the geographic layout's
4
+ // model, on the three mapped OpenStreetMap parts. Part 284816228 is the south-west half,
5
+ // 284816229 the north-east half, and 284816227 the wedge between them whose 152.5 m top is
6
+ // the slot's floor. The parts' outlines are kept exactly; the tower itself is the shared
7
+ // generator in crain-tower.ts, the same one the solo study's model uses.
8
+ //
9
+ // The roofs keep OSM's 133° downhill bearing. Their heights come from the photograph the
10
+ // drawing was traced from, not from OSM's roof tags. Those put the south-west peak 5 m below
11
+ // the north-east one and fall 75 and 73 m, 54° and 56°. Measured through the solved skyline
12
+ // camera, both peaks stand within half a metre of each other at the published 177.4 m, and
13
+ // both roofs fall 1.225 m per metre, about 51°. Those planes end the north-east half about
14
+ // 6.9 m above the south-west half's foot, where the drawing shows 7.3 m. See
15
+ // docs/crain-reference.md. Units are meters; +x is east, +z is south.
16
+ export const crainGeographicLevels = Object.freeze({
17
+ tip: 177.4,
18
+ fall: 1.225,
19
+ bearing: 133,
20
+ mainRoof: crainMainRoof,
21
+ floors: 41,
22
+ });
23
+ const h = crainGeographicLevels;
24
+ const angle = h.bearing * Math.PI / 180;
25
+ const downhill = [Math.sin(angle), -Math.cos(angle)];
26
+ // One-based facade rows above the lobby, not occupied floor numbers.
27
+ const crownBand = (row) => {
28
+ const level = crainFloors.lobbyTop + (row - 1) * crainFloors.pitch;
29
+ return [level + crainFloors.sill, level + crainFloors.head];
30
+ };
31
+ // Height falls along the downhill bearing from the part's highest corner.
32
+ function roofOf(corners) {
33
+ const along = ([x, z]) => x * downhill[0] + z * downhill[1];
34
+ const top = Math.min(...corners.map(along));
35
+ return (point) => h.tip - h.fall * (along(point) - top);
36
+ }
37
+ export function createCrainGeographicBuilding(record, projectPlan, offset) {
38
+ const corners = (part) => polygonOf(projectPlan(part.coordinates));
39
+ const volumes = record.parts.map((part) => {
40
+ const outline = corners(part);
41
+ return part.roofSlope
42
+ ? { corners: outline, roof: roofOf(outline), glazed: true }
43
+ : { corners: outline, roof: () => part.top, glazed: false };
44
+ });
45
+ // Epstein's exterior photographs show downslope mullions and level crossbars,
46
+ // rather than the drawing model's grid aligned with the street axes.
47
+ const south = polygonOf(projectPlan(record.footprint.coordinates));
48
+ const southWest = south.reduce((best, p) => (p[1] - p[0] > best[1] - best[0] ? p : best));
49
+ const model = buildCrainTower({
50
+ name: record.name,
51
+ id: record.id,
52
+ base: 0,
53
+ grid: { origin: southWest, axis: downhill, edgeInset: 0.1 },
54
+ // Two louver bands beneath the slot, three dark crown strips, then a solid
55
+ // aluminum tip. Counts are observed; elevations follow the estimated 3.5 m
56
+ // story rhythm. See the dated evidence and uncertainties in the audit.
57
+ crown: {
58
+ officeTop: crownBand(40)[0],
59
+ louvers: [crownBand(40), crownBand(41)],
60
+ // Continue the rhythm as virtual rows, not additional occupied floors.
61
+ shadowStrips: [crownBand(43), crownBand(44), crownBand(45)],
62
+ },
63
+ volumes,
64
+ });
65
+ model.building.position.set(offset[0], 0, offset[1]);
66
+ model.building.userData["geography"] = record;
67
+ model.building.userData["geographicDetail"] = { levels: h, floors: h.floors, source: "docs/crain-reference.md" };
68
+ return model;
69
+ }
@@ -0,0 +1,10 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="2718 1556 515 1156" role="img" aria-labelledby="title"><title id="title">Crain Communications Building from the existing skyline illustration</title><g id="building-crain-communications" class="interactive-building" aria-label="Crain Communications Building">
2
+ <path stroke="#000000" stroke-miterlimit="10" d="m 2748.3486,1850.1344 v 829.8635 l 234.872,2.0029 4.0058,-545.4533 z" id="path468" style="stroke-width:1.33526" />
3
+ <path fill="#a8a8a8" stroke="#ececec" stroke-miterlimit="10" d="m 2977.8796,2657.0315 -229.531,-0.2671 v -12.4179 l 232.335,-3.0711 v -10.6821 l -232.335,2.1365 v -15.4891 l 232.335,-3.6052 v -11.0826 l -232.335,2.1364 v -13.3526 l 232.335,-3.4717 v -12.5514 l -232.335,1.8693 v -12.9519 l 232.335,-5.3411 v -12.952 l -232.335,4.5399 v -13.8867 l 232.335,-4.0058 0.5341,-11.4832 h -232.8691 v -12.952 l 232.335,-4.0058 v -12.9521 l -232.335,3.6052 v -14.2871 l 232.335,-3.4718 v -12.0173 l -232.335,3.4717 v -14.1538 l 232.335,-4.5399 v -13.3525 l -232.335,5.8751 v -13.8867 l 232.335,-7.0769 v -10.6821 l -232.335,4.4064 v -15.0884 l 232.335,-4.4064 v -15.2219 l -232.335,5.8751 v -13.3525 l 232.335,-4.9405 v -12.0173 l -232.335,3.6052 v -13.8867 l 232.335,-6.1423 v -12.5514 l -232.335,7.2105 v -14.6879 l 232.335,-5.8751 v -16.4238 l -232.335,7.611 v -15.6224 l 232.335,-4.9405 v -11.0827 l -232.335,5.3411 v -14.6879 l 232.335,-7.0769 v -12.5514 l -232.335,6.6762 v -16.8242 l 232.335,-3.6052 v -13.7532 l -232.335,6.1422 v -14.1537 l 232.335,-5.8751 v -12.8185 l -232.335,4.8069 v -14.1538 l 232.335,-5.8751 v -14.1537 l -232.335,8.4121 v -15.0884 l 233.6703,-6.6763 -0.2671,-7.878 -5.7416,-6.8098 -227.6616,8.8126 v -14.1537 l 217.2466,-6.6763 -10.9492,-12.8185 -206.2974,9.2133 v -16.0231 l 194.4137,-7.0768 -11.6168,-13.6197 -182.7969,7.3439 v -15.0884 l 171.1801,-5.7417 -10.2815,-12.0172 -160.8986,5.7415 v -15.2219 l 148.6143,-4.9404 -11.3497,-13.2191 -137.2646,5.341 v -15.6226 l 125.5143,-3.4716 -11.0827,-12.952 -114.4316,4.807 v -13.7532 l 102.2808,-5.3411 -9.4804,-11.0826 -92.8004,4.0057 v -15.2219 l 79.3143,-4.4063 -10.1479,-11.8839 -69.1664,3.8723 v -14.6879 l 57.5496,-2.6705 -9.0797,-10.5485 -48.4699,1.6023 v -15.0884 l 36.319,-2.1365 -11.7503,-12.4178 -24.5687,1.2016 v -14.6877 l 12.2843,-0.8012 -9.8809,-11.6168 h -2.4034 v -19.3613 l 242.75,282.0068 -3.8722,525.5578 z" id="path470" style="stroke-width:1.33526" />
4
+ <path fill="#343434" stroke="#c1c1c1" stroke-miterlimit="10" d="m 2931.6475,1594.0105 -10.65,-7.5897 -172.6489,260.509 238.8778,280.2389 v -57.1171 l 71.1693,2.27 140.2021,-219.2495 -230.1986,-262.512 -8.8127,11.8838 16.5626,156.0304 -24.9748,-0.8732 z" id="path472" style="stroke-width:1.33526" />
5
+ <path fill="none" stroke="#727272" stroke-miterlimit="10" d="m 2971.2473,1658.004 37.1817,-3.3087 m -126.0832,11.2215 51.0923,-4.5479 m -60.1654,17.9192 60.7637,-5.4078 m -69.5965,18.05 71.3296,-5.556 m -83.7915,17.6788 85.3804,-6.9073 m -91.4251,22.598 92.8552,-7.5122 m -103.7549,23.8971 104.2289,-8.4335 m -110.2202,21.0704 112.7946,-8.0957 m -112.7946,19.6884 114.0792,-8.1878 m -126.3677,20.8127 127.6695,-9.1639 m -157.5272,106.977 391.1361,-14.3688 m -401.8595,4.6107 178.2437,-7.8006 m -184.8239,-2.8708 183.514,-8.8474 m -183.514,-2.3514 182.4084,-7.5469 m -172.3005,-2.8468 171.1322,-7.6083 m -164.3477,-3.9537 163.194,-6.3705 m -155.6164,-4.9485 154.2731,-7.0728 m -146.4378,-3.7227 144.6192,-8.6445 m -138.9057,-2.4128 138.1499,-8.2586 m -150.8028,104.2757 377.1079,-13.9268 m -370.1072,24.4033 362.7699,-12.4713 m -349.6295,23.0371 344.2551,-11.8356 m -347.7588,22.8929 344.2551,-11.8357 m -298.4438,64.1045 265.8221,-9.1386 m -279.8729,-11.3269 294.3791,-10.12 m -292.1266,20.8021 284.6599,-9.7849 m -273.9778,-12.9146 287.1541,-9.8716 m -273.1393,53.8604 244.4312,-8.4028 m -239.5468,19.2931 235.2513,-8.0863 m -227.667,20.0636 218.3228,-7.5054 m -207.6848,19.2037 200.2474,-7.4521 m -187.3021,18.6215 179.6765,-6.177 m -163.6534,19.4601 151.6801,-5.2155 m -144.6699,16.4064 136.1497,-4.6801 m -127.2423,15.8855 118.6992,-4.0805 m -85.0426,-382.7614 45.7807,-4.0752 m -43.6242,17.2049 53.1606,-4.7322 m -51.9709,15.3835 62.3646,-5.552 m -60.6675,20.7352 72.4432,-6.448 m -71.0759,21.8035 83.73,-7.4521 m -81.842,21.2293 93.1237,-8.288 m -91.8258,19.9021 101.1846,-9.0063 m -99.876,20.7165 110.3699,-9.9396 m -108.8945,23.14 118.3761,-10.6607 m -117.5776,19.5241 124.7012,-11.2295 m -123.5983,23.4805 133.4444,-12.0173 m -131.9008,25.0428 141.404,-12.7344 m -139.8431,23.3804 151.6547,-10.8717 m -149.8975,31.388 168.7006,-10.8384 m -166.7231,23.7423 176.2462,-9.3227 m -179.3279,-13.9602 162.3755,-12.3071 m -328.1186,105.4535 319.4795,-10.9826 m -258.7626,-261.0484 47.1881,-4.1995 m -37.4888,-11.3937 34.6701,-3.0858 m 40.1258,12.0441 30.2437,-2.692 m -31.6217,-12.8011 30.2437,-2.6918 m 151.6707,189.5039 9.7007,79.3477 m 2.9001,-68.5588 5.8337,54.0086 m -31.1702,-75.9509 12.924,105.7084 m -26.3194,-125.0843 16.7669,137.1364 m -32.9755,-155.2398 21.2199,173.5688 m -35.65,-188.7988 25.235,206.4016 m -41.7549,-226.0192 30.3585,248.3154 m -48.2016,-273.7348 34.7928,296.0964 m -51.2312,-314.3748 39.3354,334.7642 m -55.3798,-352.0932 43.5508,370.6372 m -61.6343,-393.1336 49.5248,421.4812 m -66.1047,-440.8264 52.0311,442.8119 m -66.6361,-460.8046 54.0539,460.0235 m -65.9096,-467.0977 52.1432,466.581 m -101.8042,-460.4614 53.2475,476.4751 m -146.4299,-336.3357 21.8369,195.3898 m -9.9611,-216.5831 26.6998,238.9086 m -14.1858,-257.0387 30.6215,274.0005 m -19.2397,-292.0011 34.5631,309.2767 m -21.6352,-329.8544 38.0282,348.9446 m 16.6187,-424.8954 56.3786,504.4822 m -98.5635,-445.4438 44.3293,378.7142 m -33.017,-397.3077 43.7645,420.5092 m -34.5178,-435.6978 50.5088,451.9639 m -142.5175,-309.039 18.4052,164.6988 m -30.6615,-146.0639 14.087,126.0524 m -26.557,-105.8313 9.6819,86.6356 m -20.4561,-69.5056 5.9032,52.8228 m 185.876,-9.4856 22.9237,205.1252 m -10.3255,-206.5339 23.1374,207.0359" id="path474" style="stroke-width:1.33526" />
6
+ <path fill="#5e5e5e" stroke="#ebebeb" stroke-miterlimit="10" d="m 2921.6077,1589.9766 7.2585,5.3691 -161.9001,251.9193 216.7566,255.9251 v 7.5656 h -4.4505 l -224.8936,-263.229 z m 46.7888,4.3476 -5.2301,7.6711 216.3946,251.946 -128.6294,209.191 1.7799,5.7856 5.341,-1.7798 135.7518,-214.1022 z" id="path476" style="stroke-width:1.33526" />
7
+ <path data-name="Behind" d="m 3058.3957,2072.3216 -71.1693,-2.27 2.6117,604.4103 211.943,1.3353 -3.1833,-822.7251 c 0,0 -84.4764,136.1683 -85.3657,133.4978 z" id="path478" style="stroke-width:1.33526" />
8
+ <path data-name="Front" d="m 3192.0231,1886.4321 6.5747,-11.2015 v -22.1585 l -139.2394,217.7446 -72.5192,0.3272 v 585.7326 h 69.4334 l 142.3252,5.3411 v -14.2419 l -142.3252,-5.341 c 0,0 -64.983,2.6706 -64.983,0 v -11.5727 l 64.983,-0.8906 142.3252,5.3411 V 2620.38 l -142.3252,-5.3411 -64.983,0.8906 v -15.1338 l 64.983,2.6705 142.3252,4.4517 v -15.1337 l -142.3252,-4.4505 -64.983,0.8906 v -14.2432 h 64.983 l 142.3252,4.4504 v -13.3526 l -142.3252,-6.2302 -64.983,2.6705 v -16.9138 l 64.983,1.7799 142.3252,5.3411 v -13.3526 l -142.3252,-6.2304 -64.983,0.8893 v -15.1325 l 64.983,0.8907 142.3252,5.341 v -12.4633 l -142.3252,-7.121 -64.983,0.8907 v -14.2432 h 64.983 l 142.3252,6.2316 v -13.3525 l -142.3252,-6.2317 h -64.983 v -14.2432 l 64.983,0.8905 142.3252,5.3411 v -13.3525 l -142.3252,-7.121 -64.983,0.8893 v -14.2419 l 64.983,1.7799 142.3252,5.341 v -15.1324 l -142.3252,-5.3411 -64.983,0.8893 v -16.0231 l 64.983,0.8906 142.3252,8.0116 v -12.462 l -142.3252,-8.9022 -64.983,0.8907 v -15.1339 l 64.983,0.8907 142.3252,8.0115 v -12.462 l -142.3252,-8.9021 -64.983,0.8905 v -16.023 l 64.983,1.7798 142.3252,6.2317 v -13.3526 l -142.3252,-6.2316 c 0,0 -64.983,1.7799 -64.983,-0.8907 v -14.2418 h 64.983 l 142.3252,8.1223 v -12.6849 c 0,0 -140.3223,-8.0115 -142.3252,-8.0115 h -64.983 v -15.3556 h 64.983 l 142.3252,8.3454 v -12.685 l -142.3252,-8.3453 -64.983,0.3338 v -13.0188 l 64.983,-0.3338 142.3252,7.3439 v -13.3526 l -142.3252,-8.3453 -64.983,-0.6676 v -14.354 c 0,0 60.6434,0.6676 64.983,2.0028 4.3396,1.3353 142.3252,8.0116 142.3252,8.0116 v -13.6864 l -142.3252,-8.3454 -64.983,-1.0015 v -11.0158 l 64.983,-1.3353 142.3252,8.6791 v -14.6877 l -142.3252,-9.0131 -64.983,-0.3338 v -12.0173 h 64.983 l 142.3252,8.3453 v -12.6849 l -142.3252,-10.0144 h -64.983 v -14.0202 h 64.983 c 3.672,0 142.3252,10.0144 142.3252,10.0144 v -14.0202 l -142.3252,-8.6792 -64.983,0.3338 v -14.3541 l 64.983,0.3339 142.3252,9.6806 v -13.6864 l -142.3252,-9.6806 -64.983,-1.0015 v -13.3526 l 64.983,1.0015 142.3252,8.3453 v -13.6864 l -142.3252,-9.3467 -64.983,0.3338 v -13.3526 l 64.983,0.3337 142.3252,9.0131 v -13.3526 l -121.5179,-9.6807 6.6763,-11.3496 114.8416,8.0115 v -15.3554 c 0,0 -106.6524,-5.3411 -104.9059,-8.0116 1.7465,-2.6705 5.086,-10.0144 5.086,-10.0144 l 99.8199,4.6734 v -12.685 l -89.4717,-7.6778 8.0116,-12.0172 81.4601,6.0086 v -13.3526 l -70.778,-5.6749 6.3425,-10.3482 64.4355,3.0043 v -14.6878 l -56.424,-4.3396 7.6778,-11.6835 48.7462,2.0029 v -14.354 l -40.4009,-1.3353 6.6764,-12.3512 33.7245,1.3353 v -13.6864 l -23.3763,-1.0015 8.3453,-12.0173 15.031,1.3352 v -16.1338 z" fill="#7a7a7a" stroke="#dbdbdb" stroke-miterlimit="10" id="path480" style="stroke-width:1.33526" />
9
+ </g>
10
+ </svg>
@@ -0,0 +1,378 @@
1
+ import * as THREE from "../vendor/three-r186.js";
2
+ import { createBuilder, inside, line } from "./building-kit.js";
3
+ // The curtain wall's rhythm. The drawing and the photograph both give a 3.5 m band pitch,
4
+ // so the 41 published floors, above a taller lobby, fit under the 152.5 m main roof. Each
5
+ // floor is a ribbon of glass between sill and head, with white spandrels covering the slab.
6
+ export const crainFloors = Object.freeze({
7
+ lobbyTop: 7.0,
8
+ pitch: 3.5,
9
+ sill: 0.9,
10
+ head: 2.55,
11
+ module: 1.524,
12
+ lobbyGlass: [0.45, 5.9],
13
+ });
14
+ export const crainMainRoof = 152.5;
15
+ const orient = (corners) => {
16
+ const area = corners.reduce((sum, [x, z], i) => { const [x2, z2] = corners[(i + 1) % corners.length]; return sum + x * z2 - x2 * z; }, 0);
17
+ return area < 0 ? corners : [...corners].reverse();
18
+ };
19
+ const planOf = (corners) => corners.map((corner, i) => line(corner, corners[(i + 1) % corners.length]));
20
+ // The ribbon glazing's tones: mostly dark reflective glass, with a few lit and dim panes.
21
+ const paneTones = [0x232323, 0x2a2a2a, 0x303030, 0x1f1f1f].map((hex) => new THREE.Color(hex));
22
+ const litPane = new THREE.Color(0x8a8a8a), dimPane = new THREE.Color(0x565656), mullionTone = new THREE.Color(0x585858);
23
+ function paneColor(row, module, wall) {
24
+ const hash = (Math.imul(row + 29, 0x9e3779b1) ^ Math.imul(module + 41, 0x85ebca77) ^ Math.imul(wall + 13, 0xc2b2ae3d)) >>> 0;
25
+ const value = hash % 97;
26
+ if (value < 3)
27
+ return litPane;
28
+ if (value < 6)
29
+ return dimPane;
30
+ return paneTones[value % paneTones.length];
31
+ }
32
+ // A plane's upward normal from a height function sampled around a point.
33
+ function slopeNormal(roof, [x, z]) {
34
+ const dx = roof([x + 1, z]) - roof([x, z]), dz = roof([x, z + 1]) - roof([x, z]);
35
+ const length = Math.hypot(dx, 1, dz);
36
+ return [-dx / length, 1 / length, -dz / length];
37
+ }
38
+ function faceNormal(a, b, c) {
39
+ const u = [b[0] - a[0], b[1] - a[1], b[2] - a[2]], v = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
40
+ const n = [u[1] * v[2] - u[2] * v[1], u[2] * v[0] - u[0] * v[2], u[0] * v[1] - u[1] * v[0]];
41
+ const length = Math.hypot(...n);
42
+ return [n[0] / length, n[1] / length, n[2] / length];
43
+ }
44
+ // Where a line crosses a polygon, as the parameter intervals that lie inside it.
45
+ function clipLine(polygon, origin, direction) {
46
+ const hits = [];
47
+ for (let i = 0; i < polygon.length; i += 1) {
48
+ const a = polygon[i], b = polygon[(i + 1) % polygon.length];
49
+ const e = [b[0] - a[0], b[1] - a[1]];
50
+ const denominator = direction[0] * e[1] - direction[1] * e[0];
51
+ if (Math.abs(denominator) < 1e-12)
52
+ continue;
53
+ const w = [a[0] - origin[0], a[1] - origin[1]];
54
+ const t = (w[0] * e[1] - w[1] * e[0]) / denominator, u = (w[0] * direction[1] - w[1] * direction[0]) / denominator;
55
+ if (u >= 0 && u < 1)
56
+ hits.push(t);
57
+ }
58
+ hits.sort((p, q) => p - q);
59
+ const spans = [];
60
+ for (let i = 0; i + 1 < hits.length; i += 2)
61
+ spans.push([hits[i], hits[i + 1]]);
62
+ return spans;
63
+ }
64
+ export function buildCrainTower(form) {
65
+ const kit = createBuilder(form.name, form.id);
66
+ const shell = kit.batch("Crain · aluminum spandrels", kit.material(0xc4c4c4));
67
+ const ribbons = kit.batch("Crain · ribbon glazing", kit.material(0xffffff, { vertexColors: true }));
68
+ const glazing = kit.batch("Crain · sloped glazing", kit.material(0x333333));
69
+ const grid = kit.batch("Crain · glazing grid", kit.material(0x727272));
70
+ const lights = kit.batch("Crain · diamond outline lights", kit.material(0xf0f0f0));
71
+ const coping = kit.batch("Crain · coping", kit.material(0x5c5c5c));
72
+ const deck = kit.batch("Crain · slot floor", kit.material(0x4a4a4a));
73
+ const vents = form.crown ? kit.batch("Crain · mechanical louvers", kit.material(0xffffff, { vertexColors: true })) : ribbons;
74
+ const blades = form.crown ? kit.batch("Crain · louver blades", kit.material(0xffffff, { vertexColors: true })) : ribbons;
75
+ const shadowStrips = form.crown ? kit.batch("Crain · crown shadow strips", kit.material(0xffffff, { vertexColors: true })) : ribbons;
76
+ const ventTone = new THREE.Color(0x303030), bladeTone = new THREE.Color(0x696969), shadowTone = new THREE.Color(0x252525);
77
+ const base = form.base;
78
+ const volumes = form.volumes.map((volume) => {
79
+ const corners = orient(volume.corners);
80
+ return { ...volume, corners, plan: planOf(corners) };
81
+ });
82
+ // Model height from a real height.
83
+ const y = (real) => real - base;
84
+ // A slab of the plan lifted onto a plane `drop` below the roof. kit.slab lays a flat
85
+ // polygon; lifting its vertices keeps each triangle planar, and its winding still faces
86
+ // the way it was laid, so the normal follows the plane, flipped for a downward face.
87
+ const liftedSlab = (target, corners, roof, drop, up) => {
88
+ const start = target.positions.length;
89
+ kit.slab(target, corners, 0, up);
90
+ const n = slopeNormal(roof, corners[0]), normal = up ? n : [-n[0], -n[1], -n[2]];
91
+ for (let i = start; i < target.positions.length; i += 3) {
92
+ target.positions[i + 1] = y(roof([target.positions[i], target.positions[i + 2]]) - drop);
93
+ target.normals.splice(i, 3, ...normal);
94
+ }
95
+ };
96
+ // The masses: walls, a floor, and a lid a skin's depth under the roof, closed within the
97
+ // spandrels' mesh. The roof itself is a thin closed slab on the lid, glass on the halves
98
+ // and a deck on the slot's floor, so it stays the building's top. Neighbouring volumes
99
+ // keep their own walls on shared edges, facing opposite ways.
100
+ const skin = 0.05;
101
+ for (const volume of volumes) {
102
+ const { corners, plan, roof } = volume;
103
+ const top = volume.glazed ? glazing : deck;
104
+ for (const run of plan) {
105
+ const a = run.at(0), b = run.at(run.length), n = run.normal(0), normal = [n[0], 0, n[1]];
106
+ kit.quad(shell, [[a[0], 0, a[1]], [b[0], 0, b[1]], [b[0], y(roof(b) - skin), b[1]], [a[0], y(roof(a) - skin), a[1]]], [normal]);
107
+ kit.quad(top, [[a[0], y(roof(a) - skin), a[1]], [b[0], y(roof(b) - skin), b[1]], [b[0], y(roof(b)), b[1]], [a[0], y(roof(a)), a[1]]], [normal]);
108
+ }
109
+ kit.slab(shell, corners, 0, false);
110
+ liftedSlab(shell, corners, roof, skin, true);
111
+ liftedSlab(top, corners, roof, skin, false);
112
+ liftedSlab(top, corners, roof, 0, true);
113
+ }
114
+ // A wall is exposed where no neighbour stands against it, and a neighbour's roof covers
115
+ // it up to that roof's height. Probing just outside the wall splits a run into stretches,
116
+ // each facing open air or one neighbour, so a plan needs no corner where that changes.
117
+ const neighbourAt = (self, run, s) => volumes.findIndex((volume, index) => index !== self && inside(volume.corners, run.at(s, 0.3)));
118
+ const stretches = (self, run) => {
119
+ const out = [];
120
+ let start = 0, current = neighbourAt(self, run, Math.min(0.05, run.length / 2));
121
+ for (let s = 0.25; s < run.length - 0.05; s += 0.25) {
122
+ if (neighbourAt(self, run, s) === current)
123
+ continue;
124
+ let low = s - 0.25, high = s;
125
+ for (let i = 0; i < 30; i += 1) {
126
+ const middle = (low + high) / 2;
127
+ if (neighbourAt(self, run, middle) === current)
128
+ low = middle;
129
+ else
130
+ high = middle;
131
+ }
132
+ out.push({ from: start, to: high, neighbour: current });
133
+ start = high;
134
+ current = neighbourAt(self, run, s);
135
+ }
136
+ out.push({ from: start, to: run.length, neighbour: current });
137
+ // Near an acute corner the probe can land outside the neighbour for a few centimetres;
138
+ // no real change of neighbour comes that close to a corner, so a short stretch joins
139
+ // the longer one beside it.
140
+ for (let i = out.findIndex((piece) => piece.to - piece.from < 0.6); out.length > 1 && i >= 0; i = out.findIndex((piece) => piece.to - piece.from < 0.6)) {
141
+ const piece = out[i], before = out[i - 1], after = out[i + 1];
142
+ const into = !after || (before && before.to - before.from >= after.to - after.from) ? before : after;
143
+ into.from = Math.min(into.from, piece.from);
144
+ into.to = Math.max(into.to, piece.to);
145
+ out.splice(i, 1);
146
+ }
147
+ return out;
148
+ };
149
+ // A closed box on a run between two heights that each vary linearly along it. Its front is
150
+ // cut into panes and mullions on the curtain wall's module, counted from the run's start so
151
+ // mullions line up from floor to floor.
152
+ const strip = (run, from, to, b0, b1, t0, t1, row, wall, surface) => {
153
+ const target = surface === "glass" ? ribbons : surface === "vent" ? vents : surface === "blade" ? blades : shadowStrips;
154
+ const back = surface === "blade" ? 0.045 : 0.02, front = surface === "blade" ? 0.13 : 0.08;
155
+ const n = run.normal(0), tangent = [n[1], -n[0]];
156
+ const at = (s, depth, h) => { const p = run.at(s, depth); return [p[0], y(h), p[1]]; };
157
+ const bottom = (s) => b0 + (b1 - b0) * (s - from) / (to - from), top = (s) => t0 + (t1 - t0) * (s - from) / (to - from);
158
+ const outward = [n[0], 0, n[1]];
159
+ const cuts = [from];
160
+ const module = crainFloors.module, half = 0.035;
161
+ if (surface !== "blade")
162
+ for (let k = Math.ceil((from + half) / module); k * module < to - half; k += 1)
163
+ cuts.push(k * module - half, k * module + half);
164
+ cuts.push(to);
165
+ for (let i = 0; i + 1 < cuts.length; i += 1) {
166
+ const sa = cuts[i], sb = cuts[i + 1];
167
+ if (sb - sa < 1e-6)
168
+ continue;
169
+ const mullion = i % 2 === 1;
170
+ const color = mullion ? mullionTone : surface === "glass" ? paneColor(row, Math.floor((sa + sb) / 2 / module), wall)
171
+ : surface === "vent" ? ventTone : surface === "blade" ? bladeTone : shadowTone;
172
+ kit.quad(target, [at(sa, front, bottom(sa)), at(sb, front, bottom(sb)), at(sb, front, top(sb)), at(sa, front, top(sa))], [outward], color);
173
+ }
174
+ const dark = paneTones[0];
175
+ kit.quad(target, [at(to, back, b1), at(from, back, b0), at(from, back, t0), at(to, back, t1)], [[-n[0], 0, -n[1]]], dark);
176
+ // The top and bottom fan out from a back corner through every cut on the front, so each
177
+ // pane's edge meets its partner and the strip closes without T-junctions.
178
+ const ends = cuts.filter((s, i) => i === 0 || s - cuts[i - 1] >= 1e-6);
179
+ const fan = (height, h0, h1, up) => {
180
+ const hinge = at(from, back, h0), front0 = at(from, front, h0);
181
+ const normal = faceNormal(hinge, at(to, back, h1), front0);
182
+ const facing = (normal[1] > 0) === up ? normal : [-normal[0], -normal[1], -normal[2]];
183
+ for (let i = 0; i + 1 < ends.length; i += 1)
184
+ kit.triangle(target, [hinge, at(ends[i], front, height(ends[i])), at(ends[i + 1], front, height(ends[i + 1]))], [facing, facing, facing], dark);
185
+ kit.triangle(target, [hinge, at(to, front, h1), at(to, back, h1)], [facing, facing, facing], dark);
186
+ };
187
+ fan(top, t0, t1, true);
188
+ fan(bottom, b0, b1, false);
189
+ kit.quad(target, [at(from, front, b0), at(from, back, b0), at(from, back, t0), at(from, front, t0)], [[-tangent[0], 0, -tangent[1]]], dark);
190
+ kit.quad(target, [at(to, back, b1), at(to, front, b1), at(to, front, t1), at(to, back, t1)], [[tangent[0], 0, tangent[1]]], dark);
191
+ };
192
+ // Ribbons stop this far under the roof edge, below the deepest fascia hung there, so the
193
+ // two never meet in one plane where a traced wall splits into collinear pieces.
194
+ const underRoof = 0.8;
195
+ // One ribbon on one wall: between the band's sill and head, under the roof, and above any
196
+ // neighbour. The bounds are linear along the run, so splitting it where one bound overtakes
197
+ // another leaves pieces whose bottom and top are each a single linear function.
198
+ const ribbon = (run, clear, roof, cover, g0, g1, row, wall, surface = "glass") => {
199
+ const least = 0.05;
200
+ const from = clear[0], to = run.length - clear[1];
201
+ if (to - from < 0.3)
202
+ return;
203
+ const low = (s) => Math.max(g0, cover ? cover(s) + 0.2 : -Infinity, base);
204
+ const high = (s) => Math.min(g1, roof(s) - underRoof);
205
+ // Each bound is the larger or smaller of linear pieces; split where they trade places.
206
+ const trades = [(s) => roof(s) - underRoof - g1];
207
+ if (cover)
208
+ trades.push((s) => cover(s) + 0.2 - g0, (s) => cover(s) + 0.2 - base);
209
+ const breaks = [from, to];
210
+ for (const f of trades) {
211
+ const fa = f(from), fb = f(to);
212
+ if ((fa > 0) !== (fb > 0))
213
+ breaks.push(from + (to - from) * fa / (fa - fb));
214
+ }
215
+ breaks.sort((p, q) => p - q);
216
+ for (let i = 0; i + 1 < breaks.length; i += 1) {
217
+ let sa = breaks[i], sb = breaks[i + 1];
218
+ // Within a piece the ribbon's height is linear. Where the roof or a neighbour pinches
219
+ // it off, end the piece where it is still `least` tall rather than at a knife edge.
220
+ const ha = high(sa) - low(sa), hb = high(sb) - low(sb);
221
+ if (ha < least && hb < least)
222
+ continue;
223
+ if (ha < least)
224
+ sa += (sb - sa) * (least - ha) / (hb - ha);
225
+ else if (hb < least)
226
+ sb -= (sb - sa) * (least - hb) / (ha - hb);
227
+ if (sb - sa < 0.12)
228
+ continue;
229
+ strip(run, sa, sb, low(sa), low(sb), high(sa), high(sb), row, wall, surface);
230
+ }
231
+ };
232
+ // A fascia hung on a wall just under its roof edge: its top follows the edge, flush with
233
+ // the roof, and it stands out from the wall rather than over the roof, so it never shares
234
+ // the roof's plane and the roof stays the building's top. At a convex corner it runs on by
235
+ // its own width to close the corner with its neighbour.
236
+ const fascia = (target, run, roof, width, drop, extend) => {
237
+ const s0 = -extend[0], s1 = run.length + extend[1], n = run.normal(0), tangent = [n[1], -n[0]];
238
+ const at = (s, depth, lower) => { const p = run.at(s, depth); return [p[0], y(roof(run.at(s)) - lower), p[1]]; };
239
+ const T = [at(s0, 0, 0), at(s0, width, 0), at(s1, width, 0), at(s1, 0, 0)];
240
+ const b = [at(s0, 0, drop), at(s1, 0, drop), at(s1, width, drop), at(s0, width, drop)];
241
+ kit.quad(target, T, [faceNormal(T[0], T[1], T[2])]);
242
+ kit.quad(target, b, [faceNormal(b[0], b[1], b[2])]);
243
+ kit.quad(target, [at(s0, width, drop), at(s1, width, drop), at(s1, width, 0), at(s0, width, 0)], [[n[0], 0, n[1]]]);
244
+ kit.quad(target, [at(s1, 0, drop), at(s0, 0, drop), at(s0, 0, 0), at(s1, 0, 0)], [[-n[0], 0, -n[1]]]);
245
+ kit.quad(target, [at(s0, 0, drop), at(s0, width, drop), at(s0, width, 0), at(s0, 0, 0)], [[-tangent[0], 0, -tangent[1]]]);
246
+ kit.quad(target, [at(s1, width, drop), at(s1, 0, drop), at(s1, 0, 0), at(s1, width, 0)], [[tangent[0], 0, tangent[1]]]);
247
+ };
248
+ // A slim bar on a roof between two plan points, for the glazing grid. Every corner sits a
249
+ // fixed height off the roof plane, so its top and bottom lie parallel to the glass across
250
+ // its width as well as along it.
251
+ const roofBox = (target, p0, p1, roof, halfWidth, lift, drop) => {
252
+ const chord = Math.hypot(p1[0] - p0[0], p1[1] - p0[1]);
253
+ const t = [(p1[0] - p0[0]) / chord, (p1[1] - p0[1]) / chord], n = [-t[1], t[0]];
254
+ const corner = (end, across, offset) => {
255
+ const at = [end[0] + n[0] * across, end[1] + n[1] * across];
256
+ return [at[0], y(roof(at) + offset), at[1]];
257
+ };
258
+ const b = [corner(p0, -halfWidth, -drop), corner(p0, halfWidth, -drop), corner(p1, halfWidth, -drop), corner(p1, -halfWidth, -drop)];
259
+ const T = [corner(p0, -halfWidth, lift), corner(p0, halfWidth, lift), corner(p1, halfWidth, lift), corner(p1, -halfWidth, lift)];
260
+ kit.quad(target, [T[0], T[1], T[2], T[3]], [faceNormal(T[0], T[1], T[2])]);
261
+ kit.quad(target, [b[0], b[3], b[2], b[1]], [faceNormal(b[0], b[3], b[2])]);
262
+ for (const i of [0, 1, 2, 3]) {
263
+ const j = (i + 1) % 4;
264
+ kit.quad(target, [T[j], T[i], b[i], b[j]], [faceNormal(T[j], T[i], b[i])]);
265
+ }
266
+ };
267
+ // How a run meets its neighbour at each end: the signed angle between their outward
268
+ // normals, negative at a convex corner, positive at a concave one, and near zero where a
269
+ // traced outline splits one wall into nearly collinear pieces.
270
+ const straight = 5 * Math.PI / 180;
271
+ const turns = (plan, index) => {
272
+ const turn = (a, b) => { const n1 = a.normal(a.length), n2 = b.normal(0); return Math.atan2(n1[0] * n2[1] - n1[1] * n2[0], n1[0] * n2[0] + n1[1] * n2[1]); };
273
+ const count = plan.length, run = plan[index];
274
+ return [turn(plan[(index + count - 1) % count], run), turn(run, plan[(index + 1) % count])];
275
+ };
276
+ // Two walls' ribbons stand out 0.08 m and cross within this distance of their corner; the
277
+ // sharper the turn, the further, so a ribbon stops that far short of it. Across a nearly
278
+ // straight joint a 2 mm gap is enough, and keeps their level sills and heads from
279
+ // overlapping in the same plane.
280
+ const cornerClearance = (turn) => 0.08 * Math.tan(Math.abs(turn) / 2) + (Math.abs(turn) < straight ? 0.002 : 0.025);
281
+ let wall = 0;
282
+ volumes.forEach((volume, self) => {
283
+ volume.plan.forEach((whole, index) => {
284
+ const [startTurn, endTurn] = turns(volume.plan, index);
285
+ stretches(self, whole).forEach(({ from, to, neighbour }, piece, all) => {
286
+ wall += 1;
287
+ // A stretch is its own run, so everything below measures along it; a whole run keeps
288
+ // its mullions counted from its own start.
289
+ const run = from === 0 && to === whole.length ? whole : line(whole.at(from), whole.at(to));
290
+ const covering = neighbour < 0 ? null : volumes[neighbour];
291
+ const cover = covering ? (s) => covering.roof(run.at(s)) : null;
292
+ const roof = (s) => volume.roof(run.at(s));
293
+ // Ribbons run on across a straight joint and stop short of a corner, so the two walls'
294
+ // ribbons never overlap there.
295
+ const first = piece === 0, last = piece === all.length - 1;
296
+ const clear = [cornerClearance(first ? startTurn : 0), cornerClearance(last ? endTurn : 0)];
297
+ const n = run.normal(0), outer = Math.max(Math.abs(n[0]), Math.abs(n[1])) > 0.95;
298
+ const crown = volume.glazed && outer ? form.crown : undefined;
299
+ // Ribbons: the lobby's tall storefront, then one per floor up to the roof.
300
+ ribbon(run, clear, roof, cover, crainFloors.lobbyGlass[0], crainFloors.lobbyGlass[1], 0, wall);
301
+ const peak = Math.max(roof(0), roof(run.length));
302
+ for (let row = 1; crainFloors.lobbyTop + (row - 1) * crainFloors.pitch + crainFloors.sill < peak; row += 1) {
303
+ const level = crainFloors.lobbyTop + (row - 1) * crainFloors.pitch;
304
+ if (crown && level + crainFloors.head > crown.officeTop)
305
+ continue;
306
+ ribbon(run, clear, roof, cover, level + crainFloors.sill, level + crainFloors.head, row, wall);
307
+ }
308
+ if (crown) {
309
+ for (const [lo, hi] of crown.louvers) {
310
+ ribbon(run, clear, roof, cover, lo, hi, 0, wall, "vent");
311
+ // Slim closed blades seated in the dark backing. Extra end clearance
312
+ // accommodates their 5 cm relief at corners and collinear map joints.
313
+ const bladeClear = [clear[0] + 0.1, clear[1] + 0.1];
314
+ for (let bottom = lo + 0.12; bottom + 0.07 < hi; bottom += 0.22)
315
+ ribbon(run, bladeClear, (s) => roof(s) - 0.04, cover, bottom, bottom + 0.07, 0, wall, "blade");
316
+ }
317
+ for (const [lo, hi] of crown.shadowStrips)
318
+ ribbon(run, clear, roof, cover, lo, hi, 0, wall, "shadow");
319
+ }
320
+ // Roof edges: only where this volume stands above its neighbour. The lit outline runs
321
+ // along the four faces; the diagonal edges of the split, the notches, and the slot
322
+ // take a dark coping. Each run's fascia is a few millimetres wider and deeper than the
323
+ // last, so where two overlap at a corner no faces share a plane.
324
+ if (cover && Math.max(cover(0) - roof(0), cover(run.length) - roof(run.length)) > -0.5)
325
+ return;
326
+ const lit = volume.glazed && outer;
327
+ const step = 0.004 * index, width = (lit ? 0.42 : 0.3) + step;
328
+ // A sloped roof's fascia carries on past a convex corner, downhill only so none rises
329
+ // above the peaks. Otherwise it stops short: across a straight joint or into a concave
330
+ // corner its neighbour's top would share its plane where the two overlapped, and at a
331
+ // convex corner its end would lie in the plane of the next wall, which a taller
332
+ // neighbour may carry on up.
333
+ const adjust = (turn, at, past) => {
334
+ if (turn < -straight)
335
+ return volume.glazed && volume.roof(run.at(past)) < volume.roof(run.at(at)) ? width : -0.005;
336
+ return -(width * Math.tan(Math.abs(turn) / 2) + 0.002);
337
+ };
338
+ const extend = [adjust(first ? startTurn : 0, 0, -width), adjust(last ? endTurn : 0, run.length, run.length + width)];
339
+ fascia(lit ? lights : coping, run, volume.roof, width, (lit ? 0.7 : 0.5) + step, extend);
340
+ });
341
+ });
342
+ });
343
+ // The sloped glazing's grid, raised a little above the glass along the building's axes and
344
+ // seated in it, between the glass's top and its underside. The two directions stand and
345
+ // sit at different depths, so their crossings never share a plane.
346
+ // Bars keep their full width at least 0.3 m inside the roof's edge.
347
+ const spacing = 2 * crainFloors.module, inset = 0.9, barHalf = 0.08, barReach = barHalf + 0.3;
348
+ const { origin, axis } = form.grid;
349
+ const across = [-axis[1], axis[0]];
350
+ for (const volume of volumes.filter((v) => v.glazed)) {
351
+ for (const [direction, normal, lift, seat] of [[axis, across, 0.06, 0.02], [across, axis, 0.09, 0.03]]) {
352
+ const offsets = volume.corners.map((p) => (p[0] - origin[0]) * normal[0] + (p[1] - origin[1]) * normal[1]);
353
+ for (let k = Math.ceil(Math.min(...offsets) / spacing + 1e-9); k * spacing < Math.max(...offsets) - 1e-9; k += 1) {
354
+ const start = [origin[0] + normal[0] * k * spacing, origin[1] + normal[1] * k * spacing];
355
+ // Clip both long sides of the bar's footprint, not just its centre line, so a bar
356
+ // running nearly parallel to an edge cannot lean out over it.
357
+ const side = (offset) => [start[0] + normal[0] * offset, start[1] + normal[1] * offset];
358
+ const [left, right] = [clipLine(volume.corners, side(-barReach), direction), clipLine(volume.corners, side(barReach), direction)];
359
+ // The geographic crown follows the observed grid to the edge; the
360
+ // drawing-fit copies retain their original half-module end snapping.
361
+ // Geographic ends rely on the building-kit coplanar-overlap check to
362
+ // catch any end face meeting a crossing bar after grid/inset changes.
363
+ for (const [a0, a1] of left)
364
+ for (const [b0, b1] of right) {
365
+ const t0 = form.grid.edgeInset === undefined ? (Math.ceil((Math.max(a0, b0) + inset) / spacing - 0.5) + 0.5) * spacing : Math.max(a0, b0) + form.grid.edgeInset;
366
+ const t1 = form.grid.edgeInset === undefined ? (Math.floor((Math.min(a1, b1) - inset) / spacing - 0.5) + 0.5) * spacing : Math.min(a1, b1) - form.grid.edgeInset;
367
+ if (t1 - t0 < spacing - 1e-9)
368
+ continue;
369
+ const p0 = [start[0] + direction[0] * t0, start[1] + direction[1] * t0];
370
+ const p1 = [start[0] + direction[0] * t1, start[1] + direction[1] * t1];
371
+ roofBox(grid, p0, p1, volume.roof, barHalf, lift, seat);
372
+ }
373
+ }
374
+ }
375
+ }
376
+ const height = Math.max(...volumes.flatMap((volume) => volume.corners.map((p) => y(volume.roof(p)))));
377
+ return kit.finish({ height, outlines: [shell], opacity: 0.2 });
378
+ }