@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,176 @@
1
+ import { line } from "./building-kit.js";
2
+ // An outline's corners in the winding that gives every run of its plan an outward normal.
3
+ export const orient = (corners) => {
4
+ const area = corners.reduce((sum, [x, z], i) => { const [x2, z2] = corners[(i + 1) % corners.length]; return sum + x * z2 - x2 * z; }, 0);
5
+ return area < 0 ? corners : [...corners].reverse();
6
+ };
7
+ export const planOf = (corners) => corners.map((corner, i) => line(corner, corners[(i + 1) % corners.length]));
8
+ // The turn from run a into run b at their shared corner; negative where the outline turns
9
+ // out (a convex corner) and positive where it turns in.
10
+ export const turnAt = (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]); };
11
+ export function chainsOf(plan, straight = 5 * Math.PI / 180) {
12
+ const breaks = plan.map((run, i) => Math.abs(turnAt(plan[(i + plan.length - 1) % plan.length], run)) >= straight);
13
+ const first = Math.max(0, breaks.indexOf(true));
14
+ const chains = [];
15
+ for (let k = 0; k < plan.length; k += 1) {
16
+ const i = (first + k) % plan.length, chain = chains.at(-1);
17
+ if (breaks[i] || !chain)
18
+ chains.push({ runs: [i], starts: [0], length: plan[i].length });
19
+ else {
20
+ chain.runs.push(i);
21
+ chain.starts.push(chain.length);
22
+ chain.length += plan[i].length;
23
+ }
24
+ }
25
+ return chains;
26
+ }
27
+ // A closed box standing `width` proud of a run from s0 to s1 and lo to hi: a cap, a band
28
+ // course, a corner pier. An end given the turn at its corner is mitred on that corner's
29
+ // bisector, so the neighbouring run's box meets it in one plane, facing the other way, and
30
+ // the two never overlap; an end without one is square. At a convex corner (a negative turn)
31
+ // the outer edge runs past the wall's end; at a concave one it stops short of it.
32
+ export function mitredBox(kit, target, run, s0, s1, before, after, width, lo, hi, y = (real) => real) {
33
+ const outer0 = s0 + (before === undefined ? 0 : Math.tan(before / 2) * width), outer1 = s1 - (after === undefined ? 0 : Math.tan(after / 2) * width);
34
+ const n = run.normal(0);
35
+ const at = (s, depth, v) => { const q = run.at(s, depth); return [q[0], y(v), q[1]]; };
36
+ const inner = [at(s0, 0, lo), at(s1, 0, lo), at(s1, 0, hi), at(s0, 0, hi)];
37
+ const outer = [at(outer0, width, lo), at(outer1, width, lo), at(outer1, width, hi), at(outer0, width, hi)];
38
+ const normal = (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 g = [u[1] * v[2] - u[2] * v[1], u[2] * v[0] - u[0] * v[2], u[0] * v[1] - u[1] * v[0]], l = Math.hypot(...g);
41
+ return [g[0] / l, g[1] / l, g[2] / l];
42
+ };
43
+ kit.quad(target, [outer[0], outer[1], outer[2], outer[3]], [[n[0], 0, n[1]]]);
44
+ kit.quad(target, [inner[1], inner[0], inner[3], inner[2]], [[-n[0], 0, -n[1]]]);
45
+ kit.quad(target, [inner[3], outer[3], outer[2], inner[2]], [[0, 1, 0]]);
46
+ kit.quad(target, [inner[0], inner[1], outer[1], outer[0]], [[0, -1, 0]]);
47
+ kit.quad(target, [inner[0], outer[0], outer[3], inner[3]], [normal(inner[0], outer[0], outer[3])]);
48
+ kit.quad(target, [outer[1], inner[1], inner[2], outer[2]], [normal(outer[1], inner[1], inner[2])]);
49
+ }
50
+ // A closed box on a run, standing `back` to `front` off the wall between the first and last
51
+ // of `columns` along it and the first and last of `rows` up it, whose front is a grid of
52
+ // coloured cells: panes and mullions, window ribbons, louver blades. Its top and bottom fan
53
+ // out from a back corner through every column cut, and its ends through every row cut, so
54
+ // each cell's edge meets its partner and the box closes edge for edge, without the
55
+ // T-junctions a single top face would leave against a cut front. Heights are real heights;
56
+ // `y` maps them to the model's.
57
+ export function gridBox(kit, target, run, columns, rows, back, front, paint, sides, y = (real) => real) {
58
+ const n = run.normal(0), tangent = [n[1], -n[0]];
59
+ const at = (s, depth, height) => { const p = run.at(s, depth); return [p[0], y(height), p[1]]; };
60
+ const [s0, s1] = [columns[0], columns.at(-1)], [y0, y1] = [rows[0], rows.at(-1)];
61
+ for (let c = 0; c + 1 < columns.length; c += 1) {
62
+ for (let r = 0; r + 1 < rows.length; r += 1) {
63
+ const [a, b, lo, hi] = [columns[c], columns[c + 1], rows[r], rows[r + 1]];
64
+ kit.quad(target, [at(a, front, lo), at(b, front, lo), at(b, front, hi), at(a, front, hi)], [[n[0], 0, n[1]]], paint(c, r));
65
+ }
66
+ }
67
+ kit.quad(target, [at(s1, back, y0), at(s0, back, y0), at(s0, back, y1), at(s1, back, y1)], [[-n[0], 0, -n[1]]], sides);
68
+ const fan = (hinge, rim, normal) => {
69
+ for (let i = 0; i + 1 < rim.length; i += 1)
70
+ kit.triangle(target, [hinge, rim[i], rim[i + 1]], [normal, normal, normal], sides);
71
+ };
72
+ fan(at(s0, back, y1), [...columns.map((s) => at(s, front, y1)), at(s1, back, y1)], [0, 1, 0]);
73
+ fan(at(s0, back, y0), [...columns.map((s) => at(s, front, y0)), at(s1, back, y0)], [0, -1, 0]);
74
+ fan(at(s0, back, y0), [...rows.map((v) => at(s0, front, v)), at(s0, back, y1)], [-tangent[0], 0, -tangent[1]]);
75
+ fan(at(s1, back, y0), [...rows.map((v) => at(s1, front, v)), at(s1, back, y1)], [tangent[0], 0, tangent[1]]);
76
+ }
77
+ // A slab in a vertex-coloured batch, painted, since kit.slab lays its vertices white.
78
+ export function paintedSlab(kit, target, polygon, height, up, paint) {
79
+ const start = target.colors.length;
80
+ kit.slab(target, polygon, height, up);
81
+ for (let i = start; i < target.colors.length; i += 3)
82
+ target.colors.splice(i, 3, paint.r, paint.g, paint.b);
83
+ }
84
+ // The turns at a run's two corners, into it and out of it.
85
+ export const turnsAt = (plan, index) => [turnAt(plan[(index + plan.length - 1) % plan.length], plan[index]), turnAt(plan[index], plan[(index + 1) % plan.length])];
86
+ // Piers and mullions cross the joints of a chain in pieces, one per run, mitred on each
87
+ // joint's bisector as mitred courses are, each standing from its own run's `froms` to its
88
+ // own run's top. Where the cover changes at a joint, the less covered run's last 2 cm stand
89
+ // from the higher cover, so the piece ends clear of the neighbour's wall. A run's cells stop
90
+ // a little short of every joint, of a more covered neighbour, and of the wedge a concave
91
+ // joint would push into the next run's cells: `jointTrim` gives how far, toward `other`.
92
+ // `clearance` is how far cells stop short of a more covered neighbour.
93
+ export function jointTrim(plan, chain, froms, k, other, clearance = 0.01) {
94
+ if (other < 0 || other >= chain.runs.length)
95
+ return 0;
96
+ const [a, b] = other > k ? [chain.runs[k], chain.runs[other]] : [chain.runs[other], chain.runs[k]];
97
+ const turn = turnAt(plan[a], plan[b]);
98
+ return 0.005 + (turn > 0 ? 0.08 * Math.tan(turn / 2) : 0) + (froms[k] < froms[other] ? clearance : 0);
99
+ }
100
+ export function chainPier(kit, target, plan, chain, froms, a, b, depth, top, y = (real) => real) {
101
+ const last = chain.runs.length - 1;
102
+ chain.runs.forEach((index, k) => {
103
+ const run = plan[index], start = chain.starts[k], end = start + run.length, ceiling = typeof top === "number" ? top : top[k];
104
+ const lo = Math.max(a, start), hi = Math.min(b, end);
105
+ if (hi - lo < 0.005)
106
+ return;
107
+ const [before, after] = turnsAt(plan, index);
108
+ const mitreLo = lo === start && (a < start || (k === 0 && a === 0)) ? before : undefined;
109
+ const mitreHi = hi === end && (b > end || (k === last && b === chain.length)) ? after : undefined;
110
+ const pieces = [[lo, hi, froms[k], mitreLo, mitreHi]];
111
+ if (mitreHi !== undefined && k < last && froms[k] < froms[k + 1] && hi - 0.02 > lo)
112
+ pieces.splice(0, 1, [lo, hi - 0.02, froms[k], mitreLo, undefined], [hi - 0.02, hi, froms[k + 1], undefined, mitreHi]);
113
+ const first = pieces[0];
114
+ if (mitreLo !== undefined && k > 0 && froms[k] < froms[k - 1] && first[1] > lo + 0.02)
115
+ pieces.splice(0, 1, [lo, lo + 0.02, froms[k - 1], mitreLo, undefined], [lo + 0.02, first[1], first[2], undefined, first[4]]);
116
+ for (const [p0, p1, from, m0, m1] of pieces) {
117
+ if (from < ceiling)
118
+ mitredBox(kit, target, run, p0 - start, p1 - start, m0, m1, depth, Math.max(from, 0.3), ceiling, y);
119
+ }
120
+ });
121
+ }
122
+ // A closed box along runs k0 to k1 of a chain, from s0 to s1 along it, standing `back` to
123
+ // `front` off the walls, whose front is a grid of coloured cells cut at `columns` along the
124
+ // chain and at `rows` up it: a curtain wall's glass and spandrels round a traced curve. Where
125
+ // two of its runs meet, its faces meet on the joint's mitre, so a curve carries one box with
126
+ // ends only at s0 and s1. Its tops and bottoms fan out from a back corner through each run's
127
+ // column cuts, and its ends through every row cut, so it closes edge for edge. A cell is
128
+ // painted by its bay, the columns at or before its start, so a cell a joint splits keeps
129
+ // one colour.
130
+ // `paint` runs immediately before its front quad appends six vertices to target.
131
+ // Blue Cross records that vertex offset to address individual window lights.
132
+ export function chainSkin(kit, target, plan, chain, k0, k1, s0, s1, columns, rows, back, front, paint, sides, y = (real) => real) {
133
+ const lift = (q, v) => [q[0], y(v), q[1]];
134
+ const mitre = (k, depth) => {
135
+ const a = plan[chain.runs[k]], b = plan[chain.runs[k + 1]], v = a.at(a.length), n1 = a.normal(a.length), n2 = b.normal(0);
136
+ const scale = depth / (1 + n1[0] * n2[0] + n1[1] * n2[1]);
137
+ return [v[0] + (n1[0] + n2[0]) * scale, v[1] + (n1[1] + n2[1]) * scale];
138
+ };
139
+ const [y0, y1] = [rows[0], rows.at(-1)];
140
+ const up = [0, 1, 0], down = [0, -1, 0];
141
+ const fan = (hinge, rim, normal, paintWith = sides) => {
142
+ for (let i = 0; i + 1 < rim.length; i += 1)
143
+ kit.triangle(target, [hinge, rim[i], rim[i + 1]], [normal, normal, normal], paintWith);
144
+ };
145
+ for (let k = k0; k <= k1; k += 1) {
146
+ const run = plan[chain.runs[k]], start = chain.starts[k], n = run.normal(0), out = [n[0], 0, n[1]];
147
+ const from = k === k0 ? s0 : start, to = k === k1 ? s1 : start + run.length;
148
+ const cuts = columns.filter((s) => s > from + 1e-6 && s < to - 1e-6);
149
+ const arcs = [from, ...cuts, to];
150
+ const at = (s, depth, index) => {
151
+ if (index === 0 && k > k0)
152
+ return mitre(k - 1, depth);
153
+ if (index === arcs.length - 1 && k < k1)
154
+ return mitre(k, depth);
155
+ return run.at(s - start, depth);
156
+ };
157
+ const fronts = arcs.map((s, i) => at(s, front, i));
158
+ const [backStart, backEnd] = [at(from, back, 0), at(to, back, arcs.length - 1)];
159
+ for (let i = 0; i + 1 < fronts.length; i += 1) {
160
+ const bay = columns.filter((s) => s <= arcs[i] + 1e-6).length;
161
+ for (let r = 0; r + 1 < rows.length; r += 1) {
162
+ kit.quad(target, [lift(fronts[i], rows[r]), lift(fronts[i + 1], rows[r]), lift(fronts[i + 1], rows[r + 1]), lift(fronts[i], rows[r + 1])], [out], paint(bay, r));
163
+ }
164
+ }
165
+ kit.quad(target, [lift(backEnd, y0), lift(backStart, y0), lift(backStart, y1), lift(backEnd, y1)], [[-n[0], 0, -n[1]]], sides);
166
+ fan(lift(backStart, y1), [...fronts.map((q) => lift(q, y1)), lift(backEnd, y1)], up);
167
+ fan(lift(backStart, y0), [...fronts.map((q) => lift(q, y0)), lift(backEnd, y0)], down);
168
+ }
169
+ // The ends, square to their runs.
170
+ const ends = [[k0, s0, -1], [k1, s1, 1]];
171
+ for (const [k, s, sign] of ends) {
172
+ // Outward along the run at s1, back along it at s0.
173
+ const run = plan[chain.runs[k]], start = chain.starts[k], n = run.normal(0), t = [n[1] * sign, 0, -n[0] * sign];
174
+ fan(lift(run.at(s - start, back), y0), [...rows.map((v) => lift(run.at(s - start, front), v)), lift(run.at(s - start, back), y1)], t);
175
+ }
176
+ }
@@ -0,0 +1,154 @@
1
+ import * as THREE from "../vendor/three-r186.js";
2
+ import { createBuilder, polygonOf } from "./building-kit.js";
3
+ import { chainSkin, chainsOf, orient, paintedSlab, planOf, turnsAt } from "./facade-grid.js";
4
+ // The Gage Building, 18 South Michigan Avenue (Holabird & Roche, with a front by Louis
5
+ // Sullivan, 1899; four storeys added in 1902): the geographic layout's model, on the mapped
6
+ // outline 124865450. The original layout has no model of it; the drawing shows its Michigan
7
+ // front and, above its lower neighbours at 24 and 30 South Michigan, its south wall, between
8
+ // the University Club and Willoughby Tower.
9
+ //
10
+ // A terracotta front of three bays between tall piers rises twelve storeys, a wide window of
11
+ // several lights to each bay and floor, to a parapet with cartouches over the inner piers.
12
+ // The south wall is common brick, windowed near the front and on two floors. The parapet is
13
+ // the City's 154 ft, the published height nearest the drawing's reading. The drawn parapet
14
+ // reads 54.2 m, so heights read on the drawing are scaled to meet it; see
15
+ // docs/gage-reference.md. Units are meters; +x is east, +z is south.
16
+ const drawn = (height) => height * 46.94 / 54.2;
17
+ export const gageLevels = Object.freeze({
18
+ shopfront: 4, // the ground floor's shop windows, under a sign band
19
+ second: 5.2, // the second floor
20
+ eighth: drawn(33.47), // the eighth floor, the lowest the drawing shows
21
+ pitch: drawn(3.715), // each floor from the eighth up, as drawn
22
+ top: 46.94, // the parapet, the City's 154 ft
23
+ cartouches: 47.74, // the cartouches over the inner piers
24
+ });
25
+ const h = gageLevels;
26
+ // Each floor's level: the ground floor, six floors of 3.97 m to the eighth, which the hill
27
+ // hides, and the drawn floors above it.
28
+ const floor = (n) => (n < 2 ? 0 : n <= 8 ? h.second + (n - 2) * (h.eighth - h.second) / 6 : h.eighth + (n - 8) * h.pitch);
29
+ // The skins' colours: the window tones, and the terracotta and the brick, which no window
30
+ // shares.
31
+ export const gagePalette = Object.freeze({ glass: [0x262626, 0x2a2a2a, 0x2e2e2e, 0x323232], lit: 0x6e6e6e, dim: 0x3a3a3a, terracotta: 0x5e5e5e, band: 0x484848, brick: 0x404040 });
32
+ const palette = gagePalette;
33
+ const color = (hex) => new THREE.Color(hex);
34
+ const glassTones = palette.glass.map(color);
35
+ const litGlass = color(palette.lit), dimGlass = color(palette.dim);
36
+ const terracotta = color(palette.terracotta), band = color(palette.band), brick = color(palette.brick);
37
+ const core = color(0x262626), roofing = color(0x3a3a3a);
38
+ function paneColor(row, bay, wall) {
39
+ const hash = (Math.imul(row + 43, 0x9e3779b1) ^ Math.imul(bay + 67, 0x85ebca77) ^ Math.imul(wall + 53, 0xc2b2ae3d)) >>> 0;
40
+ const value = hash % 101;
41
+ if (value < 4)
42
+ return litGlass;
43
+ if (value < 8)
44
+ return dimGlass;
45
+ return glassTones[value % glassTones.length];
46
+ }
47
+ const rows = [
48
+ { lo: 0.25, hi: 0.9, kind: "wall", floor: 1 },
49
+ { lo: 0.9, hi: h.shopfront, kind: "glass", floor: 1 },
50
+ { lo: h.shopfront, hi: h.second, kind: "band", floor: 1 },
51
+ ];
52
+ for (let n = 2; n <= 12; n += 1) {
53
+ rows.push({ lo: rows.at(-1).hi, hi: floor(n) + drawn(0.86), kind: "wall", floor: n });
54
+ rows.push({ lo: floor(n) + drawn(0.86), hi: floor(n) + drawn(3.29), kind: "glass", floor: n });
55
+ }
56
+ rows.push({ lo: rows.at(-1).hi, hi: h.top, kind: "wall", floor: 13 });
57
+ export function createGageGeographicBuilding(record, projectPlan, offset) {
58
+ const kit = createBuilder(record.name, record.id);
59
+ const shell = kit.batch("Gage Building · shell", kit.material(0xffffff, { vertexColors: true }));
60
+ const wall = kit.batch("Gage Building · terracotta and windows", kit.material(0xffffff, { vertexColors: true }));
61
+ const cartouches = kit.batch("Gage Building · cartouches", kit.material(0x6a6a6a));
62
+ const lot = orient(polygonOf(projectPlan(record.footprint.coordinates)));
63
+ // The lot's south-east corner, with its south wall running west along 24 South Michigan
64
+ // and its Michigan front north to the Chicago Athletic Association.
65
+ const corner = lot.reduce((best, p) => (p[0] + p[1] > best[0] + best[1] ? p : best));
66
+ const northEast = lot.reduce((best, p) => (p[0] - p[1] > best[0] - best[1] ? p : best));
67
+ const southWest = lot.reduce((best, p) => (p[1] - p[0] > best[1] - best[0] ? p : best));
68
+ const unit = (p) => { const d = Math.hypot(p[0] - corner[0], p[1] - corner[1]); return [(p[0] - corner[0]) / d, (p[1] - corner[1]) / d]; };
69
+ const west = unit(southWest), north = unit(northEast);
70
+ const at = (a, b) => [corner[0] + west[0] * a + north[0] * b, corner[1] + west[1] * a + north[1] * b];
71
+ // The inverse of `at`: a point's distances along the south wall and the Michigan front,
72
+ // which meet a little off square.
73
+ const skew = west[0] * north[1] - west[1] * north[0];
74
+ const inLot = (p) => {
75
+ const [x, z] = [p[0] - corner[0], p[1] - corner[1]];
76
+ return [(x * north[1] - z * north[0]) / skew, (west[0] * z - west[1] * x) / skew];
77
+ };
78
+ const rect = (a0, a1, b0, b1) => orient([at(a0, b0), at(a1, b0), at(a1, b1), at(a0, b1)]);
79
+ planOf(lot).forEach((run) => {
80
+ const a = run.at(0), b = run.at(run.length), n = run.normal(0);
81
+ kit.quad(shell, [[a[0], 0, a[1]], [b[0], 0, b[1]], [b[0], h.top, b[1]], [a[0], h.top, a[1]]], [[n[0], 0, n[1]]], core);
82
+ });
83
+ paintedSlab(kit, shell, lot, 0, false, roofing);
84
+ paintedSlab(kit, shell, lot, h.top, true, roofing);
85
+ // The cartouches that end the inner piers, rising clear of the parapet: two blocks on the
86
+ // roof, 5 cm inside the front's plane.
87
+ for (const b of [5.6, 11.3])
88
+ kit.prism(cartouches, planOf(rect(0.05, 0.85, b + 0.05, b + 1.15)), [h.top, h.cartouches]);
89
+ const skin = (seed, keep, openings) => {
90
+ const plan = planOf(lot), heights = [rows[0].lo, ...rows.map((row) => row.hi)];
91
+ chainsOf(plan).forEach((chain, index) => {
92
+ if (!chain.runs.every((run) => keep(plan[run])))
93
+ return;
94
+ const first = chain.runs[0], last = chain.runs.at(-1);
95
+ const s0 = turnsAt(plan, first)[0] > 0 ? 0.09 : 0.02, s1 = chain.length - (turnsAt(plan, last)[1] > 0 ? 0.09 : 0.02);
96
+ const [columns, column] = openings(chain.length, plan[first].at(0), plan[last].at(plan[last].length));
97
+ chainSkin(kit, wall, plan, chain, 0, chain.runs.length - 1, s0, s1, columns, heights, 0.02, 0.07, (bay, r) => {
98
+ const row = rows[r], { face, floors } = column(bay);
99
+ if (row.kind === "band")
100
+ return face === brick ? brick : band;
101
+ return row.kind === "glass" && floors(row.floor) ? paneColor(row.floor, bay, seed * 8 + index) : face;
102
+ }, face(seed));
103
+ });
104
+ };
105
+ const face = (seed) => (seed === 0 ? terracotta : brick);
106
+ // A position along a wall from its start, given in lot coordinates across it.
107
+ const along = (length, start, end, axis) => {
108
+ const [p, q] = [inLot(start)[axis], inLot(end)[axis]];
109
+ return [(target) => length * (target - p) / (q - p), (s) => p + (s / length) * (q - p)];
110
+ };
111
+ // Windows spanning `spans`, in lot coordinates along `axis`, glazed on the floors `floors`
112
+ // gives; a window's lights are split by its mullions at `mullions`.
113
+ const windows = (length, start, end, axis, spans, floors, face, mullions = []) => {
114
+ const [to, from] = along(length, start, end, axis);
115
+ const inside = spans.map(([a, b]) => [to(a), to(b)].sort((p, q) => p - q)).filter(([a, b]) => a > 0.1 && b < length - 0.1);
116
+ const cuts = [...inside.flat(), ...mullions.map(to).flatMap((s) => [s - 0.06, s + 0.06])].filter((s) => s > 0.1 && s < length - 0.1);
117
+ const columns = [0, ...[...new Set(cuts)].sort((p, q) => p - q), length];
118
+ // Bays count from one, each between two columns; a bay holds a window when its middle
119
+ // lies inside one and off its mullions.
120
+ return [columns, (bay) => {
121
+ const middle = (columns[bay - 1] + columns[bay]) / 2;
122
+ const glazed = inside.some(([a, b]) => middle > a && middle < b) && !mullions.map(to).some((s) => Math.abs(middle - s) < 0.06);
123
+ return { face, floors: glazed ? floors(from(middle)) : () => false };
124
+ }];
125
+ };
126
+ const facing = (x, z) => (run) => run.normal(0)[0] * x + run.normal(0)[1] * z > 0.9;
127
+ const [isMichigan, isSouth] = [facing(1, 0), facing(0, 1)];
128
+ // The Michigan front, measured on the drawing from the south corner: corner piers 2 m wide,
129
+ // inner piers 1.15 and 1.2 m; the south and north bays' windows 3.6 m wide in four lights,
130
+ // the middle one's 4.55 m in five.
131
+ const bays = [[2, 5.6], [6.75, 11.3], [12.5, 16.1]];
132
+ const lights = [2.9, 3.8, 4.7, 7.66, 8.57, 9.48, 10.39, 13.4, 14.3, 15.2];
133
+ const michigan = (length, start, end) => windows(length, start, end, 1, bays, () => () => true, terracotta, lights);
134
+ // The south wall, measured on the drawing west from the front: a window 6 to 9.5 m in on
135
+ // the top four floors, and three more between 15.3 and 28.4 m on the ninth and twelfth, as
136
+ // drawn. The University Club hides the rest.
137
+ const southWall = (length, start, end) => windows(length, start, end, 0, [[6, 9.5], [15.3, 18.8], [20.3, 23.8], [24.8, 28.4]], (a) => (a < 10 ? (n) => n >= 9 : (n) => n === 9 || n === 12), brick);
138
+ // The alley: windows 1.6 m wide spread evenly about 3.2 m apart on every floor above the
139
+ // ground floor. The north wall, shared with the Chicago Athletic Association, stays plain.
140
+ const alley = (length, start, end) => {
141
+ const [, from] = along(length, start, end, 1), count = Math.max(1, Math.round(length / 3.2));
142
+ const spans = Array.from({ length: count }, (_, i) => { const c = from(length * (i + 0.5) / count); return [c - 0.8, c + 0.8]; });
143
+ return windows(length, start, end, 1, spans, () => (n) => n >= 2, brick);
144
+ };
145
+ skin(0, isMichigan, michigan);
146
+ skin(1, isSouth, southWall);
147
+ skin(2, facing(-1, 0), alley);
148
+ skin(3, facing(0, -1), (length) => [[0, length], () => ({ face: brick, floors: () => false })]);
149
+ const model = kit.finish({ height: h.cartouches, outlines: [shell, cartouches], opacity: 0.16 });
150
+ model.building.position.set(offset[0], 0, offset[1]);
151
+ model.building.userData["geography"] = record;
152
+ model.building.userData["geographicDetail"] = { levels: h, source: "docs/gage-reference.md" };
153
+ return model;
154
+ }
@@ -0,0 +1,284 @@
1
+ import * as THREE from "../vendor/three-r186.js";
2
+ import { along, arc, bulge, cornerNormal, createBuilder, deg, evenly, inside, line, point, station } from "./building-kit.js";
3
+ // Plan and heights are fitted numerically to the source SVG through the skyline
4
+ // camera: corners, mullions, and crown fins follow the drawing. The camera-facing flat
5
+ // face and bow radius are about 1.3x the OpenStreetMap trace; the hidden north and west
6
+ // faces are simplified closures (a straight north face and one shallow west bulge), not
7
+ // that outline. Units are meters; +x is east (right in the skyline view), +z is south.
8
+ // The kit's 2.4° arc facets give two per bow bay, so the silhouette stays round.
9
+ // The drawing's floor pitch; its six-floor bands and the lower tier's three-floor
10
+ // bands share one grid. Floor 0 is the crown base, floor 30 the lower-tier ledge.
11
+ const floorHeight = 3.794;
12
+ const crownBase = 175.4;
13
+ const level = (floor) => crownBase - floor * floorHeight;
14
+ const parapet = 191.3, capSoffit = 189.1, capProud = 1.2, bandProud = 0.34;
15
+ const lowerParapet = 74.3, lowerSoffit = lowerParapet - 2.6, lowerLedge = level(30), lowerCapProud = 0.66;
16
+ const screenTop = 204.6, penthouseTop = 202.3;
17
+ // Tower plan, left to right in the skyline view: the south stub, a flat south face,
18
+ // the convex east bow, and a flat north strip along the bow's end tangent.
19
+ const joint = [13, 23];
20
+ const stubLength = 12, stubDepth = 4.4, flatLength = 18.7, stripLength = 13.95;
21
+ const stubA = [joint[0] - flatLength - stubLength, joint[1] + stubDepth];
22
+ const stubB = [joint[0] - flatLength, joint[1] + stubDepth];
23
+ const stubC = [joint[0] - flatLength, joint[1]];
24
+ const bowStart = 63 * deg, bowEnd = bowStart + 38.4 * deg, bowRadius = 57;
25
+ const bow = arc([joint[0] - bowRadius * Math.sin(bowStart), joint[1] - bowRadius * Math.cos(bowStart)], bowRadius, bowStart, bowEnd);
26
+ const bowEndPoint = bow.at(bow.length);
27
+ const strip = line(bowEndPoint, [bowEndPoint[0] + stripLength * Math.cos(bowEnd), bowEndPoint[1] - stripLength * Math.sin(bowEnd)]);
28
+ const northEast = strip.at(strip.length);
29
+ const northWest = [-13, northEast[1]], westEnd = [stubA[0], 21];
30
+ const flatFace = line(stubC, joint), north = line(northEast, northWest), westBulge = bulge(northWest, westEnd, 2.5);
31
+ const tower = [line(stubA, stubB), line(stubB, stubC), flatFace, bow, strip, north, westBulge, line(westEnd, stubA)];
32
+ // Lower tier: a shallow concave Garland Court face whose north end stands just
33
+ // proud of the bow. Its unlit south part is trimmed along the skyline view's line
34
+ // of sight, turned 3° away, so nothing appears left of the drawn silhouette.
35
+ const lowerNorth = bow.at(12.1, 0.3);
36
+ const lowerHeading = 77.1 * deg, lowerRadius = 100, lowerLength = 49.3, lowerSouthLength = 3.56;
37
+ const lowerCenter = [lowerNorth[0] + lowerRadius * Math.sin(lowerHeading), lowerNorth[1] + lowerRadius * Math.cos(lowerHeading)];
38
+ const lowerFace = arc(lowerCenter, lowerRadius, lowerHeading + lowerLength / lowerRadius, lowerHeading, true);
39
+ // Near its north end the face runs inside the tower's thick bands. Lower bands on those
40
+ // floors stop short, and a shallower piece on a run 0.04 m farther out carries each on to
41
+ // the face's end, clear of the tower band's ledges.
42
+ const junctionTrim = 0.8, junctionInset = 0.04;
43
+ const junction = arc(lowerCenter, lowerRadius - junctionInset, lowerHeading + junctionTrim / lowerRadius, lowerHeading, true);
44
+ const lowerSE = lowerFace.at(0), lowerSW = [lowerSE[0] - lowerSouthLength, lowerSE[1]];
45
+ const sightline = 38.5 * deg, lowerWestX = stubC[0] - 1.3;
46
+ const chamferLength = (lowerSW[0] - lowerWestX) / Math.sin(sightline);
47
+ const lowerChamfer = [lowerWestX, lowerSW[1] - chamferLength * Math.cos(sightline)];
48
+ const lowerWestNorth = [lowerWestX, lowerNorth[1]];
49
+ const lowerTier = [line(lowerSW, lowerSE), lowerFace, line(lowerNorth, lowerWestNorth), line(lowerWestNorth, lowerChamfer), line(lowerChamfer, lowerSW)];
50
+ // Bay stations along each run, in meters from its start. Hidden runs place mullions
51
+ // on interior stations; the camera-facing runs use the fourteen drawn mullions below.
52
+ const flatBay = (flatLength - 1) / 3, lowerPitch = 4.84, lowerMargin = 3.85;
53
+ const stripStations = [6.35, 12.7];
54
+ const towerStations = [
55
+ [0, 6, stubLength], [0, stubDepth], [0, 1, 1 + flatBay, 1 + 2 * flatBay, flatLength], evenly(bow.length, 8),
56
+ [0, ...stripStations, strip.length], evenly(north.length, 6), evenly(westBulge.length, 10), evenly(tower[7].length, 2),
57
+ ];
58
+ const lowerFinStations = Array.from({ length: 10 }, (_, i) => lowerMargin + i * lowerPitch);
59
+ const lowerStations = [[0, lowerSouthLength], [0, ...lowerFinStations, lowerLength], [0, 1], evenly(lowerTier[3].length, 4), evenly(chamferLength, 7)];
60
+ const jointStation = { at: joint, normal: cornerNormal(flatFace.normal(), bow.normal(0)) };
61
+ // Fourteen drawn mullions: three on the flat face, the joint, eight bow bays, two on
62
+ // the strip. The twelfth, at the bow's end, is the center of the balcony stack.
63
+ const mullions = [
64
+ ...[1, 1 + flatBay, 1 + 2 * flatBay].map((s) => station(flatFace, s)), jointStation,
65
+ ...evenly(bow.length, 8).slice(1).map((s) => station(bow, s)), ...stripStations.map((s) => station(strip, s)),
66
+ ];
67
+ const balconyStack = mullions[11];
68
+ const crownFins = mullions.slice(1);
69
+ // The crown continues around the hidden north and west faces, so orbits find it finished.
70
+ const hiddenFins = [
71
+ ...towerStations[5].slice(1, -1).map((s) => station(north, s)),
72
+ ...towerStations[6].slice(1, -1).map((s) => station(westBulge, s)),
73
+ ];
74
+ const lowerFins = lowerFinStations.map((s) => station(lowerFace, s));
75
+ // Screen strip over the north-east corner; the lower part of the same block,
76
+ // with its open cornice frame, steps back to the west.
77
+ const screen = { east: northEast[0] - 0.5, north: northEast[1], width: 7.4, length: 44.08 };
78
+ const penthouse = { west: screen.east - screen.width - 14.74, front: screen.north + screen.length - 1.04, overhang: 1.1 };
79
+ // The crown cap and thick bands start under the first fin, as drawn. The cap stops
80
+ // short of the north-east corner to stay inside the drawn silhouette, and resumes on
81
+ // the hidden north face once the tower itself hides its projection.
82
+ const capStart = 5.5, capEnd = strip.length - 2, northCapStart = 1.6;
83
+ const finWidth = 1.3, finDepth = 0.9, lowerFinWidth = 1.5, lowerFinDepth = 0.7;
84
+ const midpoint = (a, b) => [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2];
85
+ // Features the skyline test projects against the drawing, derived from the geometry above.
86
+ export const heritageFeatures = {
87
+ screenFrontTop: [screen.east, screenTop, screen.north + screen.length],
88
+ screenNorthTop: [screen.east, screenTop, screen.north],
89
+ screenSouthTop: [screen.east - screen.width, screenTop, screen.north + screen.length],
90
+ penthouseCorniceTip: [penthouse.west - penthouse.overhang, penthouseTop, penthouse.front + penthouse.overhang],
91
+ stubFrontRoof: point(stubB, parapet),
92
+ stubLeftRoof: point(stubA, parapet),
93
+ reentrantRoof: point(stubC, parapet),
94
+ // The cap crests on the outer edge of its chamfer over the joint.
95
+ capApex: point(midpoint(flatFace.at(flatLength, capProud), bow.at(0, capProud)), parapet),
96
+ capSouthEnd: point(flatFace.at(capStart, capProud), parapet),
97
+ capNorthEnd: point(strip.at(capEnd, capProud), parapet),
98
+ crownFootNorth: point(northEast, crownBase),
99
+ lowerCapSouthCorner: point(lowerFace.at(0, lowerCapProud), lowerParapet),
100
+ lowerCapNorth: point(lowerFace.at(lowerLength, lowerCapProud), lowerParapet),
101
+ lowerFinFootNorth: point(along(lowerFins.at(-1), lowerFinDepth, lowerFinWidth / 2), lowerLedge),
102
+ // The lower tier's leftmost wall corner, and the cap and band overhangs beside it.
103
+ lowerSouthWest: [
104
+ point(lowerSW, lowerParapet), point(lowerSW, 0),
105
+ point(lowerTier[0].at(0, lowerCapProud), lowerParapet), point(lowerTier[0].at(0, bandProud), lowerLedge),
106
+ ],
107
+ mullions: mullions.map((m) => point(m.at, 120)),
108
+ crownFins: crownFins.map((m) => point(along(m, finDepth / 2), crownBase + 6)),
109
+ lowerFins: lowerFins.map((m) => point(along(m, lowerFinDepth / 2), lowerLedge + 4)),
110
+ bowFacade: point(bow.at(bow.length / 2, 0.05), 130),
111
+ // Low on the strip just short of the north-east corner, where the drawing shows Kemper's
112
+ // left face in front of Heritage.
113
+ northStripFacade: point(strip.at(strip.length - 0.5, 0.05), 60),
114
+ };
115
+ export function createHeritageAtMillenniumParkBuilding() {
116
+ const kit = createBuilder("The Heritage at Millennium Park", "building-heritage-at-millennium-park");
117
+ const { material, batch, panel, band, box, prism } = kit;
118
+ // Base colors sit off the drawing's tones (a #474747 face, #212121 bands, #787878 precast,
119
+ // a #919191 screen) so the toon-lit result lands near them.
120
+ const facade = material(0x363636);
121
+ const glass = material(0xffffff, { vertexColors: true });
122
+ const relief = material(0x141414);
123
+ const precast = material(0x8c8c8c);
124
+ const mechanical = material(0xa4a4a4);
125
+ const shell = batch("closed tower and lower-tier shells", facade);
126
+ const panes = batch("window panes", glass);
127
+ const dark = batch("mullions, bands, and screen louvers", relief);
128
+ const stone = batch("crown fins, caps, and penthouse frame", precast);
129
+ const block = batch("rooftop screen", mechanical);
130
+ // The crown-base seam puts the drawn north crown foot on a shell vertex.
131
+ const towerPlan = prism(shell, tower, [0, crownBase, parapet]);
132
+ // The tower's floor already closes the footprint the two volumes share, and the rest
133
+ // faces the ground, which no camera above it sees.
134
+ const lowerPlan = prism(shell, lowerTier, [0, lowerParapet], { omit: ["bottom"] });
135
+ // Details hidden inside the other volume are skipped rather than buried.
136
+ const towerVisible = (at, y) => y > lowerParapet || !inside(lowerPlan, at);
137
+ const lowerVisible = (at) => !inside(towerPlan, at);
138
+ // The trimmed chamfer and west wall are cut faces, so the lower bands and cap wrap
139
+ // only the south and Garland Court faces and close with return ends.
140
+ const lowerChain = [lowerTier[0], lowerTier[1]];
141
+ // One pane per floor per bay, set just proud of the wall like Crain's windows, so only
142
+ // mullions and slab edges divide the face, as drawn. Tones vary subtly, with scattered lit and dark units.
143
+ const towerRows = [[0.6, level(45) - 0.22]];
144
+ for (let floor = 44; floor >= 0; floor -= 1)
145
+ towerRows.push([level(floor + 1) + 0.2, level(floor) - 0.22]);
146
+ const crownMid = (crownBase + capSoffit) / 2;
147
+ const tones = [0x444444, 0x474747, 0x4a4a4a].map((hex) => new THREE.Color(hex));
148
+ const lit = new THREE.Color(0x7a7a7a), unlit = new THREE.Color(0x3a3a3a);
149
+ function glaze(runs, stationsList, rows, visible, seed) {
150
+ runs.forEach((run, r) => {
151
+ const stations = stationsList[r];
152
+ for (let bay = 0; bay < stations.length - 1; bay += 1) {
153
+ if (stations[bay + 1] - stations[bay] < 1.5)
154
+ continue;
155
+ const s0 = stations[bay] + 0.24, s1 = stations[bay + 1] - 0.24;
156
+ rows.forEach(([bottom, top], row) => {
157
+ if (!visible(run.at((s0 + s1) / 2, 0.3), bottom))
158
+ return;
159
+ const hash = (row * 131 + bay * 37 + r * 59 + seed) % 97;
160
+ panel(panes, run, s0, s1, bottom, top, 0.05, hash < 5 ? lit : hash < 8 ? unlit : tones[hash % 3]);
161
+ });
162
+ }
163
+ });
164
+ }
165
+ // The stub has no crown, so its top storey runs up to the parapet.
166
+ const stubTop = [crownBase + 0.45, parapet - 1.2];
167
+ glaze(tower.slice(0, 2), towerStations.slice(0, 2), [...towerRows, stubTop], towerVisible, 3);
168
+ glaze([tower.at(-1)], [towerStations.at(-1)], [...towerRows, stubTop], towerVisible, 5);
169
+ glaze(tower.slice(2, 7), towerStations.slice(2, 7), [...towerRows, [crownBase + 0.5, crownMid - 0.3], [crownMid + 0.3, capSoffit - 0.2]], towerVisible, 11);
170
+ const lowerRows = [[0.6, level(45) - 0.22]];
171
+ for (let floor = 44; floor >= 30; floor -= 1)
172
+ lowerRows.push([level(floor + 1) + 0.2, level(floor) - 0.22]);
173
+ lowerRows.push([lowerLedge + 0.4, lowerSoffit - 0.25]);
174
+ glaze(lowerTier, lowerStations, lowerRows, lowerVisible, 23);
175
+ // Raised mullions run to the crown; the fins take over above it. Every mullion ending
176
+ // at the crown base stands inside that floor's band, whose top closes its own.
177
+ const mullion = (target, { at, normal }, y0, y1, { width = 0.28, depth = 0.22, omit = [] } = {}) => box(target, at, normal, width / 2, 0, depth, y0, y1, { omit });
178
+ const inCrownBand = ["top"];
179
+ const towerFoot = (at) => (towerVisible(at, 0) ? 0 : lowerParapet);
180
+ tower.forEach((run, r) => {
181
+ if (r >= 2 && r <= 4)
182
+ return;
183
+ const top = r < 2 || r === 7 ? parapet : crownBase;
184
+ for (const s of towerStations[r].slice(1, -1)) {
185
+ const where = station(run, s);
186
+ mullion(dark, where, towerFoot(where.at), top, { omit: top === crownBase ? inCrownBand : [] });
187
+ }
188
+ });
189
+ // The last two stations take the north strip's wide dark strips below instead. The
190
+ // first mullion stands before the thick bands begin, so it keeps its top.
191
+ for (const where of mullions.slice(0, -2)) {
192
+ if (where !== balconyStack)
193
+ mullion(dark, where, towerFoot(where.at), crownBase, { omit: where === mullions[0] ? [] : inCrownBand });
194
+ }
195
+ // The balcony stack at the bow's end is a dark panel recessed between raised jambs;
196
+ // wide dark strips follow on the north strip. The jambs stand 3 cm inside the bands'
197
+ // depth, clear of their arc facets' sag, so the bands wrap them as they wrap the mullions.
198
+ const stackHalf = 0.75;
199
+ panel(dark, bow, bow.length - stackHalf, bow.length, 0, crownBase, 0.08);
200
+ panel(dark, strip, 0, stackHalf, 0, crownBase, 0.08);
201
+ for (const jamb of [station(bow, bow.length - stackHalf), station(strip, stackHalf)])
202
+ mullion(dark, jamb, 0, crownBase, { width: 0.24, depth: 0.31, omit: inCrownBand });
203
+ for (const where of mullions.slice(-2))
204
+ mullion(dark, where, 0, crownBase, { width: 0.9, depth: 0.24, omit: inCrownBand });
205
+ // Six-floor bands on the tower and three-floor bands on the lower tier; thin slab
206
+ // edges mark the other floors on the faces the skyline view sees. Bands follow whole
207
+ // runs or straight ones, so the wall's own facets close their backs.
208
+ const onWall = ["back"];
209
+ const bandRuns = tower.slice(2, 7), stubRuns = [tower[7], tower[0], tower[1]];
210
+ for (let floor = 0; floor <= 45; floor += 1) {
211
+ const y = level(floor);
212
+ // Thick bands start under the first crown fin, as drawn; a slab edge runs before them.
213
+ if (floor % 6 === 0) {
214
+ band(dark, bandRuns, y - 0.65, y, bandProud, { omit: onWall, from: capStart, visible: towerVisible });
215
+ band(dark, [flatFace], y - 0.18, y + 0.04, 0.12, { omit: onWall, to: capStart, visible: towerVisible });
216
+ }
217
+ else
218
+ band(dark, tower.slice(2, 5), y - 0.18, y + 0.04, 0.12, { omit: onWall, visible: towerVisible });
219
+ // The stub's edges stop at the flat face's slab edge, which turns the re-entrant corner.
220
+ if (floor % 3 === 0)
221
+ band(dark, stubRuns, y - 0.2, y + 0.04, 0.12, { omit: onWall, to: stubDepth - 0.12, visible: towerVisible });
222
+ if (floor >= 30) {
223
+ if (floor % 6 === 0) {
224
+ // The trimmed band's facets differ from the wall's, so it keeps its back, and its
225
+ // end return closes the start of the shallower piece.
226
+ band(dark, lowerChain, y - 0.65, y, bandProud, { to: lowerLength - junctionTrim, visible: lowerVisible });
227
+ band(dark, [junction], y - 0.65, y, bandProud - junctionInset, { omit: ["start"], visible: lowerVisible });
228
+ }
229
+ else if (floor % 3 === 0)
230
+ band(dark, lowerChain, y - 0.65, y, bandProud, { omit: onWall, visible: lowerVisible });
231
+ else
232
+ band(dark, [lowerTier[1]], y - 0.18, y + 0.04, 0.12, { omit: onWall, visible: lowerVisible });
233
+ }
234
+ }
235
+ // Crown: radial fins under a projecting cap that follows the plan and wraps the
236
+ // hidden faces. A flush precast pier turns the north-east corner between the cap ends.
237
+ // The cap's soffit closes each fin's top, except the last strip fin, which stands past the cap's end.
238
+ for (const where of [...crownFins, ...hiddenFins]) {
239
+ box(stone, where.at, where.normal, finWidth / 2, 0, finDepth, crownBase, capSoffit, { omit: where === crownFins.at(-1) ? [] : ["top"] });
240
+ }
241
+ band(stone, tower.slice(2, 5), capSoffit, parapet, capProud, { omit: onWall, from: capStart, to: capEnd });
242
+ band(stone, [north, westBulge], capSoffit, parapet, capProud, { omit: onWall, from: northCapStart });
243
+ // The pier stands on the crown-base band, whose top closes its soffit.
244
+ band(stone, [strip, north], crownBase, parapet, 0.08, { omit: [...onWall, "soffit"], from: capEnd, to: northCapStart });
245
+ // Lower fins stand slightly proud of the lower cap, so they keep their tops.
246
+ for (const where of lowerFins)
247
+ box(stone, where.at, where.normal, lowerFinWidth / 2, 0, lowerFinDepth, lowerLedge, lowerSoffit);
248
+ band(stone, lowerChain, lowerSoffit, lowerParapet, lowerCapProud, { omit: onWall, visible: lowerVisible });
249
+ // Rooftop block: the tall screen strip, then the lower loggia with four posts and a
250
+ // dark back wall under a cornice that overhangs to the south and west. The roof closes
251
+ // the bottoms of everything standing on it. The loggia ends at the screen's west face, so
252
+ // their north faces meet without overlapping.
253
+ const south = [0, 1];
254
+ const onRoof = ["bottom"];
255
+ const screenFront = screen.north + screen.length;
256
+ box(block, [screen.east - screen.width / 2, screenFront], south, screen.width / 2, -screen.length, 0, parapet, screenTop, { omit: onRoof });
257
+ const loggiaEast = screen.east - screen.width, lintel = penthouseTop - 1.68, backWall = penthouse.front - 1.5;
258
+ box(shell, [(penthouse.west + loggiaEast) / 2, backWall], south, (loggiaEast - penthouse.west) / 2, screen.north - backWall, 0, parapet, lintel, { omit: onRoof });
259
+ const corniceWest = penthouse.west - penthouse.overhang, corniceFront = penthouse.front + penthouse.overhang;
260
+ // The back wall stops at the lintel, so the cornice closes its own north face.
261
+ box(stone, [(corniceWest + loggiaEast) / 2, corniceFront], south, (loggiaEast - corniceWest) / 2, screen.north - corniceFront, 0, lintel, penthouseTop);
262
+ // Post spans in meters east of the loggia's west end, as drawn. The back wall and the
263
+ // cornice close each post's back and top.
264
+ const posts = [[0, 1.3], [4.5, 5.9], [9.4, 11.1], [12.3, 13.5]];
265
+ for (const [left, right] of posts) {
266
+ box(stone, [penthouse.west + (left + right) / 2, penthouse.front], south, (right - left) / 2, -1.5, 0, parapet, lintel, { omit: [...onRoof, "back", "top"] });
267
+ }
268
+ // Louvers and five rows of panel dashes on the screen's two visible faces.
269
+ const screenSouthWest = [screen.east - screen.width, screenFront], screenSouthEast = [screen.east, screenFront];
270
+ const screenRuns = [line(screenSouthWest, screenSouthEast), line(screenSouthEast, [screen.east, screen.north])];
271
+ const dashDrops = [2.06, 4.01, 5.95, 8.1, 10.3];
272
+ screenRuns.forEach((run, r) => {
273
+ const panels = r === 0 ? 2 : 10, width = run.length / panels;
274
+ for (let i = 1; i < panels; i += 1)
275
+ mullion(dark, station(run, width * i), parapet + 0.3, screenTop - 0.4, { width: 0.25, depth: 0.12 });
276
+ for (let i = 0; i < panels; i += 1) {
277
+ for (const drop of dashDrops)
278
+ panel(dark, run, width * (i + 0.14), width * (i + 0.86), screenTop - drop - 0.18, screenTop - drop, 0.08);
279
+ }
280
+ });
281
+ // Silhouette lines for the shells and the screen only. The 20° threshold is well
282
+ // above the 2.4° bow facets, so curved faces read as one surface.
283
+ return kit.finish({ height: screenTop, outlines: [shell, block] });
284
+ }