@a2f0/skyline 0.0.0-stage → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/NOTICE.md +20 -0
  2. package/README.md +52 -2
  3. package/docs/package.md +224 -0
  4. package/docs/skyline-geography.md +503 -0
  5. package/lib/models/aon-center.d.ts +4 -0
  6. package/lib/models/aon-center.js +60 -0
  7. package/lib/models/aon-geographic.d.ts +9 -0
  8. package/lib/models/aon-geographic.js +35 -0
  9. package/lib/models/aon-tower.d.ts +23 -0
  10. package/lib/models/aon-tower.js +130 -0
  11. package/lib/models/blue-cross-geographic.d.ts +14 -0
  12. package/lib/models/blue-cross-geographic.js +248 -0
  13. package/lib/models/borg-warner-geographic.d.ts +20 -0
  14. package/lib/models/borg-warner-geographic.js +143 -0
  15. package/lib/models/buckingham-geographic.d.ts +10 -0
  16. package/lib/models/buckingham-geographic.js +121 -0
  17. package/lib/models/building-kit.d.ts +84 -0
  18. package/lib/models/building-kit.js +358 -0
  19. package/lib/models/celebrations.d.ts +39 -0
  20. package/lib/models/celebrations.js +44 -0
  21. package/lib/models/chicago-athletic-association-geographic.d.ts +32 -0
  22. package/lib/models/chicago-athletic-association-geographic.js +173 -0
  23. package/lib/models/crain-communications.d.ts +5 -0
  24. package/lib/models/crain-communications.js +75 -0
  25. package/lib/models/crain-geographic.d.ts +10 -0
  26. package/lib/models/crain-geographic.js +69 -0
  27. package/lib/models/crain-tower.d.ts +32 -0
  28. package/lib/models/crain-tower.js +378 -0
  29. package/lib/models/facade-grid.d.ts +18 -0
  30. package/lib/models/facade-grid.js +176 -0
  31. package/lib/models/gage-geographic.d.ts +19 -0
  32. package/lib/models/gage-geographic.js +154 -0
  33. package/lib/models/heritage-at-millennium-park.d.ts +3 -0
  34. package/lib/models/heritage-at-millennium-park.js +284 -0
  35. package/lib/models/heritage-geographic.d.ts +11 -0
  36. package/lib/models/heritage-geographic.js +208 -0
  37. package/lib/models/hyatt-west-tower-geographic.d.ts +26 -0
  38. package/lib/models/hyatt-west-tower-geographic.js +86 -0
  39. package/lib/models/keith-ascher-geographic.d.ts +29 -0
  40. package/lib/models/keith-ascher-geographic.js +110 -0
  41. package/lib/models/kemper-geographic.d.ts +8 -0
  42. package/lib/models/kemper-geographic.js +153 -0
  43. package/lib/models/kemper.d.ts +2 -0
  44. package/lib/models/kemper.js +78 -0
  45. package/lib/models/lake-view-geographic.d.ts +15 -0
  46. package/lib/models/lake-view-geographic.js +134 -0
  47. package/lib/models/maclean-center-geographic.d.ts +20 -0
  48. package/lib/models/maclean-center-geographic.js +162 -0
  49. package/lib/models/michigan-boulevard-geographic.d.ts +12 -0
  50. package/lib/models/michigan-boulevard-geographic.js +157 -0
  51. package/lib/models/michigan-plaza-south-geographic.d.ts +7 -0
  52. package/lib/models/michigan-plaza-south-geographic.js +84 -0
  53. package/lib/models/michigan-plaza-south.d.ts +2 -0
  54. package/lib/models/michigan-plaza-south.js +68 -0
  55. package/lib/models/millennium-park-plaza-geographic.d.ts +9 -0
  56. package/lib/models/millennium-park-plaza-geographic.js +103 -0
  57. package/lib/models/monroe-geographic.d.ts +19 -0
  58. package/lib/models/monroe-geographic.js +177 -0
  59. package/lib/models/north-michigan-180-geographic.d.ts +14 -0
  60. package/lib/models/north-michigan-180-geographic.js +117 -0
  61. package/lib/models/north-wabash-geographic.d.ts +14 -0
  62. package/lib/models/north-wabash-geographic.js +122 -0
  63. package/lib/models/on-the-park-geographic.d.ts +11 -0
  64. package/lib/models/on-the-park-geographic.js +245 -0
  65. package/lib/models/one-prudential-geographic.d.ts +12 -0
  66. package/lib/models/one-prudential-geographic.js +55 -0
  67. package/lib/models/one-prudential-plaza.d.ts +5 -0
  68. package/lib/models/one-prudential-plaza.js +72 -0
  69. package/lib/models/one-prudential-tower.d.ts +45 -0
  70. package/lib/models/one-prudential-tower.js +246 -0
  71. package/lib/models/peoples-gas-geographic.d.ts +23 -0
  72. package/lib/models/peoples-gas-geographic.js +250 -0
  73. package/lib/models/railway-exchange-geographic.d.ts +18 -0
  74. package/lib/models/railway-exchange-geographic.js +176 -0
  75. package/lib/models/river-plaza-geographic.d.ts +16 -0
  76. package/lib/models/river-plaza-geographic.js +104 -0
  77. package/lib/models/sheraton-grand-geographic.d.ts +24 -0
  78. package/lib/models/sheraton-grand-geographic.js +112 -0
  79. package/lib/models/six-north-michigan-geographic.d.ts +16 -0
  80. package/lib/models/six-north-michigan-geographic.js +180 -0
  81. package/lib/models/skyline-geography-data.d.ts +44 -0
  82. package/lib/models/skyline-geography-data.js +607 -0
  83. package/lib/models/skyline-geography.d.ts +23 -0
  84. package/lib/models/skyline-geography.js +322 -0
  85. package/lib/models/swissotel-geographic.d.ts +24 -0
  86. package/lib/models/swissotel-geographic.js +79 -0
  87. package/lib/models/three-illinois-center-geographic.d.ts +26 -0
  88. package/lib/models/three-illinois-center-geographic.js +86 -0
  89. package/lib/models/trump-geographic.d.ts +14 -0
  90. package/lib/models/trump-geographic.js +51 -0
  91. package/lib/models/trump-international-tower.d.ts +4 -0
  92. package/lib/models/trump-international-tower.js +84 -0
  93. package/lib/models/trump-tower.d.ts +43 -0
  94. package/lib/models/trump-tower.js +229 -0
  95. package/lib/models/two-illinois-center-geographic.d.ts +26 -0
  96. package/lib/models/two-illinois-center-geographic.js +85 -0
  97. package/lib/models/two-prudential-geographic.d.ts +25 -0
  98. package/lib/models/two-prudential-geographic.js +47 -0
  99. package/lib/models/two-prudential-plaza.d.ts +8 -0
  100. package/lib/models/two-prudential-plaza.js +55 -0
  101. package/lib/models/two-prudential-tower.d.ts +53 -0
  102. package/lib/models/two-prudential-tower.js +441 -0
  103. package/lib/models/university-club-geographic.d.ts +24 -0
  104. package/lib/models/university-club-geographic.js +217 -0
  105. package/lib/models/willoughby-tower-geographic.d.ts +17 -0
  106. package/lib/models/willoughby-tower-geographic.js +195 -0
  107. package/lib/models/window-illumination.d.ts +14 -0
  108. package/lib/models/window-illumination.js +64 -0
  109. package/lib/package-assets.d.ts +5 -0
  110. package/lib/package-assets.js +9 -0
  111. package/lib/skyline-package.d.ts +23 -0
  112. package/lib/skyline-package.js +25 -0
  113. package/lib/skyline-scene.d.ts +25 -0
  114. package/lib/skyline-scene.js +86 -0
  115. package/lib/study-types.d.ts +49 -0
  116. package/lib/study-types.js +1 -0
  117. package/lib/vendor/three-r186.d.ts +1 -0
  118. package/lib/vendor/three-r186.js +1 -0
  119. package/package.json +64 -4
  120. package/site/building-detail.html +54 -0
  121. package/site/building-detail.js +84 -0
  122. package/site/building-study.html +58 -0
  123. package/site/building-study.js +3 -0
  124. package/site/index.html +407 -0
  125. package/site/models/aon-center.js +60 -0
  126. package/site/models/aon-geographic.js +35 -0
  127. package/site/models/aon-tower.js +130 -0
  128. package/site/models/blue-cross-geographic.js +248 -0
  129. package/site/models/borg-warner-geographic.js +143 -0
  130. package/site/models/buckingham-geographic.js +121 -0
  131. package/site/models/building-kit.js +358 -0
  132. package/site/models/celebrations.js +44 -0
  133. package/site/models/chicago-athletic-association-geographic.js +173 -0
  134. package/site/models/crain-communications.js +75 -0
  135. package/site/models/crain-geographic.js +69 -0
  136. package/site/models/crain-reference.svg +10 -0
  137. package/site/models/crain-tower.js +378 -0
  138. package/site/models/facade-grid.js +176 -0
  139. package/site/models/gage-geographic.js +154 -0
  140. package/site/models/heritage-at-millennium-park.js +284 -0
  141. package/site/models/heritage-geographic.js +208 -0
  142. package/site/models/hyatt-west-tower-geographic.js +86 -0
  143. package/site/models/keith-ascher-geographic.js +110 -0
  144. package/site/models/kemper-geographic.js +153 -0
  145. package/site/models/kemper.js +78 -0
  146. package/site/models/lake-view-geographic.js +134 -0
  147. package/site/models/maclean-center-geographic.js +162 -0
  148. package/site/models/michigan-boulevard-geographic.js +157 -0
  149. package/site/models/michigan-plaza-south-geographic.js +84 -0
  150. package/site/models/michigan-plaza-south.js +68 -0
  151. package/site/models/millennium-park-plaza-geographic.js +103 -0
  152. package/site/models/monroe-geographic.js +177 -0
  153. package/site/models/north-michigan-180-geographic.js +117 -0
  154. package/site/models/north-wabash-geographic.js +122 -0
  155. package/site/models/on-the-park-geographic.js +245 -0
  156. package/site/models/one-prudential-geographic.js +55 -0
  157. package/site/models/one-prudential-plaza.js +72 -0
  158. package/site/models/one-prudential-tower.js +246 -0
  159. package/site/models/peoples-gas-geographic.js +250 -0
  160. package/site/models/railway-exchange-geographic.js +176 -0
  161. package/site/models/river-plaza-geographic.js +104 -0
  162. package/site/models/sheraton-grand-geographic.js +112 -0
  163. package/site/models/six-north-michigan-geographic.js +180 -0
  164. package/site/models/skyline-geography-data.js +607 -0
  165. package/site/models/skyline-geography.js +322 -0
  166. package/site/models/skyline-panorama.svg +3491 -0
  167. package/site/models/skyline-reference.svg +3090 -0
  168. package/site/models/swissotel-geographic.js +79 -0
  169. package/site/models/three-illinois-center-geographic.js +86 -0
  170. package/site/models/trump-geographic.js +51 -0
  171. package/site/models/trump-international-tower.js +84 -0
  172. package/site/models/trump-tower.js +229 -0
  173. package/site/models/two-illinois-center-geographic.js +85 -0
  174. package/site/models/two-prudential-geographic.js +47 -0
  175. package/site/models/two-prudential-plaza.js +55 -0
  176. package/site/models/two-prudential-tower.js +441 -0
  177. package/site/models/university-club-geographic.js +217 -0
  178. package/site/models/willoughby-tower-geographic.js +195 -0
  179. package/site/models/window-illumination.js +64 -0
  180. package/site/skyline-3d.html +92 -0
  181. package/site/skyline-3d.js +333 -0
  182. package/site/skyline-animated.svg +18795 -0
  183. package/site/skyline-comparison.js +97 -0
  184. package/site/skyline-original-fit.svg +18312 -0
  185. package/site/skyline-scene.js +86 -0
  186. package/site/skyline-study.html +83 -0
  187. package/site/skyline-study.js +109 -0
  188. package/site/skyline-webgl.html +667 -0
  189. package/site/stars.svg +176 -0
  190. package/site/study-loader.js +20 -0
  191. package/site/study-types.js +1 -0
  192. package/site/study-viewer.js +605 -0
  193. package/site/study.css +204 -0
  194. package/site/vendor/THREE-LICENSE.txt +21 -0
  195. package/site/vendor/three-r186.js +4228 -0
