@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.
- package/NOTICE.md +20 -0
- package/README.md +52 -2
- package/docs/package.md +222 -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,358 @@
|
|
|
1
|
+
import * as THREE from "../vendor/three-r186.js";
|
|
2
|
+
// Shared geometry for fitted skyline models. A plan is a list of runs, straight or
|
|
3
|
+
// arced, counterclockwise from above, so each run's outward normal is to its right.
|
|
4
|
+
// Units are meters; +x is east (right in the skyline view) and +z is south.
|
|
5
|
+
export const deg = Math.PI / 180;
|
|
6
|
+
export function line(a, b) {
|
|
7
|
+
const length = Math.hypot(b[0] - a[0], b[1] - a[1]);
|
|
8
|
+
const tangent = [(b[0] - a[0]) / length, (b[1] - a[1]) / length];
|
|
9
|
+
const normal = [-tangent[1], tangent[0]];
|
|
10
|
+
return {
|
|
11
|
+
length,
|
|
12
|
+
pieces: () => 1,
|
|
13
|
+
at: (s, offset = 0) => [a[0] + tangent[0] * s + normal[0] * offset, a[1] + tangent[1] * s + normal[1] * offset],
|
|
14
|
+
normal: () => normal,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
// Angles measure the outward normal from +z toward +x. A concave arc keeps its
|
|
18
|
+
// center outside the building, so the same angle gives the same normal. Facets stay
|
|
19
|
+
// within `step`; 2.4° keeps silhouettes round without faceted toon bands.
|
|
20
|
+
export function arc(center, radius, from, to, concave = false, step = 2.4 * deg) {
|
|
21
|
+
const length = radius * Math.abs(to - from);
|
|
22
|
+
const angle = (s) => from + (to - from) * s / length;
|
|
23
|
+
const side = concave ? -1 : 1;
|
|
24
|
+
return {
|
|
25
|
+
length,
|
|
26
|
+
pieces: (s0 = 0, s1 = length) => Math.max(1, Math.ceil(Math.abs(to - from) * (s1 - s0) / length / step - 1e-9)),
|
|
27
|
+
at(s, offset = 0) {
|
|
28
|
+
const a = angle(s), reach = side * radius + offset;
|
|
29
|
+
return [center[0] + Math.sin(a) * reach, center[1] + Math.cos(a) * reach];
|
|
30
|
+
},
|
|
31
|
+
normal: (s = 0) => [Math.sin(angle(s)), Math.cos(angle(s))],
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
// A gentle convex arc between two corners, bulging outward by its sagitta.
|
|
35
|
+
export function bulge(a, b, sagitta, step) {
|
|
36
|
+
const chord = Math.hypot(b[0] - a[0], b[1] - a[1]);
|
|
37
|
+
const radius = chord * chord / (8 * sagitta) + sagitta / 2;
|
|
38
|
+
const mid = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2];
|
|
39
|
+
const inward = [(b[1] - a[1]) / chord, -(b[0] - a[0]) / chord];
|
|
40
|
+
const center = [mid[0] + inward[0] * (radius - sagitta), mid[1] + inward[1] * (radius - sagitta)];
|
|
41
|
+
const angleOf = (p) => Math.atan2(p[0] - center[0], p[1] - center[1]);
|
|
42
|
+
let from = angleOf(a), to = angleOf(b);
|
|
43
|
+
if (to < from)
|
|
44
|
+
to += Math.PI * 2;
|
|
45
|
+
return arc(center, radius, from, to, false, step);
|
|
46
|
+
}
|
|
47
|
+
// A rectangular plan from its bounds, counterclockwise from above: the south, east, north,
|
|
48
|
+
// then west faces. North is the smaller z.
|
|
49
|
+
export function rectangle(west, east, north, south) {
|
|
50
|
+
const [sw, se, ne, nw] = [[west, south], [east, south], [east, north], [west, north]];
|
|
51
|
+
return [line(sw, se), line(se, ne), line(ne, nw), line(nw, sw)];
|
|
52
|
+
}
|
|
53
|
+
export const evenly = (length, count) => Array.from({ length: count + 1 }, (_, i) => length * i / count);
|
|
54
|
+
export const cornerNormal = (a, b) => { const l = Math.hypot(a[0] + b[0], a[1] + b[1]); return [(a[0] + b[0]) / l, (a[1] + b[1]) / l]; };
|
|
55
|
+
export const station = (run, s) => ({ at: run.at(s), normal: run.normal(s) });
|
|
56
|
+
// A plan point offset outward along a station's normal and sideways along its run.
|
|
57
|
+
export const along = ({ at, normal }, offset, side = 0) => [at[0] + normal[0] * offset + normal[1] * side, at[1] + normal[1] * offset - normal[0] * side];
|
|
58
|
+
export const point = ([x, z], y) => [x, y, z];
|
|
59
|
+
// The plan outline with the same facets the walls use, so roofs meet them edge to edge.
|
|
60
|
+
export function polygonOf(runs) {
|
|
61
|
+
return runs.flatMap((run) => { const n = run.pieces(0, run.length); return Array.from({ length: n }, (_, i) => run.at(run.length * i / n)); });
|
|
62
|
+
}
|
|
63
|
+
// Twice the signed area with +z south, so a counterclockwise plan from above is negative.
|
|
64
|
+
const shoelace = (polygon) => polygon.reduce((sum, [x, z], i) => { const [x2, z2] = polygon[(i + 1) % polygon.length]; return sum + x * z2 - x2 * z; }, 0);
|
|
65
|
+
export function inside(polygon, [x, z]) {
|
|
66
|
+
let result = false;
|
|
67
|
+
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i, i += 1) {
|
|
68
|
+
const [xi, zi] = polygon[i], [xj, zj] = polygon[j];
|
|
69
|
+
if ((zi > z) !== (zj > z) && x < (xj - xi) * (z - zi) / (zj - zi) + xi)
|
|
70
|
+
result = !result;
|
|
71
|
+
}
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
// A model under construction: toon materials, batched geometry, and the standard model
|
|
75
|
+
// API once finished. Solids are closed by default. A caller may omit only faces another
|
|
76
|
+
// surface covers, by name, and each omission is kept in building.userData.omitted so a
|
|
77
|
+
// test can prove the cover.
|
|
78
|
+
export function createBuilder(name, buildingId, { gradient = [70, 135, 200, 255] } = {}) {
|
|
79
|
+
const building = new THREE.Group();
|
|
80
|
+
building.name = name;
|
|
81
|
+
building.userData["buildingId"] = buildingId;
|
|
82
|
+
const omitted = building.userData["omitted"] = [];
|
|
83
|
+
const ramp = new THREE.DataTexture(new Uint8Array(gradient), gradient.length, 1, THREE.RedFormat);
|
|
84
|
+
ramp.minFilter = ramp.magFilter = THREE.NearestFilter;
|
|
85
|
+
ramp.needsUpdate = true;
|
|
86
|
+
const materials = [], batches = [];
|
|
87
|
+
const material = (color, extras = {}) => {
|
|
88
|
+
const surface = new THREE.MeshToonMaterial({ color, gradientMap: ramp, ...extras });
|
|
89
|
+
materials.push(surface);
|
|
90
|
+
return surface;
|
|
91
|
+
};
|
|
92
|
+
// Each batch is one draw call. Normals are analytic, so arc facets shade as a curve.
|
|
93
|
+
const batch = (batchName, surface) => {
|
|
94
|
+
const data = { name: batchName, surface, positions: [], normals: [], colors: [] };
|
|
95
|
+
batches.push(data);
|
|
96
|
+
return data;
|
|
97
|
+
};
|
|
98
|
+
const always = () => true;
|
|
99
|
+
// Runs must hand over end to start, and a loop must come back to where it began;
|
|
100
|
+
// otherwise the walls leave a gap no omission records.
|
|
101
|
+
const joins = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1]) <= 1e-6;
|
|
102
|
+
function checkJoins(runs, label, loop) {
|
|
103
|
+
runs.forEach((run, index) => {
|
|
104
|
+
const next = runs[index + 1];
|
|
105
|
+
if (next && !joins(run.at(run.length), next.at(0)))
|
|
106
|
+
throw new Error(`A ${label}'s runs must join end to start.`);
|
|
107
|
+
});
|
|
108
|
+
const last = runs.at(-1);
|
|
109
|
+
if (loop && !joins(last.at(last.length), runs[0].at(0)))
|
|
110
|
+
throw new Error(`A ${label}'s runs must return to their start.`);
|
|
111
|
+
}
|
|
112
|
+
const white = new THREE.Color(1, 1, 1);
|
|
113
|
+
// Winding follows the first normal, so every helper stays outward-facing.
|
|
114
|
+
// Quads pass their vertices directly, avoiding temporary triangle and normal arrays
|
|
115
|
+
// for every face while retaining the same winding, vertex order, and attributes.
|
|
116
|
+
function appendTriangle(target, a, b, c, na, nb, nc, color = white) {
|
|
117
|
+
const ux = b[0] - a[0], uy = b[1] - a[1], uz = b[2] - a[2];
|
|
118
|
+
const vx = c[0] - a[0], vy = c[1] - a[1], vz = c[2] - a[2];
|
|
119
|
+
const facing = (uy * vz - uz * vy) * na[0] + (uz * vx - ux * vz) * na[1] + (ux * vy - uy * vx) * na[2];
|
|
120
|
+
if (facing < 0) {
|
|
121
|
+
const point = b;
|
|
122
|
+
b = c;
|
|
123
|
+
c = point;
|
|
124
|
+
const normal = nb;
|
|
125
|
+
nb = nc;
|
|
126
|
+
nc = normal;
|
|
127
|
+
}
|
|
128
|
+
target.positions.push(a[0], a[1], a[2], b[0], b[1], b[2], c[0], c[1], c[2]);
|
|
129
|
+
target.normals.push(na[0], na[1], na[2], nb[0], nb[1], nb[2], nc[0], nc[1], nc[2]);
|
|
130
|
+
target.colors.push(color.r, color.g, color.b, color.r, color.g, color.b, color.r, color.g, color.b);
|
|
131
|
+
}
|
|
132
|
+
function triangle(target, points, normals, color) {
|
|
133
|
+
appendTriangle(target, points[0], points[1], points[2], normals[0], normals[1], normals[2], color);
|
|
134
|
+
}
|
|
135
|
+
function quad(target, [a, b, c, d], normals, color) {
|
|
136
|
+
const na = normals[0], flat = normals.length === 1;
|
|
137
|
+
const nb = flat ? na : normals[1], nc = flat ? na : normals[2], nd = flat ? na : normals[3];
|
|
138
|
+
appendTriangle(target, a, b, c, na, nb, nc, color);
|
|
139
|
+
appendTriangle(target, a, c, d, na, nc, nd, color);
|
|
140
|
+
}
|
|
141
|
+
// Names a face that does not exist, so a typo cannot silently keep it.
|
|
142
|
+
const checkOmit = (omit, allowed, what) => {
|
|
143
|
+
const unknown = (omit ?? []).find((faceName) => !allowed.includes(faceName));
|
|
144
|
+
if (unknown)
|
|
145
|
+
throw new Error(`A ${what} has no "${unknown}" face; it has ${allowed.join(", ")}.`);
|
|
146
|
+
};
|
|
147
|
+
// A flat face of a solid, unless the caller omitted it as covered.
|
|
148
|
+
function face(target, corners, normal, skip) {
|
|
149
|
+
if (skip)
|
|
150
|
+
omitted.push({ batch: target.name, corners, normal });
|
|
151
|
+
else
|
|
152
|
+
quad(target, corners, [normal]);
|
|
153
|
+
}
|
|
154
|
+
// A rectangle mapped onto a plan run, split into facets that follow an arc. Panels
|
|
155
|
+
// are single surfaces for panes and trim, not solids.
|
|
156
|
+
function panel(target, run, s0, s1, y0, y1, offset, color) {
|
|
157
|
+
const n = run.pieces(s0, s1);
|
|
158
|
+
for (let i = 0; i < n; i += 1) {
|
|
159
|
+
const sa = s0 + (s1 - s0) * i / n, sb = s0 + (s1 - s0) * (i + 1) / n;
|
|
160
|
+
const [pa, pb] = [run.at(sa, offset), run.at(sb, offset)];
|
|
161
|
+
const [na, nb] = [run.normal(sa), run.normal(sb)].map(([x, z]) => [x, 0, z]);
|
|
162
|
+
quad(target, [[pa[0], y0, pa[1]], [pb[0], y0, pb[1]], [pb[0], y1, pb[1]], [pa[0], y1, pa[1]]], [na, nb, nb, na], color);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// A horizontal strip between two offsets from a run: a soffit or a ledge top.
|
|
166
|
+
function ledge(target, run, s0, s1, y, inner, outer, up) {
|
|
167
|
+
const n = run.pieces(s0, s1), normal = [[0, up ? 1 : -1, 0]];
|
|
168
|
+
for (let i = 0; i < n; i += 1) {
|
|
169
|
+
const sa = s0 + (s1 - s0) * i / n, sb = s0 + (s1 - s0) * (i + 1) / n;
|
|
170
|
+
quad(target, [point(run.at(sa, inner), y), point(run.at(sb, inner), y), point(run.at(sb, outer), y), point(run.at(sa, outer), y)], normal);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// A projecting band along consecutive runs, chamfered at convex corners. It is closed
|
|
174
|
+
// by its back, soffit, top, and start and end returns unless `omit` names them; a
|
|
175
|
+
// `closed` band, such as a parapet around a whole roof, turns its last corner onto its
|
|
176
|
+
// first run instead of ending. `visible` skips pieces buried inside another volume, and
|
|
177
|
+
// the band closes itself wherever it stops and reopens.
|
|
178
|
+
function band(target, runs, y0, y1, proud, { omit = [], from = 0, to = Infinity, closed = false, visible = always } = {}) {
|
|
179
|
+
if (closed && (from !== 0 || to !== Infinity))
|
|
180
|
+
throw new Error("A closed band wraps whole runs, so it takes no from or to.");
|
|
181
|
+
if (closed && visible !== always)
|
|
182
|
+
throw new Error("A closed band cannot also be trimmed by visible; split it into open bands.");
|
|
183
|
+
checkJoins(runs, closed ? "closed band" : "band", closed);
|
|
184
|
+
const faceNames = ["back", "soffit", "top", "start", "end"];
|
|
185
|
+
checkOmit(omit, faceNames, "band");
|
|
186
|
+
const [back, soffit, top, start, end] = faceNames.map((faceName) => !omit.includes(faceName));
|
|
187
|
+
const endFace = (run, s, sign, keep) => {
|
|
188
|
+
const [inner, outer] = [run.at(s), run.at(s, proud)], n = run.normal(s);
|
|
189
|
+
face(target, [point(inner, y0), point(outer, y0), point(outer, y1), point(inner, y1)], [n[1] * sign, 0, -n[0] * sign], !keep);
|
|
190
|
+
};
|
|
191
|
+
// Outward normals turn counterclockwise from above at a convex corner, opening a wedge to fill.
|
|
192
|
+
const cornerWedge = (run, next) => {
|
|
193
|
+
const at = run.at(run.length), n1 = run.normal(run.length), n2 = next.normal(0);
|
|
194
|
+
const turn = n1[0] * n2[1] - n1[1] * n2[0];
|
|
195
|
+
// Runs that double back share a point but face opposite ways, so no wedge can close
|
|
196
|
+
// the joint and both returns are missing: a hole no omission would record. Runs that
|
|
197
|
+
// merely continue straight face the same way and need no wedge at all.
|
|
198
|
+
if (Math.abs(turn) <= 1e-6 && n1[0] * n2[0] + n1[1] * n2[1] < 0)
|
|
199
|
+
throw new Error("A band cannot double back on itself; end the band and start another.");
|
|
200
|
+
if (turn > 1e-6)
|
|
201
|
+
throw new Error("A band cannot turn a concave corner; split it into two bands there.");
|
|
202
|
+
if (turn >= -1e-6)
|
|
203
|
+
return;
|
|
204
|
+
const a = run.at(run.length, proud), b = next.at(0, proud), mitre = cornerNormal(n1, n2);
|
|
205
|
+
quad(target, [point(a, y0), point(b, y0), point(b, y1), point(a, y1)], [[mitre[0], 0, mitre[1]]]);
|
|
206
|
+
if (soffit)
|
|
207
|
+
triangle(target, [point(at, y0), point(b, y0), point(a, y0)], [[0, -1, 0], [0, -1, 0], [0, -1, 0]]);
|
|
208
|
+
else
|
|
209
|
+
omitted.push({ batch: target.name, corners: [point(at, y0), point(b, y0), point(a, y0)], normal: [0, -1, 0] });
|
|
210
|
+
if (top)
|
|
211
|
+
triangle(target, [point(at, y1), point(a, y1), point(b, y1)], [[0, 1, 0], [0, 1, 0], [0, 1, 0]]);
|
|
212
|
+
else
|
|
213
|
+
omitted.push({ batch: target.name, corners: [point(at, y1), point(a, y1), point(b, y1)], normal: [0, 1, 0] });
|
|
214
|
+
};
|
|
215
|
+
const pieces = [];
|
|
216
|
+
runs.forEach((run, index) => {
|
|
217
|
+
const first = index === 0 ? from : 0, last = Math.min(run.length, index === runs.length - 1 ? to : Infinity);
|
|
218
|
+
if (last - first <= 1e-9)
|
|
219
|
+
return;
|
|
220
|
+
const count = run.pieces(first, last);
|
|
221
|
+
for (let i = 0; i < count; i += 1) {
|
|
222
|
+
const sa = first + (last - first) * i / count, sb = first + (last - first) * (i + 1) / count;
|
|
223
|
+
pieces.push({ run, index, sa, sb, shown: visible(run.at((sa + sb) / 2, proud + 0.2), y0) });
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
let drawn = false, opened = false, previous = null;
|
|
227
|
+
for (const piece of pieces) {
|
|
228
|
+
const { run, sa, sb, shown } = piece;
|
|
229
|
+
// A closed band opens with no return: its wrap-around corner closes it instead.
|
|
230
|
+
if (shown && !drawn && !(closed && !opened))
|
|
231
|
+
endFace(run, sa, -1, opened ? true : start);
|
|
232
|
+
if (!shown && drawn && previous)
|
|
233
|
+
endFace(previous.run, previous.sb, 1, true);
|
|
234
|
+
if (shown && drawn && previous && previous.index !== piece.index)
|
|
235
|
+
cornerWedge(previous.run, run);
|
|
236
|
+
drawn = shown;
|
|
237
|
+
previous = piece;
|
|
238
|
+
if (!shown)
|
|
239
|
+
continue;
|
|
240
|
+
opened = true;
|
|
241
|
+
panel(target, run, sa, sb, y0, y1, proud);
|
|
242
|
+
const [ia, ib, oa, ob] = [run.at(sa), run.at(sb), run.at(sa, proud), run.at(sb, proud)];
|
|
243
|
+
if (soffit)
|
|
244
|
+
ledge(target, run, sa, sb, y0, 0, proud, false);
|
|
245
|
+
else
|
|
246
|
+
omitted.push({ batch: target.name, corners: [point(ia, y0), point(ib, y0), point(ob, y0), point(oa, y0)], normal: [0, -1, 0] });
|
|
247
|
+
if (top)
|
|
248
|
+
ledge(target, run, sa, sb, y1, 0, proud, true);
|
|
249
|
+
else
|
|
250
|
+
omitted.push({ batch: target.name, corners: [point(ia, y1), point(ib, y1), point(ob, y1), point(oa, y1)], normal: [0, 1, 0] });
|
|
251
|
+
const [na, nb] = [run.normal(sa), run.normal(sb)].map(([x, z]) => [-x, 0, -z]);
|
|
252
|
+
const backCorners = [point(ib, y0), point(ia, y0), point(ia, y1), point(ib, y1)];
|
|
253
|
+
if (back)
|
|
254
|
+
quad(target, backCorners, [nb, na, na, nb]);
|
|
255
|
+
else
|
|
256
|
+
omitted.push({ batch: target.name, corners: backCorners, normal: [(na[0] + nb[0]) / 2, 0, (na[2] + nb[2]) / 2] });
|
|
257
|
+
}
|
|
258
|
+
if (drawn && closed && previous)
|
|
259
|
+
cornerWedge(previous.run, runs[0]);
|
|
260
|
+
else if (drawn && previous)
|
|
261
|
+
endFace(previous.run, previous.sb, 1, end);
|
|
262
|
+
}
|
|
263
|
+
// A box on a local frame: fins, mullions, posts, and rooftop masses. Depths run along
|
|
264
|
+
// the frame's normal from `back` to `front`.
|
|
265
|
+
function box(target, [x, z], normal, halfWidth, back, front, y0, y1, { omit = [] } = {}) {
|
|
266
|
+
const t = [normal[1], -normal[0]];
|
|
267
|
+
const p = (side, depth, y) => [x + t[0] * side + normal[0] * depth, y, z + t[1] * side + normal[1] * depth];
|
|
268
|
+
const faces = {
|
|
269
|
+
front: [[p(-halfWidth, front, y0), p(halfWidth, front, y0), p(halfWidth, front, y1), p(-halfWidth, front, y1)], [normal[0], 0, normal[1]]],
|
|
270
|
+
back: [[p(halfWidth, back, y0), p(-halfWidth, back, y0), p(-halfWidth, back, y1), p(halfWidth, back, y1)], [-normal[0], 0, -normal[1]]],
|
|
271
|
+
left: [[p(-halfWidth, back, y0), p(-halfWidth, front, y0), p(-halfWidth, front, y1), p(-halfWidth, back, y1)], [-t[0], 0, -t[1]]],
|
|
272
|
+
right: [[p(halfWidth, front, y0), p(halfWidth, back, y0), p(halfWidth, back, y1), p(halfWidth, front, y1)], [t[0], 0, t[1]]],
|
|
273
|
+
bottom: [[p(-halfWidth, back, y0), p(halfWidth, back, y0), p(halfWidth, front, y0), p(-halfWidth, front, y0)], [0, -1, 0]],
|
|
274
|
+
top: [[p(-halfWidth, back, y1), p(-halfWidth, front, y1), p(halfWidth, front, y1), p(halfWidth, back, y1)], [0, 1, 0]],
|
|
275
|
+
};
|
|
276
|
+
checkOmit(omit, Object.keys(faces), "box");
|
|
277
|
+
for (const [faceName, [corners, n]] of Object.entries(faces))
|
|
278
|
+
face(target, corners, n, omit.includes(faceName));
|
|
279
|
+
}
|
|
280
|
+
// Ear clipping for non-convex roofs and floors; the bundle omits ShapeUtils.
|
|
281
|
+
function slab(target, polygon, y, up) {
|
|
282
|
+
const area = shoelace(polygon);
|
|
283
|
+
const ring = polygon.map((_, i) => i), normal = [0, up ? 1 : -1, 0];
|
|
284
|
+
const convex = (a, b, c) => {
|
|
285
|
+
const pa = polygon[a], pb = polygon[b], pc = polygon[c];
|
|
286
|
+
return ((pb[0] - pa[0]) * (pc[1] - pa[1]) - (pb[1] - pa[1]) * (pc[0] - pa[0])) * area > 0;
|
|
287
|
+
};
|
|
288
|
+
const touches = (a, b, q) => {
|
|
289
|
+
const pa = polygon[a], pb = polygon[b];
|
|
290
|
+
return ((pb[0] - pa[0]) * (q[1] - pa[1]) - (pb[1] - pa[1]) * (q[0] - pa[0])) * area >= -1e-9;
|
|
291
|
+
};
|
|
292
|
+
const contains = (a, b, c, q) => touches(a, b, q) && touches(b, c, q) && touches(c, a, q);
|
|
293
|
+
for (let guard = 0; ring.length > 3 && guard < polygon.length * polygon.length; guard += 1) {
|
|
294
|
+
for (let i = 0; i < ring.length; i += 1) {
|
|
295
|
+
const [a, b, c] = [ring[(i + ring.length - 1) % ring.length], ring[i], ring[(i + 1) % ring.length]];
|
|
296
|
+
if (!convex(a, b, c) || ring.some((k) => k !== a && k !== b && k !== c && contains(a, b, c, polygon[k])))
|
|
297
|
+
continue;
|
|
298
|
+
triangle(target, [point(polygon[a], y), point(polygon[b], y), point(polygon[c], y)], [normal, normal, normal]);
|
|
299
|
+
ring.splice(i, 1);
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (ring.length > 3)
|
|
304
|
+
throw new Error(`A slab's polygon could not be triangulated; ${ring.length} vertices remain, so check it for repeated or crossing edges.`);
|
|
305
|
+
triangle(target, ring.map((k) => point(polygon[k], y)), [normal, normal, normal]);
|
|
306
|
+
}
|
|
307
|
+
// Walls rise through the given heights, so a seam can land on a drawn corner; a roof
|
|
308
|
+
// and a floor close the volume. Returns the plan polygon.
|
|
309
|
+
function prism(target, runs, heights, { omit = [] } = {}) {
|
|
310
|
+
checkJoins(runs, "prism", true);
|
|
311
|
+
const polygon = polygonOf(runs);
|
|
312
|
+
if (shoelace(polygon) >= 0)
|
|
313
|
+
throw new Error("A prism's plan must run counterclockwise from above, or its walls face inward.");
|
|
314
|
+
runs.forEach((run) => heights.slice(1).forEach((top, i) => panel(target, run, 0, run.length, heights[i], top, 0)));
|
|
315
|
+
checkOmit(omit, ["top", "bottom"], "prism");
|
|
316
|
+
for (const [faceName, y, up] of [["top", heights.at(-1), true], ["bottom", heights[0], false]]) {
|
|
317
|
+
if (!omit.includes(faceName))
|
|
318
|
+
slab(target, polygon, y, up);
|
|
319
|
+
else
|
|
320
|
+
omitted.push({ batch: target.name, corners: polygon.map((corner) => point(corner, y)), normal: [0, up ? 1 : -1, 0] });
|
|
321
|
+
}
|
|
322
|
+
return polygon;
|
|
323
|
+
}
|
|
324
|
+
// Batches become meshes in creation order, with silhouette lines for `outlines` only.
|
|
325
|
+
function finish({ height, outlines = [], threshold = 20, opacity = 0.3 }) {
|
|
326
|
+
const meshes = new Map(batches.map((data) => {
|
|
327
|
+
const geometry = new THREE.BufferGeometry();
|
|
328
|
+
geometry.setAttribute("position", new THREE.Float32BufferAttribute(data.positions, 3));
|
|
329
|
+
geometry.setAttribute("normal", new THREE.Float32BufferAttribute(data.normals, 3));
|
|
330
|
+
if (data.surface.vertexColors)
|
|
331
|
+
geometry.setAttribute("color", new THREE.Float32BufferAttribute(data.colors, 3));
|
|
332
|
+
const mesh = new THREE.Mesh(geometry, data.surface);
|
|
333
|
+
mesh.name = data.name;
|
|
334
|
+
mesh.castShadow = mesh.receiveShadow = true;
|
|
335
|
+
building.add(mesh);
|
|
336
|
+
return [data, mesh];
|
|
337
|
+
}));
|
|
338
|
+
const lines = outlines.flatMap((data) => [...new THREE.EdgesGeometry(meshes.get(data).geometry, threshold).getAttribute("position").array]);
|
|
339
|
+
const edgeGeometry = new THREE.BufferGeometry();
|
|
340
|
+
edgeGeometry.setAttribute("position", new THREE.Float32BufferAttribute(lines, 3));
|
|
341
|
+
const edges = new THREE.LineSegments(edgeGeometry, new THREE.LineBasicMaterial({ color: 0xcccccc, transparent: true, opacity }));
|
|
342
|
+
edges.name = "silhouette edges";
|
|
343
|
+
building.add(edges);
|
|
344
|
+
return {
|
|
345
|
+
building,
|
|
346
|
+
height,
|
|
347
|
+
triangleCount: batches.reduce((count, data) => count + data.positions.length / 9, 0),
|
|
348
|
+
setHighlighted(highlighted) {
|
|
349
|
+
materials.forEach((surface) => surface.emissive.setHex(highlighted ? 0x222222 : 0x000000));
|
|
350
|
+
},
|
|
351
|
+
setWireframe(enabled) {
|
|
352
|
+
materials.forEach((surface) => { surface.wireframe = enabled; });
|
|
353
|
+
edges.visible = !enabled;
|
|
354
|
+
},
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
return { building, material, batch, triangle, quad, panel, ledge, band, box, slab, prism, finish };
|
|
358
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Documented Chicago window displays, adapted to the modeled south facade.
|
|
2
|
+
// Sources and the limits of each reconstruction: docs/celebration-lighting.md.
|
|
3
|
+
// `adapted` identifies unverified wording; all window placements are reconstructed.
|
|
4
|
+
export const celebrations = [
|
|
5
|
+
{ id: "bulls", label: "Bulls", lines: ["GO", "BULLS"], adapted: true },
|
|
6
|
+
{ id: "cubs", label: "Cubs", lines: ["GO", "CUBS", "GO"], adapted: false },
|
|
7
|
+
{ id: "sox", label: "White Sox", lines: ["SOX", "PRIDE"], adapted: false },
|
|
8
|
+
{ id: "bears", label: "Bears", lines: ["BEAR", "DOWN"], adapted: false },
|
|
9
|
+
{ id: "hawks", label: "Blackhawks", lines: ["HAWKS", "WIN"], adapted: false },
|
|
10
|
+
{ id: "thanks", label: "Thanksgiving", lines: ["GIVE", "THANKS"], adapted: false },
|
|
11
|
+
];
|
|
12
|
+
// Five floors per letter, with a dark window between letters. These are compact
|
|
13
|
+
// reconstructions, not claims to reproduce an electrician's original shade plan.
|
|
14
|
+
const letters = {
|
|
15
|
+
A: ["01110", "10001", "11111", "10001", "10001"],
|
|
16
|
+
B: ["11110", "10001", "11110", "10001", "11110"],
|
|
17
|
+
C: ["01111", "10000", "10000", "10000", "01111"],
|
|
18
|
+
D: ["11110", "10001", "10001", "10001", "11110"],
|
|
19
|
+
E: ["11111", "10000", "11110", "10000", "11111"],
|
|
20
|
+
G: ["01111", "10000", "10111", "10001", "01111"],
|
|
21
|
+
H: ["10001", "10001", "11111", "10001", "10001"],
|
|
22
|
+
I: ["11111", "00100", "00100", "00100", "11111"],
|
|
23
|
+
K: ["10001", "10010", "11100", "10010", "10001"],
|
|
24
|
+
L: ["10000", "10000", "10000", "10000", "11111"],
|
|
25
|
+
N: ["10001", "11001", "10101", "10011", "10001"],
|
|
26
|
+
O: ["01110", "10001", "10001", "10001", "01110"],
|
|
27
|
+
P: ["11110", "10001", "11110", "10000", "10000"],
|
|
28
|
+
R: ["11110", "10001", "11110", "10010", "10001"],
|
|
29
|
+
S: ["01111", "10000", "01110", "00001", "11110"],
|
|
30
|
+
T: ["11111", "00100", "00100", "00100", "00100"],
|
|
31
|
+
U: ["10001", "10001", "10001", "10001", "01110"],
|
|
32
|
+
V: ["10001", "10001", "10001", "01010", "00100"],
|
|
33
|
+
W: ["10001", "10001", "10101", "10101", "01010"],
|
|
34
|
+
X: ["10001", "01010", "00100", "01010", "10001"],
|
|
35
|
+
};
|
|
36
|
+
export function windowWord(word) {
|
|
37
|
+
const glyphs = [...word].map((letter) => {
|
|
38
|
+
const glyph = letters[letter];
|
|
39
|
+
if (!glyph)
|
|
40
|
+
throw new Error(`Unsupported window letter: ${letter}`);
|
|
41
|
+
return glyph;
|
|
42
|
+
});
|
|
43
|
+
return Array.from({ length: 5 }, (_, row) => glyphs.map((glyph) => glyph[row]).join("0"));
|
|
44
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
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 Chicago Athletic Association, 12 South Michigan Avenue (Henry Ives Cobb, 1893): the
|
|
5
|
+
// geographic layout's model, on the mapped outline 147476152. The original layout has no
|
|
6
|
+
// model of it; the drawing paints its Michigan front in Willoughby Tower's group, below and
|
|
7
|
+
// left of the tower, right of the Gage Building.
|
|
8
|
+
//
|
|
9
|
+
// A Venetian Gothic front of stone and brick in three bays: an arched ground floor, a row of
|
|
10
|
+
// small arches, a great arcade over three floors, two floors of windows, the eighth floor's
|
|
11
|
+
// traceried arcade, a carved frieze and a projecting cornice, and a top storey of diaper
|
|
12
|
+
// brick pierced by nine roundels. The levels are the Historic American Buildings Survey's
|
|
13
|
+
// measured drawings, HABS IL-1226, from the first floor to the parapet's 149 ft 4 in; see
|
|
14
|
+
// docs/chicago-athletic-association-reference.md. Units are meters; +x is east, +z is south.
|
|
15
|
+
const ft = (feet, inches = 0) => (feet + inches / 12) * 0.3048;
|
|
16
|
+
export const chicagoAthleticLevels = Object.freeze({
|
|
17
|
+
// Each floor's level on the survey's section, the first to the eleventh.
|
|
18
|
+
floors: [0, ft(17, 5.5), ft(33), ft(46, 6.5), ft(62), ft(73, 1.5), ft(83, 5), ft(94, 1), ft(114, 6), ft(127, 3), ft(136, 5)],
|
|
19
|
+
arcade: [ft(93, 1), ft(112, 11)], // the eighth floor's traceried lights, read on the elevation
|
|
20
|
+
cornice: [ft(120, 9.5), ft(126, 11)], // the cornice's profile, read on the section
|
|
21
|
+
roundels: ft(144), // the roundels' centres, read on the elevation
|
|
22
|
+
parapet: 45.52, // the top of the parapet, measured at 149 ft 4 in
|
|
23
|
+
});
|
|
24
|
+
const h = chicagoAthleticLevels;
|
|
25
|
+
const floor = (n) => h.floors[n - 1];
|
|
26
|
+
// The skins' colours: the window tones, and the stone, the carved frieze and the brick, which
|
|
27
|
+
// no window shares.
|
|
28
|
+
export const chicagoAthleticPalette = Object.freeze({ glass: [0x262626, 0x2a2a2a, 0x2e2e2e, 0x323232], lit: 0x6e6e6e, dim: 0x3a3a3a, stone: 0x6d6d6d, frieze: 0x7a7a7a, brick: 0x5a5a5a });
|
|
29
|
+
const palette = chicagoAthleticPalette;
|
|
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), frieze = color(palette.frieze), brick = color(palette.brick);
|
|
34
|
+
const core = color(0x262626), roofing = color(0x3a3a3a);
|
|
35
|
+
function paneColor(row, bay, wall) {
|
|
36
|
+
const hash = (Math.imul(row + 31, 0x9e3779b1) ^ Math.imul(bay + 79, 0x85ebca77) ^ Math.imul(wall + 61, 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
|
+
export const chicagoAthleticRows = [
|
|
45
|
+
{ lo: 0.25, hi: 0.9, kind: "stone", floor: 1 },
|
|
46
|
+
{ lo: 0.9, hi: 4.3, kind: "ground", floor: 1 },
|
|
47
|
+
{ lo: 4.3, hi: floor(2) + 1.2, kind: "stone", floor: 2 },
|
|
48
|
+
{ lo: floor(2) + 1.2, hi: floor(3) - 1, kind: "arches", floor: 2 },
|
|
49
|
+
{ lo: floor(3) - 1, hi: floor(3) + 1, kind: "stone", floor: 3 },
|
|
50
|
+
{ lo: floor(3) + 1, hi: floor(6) - 0.8, kind: "great", floor: 3 },
|
|
51
|
+
{ lo: floor(6) - 0.8, hi: floor(6) + 0.9, kind: "band", floor: 6 },
|
|
52
|
+
{ lo: floor(6) + 0.9, hi: floor(7) - 0.4, kind: "window", floor: 6 },
|
|
53
|
+
{ lo: floor(7) - 0.4, hi: floor(7) + 0.8, kind: "stone", floor: 7 },
|
|
54
|
+
{ lo: floor(7) + 0.8, hi: h.arcade[0] - 0.3, kind: "window", floor: 7 },
|
|
55
|
+
{ lo: h.arcade[0] - 0.3, hi: h.arcade[0], kind: "stone", floor: 8 },
|
|
56
|
+
{ lo: h.arcade[0], hi: h.arcade[1], kind: "arcade", floor: 8 },
|
|
57
|
+
{ lo: h.arcade[1], hi: h.cornice[0], kind: "frieze", floor: 9 },
|
|
58
|
+
{ lo: h.cornice[0], hi: h.cornice[1], kind: "stone", floor: 9 },
|
|
59
|
+
{ lo: h.cornice[1], hi: floor(10) + 0.3, kind: "brick", floor: 10 },
|
|
60
|
+
{ lo: floor(10) + 0.3, hi: floor(10) + 2.2, kind: "attic", floor: 10 },
|
|
61
|
+
{ lo: floor(10) + 2.2, hi: h.roundels - 0.45, kind: "brick", floor: 11 },
|
|
62
|
+
{ lo: h.roundels - 0.45, hi: h.roundels + 0.45, kind: "roundel", floor: 11 },
|
|
63
|
+
{ lo: h.roundels + 0.45, hi: h.parapet, kind: "brick", floor: 11 },
|
|
64
|
+
];
|
|
65
|
+
const rows = chicagoAthleticRows;
|
|
66
|
+
// Along the front, as fractions of it from the south corner, read on the measured elevation:
|
|
67
|
+
// the three bays' openings between the stone quoins, in two, eight and two lights; the nine
|
|
68
|
+
// roundels, 90 cm across; and the top storey's one window, at its north end.
|
|
69
|
+
export const chicagoAthleticFront = Object.freeze({
|
|
70
|
+
bays: [[0.053, 0.195, 2], [0.254, 0.74, 8], [0.799, 0.941, 2]],
|
|
71
|
+
roundels: Array.from({ length: 9 }, (_, k) => 0.0773 + k * 0.1065),
|
|
72
|
+
attic: [0.84, 0.947],
|
|
73
|
+
});
|
|
74
|
+
const front = chicagoAthleticFront;
|
|
75
|
+
export function createChicagoAthleticAssociationGeographicBuilding(record, projectPlan, offset) {
|
|
76
|
+
const kit = createBuilder(record.name, record.id);
|
|
77
|
+
const shell = kit.batch("Chicago Athletic Association · shell", kit.material(0xffffff, { vertexColors: true }));
|
|
78
|
+
const wall = kit.batch("Chicago Athletic Association · stone, brick and windows", kit.material(0xffffff, { vertexColors: true }));
|
|
79
|
+
const cornice = kit.batch("Chicago Athletic Association · cornice", kit.material(0x7a7a7a));
|
|
80
|
+
const lot = orient(polygonOf(projectPlan(record.footprint.coordinates)));
|
|
81
|
+
// The lot's south-east corner, on Michigan against the Gage Building, with the front running
|
|
82
|
+
// north to Willoughby Tower.
|
|
83
|
+
const corner = lot.reduce((best, p) => (p[0] + p[1] > best[0] + best[1] ? p : best));
|
|
84
|
+
const northEast = lot.reduce((best, p) => (p[0] - p[1] > best[0] - best[1] ? p : best));
|
|
85
|
+
const length = Math.hypot(northEast[0] - corner[0], northEast[1] - corner[1]);
|
|
86
|
+
const north = [(northEast[0] - corner[0]) / length, (northEast[1] - corner[1]) / length], west = [north[1], -north[0]];
|
|
87
|
+
const at = (a, b) => [corner[0] + west[0] * a + north[0] * b, corner[1] + west[1] * a + north[1] * b];
|
|
88
|
+
planOf(lot).forEach((run) => {
|
|
89
|
+
const a = run.at(0), b = run.at(run.length), n = run.normal(0);
|
|
90
|
+
kit.quad(shell, [[a[0], 0, a[1]], [b[0], 0, b[1]], [b[0], h.parapet, b[1]], [a[0], h.parapet, a[1]]], [[n[0], 0, n[1]]], core);
|
|
91
|
+
});
|
|
92
|
+
paintedSlab(kit, shell, lot, 0, false, roofing);
|
|
93
|
+
paintedSlab(kit, shell, lot, h.parapet, true, roofing);
|
|
94
|
+
// The cornice projects 88 cm along the front, as the elevation shows it, from 12 cm inside
|
|
95
|
+
// it, stopping 3 cm short of the neighbours' walls.
|
|
96
|
+
kit.prism(cornice, planOf(orient([at(-0.88, 0.03), at(-0.88, length - 0.03), at(0.12, length - 0.03), at(0.12, 0.03)])), [h.cornice[0], h.cornice[1]]);
|
|
97
|
+
// A wall's skin, one box to a chain the `keep` test accepts, held 2 cm clear of its ends
|
|
98
|
+
// or 9 cm where the outline turns in, over `span`; `cells` lays a wall's columns out across
|
|
99
|
+
// it, from its start, and paints a cell by its middle and row.
|
|
100
|
+
const skin = (seed, keep, span, cells) => {
|
|
101
|
+
const plan = planOf(lot), heights = [span[0].lo, ...span.map((row) => row.hi)];
|
|
102
|
+
chainsOf(plan).forEach((chain, index) => {
|
|
103
|
+
if (!chain.runs.every((run) => keep(plan[run])))
|
|
104
|
+
return;
|
|
105
|
+
const first = chain.runs[0], last = chain.runs.at(-1);
|
|
106
|
+
const s0 = turnsAt(plan, first)[0] > 0 ? 0.09 : 0.02, s1 = chain.length - (turnsAt(plan, last)[1] > 0 ? 0.09 : 0.02);
|
|
107
|
+
const [columns, paint] = cells(chain.length, plan[first].at(0));
|
|
108
|
+
chainSkin(kit, wall, plan, chain, 0, chain.runs.length - 1, s0, s1, columns, heights, 0.02, 0.07, (bay, r) => {
|
|
109
|
+
const row = span[r], color = paint((columns[bay - 1] + columns[bay]) / 2, row, bay);
|
|
110
|
+
return color === glassTones[0] ? paneColor(row.floor, bay, seed * 8 + index) : color;
|
|
111
|
+
}, seed === 0 ? stone : brick);
|
|
112
|
+
});
|
|
113
|
+
};
|
|
114
|
+
const facing = (x, z) => (run) => run.normal(0)[0] * x + run.normal(0)[1] * z > 0.9;
|
|
115
|
+
// The front: each bay's lights between 15 cm colonnettes, which the bands and the frieze
|
|
116
|
+
// cross; the top storey's roundels and window in its brick.
|
|
117
|
+
const michigan = (chain, start) => {
|
|
118
|
+
const fromSouth = Math.hypot(start[0] - corner[0], start[1] - corner[1]) < chain / 2;
|
|
119
|
+
const s = (fraction) => (fromSouth ? fraction * chain : chain - fraction * chain);
|
|
120
|
+
const lights = front.bays.flatMap(([a, b, count]) => Array.from({ length: count }, (_, k) => {
|
|
121
|
+
const width = (b - a) * chain, step = width / count;
|
|
122
|
+
return [s(a) + (fromSouth ? 1 : -1) * (k * step + (k > 0 ? 0.075 : 0)), s(a) + (fromSouth ? 1 : -1) * ((k + 1) * step - (k < count - 1 ? 0.075 : 0))].sort((p, q) => p - q);
|
|
123
|
+
}));
|
|
124
|
+
const roundels = front.roundels.map((c) => [s(c) - 0.45, s(c) + 0.45]);
|
|
125
|
+
const attic = front.attic.map(s).sort((p, q) => p - q);
|
|
126
|
+
const cuts = [...lights.flat(), ...roundels.flat(), ...attic].filter((x) => x > 0.1 && x < chain - 0.1);
|
|
127
|
+
const inside = (spans, x) => spans.some(([a, b]) => x > a && x < b);
|
|
128
|
+
return [[0, ...[...new Set(cuts)].sort((p, q) => p - q), chain], (middle, row) => {
|
|
129
|
+
if (row.kind === "frieze")
|
|
130
|
+
return frieze;
|
|
131
|
+
if (row.kind === "band" || row.kind === "stone")
|
|
132
|
+
return stone;
|
|
133
|
+
if (row.kind === "brick")
|
|
134
|
+
return brick;
|
|
135
|
+
if (row.kind === "roundel")
|
|
136
|
+
return inside(roundels, middle) ? glassTones[0] : brick;
|
|
137
|
+
if (row.kind === "attic")
|
|
138
|
+
return inside([attic], middle) ? glassTones[0] : brick;
|
|
139
|
+
return inside(lights, middle) ? glassTones[0] : stone;
|
|
140
|
+
}];
|
|
141
|
+
};
|
|
142
|
+
// The other walls' rows: a window 1.8 m tall on each floor from the second, its sill
|
|
143
|
+
// 90 cm up, to the parapet.
|
|
144
|
+
const sides = [{ lo: 0.25, hi: floor(2) + 0.9, kind: "brick", floor: 1 }];
|
|
145
|
+
for (let n = 2; n <= 11; n += 1) {
|
|
146
|
+
sides.push({ lo: floor(n) + 0.9, hi: floor(n) + 2.7, kind: "window", floor: n });
|
|
147
|
+
sides.push({ lo: floor(n) + 2.7, hi: n < 11 ? floor(n + 1) + 0.9 : h.parapet, kind: "brick", floor: n });
|
|
148
|
+
}
|
|
149
|
+
// The alley and the light court's walls: windows 1.6 m wide, spread evenly about 3.2 m
|
|
150
|
+
// apart.
|
|
151
|
+
const court = (chain) => {
|
|
152
|
+
const count = Math.max(1, Math.round(chain / 3.2)), centres = Array.from({ length: count }, (_, i) => chain * (i + 0.5) / count);
|
|
153
|
+
return [[0, ...centres.flatMap((c) => [c - 0.8, c + 0.8]), chain], (middle, row) => (row.kind === "window" && centres.some((c) => Math.abs(middle - c) < 0.8) ? glassTones[0] : brick)];
|
|
154
|
+
};
|
|
155
|
+
// A run's middle, in metres west of the front and north of the south corner.
|
|
156
|
+
const place = (run) => {
|
|
157
|
+
const mid = run.at(run.length / 2), [x, z] = [mid[0] - corner[0], mid[1] - corner[1]];
|
|
158
|
+
return [x * west[0] + z * west[1], x * north[0] + z * north[1]];
|
|
159
|
+
};
|
|
160
|
+
// The Michigan front, as against the light court's east wall, 39 m back.
|
|
161
|
+
const isMichigan = (run) => facing(1, 0)(run) && place(run)[0] < 1;
|
|
162
|
+
// The party walls keep the bare shell: the Gage to the south, Willoughby Tower and the
|
|
163
|
+
// annex on Madison to the north, all taller, cover them. The light court's north wall,
|
|
164
|
+
// 6.8 m in from the party line, is not one.
|
|
165
|
+
const party = (run) => (facing(0, 1)(run) ? place(run)[1] < 1 : facing(0, -1)(run) && place(run)[1] > length - 3);
|
|
166
|
+
skin(0, isMichigan, rows, michigan);
|
|
167
|
+
skin(1, (run) => !isMichigan(run) && !party(run), sides, court);
|
|
168
|
+
const model = kit.finish({ height: h.parapet, outlines: [shell, cornice], opacity: 0.16 });
|
|
169
|
+
model.building.position.set(offset[0], 0, offset[1]);
|
|
170
|
+
model.building.userData["geography"] = record;
|
|
171
|
+
model.building.userData["geographicDetail"] = { levels: h, source: "docs/chicago-athletic-association-reference.md" };
|
|
172
|
+
return model;
|
|
173
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { buildCrainTower, crainFloors, crainMainRoof } from "./crain-tower.js";
|
|
2
|
+
// The Crain Communications Building on a clean version of its mapped plan, in real meters:
|
|
3
|
+
// the solo study shows it whole, and the skyline study places a copy cut at the drawing's
|
|
4
|
+
// datum. crain-tower.ts builds it, and the geographic model on the mapped parts.
|
|
5
|
+
//
|
|
6
|
+
// The plan is the OpenStreetMap outline squared up. The south-west half is an isosceles
|
|
7
|
+
// right triangle with 38.6 m legs. The north-east half is its twin across the split, its
|
|
8
|
+
// own split line parallel to the first and 4.95 m from it. The two touch along the middle,
|
|
9
|
+
// and a V-notch opens at each end of the split: 7.6 m deep at the south-east corner, and at
|
|
10
|
+
// the north-west, where the gap between them runs 13.4 m inward as the slot. Both roofs
|
|
11
|
+
// peak at the published 177.4 m and fall 1.225 m per metre along the diagonal, about 51°,
|
|
12
|
+
// measured on the photograph the drawing was traced from. OpenStreetMap's roof tags put the
|
|
13
|
+
// two peaks 5 m apart and the slopes near 55°, which the photograph does not show.
|
|
14
|
+
// See docs/crain-reference.md. Units are meters; +x is east and +z is south.
|
|
15
|
+
const halfWidth = 19.3, halfDepth = 22.8;
|
|
16
|
+
const offset = halfDepth - halfWidth;
|
|
17
|
+
const notch = 7.6, slotLength = 13.4;
|
|
18
|
+
const tip = 177.4, fall = 1.225;
|
|
19
|
+
const diagonal = [Math.SQRT1_2, Math.SQRT1_2];
|
|
20
|
+
// The south-west half, from its peak on the west face.
|
|
21
|
+
const peakWest = [-halfWidth, halfDepth - 2 * halfWidth];
|
|
22
|
+
const southWest = [-halfWidth, halfDepth], southEast = [halfWidth, halfDepth];
|
|
23
|
+
// The north-east half, from its peak at the north-west corner.
|
|
24
|
+
const northWest = [-halfWidth, -halfDepth], northEast = [halfWidth, -halfDepth];
|
|
25
|
+
const notchApex = [halfWidth - notch, halfDepth - notch], notchEast = [halfWidth, halfDepth - 2 * notch];
|
|
26
|
+
const slotWest = [peakWest[0] + slotLength * diagonal[0], peakWest[1] + slotLength * diagonal[1]];
|
|
27
|
+
const slotEast = [slotWest[0] + offset, slotWest[1] - offset];
|
|
28
|
+
const notchWest = [peakWest[0] + offset, peakWest[1] - offset];
|
|
29
|
+
const along = (from) => (point) => (point[0] - from[0]) * diagonal[0] + (point[1] - from[1]) * diagonal[1];
|
|
30
|
+
const southWestRoof = (point) => tip - fall * along(peakWest)(point);
|
|
31
|
+
const northEastRoof = (point) => tip - fall * along(northWest)(point);
|
|
32
|
+
function createCrain(base) {
|
|
33
|
+
return buildCrainTower({
|
|
34
|
+
name: "Crain Communications Building",
|
|
35
|
+
id: "building-crain-communications",
|
|
36
|
+
base,
|
|
37
|
+
grid: { origin: southWest, axis: [1, 0] },
|
|
38
|
+
volumes: [
|
|
39
|
+
{ corners: [peakWest, southWest, southEast, notchApex, slotWest], roof: southWestRoof, glazed: true },
|
|
40
|
+
{ corners: [northWest, northEast, notchEast, notchApex, slotWest, slotEast], roof: northEastRoof, glazed: true },
|
|
41
|
+
{ corners: [peakWest, slotWest, slotEast, notchWest], roof: () => crainMainRoof, glazed: false },
|
|
42
|
+
],
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
// The whole building from the street, for the solo study.
|
|
46
|
+
export function createCrainBuilding() {
|
|
47
|
+
return createCrain(0);
|
|
48
|
+
}
|
|
49
|
+
// The skyline drawing shows the tower from the photograph's treeline up, so its platform
|
|
50
|
+
// datum crosses the tower above the street: 39.6 m, fitted with the placement in
|
|
51
|
+
// skyline-study.ts, which scales and places this copy. Its y = 0 is that datum, and the
|
|
52
|
+
// lowest drawn sill is the twelfth floor's, the first above the lobby being the second.
|
|
53
|
+
export const crainSkylineBase = 39.6;
|
|
54
|
+
export function createCrainSkylineBuilding() {
|
|
55
|
+
return createCrain(crainSkylineBase);
|
|
56
|
+
}
|
|
57
|
+
// Features the skyline test projects against the drawing, in the skyline copy's own
|
|
58
|
+
// coordinates, derived from the geometry above. The drawn sills are the twenty-nine the
|
|
59
|
+
// left face shows below its shoulder, the twelfth floor's to the fortieth's, on the
|
|
60
|
+
// south-west corner the drawing's left edge traces.
|
|
61
|
+
const at = ([x, z], real) => [x, real - crainSkylineBase, z];
|
|
62
|
+
const sill = (floor) => crainFloors.lobbyTop + (floor - 2) * crainFloors.pitch + crainFloors.sill;
|
|
63
|
+
export const crainFeatures = {
|
|
64
|
+
crainSills: Array.from({ length: 29 }, (_, i) => at(southWest, sill(12 + i))),
|
|
65
|
+
// A point on the south face, below the roof and clear of every neighbour, for hover.
|
|
66
|
+
crainFacadeProbe: [0, 60, halfDepth + 0.05],
|
|
67
|
+
crainPeakWest: at(peakWest, southWestRoof(peakWest)),
|
|
68
|
+
crainPeakEast: at(northWest, northEastRoof(northWest)),
|
|
69
|
+
crainShoulderWest: at(southWest, southWestRoof(southWest)),
|
|
70
|
+
crainShoulderEast: at(northEast, northEastRoof(northEast)),
|
|
71
|
+
crainFoot: at(southEast, southWestRoof(southEast)),
|
|
72
|
+
crainStepEast: at(notchEast, northEastRoof(notchEast)),
|
|
73
|
+
crainSlotWest: at(slotWest, southWestRoof(slotWest)),
|
|
74
|
+
crainSlotEast: at(slotEast, northEastRoof(slotEast)),
|
|
75
|
+
};
|