@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,378 @@
1
+ import * as THREE from "../vendor/three-r186.js";
2
+ import { createBuilder, inside, line } from "./building-kit.js";
3
+ // The curtain wall's rhythm. The drawing and the photograph both give a 3.5 m band pitch,
4
+ // so the 41 published floors, above a taller lobby, fit under the 152.5 m main roof. Each
5
+ // floor is a ribbon of glass between sill and head, with white spandrels covering the slab.
6
+ export const crainFloors = Object.freeze({
7
+ lobbyTop: 7.0,
8
+ pitch: 3.5,
9
+ sill: 0.9,
10
+ head: 2.55,
11
+ module: 1.524,
12
+ lobbyGlass: [0.45, 5.9],
13
+ });
14
+ export const crainMainRoof = 152.5;
15
+ const orient = (corners) => {
16
+ const area = corners.reduce((sum, [x, z], i) => { const [x2, z2] = corners[(i + 1) % corners.length]; return sum + x * z2 - x2 * z; }, 0);
17
+ return area < 0 ? corners : [...corners].reverse();
18
+ };
19
+ const planOf = (corners) => corners.map((corner, i) => line(corner, corners[(i + 1) % corners.length]));
20
+ // The ribbon glazing's tones: mostly dark reflective glass, with a few lit and dim panes.
21
+ const paneTones = [0x232323, 0x2a2a2a, 0x303030, 0x1f1f1f].map((hex) => new THREE.Color(hex));
22
+ const litPane = new THREE.Color(0x8a8a8a), dimPane = new THREE.Color(0x565656), mullionTone = new THREE.Color(0x585858);
23
+ function paneColor(row, module, wall) {
24
+ const hash = (Math.imul(row + 29, 0x9e3779b1) ^ Math.imul(module + 41, 0x85ebca77) ^ Math.imul(wall + 13, 0xc2b2ae3d)) >>> 0;
25
+ const value = hash % 97;
26
+ if (value < 3)
27
+ return litPane;
28
+ if (value < 6)
29
+ return dimPane;
30
+ return paneTones[value % paneTones.length];
31
+ }
32
+ // A plane's upward normal from a height function sampled around a point.
33
+ function slopeNormal(roof, [x, z]) {
34
+ const dx = roof([x + 1, z]) - roof([x, z]), dz = roof([x, z + 1]) - roof([x, z]);
35
+ const length = Math.hypot(dx, 1, dz);
36
+ return [-dx / length, 1 / length, -dz / length];
37
+ }
38
+ function faceNormal(a, b, c) {
39
+ const u = [b[0] - a[0], b[1] - a[1], b[2] - a[2]], v = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
40
+ const n = [u[1] * v[2] - u[2] * v[1], u[2] * v[0] - u[0] * v[2], u[0] * v[1] - u[1] * v[0]];
41
+ const length = Math.hypot(...n);
42
+ return [n[0] / length, n[1] / length, n[2] / length];
43
+ }
44
+ // Where a line crosses a polygon, as the parameter intervals that lie inside it.
45
+ function clipLine(polygon, origin, direction) {
46
+ const hits = [];
47
+ for (let i = 0; i < polygon.length; i += 1) {
48
+ const a = polygon[i], b = polygon[(i + 1) % polygon.length];
49
+ const e = [b[0] - a[0], b[1] - a[1]];
50
+ const denominator = direction[0] * e[1] - direction[1] * e[0];
51
+ if (Math.abs(denominator) < 1e-12)
52
+ continue;
53
+ const w = [a[0] - origin[0], a[1] - origin[1]];
54
+ const t = (w[0] * e[1] - w[1] * e[0]) / denominator, u = (w[0] * direction[1] - w[1] * direction[0]) / denominator;
55
+ if (u >= 0 && u < 1)
56
+ hits.push(t);
57
+ }
58
+ hits.sort((p, q) => p - q);
59
+ const spans = [];
60
+ for (let i = 0; i + 1 < hits.length; i += 2)
61
+ spans.push([hits[i], hits[i + 1]]);
62
+ return spans;
63
+ }
64
+ export function buildCrainTower(form) {
65
+ const kit = createBuilder(form.name, form.id);
66
+ const shell = kit.batch("Crain · aluminum spandrels", kit.material(0xc4c4c4));
67
+ const ribbons = kit.batch("Crain · ribbon glazing", kit.material(0xffffff, { vertexColors: true }));
68
+ const glazing = kit.batch("Crain · sloped glazing", kit.material(0x333333));
69
+ const grid = kit.batch("Crain · glazing grid", kit.material(0x727272));
70
+ const lights = kit.batch("Crain · diamond outline lights", kit.material(0xf0f0f0));
71
+ const coping = kit.batch("Crain · coping", kit.material(0x5c5c5c));
72
+ const deck = kit.batch("Crain · slot floor", kit.material(0x4a4a4a));
73
+ const vents = form.crown ? kit.batch("Crain · mechanical louvers", kit.material(0xffffff, { vertexColors: true })) : ribbons;
74
+ const blades = form.crown ? kit.batch("Crain · louver blades", kit.material(0xffffff, { vertexColors: true })) : ribbons;
75
+ const shadowStrips = form.crown ? kit.batch("Crain · crown shadow strips", kit.material(0xffffff, { vertexColors: true })) : ribbons;
76
+ const ventTone = new THREE.Color(0x303030), bladeTone = new THREE.Color(0x696969), shadowTone = new THREE.Color(0x252525);
77
+ const base = form.base;
78
+ const volumes = form.volumes.map((volume) => {
79
+ const corners = orient(volume.corners);
80
+ return { ...volume, corners, plan: planOf(corners) };
81
+ });
82
+ // Model height from a real height.
83
+ const y = (real) => real - base;
84
+ // A slab of the plan lifted onto a plane `drop` below the roof. kit.slab lays a flat
85
+ // polygon; lifting its vertices keeps each triangle planar, and its winding still faces
86
+ // the way it was laid, so the normal follows the plane, flipped for a downward face.
87
+ const liftedSlab = (target, corners, roof, drop, up) => {
88
+ const start = target.positions.length;
89
+ kit.slab(target, corners, 0, up);
90
+ const n = slopeNormal(roof, corners[0]), normal = up ? n : [-n[0], -n[1], -n[2]];
91
+ for (let i = start; i < target.positions.length; i += 3) {
92
+ target.positions[i + 1] = y(roof([target.positions[i], target.positions[i + 2]]) - drop);
93
+ target.normals.splice(i, 3, ...normal);
94
+ }
95
+ };
96
+ // The masses: walls, a floor, and a lid a skin's depth under the roof, closed within the
97
+ // spandrels' mesh. The roof itself is a thin closed slab on the lid, glass on the halves
98
+ // and a deck on the slot's floor, so it stays the building's top. Neighbouring volumes
99
+ // keep their own walls on shared edges, facing opposite ways.
100
+ const skin = 0.05;
101
+ for (const volume of volumes) {
102
+ const { corners, plan, roof } = volume;
103
+ const top = volume.glazed ? glazing : deck;
104
+ for (const run of plan) {
105
+ const a = run.at(0), b = run.at(run.length), n = run.normal(0), normal = [n[0], 0, n[1]];
106
+ kit.quad(shell, [[a[0], 0, a[1]], [b[0], 0, b[1]], [b[0], y(roof(b) - skin), b[1]], [a[0], y(roof(a) - skin), a[1]]], [normal]);
107
+ kit.quad(top, [[a[0], y(roof(a) - skin), a[1]], [b[0], y(roof(b) - skin), b[1]], [b[0], y(roof(b)), b[1]], [a[0], y(roof(a)), a[1]]], [normal]);
108
+ }
109
+ kit.slab(shell, corners, 0, false);
110
+ liftedSlab(shell, corners, roof, skin, true);
111
+ liftedSlab(top, corners, roof, skin, false);
112
+ liftedSlab(top, corners, roof, 0, true);
113
+ }
114
+ // A wall is exposed where no neighbour stands against it, and a neighbour's roof covers
115
+ // it up to that roof's height. Probing just outside the wall splits a run into stretches,
116
+ // each facing open air or one neighbour, so a plan needs no corner where that changes.
117
+ const neighbourAt = (self, run, s) => volumes.findIndex((volume, index) => index !== self && inside(volume.corners, run.at(s, 0.3)));
118
+ const stretches = (self, run) => {
119
+ const out = [];
120
+ let start = 0, current = neighbourAt(self, run, Math.min(0.05, run.length / 2));
121
+ for (let s = 0.25; s < run.length - 0.05; s += 0.25) {
122
+ if (neighbourAt(self, run, s) === current)
123
+ continue;
124
+ let low = s - 0.25, high = s;
125
+ for (let i = 0; i < 30; i += 1) {
126
+ const middle = (low + high) / 2;
127
+ if (neighbourAt(self, run, middle) === current)
128
+ low = middle;
129
+ else
130
+ high = middle;
131
+ }
132
+ out.push({ from: start, to: high, neighbour: current });
133
+ start = high;
134
+ current = neighbourAt(self, run, s);
135
+ }
136
+ out.push({ from: start, to: run.length, neighbour: current });
137
+ // Near an acute corner the probe can land outside the neighbour for a few centimetres;
138
+ // no real change of neighbour comes that close to a corner, so a short stretch joins
139
+ // the longer one beside it.
140
+ for (let i = out.findIndex((piece) => piece.to - piece.from < 0.6); out.length > 1 && i >= 0; i = out.findIndex((piece) => piece.to - piece.from < 0.6)) {
141
+ const piece = out[i], before = out[i - 1], after = out[i + 1];
142
+ const into = !after || (before && before.to - before.from >= after.to - after.from) ? before : after;
143
+ into.from = Math.min(into.from, piece.from);
144
+ into.to = Math.max(into.to, piece.to);
145
+ out.splice(i, 1);
146
+ }
147
+ return out;
148
+ };
149
+ // A closed box on a run between two heights that each vary linearly along it. Its front is
150
+ // cut into panes and mullions on the curtain wall's module, counted from the run's start so
151
+ // mullions line up from floor to floor.
152
+ const strip = (run, from, to, b0, b1, t0, t1, row, wall, surface) => {
153
+ const target = surface === "glass" ? ribbons : surface === "vent" ? vents : surface === "blade" ? blades : shadowStrips;
154
+ const back = surface === "blade" ? 0.045 : 0.02, front = surface === "blade" ? 0.13 : 0.08;
155
+ const n = run.normal(0), tangent = [n[1], -n[0]];
156
+ const at = (s, depth, h) => { const p = run.at(s, depth); return [p[0], y(h), p[1]]; };
157
+ const bottom = (s) => b0 + (b1 - b0) * (s - from) / (to - from), top = (s) => t0 + (t1 - t0) * (s - from) / (to - from);
158
+ const outward = [n[0], 0, n[1]];
159
+ const cuts = [from];
160
+ const module = crainFloors.module, half = 0.035;
161
+ if (surface !== "blade")
162
+ for (let k = Math.ceil((from + half) / module); k * module < to - half; k += 1)
163
+ cuts.push(k * module - half, k * module + half);
164
+ cuts.push(to);
165
+ for (let i = 0; i + 1 < cuts.length; i += 1) {
166
+ const sa = cuts[i], sb = cuts[i + 1];
167
+ if (sb - sa < 1e-6)
168
+ continue;
169
+ const mullion = i % 2 === 1;
170
+ const color = mullion ? mullionTone : surface === "glass" ? paneColor(row, Math.floor((sa + sb) / 2 / module), wall)
171
+ : surface === "vent" ? ventTone : surface === "blade" ? bladeTone : shadowTone;
172
+ kit.quad(target, [at(sa, front, bottom(sa)), at(sb, front, bottom(sb)), at(sb, front, top(sb)), at(sa, front, top(sa))], [outward], color);
173
+ }
174
+ const dark = paneTones[0];
175
+ kit.quad(target, [at(to, back, b1), at(from, back, b0), at(from, back, t0), at(to, back, t1)], [[-n[0], 0, -n[1]]], dark);
176
+ // The top and bottom fan out from a back corner through every cut on the front, so each
177
+ // pane's edge meets its partner and the strip closes without T-junctions.
178
+ const ends = cuts.filter((s, i) => i === 0 || s - cuts[i - 1] >= 1e-6);
179
+ const fan = (height, h0, h1, up) => {
180
+ const hinge = at(from, back, h0), front0 = at(from, front, h0);
181
+ const normal = faceNormal(hinge, at(to, back, h1), front0);
182
+ const facing = (normal[1] > 0) === up ? normal : [-normal[0], -normal[1], -normal[2]];
183
+ for (let i = 0; i + 1 < ends.length; i += 1)
184
+ kit.triangle(target, [hinge, at(ends[i], front, height(ends[i])), at(ends[i + 1], front, height(ends[i + 1]))], [facing, facing, facing], dark);
185
+ kit.triangle(target, [hinge, at(to, front, h1), at(to, back, h1)], [facing, facing, facing], dark);
186
+ };
187
+ fan(top, t0, t1, true);
188
+ fan(bottom, b0, b1, false);
189
+ kit.quad(target, [at(from, front, b0), at(from, back, b0), at(from, back, t0), at(from, front, t0)], [[-tangent[0], 0, -tangent[1]]], dark);
190
+ kit.quad(target, [at(to, back, b1), at(to, front, b1), at(to, front, t1), at(to, back, t1)], [[tangent[0], 0, tangent[1]]], dark);
191
+ };
192
+ // Ribbons stop this far under the roof edge, below the deepest fascia hung there, so the
193
+ // two never meet in one plane where a traced wall splits into collinear pieces.
194
+ const underRoof = 0.8;
195
+ // One ribbon on one wall: between the band's sill and head, under the roof, and above any
196
+ // neighbour. The bounds are linear along the run, so splitting it where one bound overtakes
197
+ // another leaves pieces whose bottom and top are each a single linear function.
198
+ const ribbon = (run, clear, roof, cover, g0, g1, row, wall, surface = "glass") => {
199
+ const least = 0.05;
200
+ const from = clear[0], to = run.length - clear[1];
201
+ if (to - from < 0.3)
202
+ return;
203
+ const low = (s) => Math.max(g0, cover ? cover(s) + 0.2 : -Infinity, base);
204
+ const high = (s) => Math.min(g1, roof(s) - underRoof);
205
+ // Each bound is the larger or smaller of linear pieces; split where they trade places.
206
+ const trades = [(s) => roof(s) - underRoof - g1];
207
+ if (cover)
208
+ trades.push((s) => cover(s) + 0.2 - g0, (s) => cover(s) + 0.2 - base);
209
+ const breaks = [from, to];
210
+ for (const f of trades) {
211
+ const fa = f(from), fb = f(to);
212
+ if ((fa > 0) !== (fb > 0))
213
+ breaks.push(from + (to - from) * fa / (fa - fb));
214
+ }
215
+ breaks.sort((p, q) => p - q);
216
+ for (let i = 0; i + 1 < breaks.length; i += 1) {
217
+ let sa = breaks[i], sb = breaks[i + 1];
218
+ // Within a piece the ribbon's height is linear. Where the roof or a neighbour pinches
219
+ // it off, end the piece where it is still `least` tall rather than at a knife edge.
220
+ const ha = high(sa) - low(sa), hb = high(sb) - low(sb);
221
+ if (ha < least && hb < least)
222
+ continue;
223
+ if (ha < least)
224
+ sa += (sb - sa) * (least - ha) / (hb - ha);
225
+ else if (hb < least)
226
+ sb -= (sb - sa) * (least - hb) / (ha - hb);
227
+ if (sb - sa < 0.12)
228
+ continue;
229
+ strip(run, sa, sb, low(sa), low(sb), high(sa), high(sb), row, wall, surface);
230
+ }
231
+ };
232
+ // A fascia hung on a wall just under its roof edge: its top follows the edge, flush with
233
+ // the roof, and it stands out from the wall rather than over the roof, so it never shares
234
+ // the roof's plane and the roof stays the building's top. At a convex corner it runs on by
235
+ // its own width to close the corner with its neighbour.
236
+ const fascia = (target, run, roof, width, drop, extend) => {
237
+ const s0 = -extend[0], s1 = run.length + extend[1], n = run.normal(0), tangent = [n[1], -n[0]];
238
+ const at = (s, depth, lower) => { const p = run.at(s, depth); return [p[0], y(roof(run.at(s)) - lower), p[1]]; };
239
+ const T = [at(s0, 0, 0), at(s0, width, 0), at(s1, width, 0), at(s1, 0, 0)];
240
+ const b = [at(s0, 0, drop), at(s1, 0, drop), at(s1, width, drop), at(s0, width, drop)];
241
+ kit.quad(target, T, [faceNormal(T[0], T[1], T[2])]);
242
+ kit.quad(target, b, [faceNormal(b[0], b[1], b[2])]);
243
+ kit.quad(target, [at(s0, width, drop), at(s1, width, drop), at(s1, width, 0), at(s0, width, 0)], [[n[0], 0, n[1]]]);
244
+ kit.quad(target, [at(s1, 0, drop), at(s0, 0, drop), at(s0, 0, 0), at(s1, 0, 0)], [[-n[0], 0, -n[1]]]);
245
+ kit.quad(target, [at(s0, 0, drop), at(s0, width, drop), at(s0, width, 0), at(s0, 0, 0)], [[-tangent[0], 0, -tangent[1]]]);
246
+ kit.quad(target, [at(s1, width, drop), at(s1, 0, drop), at(s1, 0, 0), at(s1, width, 0)], [[tangent[0], 0, tangent[1]]]);
247
+ };
248
+ // A slim bar on a roof between two plan points, for the glazing grid. Every corner sits a
249
+ // fixed height off the roof plane, so its top and bottom lie parallel to the glass across
250
+ // its width as well as along it.
251
+ const roofBox = (target, p0, p1, roof, halfWidth, lift, drop) => {
252
+ const chord = Math.hypot(p1[0] - p0[0], p1[1] - p0[1]);
253
+ const t = [(p1[0] - p0[0]) / chord, (p1[1] - p0[1]) / chord], n = [-t[1], t[0]];
254
+ const corner = (end, across, offset) => {
255
+ const at = [end[0] + n[0] * across, end[1] + n[1] * across];
256
+ return [at[0], y(roof(at) + offset), at[1]];
257
+ };
258
+ const b = [corner(p0, -halfWidth, -drop), corner(p0, halfWidth, -drop), corner(p1, halfWidth, -drop), corner(p1, -halfWidth, -drop)];
259
+ const T = [corner(p0, -halfWidth, lift), corner(p0, halfWidth, lift), corner(p1, halfWidth, lift), corner(p1, -halfWidth, lift)];
260
+ kit.quad(target, [T[0], T[1], T[2], T[3]], [faceNormal(T[0], T[1], T[2])]);
261
+ kit.quad(target, [b[0], b[3], b[2], b[1]], [faceNormal(b[0], b[3], b[2])]);
262
+ for (const i of [0, 1, 2, 3]) {
263
+ const j = (i + 1) % 4;
264
+ kit.quad(target, [T[j], T[i], b[i], b[j]], [faceNormal(T[j], T[i], b[i])]);
265
+ }
266
+ };
267
+ // How a run meets its neighbour at each end: the signed angle between their outward
268
+ // normals, negative at a convex corner, positive at a concave one, and near zero where a
269
+ // traced outline splits one wall into nearly collinear pieces.
270
+ const straight = 5 * Math.PI / 180;
271
+ const turns = (plan, index) => {
272
+ const turn = (a, b) => { const n1 = a.normal(a.length), n2 = b.normal(0); return Math.atan2(n1[0] * n2[1] - n1[1] * n2[0], n1[0] * n2[0] + n1[1] * n2[1]); };
273
+ const count = plan.length, run = plan[index];
274
+ return [turn(plan[(index + count - 1) % count], run), turn(run, plan[(index + 1) % count])];
275
+ };
276
+ // Two walls' ribbons stand out 0.08 m and cross within this distance of their corner; the
277
+ // sharper the turn, the further, so a ribbon stops that far short of it. Across a nearly
278
+ // straight joint a 2 mm gap is enough, and keeps their level sills and heads from
279
+ // overlapping in the same plane.
280
+ const cornerClearance = (turn) => 0.08 * Math.tan(Math.abs(turn) / 2) + (Math.abs(turn) < straight ? 0.002 : 0.025);
281
+ let wall = 0;
282
+ volumes.forEach((volume, self) => {
283
+ volume.plan.forEach((whole, index) => {
284
+ const [startTurn, endTurn] = turns(volume.plan, index);
285
+ stretches(self, whole).forEach(({ from, to, neighbour }, piece, all) => {
286
+ wall += 1;
287
+ // A stretch is its own run, so everything below measures along it; a whole run keeps
288
+ // its mullions counted from its own start.
289
+ const run = from === 0 && to === whole.length ? whole : line(whole.at(from), whole.at(to));
290
+ const covering = neighbour < 0 ? null : volumes[neighbour];
291
+ const cover = covering ? (s) => covering.roof(run.at(s)) : null;
292
+ const roof = (s) => volume.roof(run.at(s));
293
+ // Ribbons run on across a straight joint and stop short of a corner, so the two walls'
294
+ // ribbons never overlap there.
295
+ const first = piece === 0, last = piece === all.length - 1;
296
+ const clear = [cornerClearance(first ? startTurn : 0), cornerClearance(last ? endTurn : 0)];
297
+ const n = run.normal(0), outer = Math.max(Math.abs(n[0]), Math.abs(n[1])) > 0.95;
298
+ const crown = volume.glazed && outer ? form.crown : undefined;
299
+ // Ribbons: the lobby's tall storefront, then one per floor up to the roof.
300
+ ribbon(run, clear, roof, cover, crainFloors.lobbyGlass[0], crainFloors.lobbyGlass[1], 0, wall);
301
+ const peak = Math.max(roof(0), roof(run.length));
302
+ for (let row = 1; crainFloors.lobbyTop + (row - 1) * crainFloors.pitch + crainFloors.sill < peak; row += 1) {
303
+ const level = crainFloors.lobbyTop + (row - 1) * crainFloors.pitch;
304
+ if (crown && level + crainFloors.head > crown.officeTop)
305
+ continue;
306
+ ribbon(run, clear, roof, cover, level + crainFloors.sill, level + crainFloors.head, row, wall);
307
+ }
308
+ if (crown) {
309
+ for (const [lo, hi] of crown.louvers) {
310
+ ribbon(run, clear, roof, cover, lo, hi, 0, wall, "vent");
311
+ // Slim closed blades seated in the dark backing. Extra end clearance
312
+ // accommodates their 5 cm relief at corners and collinear map joints.
313
+ const bladeClear = [clear[0] + 0.1, clear[1] + 0.1];
314
+ for (let bottom = lo + 0.12; bottom + 0.07 < hi; bottom += 0.22)
315
+ ribbon(run, bladeClear, (s) => roof(s) - 0.04, cover, bottom, bottom + 0.07, 0, wall, "blade");
316
+ }
317
+ for (const [lo, hi] of crown.shadowStrips)
318
+ ribbon(run, clear, roof, cover, lo, hi, 0, wall, "shadow");
319
+ }
320
+ // Roof edges: only where this volume stands above its neighbour. The lit outline runs
321
+ // along the four faces; the diagonal edges of the split, the notches, and the slot
322
+ // take a dark coping. Each run's fascia is a few millimetres wider and deeper than the
323
+ // last, so where two overlap at a corner no faces share a plane.
324
+ if (cover && Math.max(cover(0) - roof(0), cover(run.length) - roof(run.length)) > -0.5)
325
+ return;
326
+ const lit = volume.glazed && outer;
327
+ const step = 0.004 * index, width = (lit ? 0.42 : 0.3) + step;
328
+ // A sloped roof's fascia carries on past a convex corner, downhill only so none rises
329
+ // above the peaks. Otherwise it stops short: across a straight joint or into a concave
330
+ // corner its neighbour's top would share its plane where the two overlapped, and at a
331
+ // convex corner its end would lie in the plane of the next wall, which a taller
332
+ // neighbour may carry on up.
333
+ const adjust = (turn, at, past) => {
334
+ if (turn < -straight)
335
+ return volume.glazed && volume.roof(run.at(past)) < volume.roof(run.at(at)) ? width : -0.005;
336
+ return -(width * Math.tan(Math.abs(turn) / 2) + 0.002);
337
+ };
338
+ const extend = [adjust(first ? startTurn : 0, 0, -width), adjust(last ? endTurn : 0, run.length, run.length + width)];
339
+ fascia(lit ? lights : coping, run, volume.roof, width, (lit ? 0.7 : 0.5) + step, extend);
340
+ });
341
+ });
342
+ });
343
+ // The sloped glazing's grid, raised a little above the glass along the building's axes and
344
+ // seated in it, between the glass's top and its underside. The two directions stand and
345
+ // sit at different depths, so their crossings never share a plane.
346
+ // Bars keep their full width at least 0.3 m inside the roof's edge.
347
+ const spacing = 2 * crainFloors.module, inset = 0.9, barHalf = 0.08, barReach = barHalf + 0.3;
348
+ const { origin, axis } = form.grid;
349
+ const across = [-axis[1], axis[0]];
350
+ for (const volume of volumes.filter((v) => v.glazed)) {
351
+ for (const [direction, normal, lift, seat] of [[axis, across, 0.06, 0.02], [across, axis, 0.09, 0.03]]) {
352
+ const offsets = volume.corners.map((p) => (p[0] - origin[0]) * normal[0] + (p[1] - origin[1]) * normal[1]);
353
+ for (let k = Math.ceil(Math.min(...offsets) / spacing + 1e-9); k * spacing < Math.max(...offsets) - 1e-9; k += 1) {
354
+ const start = [origin[0] + normal[0] * k * spacing, origin[1] + normal[1] * k * spacing];
355
+ // Clip both long sides of the bar's footprint, not just its centre line, so a bar
356
+ // running nearly parallel to an edge cannot lean out over it.
357
+ const side = (offset) => [start[0] + normal[0] * offset, start[1] + normal[1] * offset];
358
+ const [left, right] = [clipLine(volume.corners, side(-barReach), direction), clipLine(volume.corners, side(barReach), direction)];
359
+ // The geographic crown follows the observed grid to the edge; the
360
+ // drawing-fit copies retain their original half-module end snapping.
361
+ // Geographic ends rely on the building-kit coplanar-overlap check to
362
+ // catch any end face meeting a crossing bar after grid/inset changes.
363
+ for (const [a0, a1] of left)
364
+ for (const [b0, b1] of right) {
365
+ const t0 = form.grid.edgeInset === undefined ? (Math.ceil((Math.max(a0, b0) + inset) / spacing - 0.5) + 0.5) * spacing : Math.max(a0, b0) + form.grid.edgeInset;
366
+ const t1 = form.grid.edgeInset === undefined ? (Math.floor((Math.min(a1, b1) - inset) / spacing - 0.5) + 0.5) * spacing : Math.min(a1, b1) - form.grid.edgeInset;
367
+ if (t1 - t0 < spacing - 1e-9)
368
+ continue;
369
+ const p0 = [start[0] + direction[0] * t0, start[1] + direction[1] * t0];
370
+ const p1 = [start[0] + direction[0] * t1, start[1] + direction[1] * t1];
371
+ roofBox(grid, p0, p1, volume.roof, barHalf, lift, seat);
372
+ }
373
+ }
374
+ }
375
+ }
376
+ const height = Math.max(...volumes.flatMap((volume) => volume.corners.map((p) => y(volume.roof(p)))));
377
+ return kit.finish({ height, outlines: [shell], opacity: 0.2 });
378
+ }
@@ -0,0 +1,18 @@
1
+ import * as THREE from "../vendor/three-r186.js";
2
+ import type { BatchData, Builder, Plan, Run, Vec2 } from "./building-kit.js";
3
+ export declare const orient: (corners: Vec2[]) => Vec2[];
4
+ export declare const planOf: (corners: Vec2[]) => Plan;
5
+ export declare const turnAt: (a: Run, b: Run) => number;
6
+ export interface Chain {
7
+ runs: number[];
8
+ starts: number[];
9
+ length: number;
10
+ }
11
+ export declare function chainsOf(plan: Plan, straight?: number): Chain[];
12
+ export declare function mitredBox(kit: Builder, target: BatchData, run: Run, s0: number, s1: number, before: number | undefined, after: number | undefined, width: number, lo: number, hi: number, y?: (real: number) => number): void;
13
+ export declare function gridBox(kit: Builder, target: BatchData, run: Run, columns: number[], rows: number[], back: number, front: number, paint: (column: number, row: number) => THREE.Color, sides: THREE.Color, y?: (real: number) => number): void;
14
+ export declare function paintedSlab(kit: Builder, target: BatchData, polygon: [number, number][], height: number, up: boolean, paint: THREE.Color): void;
15
+ export declare const turnsAt: (plan: Plan, index: number) => readonly [number, number];
16
+ export declare function jointTrim(plan: Plan, chain: Chain, froms: number[], k: number, other: number, clearance?: number): number;
17
+ export declare function chainPier(kit: Builder, target: BatchData, plan: Plan, chain: Chain, froms: number[], a: number, b: number, depth: number, top: number | number[], y?: (real: number) => number): void;
18
+ export declare function chainSkin(kit: Builder, target: BatchData, plan: Plan, chain: Chain, k0: number, k1: number, s0: number, s1: number, columns: number[], rows: number[], back: number, front: number, paint: (bay: number, row: number) => THREE.Color, sides: THREE.Color, y?: (real: number) => number): void;
@@ -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,19 @@
1
+ import type { BuildingModel, Plan } from "./building-kit.js";
2
+ import type { GeoBuilding } from "./skyline-geography-data.js";
3
+ export declare const gageLevels: Readonly<{
4
+ shopfront: 4;
5
+ second: 5.2;
6
+ eighth: number;
7
+ pitch: number;
8
+ top: 46.94;
9
+ cartouches: 47.74;
10
+ }>;
11
+ export declare const gagePalette: Readonly<{
12
+ glass: number[];
13
+ lit: 7237230;
14
+ dim: 3815994;
15
+ terracotta: 6184542;
16
+ band: 4737096;
17
+ brick: 4210752;
18
+ }>;
19
+ export declare function createGageGeographicBuilding(record: GeoBuilding, projectPlan: (coordinates: [number, number][]) => Plan, offset: [number, number]): BuildingModel;