@@ -0,0 +1,47 @@
1
+ import { polygonOf } from "./building-kit.js";
2
+ import { chainsOf } from "./facade-grid.js";
3
+ import { buildTwoPrudentialTower, twoPrudentialCore, twoPrudentialLevels } from "./two-prudential-tower.js";
4
+ // Two Prudential Plaza: the geographic layout's model, on the mapped outline 64388666. The
5
+ // tower is the shared generator in two-prudential-tower.ts, the same one the original
6
+ // layout's copy uses. Its core stands on the outline's area centroid, square to the mapped
7
+ // south wall; the lobby fills the mapped outline to the first office floor, and the tiers
8
+ // fill the depth between the core and the mapped north and south walls. See
9
+ // docs/two-prudential-reference.md. Units are meters; +x is east, +z is south.
10
+ export const twoPrudentialGeographicLevels = twoPrudentialLevels;
11
+ // The area centroid, not the mean of the traced vertices: this outline carries three extra
12
+ // points down its west wall, which would pull a vertex mean 4.9 m west and 2.7 m south.
13
+ export function areaCentroid(outline) {
14
+ let twice = 0, x = 0, z = 0;
15
+ outline.forEach((a, i) => {
16
+ const b = outline[(i + 1) % outline.length], cross = a[0] * b[1] - b[0] * a[1];
17
+ twice += cross;
18
+ x += (a[0] + b[0]) * cross;
19
+ z += (a[1] + b[1]) * cross;
20
+ });
21
+ return [x / (3 * twice), z / (3 * twice)];
22
+ }
23
+ // The core's frame on the mapped outline: its centre, the unit vector along the mapped south
24
+ // wall, and how far the mapped south and north walls stand from the centre along its normal.
25
+ export function twoPrudentialFrame(ground) {
26
+ const outline = polygonOf(ground), centre = areaCentroid(outline);
27
+ const chains = chainsOf(ground).map((chain) => ({ chain, normal: ground[chain.runs[0]].normal(0) }));
28
+ const wall = (sign) => chains.find(({ normal }) => normal[1] * sign > 0.9).chain;
29
+ const ends = (chain) => chain.runs.flatMap((index) => [ground[index].at(0), ground[index].at(ground[index].length)]);
30
+ const [start, end] = [ends(wall(1))[0], ends(wall(1)).at(-1)];
31
+ const length = Math.hypot(end[0] - start[0], end[1] - start[1]);
32
+ const along = [(end[0] - start[0]) / length, (end[1] - start[1]) / length], south = [-along[1], along[0]];
33
+ const depth = (p) => (p[0] - centre[0]) * south[0] + (p[1] - centre[1]) * south[1];
34
+ // The nearest point of each wall, less 2 cm, so each lower tier's front stays within the
35
+ // mapped outline across the whole of its width.
36
+ const fronts = [Math.min(...ends(wall(1)).map(depth)) - 0.02, Math.min(...ends(wall(-1)).map((p) => -depth(p))) - 0.02];
37
+ return { centre, along, fronts };
38
+ }
39
+ export function createTwoPrudentialGeographicBuilding(record, projectPlan, offset) {
40
+ const ground = projectPlan(record.footprint.coordinates);
41
+ const { centre, along, fronts } = twoPrudentialFrame(ground);
42
+ const model = buildTwoPrudentialTower({ name: record.name, id: record.id, centre, along, fronts, base: 0, lobby: ground });
43
+ model.building.position.set(offset[0], 0, offset[1]);
44
+ model.building.userData["geography"] = record;
45
+ model.building.userData["geographicDetail"] = { levels: twoPrudentialGeographicLevels, core: twoPrudentialCore, floors: twoPrudentialLevels.floors, source: "docs/two-prudential-reference.md" };
46
+ return model;
47
+ }
@@ -0,0 +1,55 @@
1
+ import { buildTwoPrudentialTower, pierStations, twoPrudentialCore, twoPrudentialGables, twoPrudentialLevels } from "./two-prudential-tower.js";
2
+ // Two Prudential Plaza on a clean version of its plan, in real meters, for the skyline
3
+ // study's original layout, which places this copy in skyline-study.ts. two-prudential-tower.ts
4
+ // builds it, and the geographic model on the mapped outline. The plan is the mapped one
5
+ // squared: the 40.8 x 37.5 m core, with each pair of tiers 4.5 m deep, so the lower tiers'
6
+ // fronts stand 27.75 m either side of its centre and the tower keeps the mapped 55.5 m
7
+ // depth. See docs/two-prudential-reference.md. Units are meters; +x is east and +z is south.
8
+ const front = 27.75;
9
+ // The skyline drawing shows the tower above One Prudential's podium, so its platform datum
10
+ // crosses the tower above the street: fitted with the placement in skyline-study.ts,
11
+ // which scales, turns, and places this copy. Its y = 0 is that datum.
12
+ export const twoPrudentialSkylineBase = 35.4;
13
+ // The drawing draws each tier's gable as one slope from its shoulders to a point about 2 m
14
+ // above the line its steps climb: in the night photograph it traces, each tier's dark glass
15
+ // head runs on into the dark strip of the wall behind it. This copy keeps the drawn points.
16
+ export const twoPrudentialDrawnTierPeaks = Object.freeze({ lower: 181.9, middle: 217.2 });
17
+ export function createTwoPrudentialPlazaBuilding() {
18
+ return buildTwoPrudentialTower({ name: "Two Prudential Plaza", id: "building-two-prudential-plaza", centre: [0, 0], along: [1, 0], fronts: [front, front], base: twoPrudentialSkylineBase, tierPeaks: twoPrudentialDrawnTierPeaks });
19
+ }
20
+ // Features the skyline test projects against the drawing, in this copy's coordinates.
21
+ const h = twoPrudentialLevels, g = twoPrudentialGables, [coreA, coreB] = twoPrudentialCore, drawn = twoPrudentialDrawnTierPeaks;
22
+ const at = (x, real, z) => [x, real - twoPrudentialSkylineBase, z];
23
+ const middleFront = coreB + (front - coreB) / 2;
24
+ // A pier's point halfway out to its face, so a present pier is met a little before it.
25
+ const halfPier = 0.175;
26
+ // Every pier across a gabled face, west to east, from its middle.
27
+ const across = (gable) => { const stations = pierStations(gable); return [...stations.map((r) => -r), ...[...stations].reverse()]; };
28
+ export const twoPrudentialFeatures = {
29
+ // The core's corners at the eave, and the points of its south and east gables.
30
+ twoEaveWest: at(-coreA, h.eave, coreB),
31
+ twoEaveNear: at(coreA, h.eave, coreB),
32
+ twoEaveEast: at(coreA, h.eave, -coreB),
33
+ twoSouthChevron: at(0, h.peak, coreB + 0.03),
34
+ twoEastChevron: at(coreA + 0.03, h.peak, 0),
35
+ twoPyramid: at(0, h.apex, 0),
36
+ twoSpire: at(0, h.tip, 0),
37
+ // The tiers' points and their shoulders at their fronts' corners.
38
+ twoMiddleChevron: at(0, drawn.middle, middleFront),
39
+ twoLowerChevron: at(0, drawn.lower, front),
40
+ twoMiddleWest: at(-g.middle.half, h.middleShoulder, middleFront),
41
+ twoMiddleEast: at(g.middle.half, h.middleShoulder, middleFront),
42
+ twoLowerWest: at(-g.lower.half, h.lowerShoulder, front),
43
+ twoLowerEast: at(g.lower.half, h.lowerShoulder, front),
44
+ // The core's south piers above the middle tier, all twelve.
45
+ twoSouthPiers: across(g.front).map((x) => at(x, 222, coreB + halfPier)),
46
+ // The east face's piers from its north corner, where the face's x runs north to south
47
+ // as z does: every one but the first south of the strip, which the drawing leaves out.
48
+ twoEastPiers: across(g.side).filter((_, index) => index !== 6).map((z) => at(coreA + halfPier, 204, z)),
49
+ // The lower tier's eight piers.
50
+ twoLowerPiers: across(g.lower).map((x) => at(x, 135, front + halfPier)),
51
+ // A window on the east face, clear of every neighbour, for hover.
52
+ twoFacade: at(coreA + 0.02, 180, -10.7),
53
+ // On the lower tier's front below the podium roof, which hides it: for scene raycasts.
54
+ twoBehindPodium: at(-10, 45, front),
55
+ };
@@ -0,0 +1,441 @@
1
+ import * as THREE from "../vendor/three-r186.js";
2
+ import { createBuilder, line } from "./building-kit.js";
3
+ import { mitredBox } from "./facade-grid.js";
4
+ // Two Prudential Plaza as built, for both of the skyline study's layouts: the geographic
5
+ // model stands it on the mapped outline, and the original layout's copy on a clean plan cut
6
+ // at the drawing's datum. A 40.8 x 37.5 m limestone core rises to 229.3 m at its corners.
7
+ // Each face ends in a gable that steps up floor by floor to a pointed glass strip at its
8
+ // middle, 256 m, and a stepped pyramid turned 45° to the plan rises from those four points
9
+ // to 280.2 m, where the spire takes over to the published 303.3 m. A broad rib runs up each
10
+ // ridge from its strip's head to the apex, and the pyramid's steps end on the ribs. On the
11
+ // north and south faces two gabled tiers stand forward of the core, the lower in front of
12
+ // the middle one, and fill the rest of the mapped depth. Heights are measured on the
13
+ // photographs, down from the published tip; the plan on the photograph against the mapped
14
+ // outline. See docs/two-prudential-reference.md. Units are meters; +x is east and +z is south.
15
+ const pitch = 3.96;
16
+ // A tier's point stands on the line through its steps' corners: its glass head rises a floor
17
+ // for every 3.16 m bay, as the steps beside it do, from its 4.6 m half-strip's shoulder.
18
+ const tierPeak = (shoulder, bays) => shoulder + (bays + 4.6 / 3.16) * pitch;
19
+ export const twoPrudentialLevels = Object.freeze({
20
+ pitch, // the photograph's window rows
21
+ lobbyTop: 11.52, // the first office floor, a whole number of floors below the tiers
22
+ sill: 0.9,
23
+ head: 2.9, // a window's sill and head above its floor
24
+ lowerShoulder: 162,
25
+ lowerPeak: tierPeak(162, 3), // 179.64
26
+ middleShoulder: 193.68,
27
+ middlePeak: tierPeak(193.68, 4), // 215.28
28
+ eave: 229.32, // the core's corners
29
+ peak: 256, // each face's gable
30
+ apex: 280.2, // the pyramid
31
+ shaftTop: 295.5, // the spire's shaft, under its needle
32
+ tip: 303.3, // published architectural height
33
+ floors: 64,
34
+ });
35
+ const h = twoPrudentialLevels;
36
+ export const floorLine = (floor) => h.lobbyTop + floor * h.pitch;
37
+ const gableOf = (strip, bays, bay, shoulder, peak) => ({ strip, bays, bay, half: strip + bays * bay, shoulder, peak });
38
+ export const twoPrudentialGables = Object.freeze({
39
+ // The core's north and south faces, five bays either side of the strip.
40
+ front: gableOf(4.6, 5, 3.16, h.eave, h.peak),
41
+ // The east and west faces, four wider bays either side of a wider strip.
42
+ side: gableOf(5.7, 4, 3.2625, h.eave, h.peak),
43
+ // The tiers keep the front's bays, four and three either side of its strip.
44
+ middle: gableOf(4.6, 4, 3.16, h.middleShoulder, h.middlePeak),
45
+ lower: gableOf(4.6, 3, 3.16, h.lowerShoulder, h.lowerPeak),
46
+ });
47
+ const g = twoPrudentialGables;
48
+ // The core's half-widths, east-west and north-south.
49
+ export const twoPrudentialCore = [g.front.half, g.side.half];
50
+ export const stripShoulder = (gable) => gable.shoulder + gable.bays * h.pitch;
51
+ // A wall's top at x from its middle.
52
+ export function wallTop(gable, x) {
53
+ const r = Math.abs(x);
54
+ if (r >= gable.strip)
55
+ return gable.shoulder + Math.min(gable.bays - 1, Math.floor((gable.half - r) / gable.bay + 1e-9)) * h.pitch;
56
+ return stripShoulder(gable) + (gable.peak - stripShoulder(gable)) * (1 - r / gable.strip);
57
+ }
58
+ // The half-width of the part of a wall that reaches a height.
59
+ export function wallExtent(gable, y) {
60
+ if (y <= gable.shoulder + 1e-9)
61
+ return gable.half;
62
+ const steps = Math.ceil((y - gable.shoulder) / h.pitch - 1e-9);
63
+ if (steps < gable.bays)
64
+ return gable.half - steps * gable.bay;
65
+ if (y <= stripShoulder(gable) + 1e-9)
66
+ return gable.strip;
67
+ return Math.max(0, gable.strip * (gable.peak - y) / (gable.peak - stripShoulder(gable)));
68
+ }
69
+ // The piers stand on every bay's edges: its corner pier's centre, then the joints inward to
70
+ // the strip's edge. Windows are centred in their bays.
71
+ const pierWidth = 0.9, pierDepth = 0.35, windowHalf = 0.8;
72
+ export const pierStations = (gable) => Array.from({ length: gable.bays + 1 }, (_, i) => (i ? gable.half - i * gable.bay : gable.half - pierWidth / 2));
73
+ // Where a face's part of the pyramid ends at a height, as its half-width along the face and
74
+ // its reach from the core's centre: the face's wall to where it reaches, up to its strip's
75
+ // shoulder, and above that the inner edge of its ridge's rib, the straight line from the
76
+ // shoulder to the apex, so each floor's step ends a fixed way further up that line.
77
+ function crownEnd(gable, reach, y) {
78
+ const shoulder = stripShoulder(gable);
79
+ if (y <= shoulder + 1e-9)
80
+ return [wallExtent(gable, y), reach];
81
+ const s = (h.apex - y) / (h.apex - shoulder);
82
+ return [gable.strip * s, reach * s];
83
+ }
84
+ // The pyramid's horizontal section at a height above the eave, in the core's frame: each
85
+ // face's end, joined across each corner by the step's riser.
86
+ export function crownSection(y) {
87
+ const [a, b] = twoPrudentialCore, [front, south] = crownEnd(g.front, b, y), [side, east] = crownEnd(g.side, a, y);
88
+ return [[-front, south], [front, south], [east, side], [east, -side], [front, -south], [-front, -south], [-east, -side], [-east, side]];
89
+ }
90
+ // The crown's steps stop a floor under the apex, where a pointed cap and the spire take over.
91
+ export const crownSteps = Math.floor((h.apex - h.eave) / h.pitch - 0.25);
92
+ const color = (hex) => new THREE.Color(hex);
93
+ const stone = color(0x8a8a8a), stripSpandrel = color(0x4a4a4a), riserGlass = color(0x565656), band = color(0xcdcdcd);
94
+ const tread = color(0x7a7a7a), soffit = color(0x6a6a6a), roofGlass = color(0x3f3f3f);
95
+ const glassTones = [0x3a3a3a, 0x404040, 0x353535, 0x454545].map(color);
96
+ const litGlass = color(0x8e8e8e), dimGlass = color(0x5e5e5e);
97
+ function paneColor(floor, bay, wall) {
98
+ const hash = (Math.imul(floor + 7, 0x9e3779b1) ^ Math.imul(bay + 43, 0x85ebca77) ^ Math.imul(wall + 2, 0xc2b2ae3d)) >>> 0;
99
+ const value = hash % 97;
100
+ if (value < 3)
101
+ return litGlass;
102
+ if (value < 6)
103
+ return dimGlass;
104
+ return glassTones[value % glassTones.length];
105
+ }
106
+ export function buildTwoPrudentialTower(form) {
107
+ const kit = createBuilder(form.name, form.id);
108
+ const body = kit.batch("Two Prudential · limestone, glass and crown", kit.material(0xffffff, { vertexColors: true }));
109
+ const piers = kit.batch("Two Prudential · piers", kit.material(0x9c9c9c));
110
+ const copings = kit.batch("Two Prudential · copings", kit.material(0xc6c6c6));
111
+ const mullions = kit.batch("Two Prudential · strip mullions", kit.material(0x707070));
112
+ const ribs = kit.batch("Two Prudential · crown ribs", kit.material(0xd8d8d8));
113
+ const spire = kit.batch("Two Prudential · spire", kit.material(0xe9e9e9));
114
+ const spirePanels = kit.batch("Two Prudential · spire inset panels", kit.material(0x575757));
115
+ const y = (real) => real - form.base;
116
+ const [coreA, coreB] = twoPrudentialCore;
117
+ const middle = form.tierPeaks ? { ...g.middle, peak: form.tierPeaks.middle } : g.middle;
118
+ const lower = form.tierPeaks ? { ...g.lower, peak: form.tierPeaks.lower } : g.lower;
119
+ // The core's frame, and the same turned half a turn: the north and west halves are the
120
+ // south and east ones seen from the other side, so one description builds both.
121
+ // Frames 2 and 3 face east and west: their south axis is the side face's outward normal.
122
+ const frames = [form.along, [-form.along[0], -form.along[1]]].map((a) => ({ a, s: [-a[1], a[0]] }));
123
+ frames.push(...frames.map(({ a, s }) => ({ a: [-s[0], -s[1]], s: a })));
124
+ const at = (frame, u, v) => {
125
+ const { a, s } = frames[frame];
126
+ return [form.centre[0] + a[0] * u + s[0] * v, form.centre[1] + a[1] * u + s[1] * v];
127
+ };
128
+ const lift = ([x, z], real) => [x, y(real), z];
129
+ // Heights a solid spans, cut at the datum: none when it lies wholly below.
130
+ const above = (rows) => {
131
+ if (rows.at(-1) <= form.base + 1e-6)
132
+ return null;
133
+ return rows[0] >= form.base ? rows : [form.base, ...rows.filter((r) => r > form.base + 1e-6)];
134
+ };
135
+ // Sills and heads strictly between two heights, plus any extra offsets within a floor:
136
+ // the floor lines themselves divide stone from stone, so they need no cut.
137
+ const rowsBetween = (lo, hi, extra = []) => {
138
+ const cuts = new Set([lo, hi]);
139
+ for (let floor = Math.floor((lo - h.lobbyTop) / h.pitch) - 1; floorLine(floor) < hi; floor += 1) {
140
+ for (const offset of [h.sill, h.head, ...extra]) {
141
+ const r = Math.round((floorLine(floor) + offset) * 1e6) / 1e6;
142
+ if (r > lo + 1e-6 && r < hi - 1e-6)
143
+ cuts.add(r);
144
+ }
145
+ }
146
+ return [...cuts].sort((p, q) => p - q);
147
+ };
148
+ const floorOf = (real) => Math.floor((real - h.lobbyTop) / h.pitch + 1e-9);
149
+ const vision = (real) => { const o = real - floorLine(floorOf(real)); return o > h.sill && o < h.head; };
150
+ // A gabled wall's cuts: every window's edges and the strip's. A bay's edge divides stone
151
+ // from stone, under a pier.
152
+ const wallCuts = (gable) => {
153
+ const radii = [gable.strip];
154
+ for (let i = 0; i < gable.bays; i += 1) {
155
+ const centre = gable.half - (i + 0.5) * gable.bay;
156
+ radii.push(centre - windowHalf, centre + windowHalf);
157
+ }
158
+ return [...new Set(radii.flatMap((r) => [-r, r]))].sort((p, q) => p - q);
159
+ };
160
+ const paintWall = (wall, x, real) => {
161
+ const r = Math.abs(x), gable = wall.gable;
162
+ if (r < gable.strip)
163
+ return vision(real) ? paneColor(floorOf(real), Math.floor((x + gable.strip) / 1.8), wall.seed + 50) : stripSpandrel;
164
+ const bay = Math.min(gable.bays - 1, Math.floor((gable.half - r) / gable.bay));
165
+ const centre = gable.half - (bay + 0.5) * gable.bay;
166
+ return vision(real) && Math.abs(r - centre) < windowHalf ? paneColor(floorOf(real), Math.sign(x) * (bay + 1), wall.seed) : stone;
167
+ };
168
+ // A closed prism on a convex ring, counterclockwise from above, whose walls are cut into
169
+ // painted cells along each edge and at every row. Its top and bottom fan out from its
170
+ // centre through every cut, so each cell's edge meets its partner.
171
+ const prism = (ring, rowsIn, edges, top = tread, bottom = soffit) => {
172
+ const rows = above(rowsIn);
173
+ if (!rows)
174
+ return;
175
+ const loop = [];
176
+ ring.forEach((a, i) => {
177
+ const b = ring[(i + 1) % ring.length], length = Math.hypot(b[0] - a[0], b[1] - a[1]);
178
+ const t = [(b[0] - a[0]) / length, (b[1] - a[1]) / length], out = [-t[1], 0, t[0]], edge = edges[i];
179
+ let cuts = [];
180
+ if ("wall" in edge)
181
+ cuts = wallCuts(edge.wall.gable).map((x) => x - edge.from);
182
+ else if ("ribbon" in edge)
183
+ cuts = [pierWidth + 0.05, length - 0.35];
184
+ const stations = [0, ...cuts.filter((s) => s > 1e-3 && s < length - 1e-3), length];
185
+ const points = stations.map((s, k) => (k === 0 ? a : k === stations.length - 1 ? b : [a[0] + t[0] * s, a[1] + t[1] * s]));
186
+ loop.push(...points.slice(0, -1));
187
+ for (let k = 0; k + 1 < points.length; k += 1) {
188
+ const middle = (stations[k] + stations[k + 1]) / 2;
189
+ for (let r = 0; r + 1 < rows.length; r += 1) {
190
+ const [lo, hi] = [rows[r], rows[r + 1]], real = (lo + hi) / 2;
191
+ let paint = stone;
192
+ if ("wall" in edge)
193
+ paint = paintWall(edge.wall, edge.from + middle, real);
194
+ else if ("ribbon" in edge)
195
+ paint = k === 1 && vision(real) ? paneColor(floorOf(real), 90 + i, 7) : stone;
196
+ else if ("riser" in edge)
197
+ paint = real - floorLine(floorOf(real)) < 2.5 ? riserGlass : band;
198
+ else
199
+ paint = edge.plain;
200
+ const [p, q] = [points[k], points[k + 1]];
201
+ kit.quad(body, [lift(p, lo), lift(q, lo), lift(q, hi), lift(p, hi)], [out], paint);
202
+ }
203
+ }
204
+ });
205
+ const centre = [ring.reduce((sum, p) => sum + p[0], 0) / ring.length, ring.reduce((sum, p) => sum + p[1], 0) / ring.length];
206
+ const [lo, hi] = [rows[0], rows.at(-1)];
207
+ loop.forEach((p, i) => {
208
+ const q = loop[(i + 1) % loop.length];
209
+ kit.triangle(body, [lift(centre, hi), lift(p, hi), lift(q, hi)], [[0, 1, 0], [0, 1, 0], [0, 1, 0]], top);
210
+ kit.triangle(body, [lift(centre, lo), lift(p, lo), lift(q, lo)], [[0, -1, 0], [0, -1, 0], [0, -1, 0]], bottom);
211
+ });
212
+ };
213
+ const ringOf = (frame, corners) => corners.map(([u, v]) => at(frame, u, v));
214
+ // A closed solid extruded along a frame's south axis from a convex profile in (u, height):
215
+ // a strip's pointed head, or a tier's strip with its gabled roof.
216
+ const extrude = (target, frame, profile, v0, v1, front, sides) => {
217
+ const rows = profile.map(([, real]) => real);
218
+ if (Math.max(...rows) <= form.base)
219
+ return;
220
+ const cut = profile.map(([u, real]) => [u, Math.max(real, form.base)]);
221
+ const { a, s } = frames[frame], point = (u, real, v) => lift(at(frame, u, v), real);
222
+ const centroid = [cut.reduce((sum, p) => sum + p[0], 0) / cut.length, cut.reduce((sum, p) => sum + p[1], 0) / cut.length];
223
+ const fan = (v, normal, paint) => cut.forEach((p, i) => {
224
+ const q = cut[(i + 1) % cut.length];
225
+ kit.triangle(target, [point(centroid[0], centroid[1], v), point(p[0], p[1], v), point(q[0], q[1], v)], [normal, normal, normal], paint);
226
+ });
227
+ fan(v1, [s[0], 0, s[1]], front);
228
+ fan(v0, [-s[0], 0, -s[1]], sides);
229
+ cut.forEach((p, i) => {
230
+ const q = cut[(i + 1) % cut.length], du = q[0] - p[0], dy = q[1] - p[1], length = Math.hypot(du, dy);
231
+ if (length < 1e-6)
232
+ return;
233
+ let [nu, ny] = [dy / length, -du / length];
234
+ if (nu * ((p[0] + q[0]) / 2 - centroid[0]) + ny * ((p[1] + q[1]) / 2 - centroid[1]) < 0)
235
+ [nu, ny] = [-nu, -ny];
236
+ kit.quad(target, [point(p[0], p[1], v0), point(q[0], q[1], v0), point(q[0], q[1], v1), point(p[0], p[1], v1)], [[a[0] * nu, ny, a[1] * nu]], sides);
237
+ });
238
+ };
239
+ // The lobby: the mapped outline from grade to the first office floor.
240
+ if (form.lobby && form.base < h.lobbyTop)
241
+ kit.prism(kit.batch("Two Prudential · lobby", kit.material(0x5a5a5a)), form.lobby, [y(Math.max(0, form.base)), y(h.lobbyTop)]);
242
+ // The core: its shaft to the eave, in one piece, so no seam crosses its faces where the
243
+ // tiers stop covering them.
244
+ const walls = (frame) => ({ front: { gable: g.front, seed: frame * 4 }, side: { gable: g.side, seed: frame * 4 + 1 } });
245
+ prism(ringOf(0, [[-coreA, coreB], [coreA, coreB], [coreA, -coreB], [-coreA, -coreB]]), rowsBetween(h.lobbyTop, h.eave), [0, 1].flatMap((frame) => [{ wall: walls(frame).front, from: -coreA }, { wall: walls(frame).side, from: -coreB }]));
246
+ // The crown: one floor's step at a time, each on the pyramid's section at its top, so
247
+ // every step's nosing lies on the pyramid. Up to a strip's shoulder a face's end is its
248
+ // gable's stepped bays; above it the end stands back under the ridge's rib, plain stone.
249
+ let crownTop = h.eave;
250
+ for (let step = 1; step <= crownSteps; step += 1) {
251
+ const lo = h.eave + (step - 1) * h.pitch, hi = lo + h.pitch, section = crownSection(hi);
252
+ const end = (wall) => (hi <= stripShoulder(wall.gable) + 1e-9 ? { wall, from: -wallExtent(wall.gable, hi) } : { plain: stone });
253
+ const edges = [0, 1].flatMap((frame) => [end(walls(frame).front), { riser: true }, end(walls(frame).side), { riser: true }]);
254
+ prism(ringOf(0, section), rowsBetween(lo, hi, [2.5]), edges);
255
+ crownTop = hi;
256
+ }
257
+ // The cap: from the last step's section to the apex.
258
+ {
259
+ const section = ringOf(0, crownSection(crownTop)), apex = lift(form.centre, h.apex);
260
+ section.forEach((p, i) => {
261
+ const q = section[(i + 1) % section.length], [pp, qq] = [lift(p, crownTop), lift(q, crownTop)];
262
+ const u = pp.map((value, k) => value - apex[k]), w = qq.map((value, k) => value - apex[k]);
263
+ const n = [u[1] * w[2] - u[2] * w[1], u[2] * w[0] - u[0] * w[2], u[0] * w[1] - u[1] * w[0]];
264
+ kit.triangle(body, [apex, pp, qq], [n, n, n].map((v) => (v[1] < 0 ? v.map((c) => -c) : v)), band);
265
+ });
266
+ for (let i = 1; i + 1 < section.length; i += 1) {
267
+ kit.triangle(body, [lift(section[0], crownTop), lift(section[i], crownTop), lift(section[i + 1], crownTop)], [[0, -1, 0], [0, -1, 0], [0, -1, 0]], soffit);
268
+ }
269
+ }
270
+ // The tiers, on the north and south faces: each a block to its shoulder and a step per
271
+ // bay inward, with its strip's gabled head over the last.
272
+ const tiers = [0, 1].flatMap((frame) => {
273
+ const depth = (form.fronts[frame] - coreB) / 2;
274
+ return [
275
+ { frame, gable: middle, back: coreB, front: coreB + depth, seed: 10 + frame * 2 },
276
+ { frame, gable: lower, back: coreB + depth, front: form.fronts[frame], seed: 11 + frame * 2 },
277
+ ];
278
+ });
279
+ for (const tier of tiers) {
280
+ const { frame, gable, back, front } = tier, wall = { gable, seed: tier.seed };
281
+ const block = (half) => ringOf(frame, [[-half, front], [half, front], [half, back], [-half, back]]);
282
+ const sides = (half, ribbon = true) => [{ wall, from: -half }, ribbon ? { ribbon: front - back } : { plain: stone }, { plain: stone }, ribbon ? { ribbon: front - back } : { plain: stone }];
283
+ prism(block(gable.half), rowsBetween(h.lobbyTop, gable.shoulder), sides(gable.half));
284
+ for (let step = 1; step < gable.bays; step += 1) {
285
+ const half = gable.half - step * gable.bay, lo = gable.shoulder + (step - 1) * h.pitch;
286
+ prism(block(half), rowsBetween(lo, lo + h.pitch), sides(half, false));
287
+ }
288
+ const foot = gable.shoulder + (gable.bays - 1) * h.pitch;
289
+ extrude(body, frame, [[-gable.strip, foot], [gable.strip, foot], [gable.strip, stripShoulder(gable)], [0, gable.peak], [-gable.strip, stripShoulder(gable)]], back, front, roofGlass, stone);
290
+ }
291
+ // The core's strips' pointed heads, 3 cm proud of the stepped walls behind them. Each
292
+ // starts in the spandrel under its shoulder, so its underside shares no plane with a
293
+ // step's.
294
+ for (const frame of [0, 1]) {
295
+ for (const [gable, local, reach] of [[g.front, frame, coreB], [g.side, frame + 2, coreA]]) {
296
+ const shoulder = stripShoulder(gable);
297
+ extrude(body, local, [[-gable.strip, shoulder - 0.3], [gable.strip, shoulder - 0.3], [gable.strip, shoulder], [0, gable.peak], [-gable.strip, shoulder]], reach - 0.5, reach + 0.03, roofGlass, roofGlass);
298
+ }
299
+ }
300
+ // Every face's piers, copings and strip mullions. A face is a run along its wall with
301
+ // its outward normal, its gable, and what covers its foot.
302
+ const faces = [0, 1].flatMap((frame) => {
303
+ const depth = (form.fronts[frame] - coreB) / 2;
304
+ return [
305
+ { run: line(at(frame, -coreA, coreB), at(frame, coreA, coreB)), gable: g.front, cover: (x) => (Math.abs(x) <= middle.half ? wallTop(middle, x) : h.lobbyTop), tier: false },
306
+ { run: line(at(frame, coreA, coreB), at(frame, coreA, -coreB)), gable: g.side, cover: () => h.lobbyTop, tier: false },
307
+ { run: line(at(frame, -middle.half, coreB + depth), at(frame, middle.half, coreB + depth)), gable: middle, cover: (x) => (Math.abs(x) <= lower.half ? wallTop(lower, x) : h.lobbyTop), tier: true },
308
+ { run: line(at(frame, -lower.half, form.fronts[frame]), at(frame, lower.half, form.fronts[frame])), gable: lower, cover: () => h.lobbyTop, tier: true },
309
+ ];
310
+ });
311
+ const rightAngle = -Math.PI / 2;
312
+ const box = (target, run, x0, x1, before, after, width, lo, hi, half) => {
313
+ const bottom = Math.max(lo, form.base);
314
+ if (hi - bottom < 0.05)
315
+ return;
316
+ mitredBox(kit, target, run, x0 + half, x1 + half, before, after, width, bottom, hi, y);
317
+ };
318
+ for (const { run, gable, cover, tier } of faces) {
319
+ const half = gable.half, span = (x0, x1, f, pick) => pick(f(x0 + 1e-4), f(x1 - 1e-4));
320
+ for (const sign of [-1, 1]) {
321
+ pierStations(gable).forEach((r, i) => {
322
+ const [lo, hi] = i ? [r - pierWidth / 2, r + pierWidth / 2] : [half - pierWidth, half];
323
+ const [x0, x1] = sign < 0 ? [-hi, -lo] : [lo, hi];
324
+ // A pier over a tier stands on the coping along that tier's step: every step's edge
325
+ // falls on a joint of the wall behind it.
326
+ const covered = span(x0, x1, cover, Math.max), bottom = covered > h.lobbyTop + 1e-6 ? covered + 0.26 : covered;
327
+ const top = span(x0, x1, (x) => wallTop(gable, x), Math.min) - 0.7;
328
+ const corner = i === 0 ? rightAngle : undefined;
329
+ box(piers, run, x0, x1, sign < 0 ? corner : undefined, sign > 0 ? corner : undefined, pierDepth, bottom, top, half);
330
+ });
331
+ for (let bay = 0; bay < gable.bays; bay += 1) {
332
+ const top = gable.shoulder + bay * h.pitch, outer = half - bay * gable.bay, inner = outer - gable.bay;
333
+ const [x0, x1] = sign < 0 ? [-outer, -inner] : [inner, outer];
334
+ // Each coping meets the side's at a convex corner: the core's corner, and every step
335
+ // of a tier. The core's steps above its eave end against the pyramid's risers.
336
+ const corner = bay === 0 || tier ? rightAngle : undefined;
337
+ box(copings, run, x0, x1, sign < 0 ? corner : undefined, sign > 0 ? corner : undefined, 0.45, top - 0.7, top + 0.25, half);
338
+ }
339
+ }
340
+ // Mullions between the strip's lites, from what covers the strip to under its head.
341
+ const lites = Math.round(2 * gable.strip / 1.85), lite = 2 * gable.strip / lites, n = run.normal(0);
342
+ for (let k = 1; k < lites; k += 1) {
343
+ const x = -gable.strip + k * lite, bottom = Math.max(cover(x - 0.06), cover(x + 0.06), form.base) + 0.05;
344
+ const top = Math.min(wallTop(gable, x - 0.06), wallTop(gable, x + 0.06)) - 0.35;
345
+ if (top - bottom > 0.5)
346
+ kit.box(mullions, run.at(x + half), n, 0.06, -0.02, 0.14, y(bottom), y(top));
347
+ }
348
+ }
349
+ // The tiers' sides: a corner pier meeting the front's, and a coping along each step's
350
+ // top from the front back to the wall behind.
351
+ for (const tier of tiers) {
352
+ const { frame, gable, back, front } = tier;
353
+ for (let step = 0; step < gable.bays; step += 1) {
354
+ const half = gable.half - step * gable.bay, top = gable.shoulder + step * h.pitch;
355
+ const east = line(at(frame, half, front), at(frame, half, back)), west = line(at(frame, -half, back), at(frame, -half, front));
356
+ const length = front - back;
357
+ box(copings, east, -length / 2, length / 2, rightAngle, undefined, 0.45, top - 0.7, top + 0.25, length / 2);
358
+ box(copings, west, -length / 2, length / 2, undefined, rightAngle, 0.45, top - 0.7, top + 0.25, length / 2);
359
+ if (step === 0) {
360
+ const bottom = h.lobbyTop;
361
+ box(piers, east, -length / 2, -length / 2 + pierWidth, rightAngle, undefined, pierDepth, bottom, top - 0.7, length / 2);
362
+ box(piers, west, length / 2 - pierWidth, length / 2, undefined, rightAngle, pierDepth, bottom, top - 0.7, length / 2);
363
+ }
364
+ }
365
+ }
366
+ // A broad rib on each ridge: a hipped plate whose front edges follow its strip's pointed
367
+ // head, from the head's shoulders to its point, and whose ridge and inner edges run on
368
+ // straight to the apex. The pyramid's steps end on its inner edges, and under it each
369
+ // step stands back, so the rib stands proud of the steps beside it.
370
+ const ribDepth = 0.35;
371
+ for (const frame of [0, 1, 2, 3]) {
372
+ const gable = frame < 2 ? g.front : g.side, reach = frame < 2 ? coreB : coreA, shoulder = stripShoulder(gable);
373
+ const corner = (u, v, real) => lift(at(frame, u, v), real);
374
+ // Its left shoulder seen from outside, the head's point, its right shoulder and the apex,
375
+ // then the same raised.
376
+ const low = [corner(-gable.strip, reach, shoulder), corner(0, reach, gable.peak), corner(gable.strip, reach, shoulder), corner(0, 0, h.apex)];
377
+ const high = low.map(([x, real, z]) => [x, real + ribDepth, z]);
378
+ // A point inside each half of the plate, to turn every face's normal outward: the plate
379
+ // is folded along its ridge, so no one centre lies inside it.
380
+ const inside = (a, b, c) => [0, 1, 2].map((k) => (a[k] + b[k] + c[k]) / 3 + (k === 1 ? ribDepth / 2 : 0));
381
+ const left = inside(low[0], low[1], low[3]), right = inside(low[1], low[2], low[3]);
382
+ const face = (points, within) => {
383
+ const u = points[1].map((value, k) => value - points[0][k]), w = points[2].map((value, k) => value - points[0][k]);
384
+ let n = [u[1] * w[2] - u[2] * w[1], u[2] * w[0] - u[0] * w[2], u[0] * w[1] - u[1] * w[0]];
385
+ const centre = points.reduce((sum, p) => sum.map((value, k) => value + p[k] / points.length), [0, 0, 0]);
386
+ if (n[0] * (centre[0] - within[0]) + n[1] * (centre[1] - within[1]) + n[2] * (centre[2] - within[2]) < 0)
387
+ n = n.map((value) => -value);
388
+ if (points.length === 3)
389
+ kit.triangle(ribs, points, [n, n, n]);
390
+ else
391
+ kit.quad(ribs, points, [n]);
392
+ };
393
+ face([high[0], high[1], high[3]], left);
394
+ face([high[1], high[2], high[3]], right);
395
+ face([low[0], low[1], low[3]], left);
396
+ face([low[1], low[2], low[3]], right);
397
+ // Its front edges over the head, then its inner edges back from the apex.
398
+ for (const [i, j, within] of [[0, 1, left], [1, 2, right], [2, 3, right], [3, 0, left]])
399
+ face([low[i], low[j], high[j], high[i]], within);
400
+ }
401
+ // The spire: a shaft turned with the pyramid, its corners on the ridges, tapering from its
402
+ // foot in the cap to a needle.
403
+ const spireAt = (r, real) => [[0, r], [r, 0], [0, -r], [-r, 0]].map(([u, v]) => lift(at(0, u, v), real));
404
+ const faceNormal = (p, q, r) => {
405
+ const u = q.map((value, k) => value - p[k]), w = r.map((value, k) => value - p[k]);
406
+ const n = [u[1] * w[2] - u[2] * w[1], u[2] * w[0] - u[0] * w[2], u[0] * w[1] - u[1] * w[0]];
407
+ const centre = lift(form.centre, p[1] + form.base);
408
+ return (n[0] * (p[0] - centre[0]) + n[2] * (p[2] - centre[2]) < 0 ? n.map((value) => -value) : n);
409
+ };
410
+ const footY = h.apex - 3.9, foot = spireAt(1.6, footY), shaft = spireAt(0.85, h.shaftTop), needle = spireAt(0.32, h.shaftTop), tip = lift(form.centre, h.tip);
411
+ for (let i = 0; i < 4; i += 1) {
412
+ const j = (i + 1) % 4, n = faceNormal(foot[i], foot[j], shaft[i]);
413
+ kit.quad(spire, [foot[i], foot[j], shaft[j], shaft[i]], [n]);
414
+ const m = faceNormal(needle[i], needle[j], tip);
415
+ kit.triangle(spire, [needle[i], needle[j], tip], [m, m, m]);
416
+ // A dark inset panel down each face, in two lengths between bright folded edges.
417
+ const unit = n.map((value) => value / Math.hypot(...n));
418
+ const radius = (real) => 1.6 + (0.85 - 1.6) * (real - footY) / (h.shaftTop - footY);
419
+ for (const [lo, hi] of [[h.apex + 0.6, h.apex + 7.4], [h.apex + 7.9, h.shaftTop - 1.2]]) {
420
+ // Half the face's width, about its middle.
421
+ const corners = [[lo, -1], [lo, 1], [hi, 1], [hi, -1]].map(([real, side]) => {
422
+ const ring = spireAt(radius(real), real), [p, q] = [ring[i], ring[j]];
423
+ return p.map((value, k) => (value + q[k]) / 2 + (q[k] - value) / 4 * side);
424
+ });
425
+ const moved = (distance) => corners.map((p) => p.map((value, k) => value + unit[k] * distance));
426
+ const [outer, inner] = [moved(0.03), moved(-0.02)];
427
+ kit.quad(spirePanels, [outer[0], outer[1], outer[2], outer[3]], [unit]);
428
+ kit.quad(spirePanels, [inner[0], inner[1], inner[2], inner[3]], [unit.map((value) => -value)]);
429
+ for (let k = 0; k < 4; k += 1) {
430
+ const l = (k + 1) % 4, quad = [inner[k], inner[l], outer[l], outer[k]];
431
+ const edge = outer[l].map((value, c) => value - outer[k][c]), side = [edge[1] * unit[2] - edge[2] * unit[1], edge[2] * unit[0] - edge[0] * unit[2], edge[0] * unit[1] - edge[1] * unit[0]];
432
+ const centre = outer.reduce((sum, v) => sum.map((value, c) => value + v[c] / 4), [0, 0, 0]), mid = outer[k].map((value, c) => (value + outer[l][c]) / 2);
433
+ kit.quad(spirePanels, quad, [side[0] * (mid[0] - centre[0]) + side[1] * (mid[1] - centre[1]) + side[2] * (mid[2] - centre[2]) < 0 ? side.map((value) => -value) : side]);
434
+ }
435
+ }
436
+ }
437
+ kit.quad(spire, [foot[0], foot[1], foot[2], foot[3]], [[0, -1, 0]]);
438
+ kit.quad(spire, [shaft[0], shaft[1], shaft[2], shaft[3]], [[0, 1, 0]]);
439
+ kit.quad(spire, [needle[0], needle[1], needle[2], needle[3]], [[0, -1, 0]]);
440
+ return kit.finish({ height: y(h.tip), outlines: [body, spire], opacity: 0.18 });
441
+ }