@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,78 @@
1
+ import * as THREE from "../vendor/three-r186.js";
2
+ // Proportions follow the existing SVG, in the Crain model's scene units.
3
+ // This is a closed, reusable mesh; the footprint and unseen faces are inferred.
4
+ export function createKemperBuilding() {
5
+ const building = new THREE.Group();
6
+ building.name = "Kemper Building";
7
+ building.userData["buildingId"] = "building-kemper";
8
+ const width = 31;
9
+ const depth = 53.5;
10
+ const height = 141;
11
+ const gradient = new THREE.DataTexture(new Uint8Array([70, 135, 200, 255]), 4, 1, THREE.RedFormat);
12
+ gradient.minFilter = gradient.magFilter = THREE.NearestFilter;
13
+ gradient.needsUpdate = true;
14
+ const material = (color) => new THREE.MeshToonMaterial({ color, gradientMap: gradient });
15
+ const stone = material(0x929292);
16
+ const glass = material(0x303030);
17
+ const ribs = material(0x9b9b9b);
18
+ const crown = material(0x383838);
19
+ const materials = [stone, glass, ribs, crown];
20
+ let triangleCount = 0;
21
+ function box(name, size, position, surface) {
22
+ const mesh = new THREE.Mesh(new THREE.BoxGeometry(...size), surface);
23
+ mesh.name = name;
24
+ mesh.position.set(...position);
25
+ mesh.castShadow = mesh.receiveShadow = true;
26
+ building.add(mesh);
27
+ triangleCount += 12;
28
+ return mesh;
29
+ }
30
+ const shell = box("closed tower shell", [width, height - 3, depth], [0, (height - 3) / 2, 0], stone);
31
+ box("recessed crown", [width + 0.1, 2.1, depth + 0.1], [0, height - 1.95, 0], crown);
32
+ box("roof cap", [width + 1.1, 0.9, depth + 1.1], [0, height - 0.45, 0], ribs);
33
+ const windowPositions = [], ribPositions = [];
34
+ function quad(target, a, b, c, d) {
35
+ for (const point of [a, b, c, a, c, d])
36
+ target.push(...point);
37
+ }
38
+ const corners = [[-width / 2, depth / 2], [width / 2, depth / 2], [width / 2, -depth / 2], [-width / 2, -depth / 2]];
39
+ for (let face = 0; face < 4; face += 1) {
40
+ const a = corners[face], b = corners[(face + 1) % 4];
41
+ const length = Math.hypot(b[0] - a[0], b[1] - a[1]);
42
+ const normal = [(a[1] - b[1]) / length, (b[0] - a[0]) / length];
43
+ const point = (t, y, offset) => [a[0] + (b[0] - a[0]) * t + normal[0] * offset, y, a[1] + (b[1] - a[1]) * t + normal[1] * offset];
44
+ const strip = (target, left, right, bottom, top, offset) => quad(target, point(left, bottom, offset), point(right, bottom, offset), point(right, top, offset), point(left, top, offset));
45
+ for (let bay = 0; bay < 14; bay += 1) {
46
+ // The SVG's left face has a broad blank panel between these window bays.
47
+ if (face !== 0 || bay === 0 || bay >= 8) {
48
+ const left = (bay + 0.18) / 14, right = (bay + 0.73) / 14;
49
+ strip(windowPositions, left, right, 5, height - 3.1, 0.025);
50
+ // Raised mullions have front and side faces, so orbiting reveals depth.
51
+ const fin = 0.16 / length;
52
+ strip(ribPositions, left - fin, left, 5, height - 3.1, 0.27);
53
+ quad(ribPositions, point(left, 5, 0.025), point(left, height - 3.1, 0.025), point(left, height - 3.1, 0.27), point(left, 5, 0.27));
54
+ strip(ribPositions, right, right + fin, 5, height - 3.1, 0.27);
55
+ quad(ribPositions, point(right, 5, 0.27), point(right, height - 3.1, 0.27), point(right, height - 3.1, 0.025), point(right, 5, 0.025));
56
+ }
57
+ strip(ribPositions, (bay + 0.3) / 14, (bay + 0.38) / 14, height - 3, height - 0.9, 0.12);
58
+ }
59
+ }
60
+ for (const [name, positions, surface] of [["vertical window bays", windowPositions, glass], ["raised mullions and crown slots", ribPositions, ribs]]) {
61
+ const geometry = new THREE.BufferGeometry();
62
+ geometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3));
63
+ geometry.computeVertexNormals();
64
+ const mesh = new THREE.Mesh(geometry, surface);
65
+ mesh.name = name;
66
+ mesh.castShadow = mesh.receiveShadow = true;
67
+ building.add(mesh);
68
+ triangleCount += positions.length / 9;
69
+ }
70
+ const edges = new THREE.LineSegments(new THREE.EdgesGeometry(shell.geometry), new THREE.LineBasicMaterial({ color: 0xcccccc, transparent: true, opacity: 0.3 }));
71
+ edges.position.copy(shell.position);
72
+ building.add(edges);
73
+ return {
74
+ building, height, triangleCount,
75
+ setHighlighted(highlighted) { materials.forEach((surface) => surface.emissive.setHex(highlighted ? 0x222222 : 0)); },
76
+ setWireframe(enabled) { materials.forEach((surface) => { surface.wireframe = enabled; }); edges.visible = !enabled; },
77
+ };
78
+ }
@@ -0,0 +1,15 @@
1
+ import type { BuildingModel, Plan } from "./building-kit.js";
2
+ import type { GeoBuilding } from "./skyline-geography-data.js";
3
+ export declare const lakeViewLevels: Readonly<{
4
+ attic: number;
5
+ cornice: number;
6
+ top: 73.2;
7
+ }>;
8
+ export declare const lakeViewPalette: Readonly<{
9
+ glass: number[];
10
+ lit: 7237230;
11
+ dim: 3947580;
12
+ stone: 7763574;
13
+ band: 8947848;
14
+ }>;
15
+ export declare function createLakeViewGeographicBuilding(record: GeoBuilding, projectPlan: (coordinates: [number, number][]) => Plan, offset: [number, number]): BuildingModel;
@@ -0,0 +1,134 @@
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 Lake View Building, 116 South Michigan Avenue (Jenney, Mundie & Jensen; its top five
5
+ // storeys added by them in 1912): the geographic layout's model, on the mapped outline
6
+ // 145498711. The original layout has no model of it; the drawing shows it between Peoples
7
+ // Gas and the MacLean Center.
8
+ //
9
+ // A narrow stone front, three windows to a floor, rises seventeen storeys: arched windows
10
+ // on the sixteenth, small attic windows on the seventeenth, and a cornice band at the top.
11
+ // The top is the Skyscraper Center's 73.2 m. The drawn front's top reads 71.9 m, so heights
12
+ // read on the drawing are scaled to meet it; see docs/lake-view-reference.md. Units are
13
+ // meters; +x is east, +z is south.
14
+ const drawn = (height) => height * 73.2 / 71.9;
15
+ export const lakeViewLevels = Object.freeze({
16
+ attic: drawn(67.9), // the seventeenth floor, with its small windows
17
+ cornice: drawn(70.8), // the cornice band
18
+ top: 73.2, // the published top
19
+ });
20
+ const h = lakeViewLevels;
21
+ // The skins' colours: the window tones, and the stone and its cornice, which no window
22
+ // shares.
23
+ export const lakeViewPalette = Object.freeze({ glass: [0x2c2c2c, 0x303030, 0x343434, 0x383838], lit: 0x6e6e6e, dim: 0x3c3c3c, stone: 0x767676, band: 0x888888 });
24
+ const palette = lakeViewPalette;
25
+ const color = (hex) => new THREE.Color(hex);
26
+ const glassTones = palette.glass.map(color);
27
+ const litGlass = color(palette.lit), dimGlass = color(palette.dim);
28
+ const stone = color(palette.stone), band = color(palette.band), core = color(0x262626), roofing = color(0x3c3c3c);
29
+ function paneColor(row, bay, wall) {
30
+ const hash = (Math.imul(row + 59, 0x9e3779b1) ^ Math.imul(bay + 83, 0x85ebca77) ^ Math.imul(wall + 71, 0xc2b2ae3d)) >>> 0;
31
+ const value = hash % 101;
32
+ if (value < 4)
33
+ return litGlass;
34
+ if (value < 8)
35
+ return dimGlass;
36
+ return glassTones[value % glassTones.length];
37
+ }
38
+ const base = (n) => 37.1 + (n - 9) * 3.85;
39
+ const rows = [
40
+ { lo: 0.25, hi: drawn(1), kind: "wall", floor: 1 },
41
+ { lo: drawn(1), hi: drawn(9), kind: "glass", floor: 1 },
42
+ ];
43
+ const push = (hi, kind, floor) => rows.push({ lo: rows.at(-1).hi, hi, kind, floor });
44
+ for (let n = 2; n <= 15; n += 1) {
45
+ push(drawn(base(n) + 0.6), "wall", n);
46
+ push(drawn(base(n) + 3.1), "glass", n);
47
+ }
48
+ push(drawn(64.5), "wall", 16);
49
+ push(drawn(65.9), "glass", 16);
50
+ push(drawn(66.5), "head", 16);
51
+ push(drawn(68.6), "wall", 17);
52
+ push(drawn(69.6), "glass", 17);
53
+ push(h.cornice, "wall", 17);
54
+ push(h.top, "band", 17);
55
+ export function createLakeViewGeographicBuilding(record, projectPlan, offset) {
56
+ const kit = createBuilder(record.name, record.id);
57
+ const shell = kit.batch("Lake View Building · shell", kit.material(0xffffff, { vertexColors: true }));
58
+ const wall = kit.batch("Lake View Building · stone and windows", kit.material(0xffffff, { vertexColors: true }));
59
+ const lot = orient(polygonOf(projectPlan(record.footprint.coordinates)));
60
+ // The lot's south-east corner, with its south wall running west and its Michigan front
61
+ // north to the MacLean Center.
62
+ const corner = lot.reduce((best, p) => (p[0] + p[1] > best[0] + best[1] ? p : best));
63
+ const northEast = lot.reduce((best, p) => (p[0] - p[1] > best[0] - best[1] ? p : best));
64
+ const neighbours = [lot[(lot.indexOf(corner) + 1) % lot.length], lot[(lot.indexOf(corner) + lot.length - 1) % lot.length]];
65
+ 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]; };
66
+ const west = unit(neighbours[0][0] < neighbours[1][0] ? neighbours[0] : neighbours[1]), north = unit(northEast);
67
+ // A point's distances along the south wall and the Michigan front, which meet a little
68
+ // off square.
69
+ const skew = west[0] * north[1] - west[1] * north[0];
70
+ const inLot = (p) => {
71
+ const [x, z] = [p[0] - corner[0], p[1] - corner[1]];
72
+ return [(x * north[1] - z * north[0]) / skew, (west[0] * z - west[1] * x) / skew];
73
+ };
74
+ const front = inLot(northEast)[1];
75
+ planOf(lot).forEach((run) => {
76
+ const a = run.at(0), b = run.at(run.length), n = run.normal(0);
77
+ 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);
78
+ });
79
+ paintedSlab(kit, shell, lot, 0, false, roofing);
80
+ paintedSlab(kit, shell, lot, h.top, true, roofing);
81
+ // A wall's skin, one box to a chain the `keep` test accepts, held 2 cm clear of its ends
82
+ // or 9 cm where the outline turns in. `openings` lays a wall's columns out across it and
83
+ // says which cross a window's side (1) or its middle (2), which alone continues into an
84
+ // arched head. Only a `banded` wall carries the cornice band.
85
+ const skin = (seed, keep, openings, banded = false) => {
86
+ const plan = planOf(lot), heights = [rows[0].lo, ...rows.map((row) => row.hi)];
87
+ chainsOf(plan).forEach((chain, index) => {
88
+ if (!chain.runs.every((run) => keep(plan[run])))
89
+ return;
90
+ const first = chain.runs[0], last = chain.runs.at(-1);
91
+ const s0 = turnsAt(plan, first)[0] > 0 ? 0.09 : 0.02, s1 = chain.length - (turnsAt(plan, last)[1] > 0 ? 0.09 : 0.02);
92
+ const [columns, crossing] = openings(chain.length, plan[first].at(0), plan[last].at(plan[last].length));
93
+ chainSkin(kit, wall, plan, chain, 0, chain.runs.length - 1, s0, s1, columns, heights, 0.02, 0.07, (bay, r) => {
94
+ const row = rows[r], into = crossing(bay);
95
+ if (row.kind === "band")
96
+ return banded ? band : stone;
97
+ return (row.kind === "glass" && into >= 1) || (row.kind === "head" && into >= 2) ? paneColor(row.floor, bay, seed * 8 + index) : stone;
98
+ }, stone);
99
+ });
100
+ };
101
+ // Windows `width` wide at `centres`, positions along a wall from its start, each split
102
+ // into sides and a middle half.
103
+ const windows = (length, centres, width) => {
104
+ const inside = centres.filter((c) => c - width / 2 > 0.1 && c + width / 2 < length - 0.1).sort((p, q) => p - q);
105
+ const columns = [0, ...inside.flatMap((c) => [c - width / 2, c - width / 4, c + width / 4, c + width / 2]), length];
106
+ // Bays count from one: a pier, then side, middle, side for each window, then the last pier.
107
+ return [columns, (bay) => { const k = (bay - 1) % 4; return bay > 4 * inside.length || k === 0 ? 0 : k === 2 ? 2 : 1; }];
108
+ };
109
+ // The Michigan front's three windows, 2.5 m wide and 3.5 m apart about its middle, as
110
+ // drawn; the alley's, which the drawing does not show, 1.6 m wide and spread evenly about
111
+ // 3 m apart.
112
+ const michigan = (length, start, end) => {
113
+ const [b0, b1] = [inLot(start)[1], inLot(end)[1]], along = (b) => length * (b - b0) / (b1 - b0);
114
+ return windows(length, [-3.5, 0, 3.5].map((d) => along(front / 2 + d)), 2.5);
115
+ };
116
+ const alley = (length) => {
117
+ const count = Math.max(1, Math.round(length / 3.2)), [columns, crossing] = windows(length, Array.from({ length: count }, (_, k) => length * (k + 0.5) / count), 1.6);
118
+ return [columns, (bay) => Math.min(1, crossing(bay))];
119
+ };
120
+ // The Michigan front and the alley by their facing and their place in the lot; the side
121
+ // walls, shared with the taller MacLean Center and Peoples Gas, and the south light
122
+ // court's walls stay plain. The cornice band runs only along Michigan.
123
+ const facing = (x, z) => (run) => run.normal(0)[0] * x + run.normal(0)[1] * z > 0.9;
124
+ const isMichigan = (run) => facing(1, 0)(run) && inLot(run.at(run.length / 2))[0] < 1;
125
+ const isAlley = (run) => facing(-1, 0)(run) && inLot(run.at(run.length / 2))[0] > 40;
126
+ skin(0, isMichigan, michigan, true);
127
+ skin(1, isAlley, alley);
128
+ skin(2, (run) => !isMichigan(run) && !isAlley(run), (length) => [[0, length], () => 0]);
129
+ const model = kit.finish({ height: h.top, outlines: [shell], opacity: 0.16 });
130
+ model.building.position.set(offset[0], 0, offset[1]);
131
+ model.building.userData["geography"] = record;
132
+ model.building.userData["geographicDetail"] = { levels: h, source: "docs/lake-view-reference.md" };
133
+ return model;
134
+ }
@@ -0,0 +1,20 @@
1
+ import type { BuildingModel, Plan } from "./building-kit.js";
2
+ import type { GeoBuilding } from "./skyline-geography-data.js";
3
+ export declare const macleanLevels: Readonly<{
4
+ band: number;
5
+ cornice: number;
6
+ frieze: number;
7
+ addition: number;
8
+ openings: number;
9
+ parapet: number;
10
+ top: 77.4;
11
+ }>;
12
+ export declare const macleanPalette: Readonly<{
13
+ glass: number[];
14
+ lit: 7237230;
15
+ dim: 3947580;
16
+ stone: 8289918;
17
+ band: 9342606;
18
+ shadow: 2368548;
19
+ }>;
20
+ export declare function createMacleanCenterGeographicBuilding(record: GeoBuilding, projectPlan: (coordinates: [number, number][]) => Plan, offset: [number, number]): BuildingModel;
@@ -0,0 +1,162 @@
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 MacLean Center, 112 South Michigan Avenue, built as the Illinois Athletic Club
5
+ // (Barnett, Haynes & Barnett, 1908) and raised six floors by Swann & Weiskopf in 1985: the
6
+ // geographic layout's model, on the mapped outline 145498712. The original layout has no
7
+ // model of it; the drawing shows it between the Lake View and Monroe Buildings.
8
+ //
9
+ // A narrow Michigan front, a narrow window and three wide ones to a floor and another
10
+ // narrow one, each section at its drawn widths, rises twelve storeys to the old club's
11
+ // projecting cornice and a frieze of round windows; the addition carries six more floors,
12
+ // arched on the top one, to a pierced parapet. The parapet's top is the Skyscraper Center's
13
+ // 77.4 m. The drawn parapet reads 73.8 m, so heights read on the drawing are scaled to meet
14
+ // it; see docs/maclean-center-reference.md. Units are meters; +x is east, +z is south.
15
+ const drawn = (height) => height * 77.4 / 73.8;
16
+ export const macleanLevels = Object.freeze({
17
+ band: drawn(40.9), // the old club's bands under its cornice
18
+ cornice: drawn(43.8), // the projecting cornice
19
+ frieze: drawn(45.6), // the frieze of round windows
20
+ addition: drawn(48.5), // the 1985 floors, over the frieze
21
+ openings: drawn(70.9), // the parapet's openings
22
+ parapet: drawn(72.6), // its top band
23
+ top: 77.4, // the parapet's top, the published height
24
+ });
25
+ const h = macleanLevels;
26
+ // The skins' colours: the window tones, and the stone, bands and the parapet's shadowed
27
+ // openings, which no window shares.
28
+ export const macleanPalette = Object.freeze({ glass: [0x2c2c2c, 0x303030, 0x343434, 0x383838], lit: 0x6e6e6e, dim: 0x3c3c3c, stone: 0x7e7e7e, band: 0x8e8e8e, shadow: 0x242424 });
29
+ const palette = macleanPalette;
30
+ const color = (hex) => new THREE.Color(hex);
31
+ const glassTones = palette.glass.map(color);
32
+ const litGlass = color(palette.lit), dimGlass = color(palette.dim);
33
+ const stone = color(palette.stone), band = color(palette.band), shadow = color(palette.shadow);
34
+ const core = color(0x262626), roofing = color(0x3c3c3c);
35
+ function paneColor(row, bay, wall) {
36
+ const hash = (Math.imul(row + 53, 0x9e3779b1) ^ Math.imul(bay + 79, 0x85ebca77) ^ Math.imul(wall + 67, 0xc2b2ae3d)) >>> 0;
37
+ const value = hash % 101;
38
+ if (value < 4)
39
+ return litGlass;
40
+ if (value < 8)
41
+ return dimGlass;
42
+ return glassTones[value % glassTones.length];
43
+ }
44
+ const rows = [
45
+ { lo: 0.25, hi: drawn(1), kind: "wall", floor: 1 },
46
+ { lo: drawn(1), hi: drawn(4.5), kind: "glass", floor: 1 },
47
+ ];
48
+ const push = (hi, kind, floor, dy) => rows.push({ lo: rows.at(-1).hi, hi, kind, floor, ...(dy === undefined ? {} : { dy }) });
49
+ for (let n = 2; n <= 12; n += 1) {
50
+ const base = 5.1 + (n - 2) * 3.2;
51
+ push(drawn(base + 0.6), "wall", n);
52
+ push(drawn(base + 3.1), "glass", n);
53
+ }
54
+ push(h.band, "wall", 12);
55
+ push(h.frieze, "band", 12);
56
+ push(drawn(46.5), "wall", 12);
57
+ // The round windows, 90 cm across and centred at 46.95 m as drawn, in five slices, each
58
+ // carrying its middle's drawn height from the centre.
59
+ for (const [lo, hi] of [[46.5, 46.65], [46.65, 46.85], [46.85, 47.05], [47.05, 47.25], [47.25, 47.4]])
60
+ push(drawn(hi), "oculus", 12, Math.abs((lo + hi) / 2 - 46.95));
61
+ push(h.addition, "wall", 12);
62
+ for (const [floor, lo, hi] of [[13, 49.06, 51.48], [14, 52.17, 54.72], [15, 55.98, 58.54], [16, 59.67, 62.21], [17, 63.37, 65.92], [18, 66.7, 68.23]]) {
63
+ push(drawn(lo), "wall", floor);
64
+ push(drawn(hi), "glass", floor);
65
+ }
66
+ push(drawn(68.83), "head", 18);
67
+ push(h.openings, "wall", 18);
68
+ rows.push({ lo: h.openings, hi: h.parapet, kind: "opening", floor: 18 });
69
+ rows.push({ lo: h.parapet, hi: h.top, kind: "wall", floor: 18 });
70
+ export function createMacleanCenterGeographicBuilding(record, projectPlan, offset) {
71
+ const kit = createBuilder(record.name, record.id);
72
+ const shell = kit.batch("MacLean Center · shell", kit.material(0xffffff, { vertexColors: true }));
73
+ const wall = kit.batch("MacLean Center · stone and windows", kit.material(0xffffff, { vertexColors: true }));
74
+ const cornice = kit.batch("MacLean Center · cornice", kit.material(0x8a8a8a));
75
+ const lot = orient(polygonOf(projectPlan(record.footprint.coordinates)));
76
+ // The lot's south-east corner, with its south wall running west and its Michigan front
77
+ // north to the Monroe Building.
78
+ const corner = lot.reduce((best, p) => (p[0] + p[1] > best[0] + best[1] ? p : best));
79
+ const northEast = lot.reduce((best, p) => (p[0] - p[1] > best[0] - best[1] ? p : best));
80
+ const neighbours = [lot[(lot.indexOf(corner) + 1) % lot.length], lot[(lot.indexOf(corner) + lot.length - 1) % lot.length]];
81
+ 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]; };
82
+ const west = unit(neighbours[0][0] < neighbours[1][0] ? neighbours[0] : neighbours[1]), north = unit(northEast);
83
+ const at = (a, b) => [corner[0] + west[0] * a + north[0] * b, corner[1] + west[1] * a + north[1] * b];
84
+ // The inverse of `at`: a point's distances along the south wall and the Michigan front,
85
+ // which meet a little off square.
86
+ const skew = west[0] * north[1] - west[1] * north[0];
87
+ const inLot = (p) => {
88
+ const [x, z] = [p[0] - corner[0], p[1] - corner[1]];
89
+ return [(x * north[1] - z * north[0]) / skew, (west[0] * z - west[1] * x) / skew];
90
+ };
91
+ const front = inLot(northEast)[1];
92
+ const rect = (a0, a1, b0, b1) => orient([at(a0, b0), at(a1, b0), at(a1, b1), at(a0, b1)]);
93
+ planOf(lot).forEach((run) => {
94
+ const a = run.at(0), b = run.at(run.length), n = run.normal(0);
95
+ 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);
96
+ });
97
+ paintedSlab(kit, shell, lot, 0, false, roofing);
98
+ paintedSlab(kit, shell, lot, h.top, true, roofing);
99
+ // The old club's cornice projects 60 cm along the Michigan front, 3 cm short of the
100
+ // neighbours' walls, past the skin's ends 2 cm in, and 5 cm into its own.
101
+ kit.prism(cornice, planOf(rect(-0.6, 0.05, 0.03, front - 0.03)), [h.cornice, h.frieze]);
102
+ const skin = (seed, keep, openings, plain = false) => {
103
+ const plan = planOf(lot), heights = [rows[0].lo, ...rows.map((row) => row.hi)];
104
+ chainsOf(plan).forEach((chain, index) => {
105
+ if (!chain.runs.every((run) => keep(plan[run])))
106
+ return;
107
+ const first = chain.runs[0], last = chain.runs.at(-1);
108
+ const s0 = turnsAt(plan, first)[0] > 0 ? 0.09 : 0.02, s1 = chain.length - (turnsAt(plan, last)[1] > 0 ? 0.09 : 0.02);
109
+ const [columns, cell] = openings(chain.length, plan[first].at(0), plan[last].at(plan[last].length));
110
+ chainSkin(kit, wall, plan, chain, 0, chain.runs.length - 1, s0, s1, columns, heights, 0.02, 0.07, (bay, r) => {
111
+ const row = rows[r];
112
+ if (row.kind === "band")
113
+ return plain ? stone : band;
114
+ const shows = cell(bay, row);
115
+ return shows === "glass" ? paneColor(row.floor, bay, seed * 8 + index) : shows === "shadow" ? shadow : stone;
116
+ }, stone);
117
+ });
118
+ };
119
+ // The Michigan front's five columns, narrow windows 8.4 m either side of the middle and
120
+ // wide ones at the middle and 4.35 m either side, with each section's widths as drawn: the
121
+ // old club's windows 1.4 and 2.36 m wide, the addition's 2 and 3.15 m, and the parapet's
122
+ // openings 2.9 m. Only the wide columns have arched heads, their middle half, and round
123
+ // windows over them.
124
+ const michigan = (length, start, end) => {
125
+ const [b0, b1] = [inLot(start)[1], inLot(end)[1]], along = (b) => length * (b - b0) / (b1 - b0), middle = front / 2;
126
+ const windows = [[-8.4, false], [-4.35, true], [0, true], [4.35, true], [8.4, false]].map(([d, wide]) => ({ c: along(middle + d), wide }));
127
+ const halves = (wide) => (wide ? [0.25, 0.42, 0.45, 0.79, 1.18, 1.45, 1.575] : [0.7, 1, 1.45]);
128
+ const cuts = windows.flatMap(({ c, wide }) => halves(wide).flatMap((half) => [c - half, c + half]));
129
+ const columns = [0, ...[...new Set(cuts)].sort((p, q) => p - q), length];
130
+ return [columns, (bay, row) => {
131
+ const middleOf = (columns[bay - 1] + columns[bay]) / 2, window = windows.find(({ c, wide }) => Math.abs(middleOf - c) < (wide ? 1.575 : 1.45));
132
+ if (!window)
133
+ return "wall";
134
+ const dx = Math.abs(middleOf - window.c), wide = window.wide, old = row.floor <= 12;
135
+ if (row.kind === "glass")
136
+ return dx < (wide ? (old ? 1.18 : 1.575) : (old ? 0.7 : 1)) ? "glass" : "wall";
137
+ if (row.kind === "head")
138
+ return wide && dx < 0.79 ? "glass" : "wall";
139
+ if (row.kind === "oculus")
140
+ return wide && dx * dx + row.dy * row.dy <= 0.45 * 0.45 ? "glass" : "wall";
141
+ if (row.kind === "opening")
142
+ return dx < 1.45 ? "shadow" : "wall";
143
+ return "wall";
144
+ }];
145
+ };
146
+ // The alley's windows, 1.6 m wide every 3.2 m.
147
+ const alley = (length) => {
148
+ const count = Math.max(1, Math.round(length / 3.2)), centres = Array.from({ length: count }, (_, k) => length * (k + 0.5) / count);
149
+ return [[0, ...centres.flatMap((c) => [c - 0.8, c + 0.8]), length], (bay, row) => (row.kind === "glass" && bay % 2 === 0 && bay <= 2 * count ? "glass" : "wall")];
150
+ };
151
+ const facing = (x, z) => (run) => run.normal(0)[0] * x + run.normal(0)[1] * z > 0.9;
152
+ // The bands run only along Michigan; the side walls are shared with the Lake View and
153
+ // Monroe Buildings and stay plain.
154
+ skin(0, facing(1, 0), michigan);
155
+ skin(1, facing(-1, 0), alley, true);
156
+ skin(2, (run) => !facing(1, 0)(run) && !facing(-1, 0)(run), (length) => [[0, length], () => "wall"], true);
157
+ const model = kit.finish({ height: h.top, outlines: [shell, cornice], opacity: 0.16 });
158
+ model.building.position.set(offset[0], 0, offset[1]);
159
+ model.building.userData["geography"] = record;
160
+ model.building.userData["geographicDetail"] = { levels: h, source: "docs/maclean-center-reference.md" };
161
+ return model;
162
+ }
@@ -0,0 +1,12 @@
1
+ import type { BuildingModel, Plan } from "./building-kit.js";
2
+ import type { GeoBuilding } from "./skyline-geography-data.js";
3
+ export declare const michiganBoulevardLevels: Readonly<{
4
+ belt: 49;
5
+ beltTop: 50.2;
6
+ attic: 78.16;
7
+ top: 82;
8
+ endParapet: 83.3;
9
+ pedestals: 82.6;
10
+ returnBay: 5.6;
11
+ }>;
12
+ export declare function createMichiganBoulevardGeographicBuilding(record: GeoBuilding, projectPlan: (coordinates: [number, number][]) => Plan, offset: [number, number]): BuildingModel;
@@ -0,0 +1,157 @@
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 Michigan Boulevard Building, 30 North Michigan Avenue (Jarvis Hunt, 1914, fifteen
5
+ // storeys; six added by Hunt in 1923): the geographic layout's model, on the mapped outline
6
+ // 126982630. The original layout has no model of it; the drawing shows its Michigan front
7
+ // and, above 20 North Michigan, its south wall, right of Six North Michigan's tower.
8
+ //
9
+ // A terracotta street front of five bays, a single window and four pairs to a floor, over
10
+ // twenty-one storeys, with a belt course over the thirteenth and an attic of panels under a
11
+ // parapet at the published 82 m. The terracotta turns one bay onto the south wall; beyond
12
+ // it the wall is common brick, shared with 20 North Michigan to its eighth floor and blank
13
+ // to the eighteenth. Heights are measured on the drawing down from that top; see
14
+ // docs/michigan-boulevard-reference.md. Units are meters; +x is east, +z is south.
15
+ export const michiganBoulevardLevels = Object.freeze({
16
+ belt: 49, // the belt course over the thirteenth floor
17
+ beltTop: 50.2,
18
+ attic: 78.16, // the attic, over the twenty-first floor's windows
19
+ top: 82, // the parapet, the published height
20
+ endParapet: 83.3, // the raised parapet over the front's south bay
21
+ pedestals: 82.6, // the pedestals over the front's piers
22
+ returnBay: 5.6, // the terracotta's return onto the south wall
23
+ });
24
+ const h = michiganBoulevardLevels;
25
+ // Each floor's level: a 4.8 m ground floor, 3.7 m floors to the thirteenth, which the belt
26
+ // course makes 4.7 m, and 3.58 m floors above.
27
+ const floor = (n) => (n < 2 ? 0 : n <= 13 ? 4.8 + (n - 2) * 3.7 : 50.2 + (n - 14) * 3.58);
28
+ const color = (hex) => new THREE.Color(hex);
29
+ const glassTones = [0x262626, 0x2a2a2a, 0x2e2e2e, 0x323232].map(color);
30
+ const litGlass = color(0x6e6e6e), dimGlass = color(0x3a3a3a);
31
+ const terracotta = color(0x444444), returnBay = color(0x585858), brick = color(0x404040), belt = color(0x505050), panel = color(0x363636);
32
+ const core = color(0x262626), roofing = color(0x3a3a3a);
33
+ function paneColor(row, bay, wall) {
34
+ const hash = (Math.imul(row + 37, 0x9e3779b1) ^ Math.imul(bay + 61, 0x85ebca77) ^ Math.imul(wall + 47, 0xc2b2ae3d)) >>> 0;
35
+ const value = hash % 101;
36
+ if (value < 4)
37
+ return litGlass;
38
+ if (value < 8)
39
+ return dimGlass;
40
+ return glassTones[value % glassTones.length];
41
+ }
42
+ const rows = [
43
+ { lo: 0.25, hi: 0.9, kind: "wall", floor: 1 },
44
+ { lo: 0.9, hi: 4.2, kind: "glass", floor: 1 },
45
+ ];
46
+ for (let n = 2; n <= 21; n += 1) {
47
+ const below = rows.at(-1).hi;
48
+ if (n === 14) {
49
+ rows.push({ lo: below, hi: h.belt, kind: "wall", floor: n });
50
+ rows.push({ lo: h.belt, hi: h.beltTop, kind: "belt", floor: n });
51
+ rows.push({ lo: h.beltTop, hi: floor(n) + 0.9, kind: "wall", floor: n });
52
+ }
53
+ else
54
+ rows.push({ lo: below, hi: floor(n) + 0.9, kind: "wall", floor: n });
55
+ rows.push({ lo: floor(n) + 0.9, hi: floor(n) + 2.9, kind: "glass", floor: n });
56
+ }
57
+ rows.push({ lo: h.attic, hi: 79.2, kind: "attic", floor: 22 });
58
+ rows.push({ lo: 79.2, hi: 80.9, kind: "panel", floor: 22 });
59
+ rows.push({ lo: 80.9, hi: h.top, kind: "attic", floor: 22 });
60
+ export function createMichiganBoulevardGeographicBuilding(record, projectPlan, offset) {
61
+ const kit = createBuilder(record.name, record.id);
62
+ const shell = kit.batch("Michigan Boulevard Building · shell", kit.material(0xffffff, { vertexColors: true }));
63
+ const wall = kit.batch("Michigan Boulevard Building · terracotta and windows", kit.material(0xffffff, { vertexColors: true }));
64
+ const parapets = kit.batch("Michigan Boulevard Building · parapet", kit.material(0x4c4c4c));
65
+ const lot = orient(polygonOf(projectPlan(record.footprint.coordinates)));
66
+ // The lot's south-east corner, with its south wall running west and its Michigan front
67
+ // north to the Washington Street corner.
68
+ const corner = lot.reduce((best, p) => (p[0] + p[1] > best[0] + best[1] ? p : best));
69
+ const northEast = lot.reduce((best, p) => (p[0] - p[1] > best[0] - best[1] ? p : best));
70
+ const neighbours = [lot[(lot.indexOf(corner) + 1) % lot.length], lot[(lot.indexOf(corner) + lot.length - 1) % lot.length]];
71
+ 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]; };
72
+ const west = unit(neighbours[0][0] < neighbours[1][0] ? neighbours[0] : neighbours[1]), north = unit(northEast);
73
+ const at = (a, b) => [corner[0] + west[0] * a + north[0] * b, corner[1] + west[1] * a + north[1] * b];
74
+ // The inverse of `at`: a point's distances along the south wall and the Michigan front,
75
+ // which meet a little off square.
76
+ const skew = west[0] * north[1] - west[1] * north[0];
77
+ const inLot = (p) => {
78
+ const [x, z] = [p[0] - corner[0], p[1] - corner[1]];
79
+ return [(x * north[1] - z * north[0]) / skew, (west[0] * z - west[1] * x) / skew];
80
+ };
81
+ const front = inLot(northEast)[1];
82
+ const rect = (a0, a1, b0, b1) => orient([at(a0, b0), at(a1, b0), at(a1, b1), at(a0, b1)]);
83
+ planOf(lot).forEach((run) => {
84
+ const a = run.at(0), b = run.at(run.length), n = run.normal(0);
85
+ 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);
86
+ });
87
+ paintedSlab(kit, shell, lot, 0, false, roofing);
88
+ paintedSlab(kit, shell, lot, h.top, true, roofing);
89
+ // Over the front's south bay the parapet rises, and pedestals stand over the piers between
90
+ // the pairs and at the Washington corner, 5 cm inside the walls' planes.
91
+ kit.prism(parapets, planOf(rect(0.05, 0.65, 0.05, 6.2)), [h.top, h.endParapet]);
92
+ for (const b of [12.4, 18.5, 24.6, front - 0.45])
93
+ kit.prism(parapets, planOf(rect(0.05, 0.65, b - 0.4, b + 0.4)), [h.top, h.pedestals]);
94
+ const skin = (seed, keep, openings) => {
95
+ const plan = planOf(lot), heights = [rows[0].lo, ...rows.map((row) => row.hi)];
96
+ chainsOf(plan).forEach((chain, index) => {
97
+ if (!chain.runs.every((run) => keep(plan[run])))
98
+ return;
99
+ const first = chain.runs[0], last = chain.runs.at(-1);
100
+ const s0 = turnsAt(plan, first)[0] > 0 ? 0.09 : 0.02, s1 = chain.length - (turnsAt(plan, last)[1] > 0 ? 0.09 : 0.02);
101
+ const [columns, column] = openings(chain.length, plan[first].at(0), plan[last].at(plan[last].length));
102
+ chainSkin(kit, wall, plan, chain, 0, chain.runs.length - 1, s0, s1, columns, heights, 0.02, 0.07, (bay, r) => {
103
+ const row = rows[r], { face, window, from, panel: paneled } = column(bay);
104
+ if (row.kind === "belt")
105
+ return face === brick ? brick : belt;
106
+ if (row.kind === "panel")
107
+ return paneled ? panel : face;
108
+ return row.kind === "glass" && window && row.floor >= from ? paneColor(row.floor, bay, seed * 8 + index) : face;
109
+ }, terracotta);
110
+ });
111
+ };
112
+ // A position along a wall from its start, given in lot coordinates across it, and back.
113
+ const along = (length, start, end, axis) => {
114
+ const [p, q] = [inLot(start)[axis], inLot(end)[axis]];
115
+ return [(target) => length * (target - p) / (q - p), (s) => p + (s / length) * (q - p)];
116
+ };
117
+ // Windows `width` wide centred at `centres`, in lot coordinates along `axis`, with the
118
+ // walls also split at `breaks`; `column` says, in the same coordinates, what else each
119
+ // column carries.
120
+ const windows = (length, start, end, axis, centres, width, column, breaks = []) => {
121
+ const [to, from] = along(length, start, end, axis);
122
+ const inside = centres.map(to).filter((c) => c - width / 2 > 0.1 && c + width / 2 < length - 0.1);
123
+ const cuts = [...inside.flatMap((c) => [c - width / 2, c + width / 2]), ...breaks.map(to).filter((s) => s > 0.1 && s < length - 0.1)];
124
+ const columns = [0, ...[...new Set(cuts)].sort((p, q) => p - q), length];
125
+ // Bays count from one, each between two columns; a bay holds a window when its middle
126
+ // lies inside one.
127
+ return [columns, (bay) => {
128
+ const middle = (columns[bay - 1] + columns[bay]) / 2;
129
+ return { ...column(from(middle)), window: inside.some((c) => Math.abs(middle - c) < width / 2) };
130
+ }];
131
+ };
132
+ const pairs = (centres) => centres.flatMap((c) => [c - 1, c + 1]);
133
+ const facing = (x, z) => (run) => run.normal(0)[0] * x + run.normal(0)[1] * z > 0.9;
134
+ const [isMichigan, isSouth, isWashington] = [facing(1, 0), facing(0, 1), facing(0, -1)];
135
+ // The Michigan front: a single window in its south bay and a pair in each of the other
136
+ // four, 2 m apart; the attic's panel spans each bay's windows.
137
+ const frontBays = [[3.1, 0.725], ...[9.3, 15.45, 21.55, 27.75].map((c) => [c, 1.725])];
138
+ const michigan = (length, start, end) => windows(length, start, end, 1, [3.1, ...pairs([9.3, 15.45, 21.55, 27.75])], 1.45, (b) => ({ face: terracotta, from: 1, panel: frontBays.some(([c, half]) => Math.abs(b - c) < half) }));
139
+ // The south wall: the terracotta's return bay with a pair of windows over 20 North
140
+ // Michigan, then brick with a pair every 5.1 m on the top four floors, as drawn.
141
+ const southWall = (length, start, end) => windows(length, start, end, 0, pairs([2.5, 8, 13.2, 18.3, 23.4, 28.5, 33.6, 38.7, 43.8]), 1.1, (a) => (a < h.returnBay ? { face: returnBay, from: 9, panel: false } : { face: brick, from: 18, panel: false }), [h.returnBay]);
142
+ // Washington Street's terracotta front and the brick alley: a pair every 6.1 m.
143
+ const rear = (face) => (length, start, end) => {
144
+ const axis = Math.abs(inLot(end)[0] - inLot(start)[0]) > Math.abs(inLot(end)[1] - inLot(start)[1]) ? 0 : 1, [, from] = along(length, start, end, axis);
145
+ const count = Math.max(1, Math.round(length / 6.1));
146
+ return windows(length, start, end, axis, pairs(Array.from({ length: count }, (_, i) => from(length * (i + 0.5) / count))), 1.1, () => ({ face, from: 1, panel: false }));
147
+ };
148
+ skin(0, isMichigan, michigan);
149
+ skin(1, isSouth, southWall);
150
+ skin(2, isWashington, rear(terracotta));
151
+ skin(3, (run) => !isMichigan(run) && !isSouth(run) && !isWashington(run), rear(brick));
152
+ const model = kit.finish({ height: h.endParapet, outlines: [shell, parapets], opacity: 0.16 });
153
+ model.building.position.set(offset[0], 0, offset[1]);
154
+ model.building.userData["geography"] = record;
155
+ model.building.userData["geographicDetail"] = { levels: h, source: "docs/michigan-boulevard-reference.md" };
156
+ return model;
157
+ }
@@ -0,0 +1,7 @@
1
+ import type { BuildingModel, Plan } from "./building-kit.js";
2
+ import type { GeoBuilding } from "./skyline-geography-data.js";
3
+ export declare const michiganPlazaSouthGeographicLevels: Readonly<{
4
+ roof: 168.6;
5
+ floors: 44;
6
+ }>;
7
+ export declare function createMichiganPlazaSouthGeographicBuilding(record: GeoBuilding, projectPlan: (coordinates: [number, number][]) => Plan, offset: [number, number]): BuildingModel;