@a2f0/skyline 0.0.0-stage → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE.md +20 -0
- package/README.md +52 -2
- package/docs/package.md +224 -0
- package/docs/skyline-geography.md +503 -0
- package/lib/models/aon-center.d.ts +4 -0
- package/lib/models/aon-center.js +60 -0
- package/lib/models/aon-geographic.d.ts +9 -0
- package/lib/models/aon-geographic.js +35 -0
- package/lib/models/aon-tower.d.ts +23 -0
- package/lib/models/aon-tower.js +130 -0
- package/lib/models/blue-cross-geographic.d.ts +14 -0
- package/lib/models/blue-cross-geographic.js +248 -0
- package/lib/models/borg-warner-geographic.d.ts +20 -0
- package/lib/models/borg-warner-geographic.js +143 -0
- package/lib/models/buckingham-geographic.d.ts +10 -0
- package/lib/models/buckingham-geographic.js +121 -0
- package/lib/models/building-kit.d.ts +84 -0
- package/lib/models/building-kit.js +358 -0
- package/lib/models/celebrations.d.ts +39 -0
- package/lib/models/celebrations.js +44 -0
- package/lib/models/chicago-athletic-association-geographic.d.ts +32 -0
- package/lib/models/chicago-athletic-association-geographic.js +173 -0
- package/lib/models/crain-communications.d.ts +5 -0
- package/lib/models/crain-communications.js +75 -0
- package/lib/models/crain-geographic.d.ts +10 -0
- package/lib/models/crain-geographic.js +69 -0
- package/lib/models/crain-tower.d.ts +32 -0
- package/lib/models/crain-tower.js +378 -0
- package/lib/models/facade-grid.d.ts +18 -0
- package/lib/models/facade-grid.js +176 -0
- package/lib/models/gage-geographic.d.ts +19 -0
- package/lib/models/gage-geographic.js +154 -0
- package/lib/models/heritage-at-millennium-park.d.ts +3 -0
- package/lib/models/heritage-at-millennium-park.js +284 -0
- package/lib/models/heritage-geographic.d.ts +11 -0
- package/lib/models/heritage-geographic.js +208 -0
- package/lib/models/hyatt-west-tower-geographic.d.ts +26 -0
- package/lib/models/hyatt-west-tower-geographic.js +86 -0
- package/lib/models/keith-ascher-geographic.d.ts +29 -0
- package/lib/models/keith-ascher-geographic.js +110 -0
- package/lib/models/kemper-geographic.d.ts +8 -0
- package/lib/models/kemper-geographic.js +153 -0
- package/lib/models/kemper.d.ts +2 -0
- package/lib/models/kemper.js +78 -0
- package/lib/models/lake-view-geographic.d.ts +15 -0
- package/lib/models/lake-view-geographic.js +134 -0
- package/lib/models/maclean-center-geographic.d.ts +20 -0
- package/lib/models/maclean-center-geographic.js +162 -0
- package/lib/models/michigan-boulevard-geographic.d.ts +12 -0
- package/lib/models/michigan-boulevard-geographic.js +157 -0
- package/lib/models/michigan-plaza-south-geographic.d.ts +7 -0
- package/lib/models/michigan-plaza-south-geographic.js +84 -0
- package/lib/models/michigan-plaza-south.d.ts +2 -0
- package/lib/models/michigan-plaza-south.js +68 -0
- package/lib/models/millennium-park-plaza-geographic.d.ts +9 -0
- package/lib/models/millennium-park-plaza-geographic.js +103 -0
- package/lib/models/monroe-geographic.d.ts +19 -0
- package/lib/models/monroe-geographic.js +177 -0
- package/lib/models/north-michigan-180-geographic.d.ts +14 -0
- package/lib/models/north-michigan-180-geographic.js +117 -0
- package/lib/models/north-wabash-geographic.d.ts +14 -0
- package/lib/models/north-wabash-geographic.js +122 -0
- package/lib/models/on-the-park-geographic.d.ts +11 -0
- package/lib/models/on-the-park-geographic.js +245 -0
- package/lib/models/one-prudential-geographic.d.ts +12 -0
- package/lib/models/one-prudential-geographic.js +55 -0
- package/lib/models/one-prudential-plaza.d.ts +5 -0
- package/lib/models/one-prudential-plaza.js +72 -0
- package/lib/models/one-prudential-tower.d.ts +45 -0
- package/lib/models/one-prudential-tower.js +246 -0
- package/lib/models/peoples-gas-geographic.d.ts +23 -0
- package/lib/models/peoples-gas-geographic.js +250 -0
- package/lib/models/railway-exchange-geographic.d.ts +18 -0
- package/lib/models/railway-exchange-geographic.js +176 -0
- package/lib/models/river-plaza-geographic.d.ts +16 -0
- package/lib/models/river-plaza-geographic.js +104 -0
- package/lib/models/sheraton-grand-geographic.d.ts +24 -0
- package/lib/models/sheraton-grand-geographic.js +112 -0
- package/lib/models/six-north-michigan-geographic.d.ts +16 -0
- package/lib/models/six-north-michigan-geographic.js +180 -0
- package/lib/models/skyline-geography-data.d.ts +44 -0
- package/lib/models/skyline-geography-data.js +607 -0
- package/lib/models/skyline-geography.d.ts +23 -0
- package/lib/models/skyline-geography.js +322 -0
- package/lib/models/swissotel-geographic.d.ts +24 -0
- package/lib/models/swissotel-geographic.js +79 -0
- package/lib/models/three-illinois-center-geographic.d.ts +26 -0
- package/lib/models/three-illinois-center-geographic.js +86 -0
- package/lib/models/trump-geographic.d.ts +14 -0
- package/lib/models/trump-geographic.js +51 -0
- package/lib/models/trump-international-tower.d.ts +4 -0
- package/lib/models/trump-international-tower.js +84 -0
- package/lib/models/trump-tower.d.ts +43 -0
- package/lib/models/trump-tower.js +229 -0
- package/lib/models/two-illinois-center-geographic.d.ts +26 -0
- package/lib/models/two-illinois-center-geographic.js +85 -0
- package/lib/models/two-prudential-geographic.d.ts +25 -0
- package/lib/models/two-prudential-geographic.js +47 -0
- package/lib/models/two-prudential-plaza.d.ts +8 -0
- package/lib/models/two-prudential-plaza.js +55 -0
- package/lib/models/two-prudential-tower.d.ts +53 -0
- package/lib/models/two-prudential-tower.js +441 -0
- package/lib/models/university-club-geographic.d.ts +24 -0
- package/lib/models/university-club-geographic.js +217 -0
- package/lib/models/willoughby-tower-geographic.d.ts +17 -0
- package/lib/models/willoughby-tower-geographic.js +195 -0
- package/lib/models/window-illumination.d.ts +14 -0
- package/lib/models/window-illumination.js +64 -0
- package/lib/package-assets.d.ts +5 -0
- package/lib/package-assets.js +9 -0
- package/lib/skyline-package.d.ts +23 -0
- package/lib/skyline-package.js +25 -0
- package/lib/skyline-scene.d.ts +25 -0
- package/lib/skyline-scene.js +86 -0
- package/lib/study-types.d.ts +49 -0
- package/lib/study-types.js +1 -0
- package/lib/vendor/three-r186.d.ts +1 -0
- package/lib/vendor/three-r186.js +1 -0
- package/package.json +64 -4
- package/site/building-detail.html +54 -0
- package/site/building-detail.js +84 -0
- package/site/building-study.html +58 -0
- package/site/building-study.js +3 -0
- package/site/index.html +407 -0
- package/site/models/aon-center.js +60 -0
- package/site/models/aon-geographic.js +35 -0
- package/site/models/aon-tower.js +130 -0
- package/site/models/blue-cross-geographic.js +248 -0
- package/site/models/borg-warner-geographic.js +143 -0
- package/site/models/buckingham-geographic.js +121 -0
- package/site/models/building-kit.js +358 -0
- package/site/models/celebrations.js +44 -0
- package/site/models/chicago-athletic-association-geographic.js +173 -0
- package/site/models/crain-communications.js +75 -0
- package/site/models/crain-geographic.js +69 -0
- package/site/models/crain-reference.svg +10 -0
- package/site/models/crain-tower.js +378 -0
- package/site/models/facade-grid.js +176 -0
- package/site/models/gage-geographic.js +154 -0
- package/site/models/heritage-at-millennium-park.js +284 -0
- package/site/models/heritage-geographic.js +208 -0
- package/site/models/hyatt-west-tower-geographic.js +86 -0
- package/site/models/keith-ascher-geographic.js +110 -0
- package/site/models/kemper-geographic.js +153 -0
- package/site/models/kemper.js +78 -0
- package/site/models/lake-view-geographic.js +134 -0
- package/site/models/maclean-center-geographic.js +162 -0
- package/site/models/michigan-boulevard-geographic.js +157 -0
- package/site/models/michigan-plaza-south-geographic.js +84 -0
- package/site/models/michigan-plaza-south.js +68 -0
- package/site/models/millennium-park-plaza-geographic.js +103 -0
- package/site/models/monroe-geographic.js +177 -0
- package/site/models/north-michigan-180-geographic.js +117 -0
- package/site/models/north-wabash-geographic.js +122 -0
- package/site/models/on-the-park-geographic.js +245 -0
- package/site/models/one-prudential-geographic.js +55 -0
- package/site/models/one-prudential-plaza.js +72 -0
- package/site/models/one-prudential-tower.js +246 -0
- package/site/models/peoples-gas-geographic.js +250 -0
- package/site/models/railway-exchange-geographic.js +176 -0
- package/site/models/river-plaza-geographic.js +104 -0
- package/site/models/sheraton-grand-geographic.js +112 -0
- package/site/models/six-north-michigan-geographic.js +180 -0
- package/site/models/skyline-geography-data.js +607 -0
- package/site/models/skyline-geography.js +322 -0
- package/site/models/skyline-panorama.svg +3491 -0
- package/site/models/skyline-reference.svg +3090 -0
- package/site/models/swissotel-geographic.js +79 -0
- package/site/models/three-illinois-center-geographic.js +86 -0
- package/site/models/trump-geographic.js +51 -0
- package/site/models/trump-international-tower.js +84 -0
- package/site/models/trump-tower.js +229 -0
- package/site/models/two-illinois-center-geographic.js +85 -0
- package/site/models/two-prudential-geographic.js +47 -0
- package/site/models/two-prudential-plaza.js +55 -0
- package/site/models/two-prudential-tower.js +441 -0
- package/site/models/university-club-geographic.js +217 -0
- package/site/models/willoughby-tower-geographic.js +195 -0
- package/site/models/window-illumination.js +64 -0
- package/site/skyline-3d.html +92 -0
- package/site/skyline-3d.js +333 -0
- package/site/skyline-animated.svg +18795 -0
- package/site/skyline-comparison.js +97 -0
- package/site/skyline-original-fit.svg +18312 -0
- package/site/skyline-scene.js +86 -0
- package/site/skyline-study.html +83 -0
- package/site/skyline-study.js +109 -0
- package/site/skyline-webgl.html +667 -0
- package/site/stars.svg +176 -0
- package/site/study-loader.js +20 -0
- package/site/study-types.js +1 -0
- package/site/study-viewer.js +605 -0
- package/site/study.css +204 -0
- package/site/vendor/THREE-LICENSE.txt +21 -0
- 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;
|