@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,217 @@
|
|
|
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 University Club of Chicago, 76 East Monroe Street (Holabird & Roche, 1909): the
|
|
5
|
+
// geographic layout's model, on the mapped outline 126982632. The original layout has no
|
|
6
|
+
// model of it; the drawing shows it left of Willoughby Tower, behind the Monroe Building.
|
|
7
|
+
//
|
|
8
|
+
// A Gothic club on the corner of Michigan and Monroe. Its walls rise to a crenellated
|
|
9
|
+
// parapet, with the tall arched windows of its top hall in four bays to each street front.
|
|
10
|
+
// Behind the parapet an upper floor carries a steep roof whose gable faces Michigan, with
|
|
11
|
+
// pinnacles at its foot and a cross on its peak at OpenStreetMap's 67.7 m, and a small
|
|
12
|
+
// gable with its own cross on the Monroe side. Heights are measured on the drawing down from that peak; see
|
|
13
|
+
// docs/university-club-reference.md. Units are meters; +x is east, +z is south.
|
|
14
|
+
export const universityClubLevels = Object.freeze({
|
|
15
|
+
band: 46.2, // the band under the parapet
|
|
16
|
+
parapet: 48.95, // the parapet wall
|
|
17
|
+
walls: 51, // the parapet's top, between the merlons
|
|
18
|
+
merlons: 51.75,
|
|
19
|
+
eaves: 56.4, // the upper floor's eaves
|
|
20
|
+
pinnacles: 62.5,
|
|
21
|
+
ridge: 67.7, // the gable's peak, OpenStreetMap's height
|
|
22
|
+
cross: 69.75,
|
|
23
|
+
dormer: 59.3, // the Monroe side's small gable
|
|
24
|
+
dormerCross: 60.4,
|
|
25
|
+
back: 3.3, // the upper floor's setback from Michigan
|
|
26
|
+
sides: 1.3, // and from Monroe and the north wall
|
|
27
|
+
});
|
|
28
|
+
const h = universityClubLevels;
|
|
29
|
+
// The skins' colours: the window tones, and the stone and its band, which no window shares.
|
|
30
|
+
export const universityClubPalette = Object.freeze({ glass: [0x4a4a4a, 0x4e4e4e, 0x525252, 0x565656], lit: 0x8c8c8c, dim: 0x444444, stone: 0xa4a4a4, band: 0xb4b4b4 });
|
|
31
|
+
const palette = universityClubPalette;
|
|
32
|
+
const color = (hex) => new THREE.Color(hex);
|
|
33
|
+
const glassTones = palette.glass.map(color);
|
|
34
|
+
const litGlass = color(palette.lit), dimGlass = color(palette.dim);
|
|
35
|
+
const stone = color(palette.stone), band = color(palette.band), core = color(0x2e2e2e), roofing = color(0x343434), flat = color(0x4a4a4a);
|
|
36
|
+
function paneColor(row, bay, wall) {
|
|
37
|
+
const hash = (Math.imul(row + 43, 0x9e3779b1) ^ Math.imul(bay + 71, 0x85ebca77) ^ Math.imul(wall + 59, 0xc2b2ae3d)) >>> 0;
|
|
38
|
+
const value = hash % 101;
|
|
39
|
+
if (value < 4)
|
|
40
|
+
return litGlass;
|
|
41
|
+
if (value < 8)
|
|
42
|
+
return dimGlass;
|
|
43
|
+
return glassTones[value % glassTones.length];
|
|
44
|
+
}
|
|
45
|
+
const wallRows = [
|
|
46
|
+
{ lo: 0.25, hi: 1, kind: "wall", floor: 1 },
|
|
47
|
+
{ lo: 1, hi: 4.4, kind: "full", floor: 1 },
|
|
48
|
+
];
|
|
49
|
+
for (let n = 2; n <= 9; n += 1) {
|
|
50
|
+
const base = 5 + (n - 2) * 3.525;
|
|
51
|
+
wallRows.push({ lo: wallRows.at(-1).hi, hi: base + 0.8, kind: "wall", floor: n });
|
|
52
|
+
wallRows.push({ lo: base + 0.8, hi: base + 2.9, kind: "pair", floor: n });
|
|
53
|
+
}
|
|
54
|
+
wallRows.push({ lo: wallRows.at(-1).hi, hi: 33.65, kind: "wall", floor: 10 });
|
|
55
|
+
wallRows.push({ lo: 33.65, hi: 36.7, kind: "full", floor: 10 });
|
|
56
|
+
wallRows.push({ lo: 36.7, hi: 37.3, kind: "wall", floor: 11 });
|
|
57
|
+
wallRows.push({ lo: 37.3, hi: 43.1, kind: "full", floor: 11 });
|
|
58
|
+
wallRows.push({ lo: 43.1, hi: 43.9, kind: "head", floor: 11 });
|
|
59
|
+
wallRows.push({ lo: 43.9, hi: h.band, kind: "wall", floor: 11 });
|
|
60
|
+
wallRows.push({ lo: h.band, hi: h.parapet, kind: "band", floor: 12 });
|
|
61
|
+
wallRows.push({ lo: h.parapet, hi: h.walls, kind: "wall", floor: 12 });
|
|
62
|
+
// The upper floor's windows under its eaves, and the Monroe gable's.
|
|
63
|
+
const upperRows = [
|
|
64
|
+
{ lo: h.walls, hi: 52, kind: "wall", floor: 13 },
|
|
65
|
+
{ lo: 52, hi: 55.9, kind: "full", floor: 13 },
|
|
66
|
+
{ lo: 55.9, hi: h.eaves, kind: "wall", floor: 13 },
|
|
67
|
+
];
|
|
68
|
+
const dormerRows = [
|
|
69
|
+
{ lo: h.walls, hi: 53.7, kind: "wall", floor: 13 },
|
|
70
|
+
{ lo: 53.7, hi: 55.9, kind: "full", floor: 13 },
|
|
71
|
+
{ lo: 55.9, hi: h.eaves, kind: "wall", floor: 13 },
|
|
72
|
+
];
|
|
73
|
+
export function createUniversityClubGeographicBuilding(record, projectPlan, offset) {
|
|
74
|
+
const kit = createBuilder(record.name, record.id);
|
|
75
|
+
const shell = kit.batch("University Club · shell", kit.material(0xffffff, { vertexColors: true }));
|
|
76
|
+
const wall = kit.batch("University Club · stone and windows", kit.material(0xffffff, { vertexColors: true }));
|
|
77
|
+
const ornament = kit.batch("University Club · merlons and pinnacles", kit.material(0xa0a0a0));
|
|
78
|
+
const lot = orient(polygonOf(projectPlan(record.footprint.coordinates)));
|
|
79
|
+
// The lot's south-east corner, with its Monroe front running west and its Michigan front
|
|
80
|
+
// north.
|
|
81
|
+
const corner = lot.reduce((best, p) => (p[0] + p[1] > best[0] + best[1] ? p : best));
|
|
82
|
+
const northEast = lot.reduce((best, p) => (p[0] - p[1] > best[0] - best[1] ? p : best));
|
|
83
|
+
const neighbours = [lot[(lot.indexOf(corner) + 1) % lot.length], lot[(lot.indexOf(corner) + lot.length - 1) % lot.length]];
|
|
84
|
+
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]; };
|
|
85
|
+
const west = unit(neighbours[0][0] < neighbours[1][0] ? neighbours[0] : neighbours[1]), north = unit(northEast);
|
|
86
|
+
const at = (a, b) => [corner[0] + west[0] * a + north[0] * b, corner[1] + west[1] * a + north[1] * b];
|
|
87
|
+
// The inverse of `at`: a point's distances along Monroe and Michigan, whose fronts meet a
|
|
88
|
+
// little off square.
|
|
89
|
+
const skew = west[0] * north[1] - west[1] * north[0];
|
|
90
|
+
const inLot = (p) => {
|
|
91
|
+
const [x, z] = [p[0] - corner[0], p[1] - corner[1]];
|
|
92
|
+
return [(x * north[1] - z * north[0]) / skew, (west[0] * z - west[1] * x) / skew];
|
|
93
|
+
};
|
|
94
|
+
const [length, depth] = [Math.max(...lot.map((p) => inLot(p)[0])), inLot(northEast)[1]];
|
|
95
|
+
const rect = (a0, a1, b0, b1) => orient([at(a0, b0), at(a1, b0), at(a1, b1), at(a0, b1)]);
|
|
96
|
+
const lift = ([x, z], y) => [x, y, z];
|
|
97
|
+
// The main block, to the parapet's top.
|
|
98
|
+
planOf(lot).forEach((run) => {
|
|
99
|
+
const a = run.at(0), b = run.at(run.length), n = run.normal(0);
|
|
100
|
+
kit.quad(shell, [[a[0], 0, a[1]], [b[0], 0, b[1]], [b[0], h.walls, b[1]], [a[0], h.walls, a[1]]], [[n[0], 0, n[1]]], core);
|
|
101
|
+
});
|
|
102
|
+
paintedSlab(kit, shell, lot, 0, false, flat);
|
|
103
|
+
paintedSlab(kit, shell, lot, h.walls, true, flat);
|
|
104
|
+
// A closed block whose walls rise from `base` to `eaves` under a gable roof to `ridge`,
|
|
105
|
+
// its ridge running along the lot's a axis (west from Michigan) or its b axis (north
|
|
106
|
+
// from Monroe); the gables are stone and the slopes roofing.
|
|
107
|
+
const gabled = (a0, a1, b0, b1, base, eaves, ridge, along) => {
|
|
108
|
+
const corners = rect(a0, a1, b0, b1);
|
|
109
|
+
planOf(corners).forEach((run) => {
|
|
110
|
+
const p = run.at(0), q = run.at(run.length), n = run.normal(0);
|
|
111
|
+
kit.quad(shell, [lift(p, base), lift(q, base), lift(q, eaves), lift(p, eaves)], [[n[0], 0, n[1]]], core);
|
|
112
|
+
});
|
|
113
|
+
paintedSlab(kit, shell, corners, base, false, flat);
|
|
114
|
+
const middle = along === "a" ? (b0 + b1) / 2 : (a0 + a1) / 2;
|
|
115
|
+
const ridgeAt = (s) => lift(along === "a" ? at(s, middle) : at(middle, s), ridge);
|
|
116
|
+
const [s0, s1] = along === "a" ? [a0, a1] : [b0, b1];
|
|
117
|
+
const eave = (s, t) => lift(along === "a" ? at(s, t) : at(t, s), eaves);
|
|
118
|
+
const [t0, t1] = along === "a" ? [b0, b1] : [a0, a1];
|
|
119
|
+
const centre = lift(at((a0 + a1) / 2, (b0 + b1) / 2), (eaves + ridge) / 2);
|
|
120
|
+
// Each face's outward normal, from its points and the block's middle.
|
|
121
|
+
const face = (points, paint) => {
|
|
122
|
+
const [a, b, c] = points;
|
|
123
|
+
const u = new THREE.Vector3(b[0] - a[0], b[1] - a[1], b[2] - a[2]), v = new THREE.Vector3(c[0] - a[0], c[1] - a[1], c[2] - a[2]);
|
|
124
|
+
const n = u.cross(v).normalize(), mid = points.reduce((sum, p) => [sum[0] + p[0] / points.length, sum[1] + p[1] / points.length, sum[2] + p[2] / points.length], [0, 0, 0]);
|
|
125
|
+
if (n.x * (mid[0] - centre[0]) + n.y * (mid[1] - centre[1]) + n.z * (mid[2] - centre[2]) < 0)
|
|
126
|
+
n.negate();
|
|
127
|
+
const normal = [n.x, n.y, n.z];
|
|
128
|
+
if (points.length === 3)
|
|
129
|
+
kit.triangle(shell, points, [normal, normal, normal], paint);
|
|
130
|
+
else
|
|
131
|
+
kit.quad(shell, points, [normal], paint);
|
|
132
|
+
};
|
|
133
|
+
face([eave(s0, t0), eave(s1, t0), ridgeAt(s1), ridgeAt(s0)], roofing);
|
|
134
|
+
face([eave(s1, t1), eave(s0, t1), ridgeAt(s0), ridgeAt(s1)], roofing);
|
|
135
|
+
face([eave(s0, t1), eave(s0, t0), ridgeAt(s0)], stone);
|
|
136
|
+
face([eave(s1, t0), eave(s1, t1), ridgeAt(s1)], stone);
|
|
137
|
+
return corners;
|
|
138
|
+
};
|
|
139
|
+
const upper = gabled(h.back, length - h.sides, h.sides, depth - h.sides, h.walls, h.eaves, h.ridge, "a");
|
|
140
|
+
// The Monroe side's small gable stands 15 cm proud of the upper floor, clear of its skin,
|
|
141
|
+
// its foot sunk 10 cm into the main block.
|
|
142
|
+
const dormer = gabled(13.7, 18.7, h.sides - 0.15, 5, h.walls - 0.1, h.eaves, h.dormer, "b");
|
|
143
|
+
// A wall's skin, one box to a chain the `keep` test accepts, held 2 cm clear of its ends
|
|
144
|
+
// or 9 cm where the outline turns in. `openings` lays a wall's columns out across it and
|
|
145
|
+
// says how much of a window each crosses: 0 none, 1 a full window's edge, 2 a pair's
|
|
146
|
+
// window, 3 a pair's window and the head, 4 the head between a pair. Floors below
|
|
147
|
+
// `lowest` stay stone.
|
|
148
|
+
const glazing = { wall: [], band: [], pair: [2, 3], full: [1, 2, 3, 4], head: [3, 4] };
|
|
149
|
+
const skin = (corners, rows, seed, keep, openings, lowest = 1) => {
|
|
150
|
+
const plan = planOf(corners), heights = [rows[0].lo, ...rows.map((row) => row.hi)];
|
|
151
|
+
chainsOf(plan).forEach((chain, index) => {
|
|
152
|
+
if (!chain.runs.every((run) => keep(plan[run])))
|
|
153
|
+
return;
|
|
154
|
+
const first = chain.runs[0], last = chain.runs.at(-1);
|
|
155
|
+
const s0 = turnsAt(plan, first)[0] > 0 ? 0.09 : 0.02, s1 = chain.length - (turnsAt(plan, last)[1] > 0 ? 0.09 : 0.02);
|
|
156
|
+
const [columns, crossing] = openings(chain.length, plan[first].at(0), plan[last].at(plan[last].length));
|
|
157
|
+
chainSkin(kit, wall, plan, chain, 0, chain.runs.length - 1, s0, s1, columns, heights, 0.02, 0.07, (bay, r) => {
|
|
158
|
+
const row = rows[r];
|
|
159
|
+
if (row.kind === "band")
|
|
160
|
+
return band;
|
|
161
|
+
return row.floor >= lowest && glazing[row.kind].includes(crossing(bay)) ? paneColor(row.floor, bay, seed * 8 + index) : stone;
|
|
162
|
+
}, stone);
|
|
163
|
+
});
|
|
164
|
+
};
|
|
165
|
+
// A position along a wall from its start, given in lot coordinates across it.
|
|
166
|
+
const along = (length, start, end, target) => {
|
|
167
|
+
const [p, q] = [inLot(start), inLot(end)], axis = Math.abs(q[0] - p[0]) > Math.abs(q[1] - p[1]) ? 0 : 1;
|
|
168
|
+
return length * (target - p[axis]) / (q[axis] - p[axis]);
|
|
169
|
+
};
|
|
170
|
+
// The street fronts' bays, 3.5 m wide and 4.55 m apart from 3.1 m along each front, as the
|
|
171
|
+
// drawing has them; each splits into a full window's edges, a pair's two windows, and the
|
|
172
|
+
// head over their mullion.
|
|
173
|
+
const bays = (length, start, end) => {
|
|
174
|
+
const centres = Array.from({ length: 12 }, (_, k) => along(length, start, end, 3.1 + 4.55 * k)).filter((c) => c - 1.75 > 0.1 && c + 1.75 < length - 0.1).sort((p, q) => p - q);
|
|
175
|
+
const steps = [-1.75, -1.5, -0.875, -0.3, 0.3, 0.875, 1.5, 1.75];
|
|
176
|
+
const columns = [0, ...centres.flatMap((c) => steps.map((s) => c + s)), length];
|
|
177
|
+
// Bays count from one: a pier, then 1, 2, 3, 4, 3, 2, 1 across each window.
|
|
178
|
+
return [columns, (bay) => (bay > 8 * centres.length || (bay - 1) % 8 === 0 ? 0 : [0, 1, 2, 3, 4, 3, 2, 1][(bay - 1) % 8])];
|
|
179
|
+
};
|
|
180
|
+
// Windows `width` wide every `pitch` metres, centred on each wall, glazed as a pair's.
|
|
181
|
+
const spaced = (width, pitch) => (length) => {
|
|
182
|
+
const count = Math.max(1, Math.round(length / pitch)), centres = Array.from({ length: count }, (_, k) => length * (k + 0.5) / count);
|
|
183
|
+
const columns = [0, ...centres.flatMap((c) => [c - width / 2, c + width / 2]), length];
|
|
184
|
+
return [columns, (bay) => (bay % 2 === 0 && bay <= 2 * count ? 2 : 0)];
|
|
185
|
+
};
|
|
186
|
+
const facing = (x, z) => (run) => run.normal(0)[0] * x + run.normal(0)[1] * z > 0.9;
|
|
187
|
+
const street = (run) => facing(1, 0)(run) || facing(0, 1)(run), party = facing(0, -1);
|
|
188
|
+
skin(lot, wallRows, 0, street, bays);
|
|
189
|
+
// The west wall's windows run from the ground; the north wall is shared with the
|
|
190
|
+
// six-storey 30 South Michigan and stays blank below its eighth floor.
|
|
191
|
+
skin(lot, wallRows, 1, (run) => !street(run) && !party(run), spaced(2.4, 4.55));
|
|
192
|
+
skin(lot, wallRows, 4, party, spaced(2.4, 4.55), 8);
|
|
193
|
+
// The upper floor's windows, 3.6 m wide about 5.5 m apart, and the small gable's one.
|
|
194
|
+
skin(upper, upperRows, 2, () => true, spaced(3.6, 5.5));
|
|
195
|
+
skin(dormer, dormerRows, 3, facing(0, 1), spaced(2.4, 5));
|
|
196
|
+
// Merlons along the street fronts' parapet, 1.2 m wide every 2.1 m, 3 cm inside the walls;
|
|
197
|
+
// pinnacles at the gable's foot, rising from 10 cm inside the main block; and a cross on
|
|
198
|
+
// each gable's peak, its foot inside the roof: the Michigan gable's facing Michigan, the
|
|
199
|
+
// small gable's facing Monroe.
|
|
200
|
+
const block = (a0, a1, b0, b1, lo, hi) => kit.prism(ornament, planOf(rect(a0, a1, b0, b1)), [lo, hi]);
|
|
201
|
+
for (let s = 1.05; s + 0.6 < depth; s += 2.1)
|
|
202
|
+
block(0.03, 0.53, s - 0.6, s + 0.6, h.walls, h.merlons);
|
|
203
|
+
for (let s = 1.05; s + 0.6 < length; s += 2.1)
|
|
204
|
+
block(Math.max(s - 0.6, 0.56), s + 0.6, 0.03, 0.53, h.walls, h.merlons);
|
|
205
|
+
for (const b of [h.sides, depth - h.sides])
|
|
206
|
+
block(h.back - 0.6, h.back + 0.6, b - 0.6, b + 0.6, h.walls - 0.1, h.pinnacles);
|
|
207
|
+
const peak = depth / 2;
|
|
208
|
+
block(h.back + 0.15, h.back + 0.45, peak - 0.15, peak + 0.15, h.ridge - 0.7, h.cross);
|
|
209
|
+
block(h.back + 0.2, h.back + 0.4, peak - 0.6, peak + 0.6, 68.9, 69.25);
|
|
210
|
+
block(16.075, 16.325, h.sides + 0.2, h.sides + 0.45, h.dormer - 0.5, h.dormerCross);
|
|
211
|
+
block(15.7, 16.7, h.sides + 0.25, h.sides + 0.4, 59.65, 60);
|
|
212
|
+
const model = kit.finish({ height: h.cross, outlines: [shell, ornament], opacity: 0.16 });
|
|
213
|
+
model.building.position.set(offset[0], 0, offset[1]);
|
|
214
|
+
model.building.userData["geography"] = record;
|
|
215
|
+
model.building.userData["geographicDetail"] = { levels: h, source: "docs/university-club-reference.md" };
|
|
216
|
+
return model;
|
|
217
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { BuildingModel, Plan } from "./building-kit.js";
|
|
2
|
+
import type { GeoBuilding } from "./skyline-geography-data.js";
|
|
3
|
+
export declare const willoughbyLevels: Readonly<{
|
|
4
|
+
setback: 80.5;
|
|
5
|
+
shoulder: 92;
|
|
6
|
+
shaft: 123.5;
|
|
7
|
+
crown: 131.7;
|
|
8
|
+
gable: 132.2;
|
|
9
|
+
sidePinnacles: 132.5;
|
|
10
|
+
top: 133.5;
|
|
11
|
+
shaftWidth: 17.4;
|
|
12
|
+
shaftDepth: 11;
|
|
13
|
+
crownEnds: 2.1;
|
|
14
|
+
crownSides: 1.2;
|
|
15
|
+
crownWindows: 10.2;
|
|
16
|
+
}>;
|
|
17
|
+
export declare function createWilloughbyTowerGeographicBuilding(record: GeoBuilding, projectPlan: (coordinates: [number, number][]) => Plan, offset: [number, number]): BuildingModel;
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import * as THREE from "../vendor/three-r186.js";
|
|
2
|
+
import { createBuilder, inside, polygonOf } from "./building-kit.js";
|
|
3
|
+
import { chainSkin, chainsOf, orient, paintedSlab, planOf, turnsAt } from "./facade-grid.js";
|
|
4
|
+
// Willoughby Tower, 8 South Michigan Avenue (Samuel N. Crowen, 1929): the geographic
|
|
5
|
+
// layout's model, on the mapped outline 124873939. The original layout has no model of it;
|
|
6
|
+
// the drawing shows it left of Six North Michigan, outside the excerpt it is fitted to.
|
|
7
|
+
//
|
|
8
|
+
// A limestone Gothic tower on an L-shaped lot at Michigan and Madison. The base fills the
|
|
9
|
+
// lot to the setback over the 23rd floor, under a parapet of pinnacles. A shaft rises at
|
|
10
|
+
// the lot's south-east corner, 17.4 m along its south wall and 11 m along Michigan, with a
|
|
11
|
+
// shoulder four floors high west of it, to a crown of arched windows under pinnacles and
|
|
12
|
+
// gables, its tallest pinnacles at the published 133.5 m. Heights are measured on the
|
|
13
|
+
// drawing down from that top; see docs/willoughby-tower-reference.md. Units are meters; +x
|
|
14
|
+
// is east, +z is south.
|
|
15
|
+
export const willoughbyLevels = Object.freeze({
|
|
16
|
+
setback: 80.5, // the base's parapet, over the 23rd floor
|
|
17
|
+
shoulder: 92, // the shoulder's roof, west of the shaft
|
|
18
|
+
shaft: 123.5, // the shaft's top, under the crown
|
|
19
|
+
crown: 131.7, // the crown's parapet
|
|
20
|
+
gable: 132.2, // the gables over the crown's long faces
|
|
21
|
+
sidePinnacles: 132.5, // the pinnacles flanking the crown's short faces
|
|
22
|
+
top: 133.5, // the crown's central pinnacles, the published architectural height
|
|
23
|
+
shaftWidth: 17.4, // along the south wall from the south-east corner
|
|
24
|
+
shaftDepth: 11, // along Michigan from the south-east corner
|
|
25
|
+
crownEnds: 2.1, // the crown's inset from the shaft's Michigan and west faces
|
|
26
|
+
crownSides: 1.2, // and from its south and north faces
|
|
27
|
+
crownWindows: 10.2, // the long faces' windows, centred this far west of Michigan
|
|
28
|
+
});
|
|
29
|
+
const h = willoughbyLevels;
|
|
30
|
+
// Each floor's level: a two-storey granite base, then 3.35 m floors to the crown's.
|
|
31
|
+
const floor = (n) => (n < 2 ? 0 : 6 + (n - 2) * 3.35);
|
|
32
|
+
const color = (hex) => new THREE.Color(hex);
|
|
33
|
+
const glassTones = [0x2a2a2a, 0x2e2e2e, 0x323232, 0x363636].map(color);
|
|
34
|
+
const litGlass = color(0x6e6e6e), dimGlass = color(0x444444);
|
|
35
|
+
const stone = color(0x585858), granite = color(0x4a4a4a), strip = color(0x2d2d2d), core = color(0x262626), roofing = color(0x3e3e3e);
|
|
36
|
+
function paneColor(row, bay, wall) {
|
|
37
|
+
const hash = (Math.imul(row + 29, 0x9e3779b1) ^ Math.imul(bay + 53, 0x85ebca77) ^ Math.imul(wall + 41, 0xc2b2ae3d)) >>> 0;
|
|
38
|
+
const value = hash % 101;
|
|
39
|
+
if (value < 4)
|
|
40
|
+
return litGlass;
|
|
41
|
+
if (value < 8)
|
|
42
|
+
return dimGlass;
|
|
43
|
+
return glassTones[value % glassTones.length];
|
|
44
|
+
}
|
|
45
|
+
const allRows = [{ lo: 0.25, hi: floor(3) - 0.3, kind: "granite", floor: 1 }];
|
|
46
|
+
for (let n = 3; n <= 36; n += 1) {
|
|
47
|
+
allRows.push({ lo: floor(n) - 0.3, hi: floor(n) + 0.9, kind: "stone", floor: n });
|
|
48
|
+
allRows.push({ lo: floor(n) + 0.9, hi: floor(n + 1) - 0.3, kind: "glass", floor: n });
|
|
49
|
+
}
|
|
50
|
+
allRows.push({ lo: floor(37) - 0.3, hi: 124.7, kind: "stone", floor: 37 });
|
|
51
|
+
allRows.push({ lo: 124.7, hi: 127.1, kind: "tall", floor: 37 });
|
|
52
|
+
allRows.push({ lo: 127.1, hi: 128.6, kind: "arch", floor: 37 });
|
|
53
|
+
allRows.push({ lo: 128.6, hi: 129.1, kind: "head", floor: 37 });
|
|
54
|
+
allRows.push({ lo: 129.1, hi: h.crown, kind: "stone", floor: 38 });
|
|
55
|
+
const rowsBetween = (lo, hi) => allRows
|
|
56
|
+
.map((row) => ({ ...row, lo: Math.max(row.lo, lo), hi: Math.min(row.hi, hi) }))
|
|
57
|
+
.filter((row) => row.hi - row.lo > 0.05);
|
|
58
|
+
export function createWilloughbyTowerGeographicBuilding(record, projectPlan, offset) {
|
|
59
|
+
const kit = createBuilder(record.name, record.id);
|
|
60
|
+
const shell = kit.batch("Willoughby Tower · shell", kit.material(0xffffff, { vertexColors: true }));
|
|
61
|
+
const wall = kit.batch("Willoughby Tower · stone and windows", kit.material(0xffffff, { vertexColors: true }));
|
|
62
|
+
const pinnacles = kit.batch("Willoughby Tower · pinnacles", kit.material(0x626262));
|
|
63
|
+
const lot = orient(polygonOf(projectPlan(record.footprint.coordinates)));
|
|
64
|
+
// The lot's south-east corner, and its south wall and Michigan front running from it.
|
|
65
|
+
const corner = lot.reduce((best, p) => (p[0] + p[1] > best[0] + best[1] ? p : best));
|
|
66
|
+
const neighbours = [lot[(lot.indexOf(corner) + 1) % lot.length], lot[(lot.indexOf(corner) + lot.length - 1) % lot.length]];
|
|
67
|
+
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]; };
|
|
68
|
+
const [west, north] = neighbours[0][0] < neighbours[1][0] ? [unit(neighbours[0]), unit(neighbours[1])] : [unit(neighbours[1]), unit(neighbours[0])];
|
|
69
|
+
const at = (a, b) => [corner[0] + west[0] * a + north[0] * b, corner[1] + west[1] * a + north[1] * b];
|
|
70
|
+
const rect = (a0, a1, b0, b1) => orient([at(a0, b0), at(a1, b0), at(a1, b1), at(a0, b1)]);
|
|
71
|
+
// The south wall's length: the farthest lot corner on its line.
|
|
72
|
+
const southWall = Math.max(...lot.filter((p) => Math.abs((p[0] - corner[0]) * west[1] - (p[1] - corner[1]) * west[0]) < 0.5).map((p) => (p[0] - corner[0]) * west[0] + (p[1] - corner[1]) * west[1]));
|
|
73
|
+
const shaft = rect(0, h.shaftWidth, 0, h.shaftDepth), shoulder = rect(h.shaftWidth, southWall, 0, h.shaftDepth), crown = rect(h.crownEnds, h.shaftWidth - h.crownEnds, h.crownSides, h.shaftDepth - h.crownSides);
|
|
74
|
+
// A solid on an outline between two heights.
|
|
75
|
+
const solid = (corners, lo, hi) => {
|
|
76
|
+
planOf(corners).forEach((run) => {
|
|
77
|
+
const a = run.at(0), b = run.at(run.length), n = run.normal(0);
|
|
78
|
+
kit.quad(shell, [[a[0], lo, a[1]], [b[0], lo, b[1]], [b[0], hi, b[1]], [a[0], hi, a[1]]], [[n[0], 0, n[1]]], core);
|
|
79
|
+
});
|
|
80
|
+
paintedSlab(kit, shell, corners, lo, false, roofing);
|
|
81
|
+
paintedSlab(kit, shell, corners, hi, true, roofing);
|
|
82
|
+
};
|
|
83
|
+
solid(lot, 0, h.setback);
|
|
84
|
+
solid(shoulder, h.setback, h.shoulder);
|
|
85
|
+
solid(shaft, h.setback, h.shaft);
|
|
86
|
+
solid(crown, h.shaft, h.crown);
|
|
87
|
+
// Whether a run lies along an outline's edges.
|
|
88
|
+
const lies = (run, on) => [0, run.length / 2, run.length].every((s) => {
|
|
89
|
+
const p = run.at(s);
|
|
90
|
+
return on.some((q, i) => {
|
|
91
|
+
const r = on[(i + 1) % on.length], [dx, dz] = [r[0] - q[0], r[1] - q[1]], l = Math.hypot(dx, dz);
|
|
92
|
+
const t = Math.max(0, Math.min(1, ((p[0] - q[0]) * dx + (p[1] - q[1]) * dz) / (l * l)));
|
|
93
|
+
return Math.hypot(q[0] + dx * t - p[0], q[1] + dz * t - p[1]) < 0.05;
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
const crownGlass = { tall: ["tallSide", "tallMiddle"], arch: ["tallSide", "tallMiddle", "side", "middle"], head: ["tallMiddle", "middle"] };
|
|
97
|
+
const skin = (corners, lo, hi, seed, keep, openings) => {
|
|
98
|
+
const plan = planOf(corners), rows = rowsBetween(lo, hi), heights = [rows[0].lo, ...rows.map((row) => row.hi)];
|
|
99
|
+
chainsOf(plan).forEach((chain, index) => {
|
|
100
|
+
if (!chain.runs.every((run) => keep(plan[run])))
|
|
101
|
+
return;
|
|
102
|
+
const first = chain.runs[0], last = chain.runs.at(-1);
|
|
103
|
+
const s0 = turnsAt(plan, first)[0] > 0 ? 0.09 : 0.02, s1 = chain.length - (turnsAt(plan, last)[1] > 0 ? 0.09 : 0.02);
|
|
104
|
+
const [columns, kind] = openings(chain.length, plan[first].at(0), plan[last].at(plan[last].length));
|
|
105
|
+
chainSkin(kit, wall, plan, chain, 0, chain.runs.length - 1, s0, s1, columns, heights, 0.02, 0.07, (bay, r) => {
|
|
106
|
+
const row = rows[r], opening = kind(bay);
|
|
107
|
+
if (row.kind === "granite")
|
|
108
|
+
return granite;
|
|
109
|
+
if (opening === "strip")
|
|
110
|
+
return row.floor >= 37 ? stone : strip;
|
|
111
|
+
if (row.kind === "tall" || row.kind === "arch" || row.kind === "head")
|
|
112
|
+
return crownGlass[row.kind].includes(opening) ? paneColor(row.floor, bay, seed * 8 + index) : stone;
|
|
113
|
+
if (row.kind === "stone" || opening === "stone")
|
|
114
|
+
return stone;
|
|
115
|
+
return paneColor(row.floor, bay, seed * 8 + index);
|
|
116
|
+
}, stone);
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
// Windows every 3 m, 1.6 m wide, between stone piers; a short wall stays stone.
|
|
120
|
+
const punched = (length) => {
|
|
121
|
+
const bays = Math.round(length / 3);
|
|
122
|
+
if (bays < 1)
|
|
123
|
+
return [[0, length], () => "stone"];
|
|
124
|
+
const columns = [0, ...Array.from({ length: bays }, (_, i) => [length * (i + 0.5) / bays - 0.8, length * (i + 0.5) / bays + 0.8]).flat(), length];
|
|
125
|
+
return [columns, (bay) => (bay % 2 === 0 && bay >= 2 && bay <= 2 * bays ? "window" : "stone")];
|
|
126
|
+
};
|
|
127
|
+
// The shaft's Michigan face: three dark strips in the middle, a window near each edge,
|
|
128
|
+
// symmetrical about its middle.
|
|
129
|
+
const stripped = (length) => {
|
|
130
|
+
const at = (f) => length * f;
|
|
131
|
+
const columns = [0, at(0.15), at(0.25), at(0.315), at(0.385), at(0.465), at(0.535), at(0.615), at(0.685), at(0.75), at(0.85), length];
|
|
132
|
+
return [columns, (bay) => (bay === 2 || bay === 10 ? "window" : bay === 4 || bay === 6 || bay === 8 ? "strip" : "stone")];
|
|
133
|
+
};
|
|
134
|
+
// The inverse of `at`: a point's distances along the south wall and the Michigan front,
|
|
135
|
+
// which meet a degree off square.
|
|
136
|
+
const skew = west[0] * north[1] - west[1] * north[0];
|
|
137
|
+
const inLot = (p) => {
|
|
138
|
+
const [x, z] = [p[0] - corner[0], p[1] - corner[1]];
|
|
139
|
+
return [(x * north[1] - z * north[0]) / skew, (west[0] * z - west[1] * x) / skew];
|
|
140
|
+
};
|
|
141
|
+
// The crown: three arched windows to a face, 0.9 m wide and 1.7 m apart, their heads
|
|
142
|
+
// narrowing to the middle 0.45 m. A short face's are tall and centred on it; a long
|
|
143
|
+
// face's are short, centred crownWindows west of Michigan, as the drawing has them.
|
|
144
|
+
const arched = (length, start, end) => {
|
|
145
|
+
const [a0, b0] = inLot(start), [a1, b1] = inLot(end), long = Math.abs(a1 - a0) > Math.abs(b1 - b0);
|
|
146
|
+
const middle = long ? length * (h.crownWindows - a0) / (a1 - a0) : length * (h.shaftDepth / 2 - b0) / (b1 - b0);
|
|
147
|
+
const columns = [0, ...[-1.7, 0, 1.7].flatMap((d) => [middle + d - 0.45, middle + d - 0.225, middle + d + 0.225, middle + d + 0.45]), length];
|
|
148
|
+
// Bays count from one: a pier, then side, middle, side for each window, then the last pier.
|
|
149
|
+
return [columns, (bay) => { const k = (bay - 1) % 4; return bay > 12 || k === 0 ? "stone" : k === 2 ? (long ? "middle" : "tallMiddle") : long ? "side" : "tallSide"; }];
|
|
150
|
+
};
|
|
151
|
+
const michigan = (run) => run.normal(0)[0] > 0.9;
|
|
152
|
+
skin(lot, 0.25, h.setback, 0, () => true, punched);
|
|
153
|
+
skin(shoulder, h.setback, h.shoulder, 1, (run) => !lies(run, shaft), punched);
|
|
154
|
+
skin(shaft, h.setback, h.shaft, 2, (run) => michigan(run), stripped);
|
|
155
|
+
skin(shaft, h.setback, h.shaft, 3, (run) => !michigan(run) && !lies(run, shoulder), punched);
|
|
156
|
+
skin(shaft, h.shoulder, h.shaft, 4, (run) => lies(run, shoulder), punched);
|
|
157
|
+
skin(crown, h.shaft, h.crown, 5, () => true, arched);
|
|
158
|
+
// Pinnacles on the base's parapet along the two street fronts, clear of the shaft. On the
|
|
159
|
+
// crown, a central pinnacle reaching the published top stands over each face's windows,
|
|
160
|
+
// flanked on the short faces by lower pinnacles and on the long faces by a gable.
|
|
161
|
+
const pinnacle = (p, lo, hi, size) => {
|
|
162
|
+
const c = size / 2;
|
|
163
|
+
kit.prism(pinnacles, planOf(orient([[p[0] - c, p[1] - c], [p[0] + c, p[1] - c], [p[0] + c, p[1] + c], [p[0] - c, p[1] + c]])), [lo, hi]);
|
|
164
|
+
};
|
|
165
|
+
const stations = [];
|
|
166
|
+
planOf(lot).forEach((run) => {
|
|
167
|
+
const n = run.normal(0);
|
|
168
|
+
if (!(n[0] > 0.9 || n[1] < -0.9))
|
|
169
|
+
return;
|
|
170
|
+
const count = Math.max(1, Math.round(run.length / 6));
|
|
171
|
+
for (let i = 0; i <= count; i += 1) {
|
|
172
|
+
const p = run.at(run.length * i / count);
|
|
173
|
+
if (inside(rect(-1.5, h.shaftWidth + 1.5, -1.5, h.shaftDepth + 1.5), p) || stations.some((q) => Math.hypot(p[0] - q[0], p[1] - q[1]) < 2))
|
|
174
|
+
continue;
|
|
175
|
+
stations.push(p);
|
|
176
|
+
pinnacle(p, h.setback, h.setback + 3, 0.9);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
for (const a of [h.crownEnds, h.shaftWidth - h.crownEnds]) {
|
|
180
|
+
pinnacle(at(a, h.shaftDepth / 2), h.crown, h.top, 1.6);
|
|
181
|
+
for (const d of [-2.9, 2.9])
|
|
182
|
+
pinnacle(at(a, h.shaftDepth / 2 + d), h.crown, h.sidePinnacles, 1.1);
|
|
183
|
+
}
|
|
184
|
+
for (const [b, inward] of [[h.crownSides, 1], [h.shaftDepth - h.crownSides, -1]]) {
|
|
185
|
+
pinnacle(at(h.crownWindows, b), h.crown, h.top, 1.6);
|
|
186
|
+
// The gable stands 10 cm inside the wall, its foot sunk into the crown.
|
|
187
|
+
const gable = rect(h.crownWindows - 3.45, h.crownWindows + 3.45, b + inward * 0.1, b + inward * 0.6);
|
|
188
|
+
kit.prism(pinnacles, planOf(gable), [h.crown - 0.1, h.gable]);
|
|
189
|
+
}
|
|
190
|
+
const model = kit.finish({ height: h.top, outlines: [shell, pinnacles], opacity: 0.16 });
|
|
191
|
+
model.building.position.set(offset[0], 0, offset[1]);
|
|
192
|
+
model.building.userData["geography"] = record;
|
|
193
|
+
model.building.userData["geographicDetail"] = { levels: h, source: "docs/willoughby-tower-reference.md" };
|
|
194
|
+
return model;
|
|
195
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as THREE from "../vendor/three-r186.js";
|
|
2
|
+
import type { Celebration, CelebrationId } from "./celebrations.js";
|
|
3
|
+
export interface WindowCell {
|
|
4
|
+
vertex: number;
|
|
5
|
+
column: number;
|
|
6
|
+
floor: number;
|
|
7
|
+
}
|
|
8
|
+
export interface WindowIllumination {
|
|
9
|
+
readonly presets: readonly Celebration[];
|
|
10
|
+
readonly active: CelebrationId | null;
|
|
11
|
+
readonly litWindows: number;
|
|
12
|
+
set(active: CelebrationId | null): void;
|
|
13
|
+
}
|
|
14
|
+
export declare function createWindowIllumination(geometry: THREE.BufferGeometry, material: THREE.MeshToonMaterial, cells: readonly WindowCell[], columns: number, lineTopFloors: readonly number[]): WindowIllumination;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import * as THREE from "../vendor/three-r186.js";
|
|
2
|
+
import { celebrations, windowWord } from "./celebrations.js";
|
|
3
|
+
// Reuse the actual glass triangles. The emission mask changes neither the
|
|
4
|
+
// silhouette nor depth/hover ownership, and needs no overlay or extra draw call.
|
|
5
|
+
export function createWindowIllumination(geometry, material, cells, columns, lineTopFloors) {
|
|
6
|
+
const colors = geometry.getAttribute("color");
|
|
7
|
+
const original = new Float32Array(colors.array);
|
|
8
|
+
const emission = new THREE.Float32BufferAttribute(new Float32Array(colors.count), 1);
|
|
9
|
+
geometry.setAttribute("windowLight", emission);
|
|
10
|
+
material.onBeforeCompile = (shader) => {
|
|
11
|
+
shader.vertexShader = `attribute float windowLight;\nvarying float vWindowLight;\n${shader.vertexShader}`
|
|
12
|
+
.replace("#include <begin_vertex>", "#include <begin_vertex>\nvWindowLight = windowLight;");
|
|
13
|
+
shader.fragmentShader = `varying float vWindowLight;\n${shader.fragmentShader}`
|
|
14
|
+
.replace("#include <emissivemap_fragment>", "#include <emissivemap_fragment>\ntotalEmissiveRadiance += vec3(vWindowLight);");
|
|
15
|
+
};
|
|
16
|
+
material.customProgramCacheKey = () => "skyline-window-illumination-v1";
|
|
17
|
+
let active = null, litWindows = 0;
|
|
18
|
+
const available = new Set(cells.map((cell) => `${cell.column}:${cell.floor}`));
|
|
19
|
+
const masks = new Map(celebrations.map((preset) => {
|
|
20
|
+
const lit = new Set();
|
|
21
|
+
preset.lines.forEach((word, line) => {
|
|
22
|
+
const rows = windowWord(word), width = rows[0].length;
|
|
23
|
+
if (width > columns - 2)
|
|
24
|
+
throw new Error(`${word} does not fit the facade`);
|
|
25
|
+
const left = Math.floor((columns - width) / 2);
|
|
26
|
+
const top = lineTopFloors[line];
|
|
27
|
+
if (top === undefined)
|
|
28
|
+
throw new Error(`No facade zone for message line ${line + 1}`);
|
|
29
|
+
rows.forEach((row, down) => [...row].forEach((pixel, across) => {
|
|
30
|
+
if (pixel === "1")
|
|
31
|
+
lit.add(`${left + across}:${top - down}`);
|
|
32
|
+
}));
|
|
33
|
+
});
|
|
34
|
+
// Refuse a layout which silently drops letters into missing window cells.
|
|
35
|
+
for (const key of lit)
|
|
36
|
+
if (!available.has(key))
|
|
37
|
+
throw new Error(`Missing celebration window: ${key}`);
|
|
38
|
+
return [preset.id, lit];
|
|
39
|
+
}));
|
|
40
|
+
return {
|
|
41
|
+
presets: celebrations,
|
|
42
|
+
get active() { return active; },
|
|
43
|
+
get litWindows() { return litWindows; },
|
|
44
|
+
set(next) {
|
|
45
|
+
const lit = next === null ? undefined : masks.get(next);
|
|
46
|
+
if (next !== null && !lit)
|
|
47
|
+
throw new Error(`Unknown celebration: ${next}`);
|
|
48
|
+
active = next;
|
|
49
|
+
litWindows = lit?.size ?? 0;
|
|
50
|
+
colors.array.set(original);
|
|
51
|
+
emission.array.fill(0);
|
|
52
|
+
if (lit)
|
|
53
|
+
for (const cell of cells) {
|
|
54
|
+
const on = lit.has(`${cell.column}:${cell.floor}`);
|
|
55
|
+
for (let vertex = cell.vertex; vertex < cell.vertex + 6; vertex += 1) {
|
|
56
|
+
// Closed shades darken only the message face's office glass.
|
|
57
|
+
colors.setXYZ(vertex, 0.018, 0.018, 0.018);
|
|
58
|
+
emission.setX(vertex, on ? 1 : 0);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
colors.needsUpdate = emission.needsUpdate = true;
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { cp, mkdir } from "node:fs/promises";
|
|
2
|
+
/**
|
|
3
|
+
* Copies the viewer's complete asset directory into a host application's static
|
|
4
|
+
* directory. This module is for Node/Bun build scripts, never browser entrypoints.
|
|
5
|
+
*/
|
|
6
|
+
export async function copySkylineAssets(destination) {
|
|
7
|
+
await mkdir(destination, { recursive: true });
|
|
8
|
+
await cp(new URL("../site/", import.meta.url), destination, { recursive: true });
|
|
9
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type { BuildingModel, Vec2, Vec3 } from "./models/building-kit.js";
|
|
2
|
+
export type { FitBox, PlatformOptions, ShadowCameraOptions, StudyView, StudyLayout, StudyLabel } from "./study-types.js";
|
|
3
|
+
export type { GeographicSkyline } from "./skyline-scene.js";
|
|
4
|
+
export interface SkylineOptions {
|
|
5
|
+
/** Directory serving the package's viewer assets, e.g. "/static/skyline/". */
|
|
6
|
+
assetsUrl: string | URL;
|
|
7
|
+
/** Accessible title for the embedded viewer. */
|
|
8
|
+
title?: string;
|
|
9
|
+
/** Show the standalone toolbar and study links. Hidden by default when embedded. */
|
|
10
|
+
navigation?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface SkylineInstance {
|
|
13
|
+
/** The viewer's frame; its styles are isolated from the host application. */
|
|
14
|
+
readonly element: HTMLIFrameElement;
|
|
15
|
+
/** Removes the viewer, releasing its document, event handlers and WebGL context. */
|
|
16
|
+
destroy(): void;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Mounts the complete interactive viewer inside a host-owned container. Importing
|
|
20
|
+
* this module never touches the DOM; call it after mounting a browser component.
|
|
21
|
+
* The container controls placement and size. Destroy it when that component leaves.
|
|
22
|
+
*/
|
|
23
|
+
export declare function mountSkyline(container: HTMLElement, { assetsUrl, title, navigation }: SkylineOptions): SkylineInstance;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mounts the complete interactive viewer inside a host-owned container. Importing
|
|
3
|
+
* this module never touches the DOM; call it after mounting a browser component.
|
|
4
|
+
* The container controls placement and size. Destroy it when that component leaves.
|
|
5
|
+
*/
|
|
6
|
+
export function mountSkyline(container, { assetsUrl, title = "Interactive Chicago skyline", navigation = false }) {
|
|
7
|
+
const document = container.ownerDocument;
|
|
8
|
+
const base = new URL(assetsUrl, document.baseURI);
|
|
9
|
+
if (!/^(https?:)$/.test(base.protocol))
|
|
10
|
+
throw new TypeError("Skyline assets must be served over HTTP or HTTPS.");
|
|
11
|
+
if (base.search || base.hash)
|
|
12
|
+
throw new TypeError("Skyline assetsUrl must be a directory URL without a query or fragment.");
|
|
13
|
+
if (!base.pathname.endsWith("/"))
|
|
14
|
+
base.pathname += "/";
|
|
15
|
+
const element = document.createElement("iframe");
|
|
16
|
+
element.title = title;
|
|
17
|
+
const viewerUrl = new URL("index.html", base);
|
|
18
|
+
if (!navigation)
|
|
19
|
+
viewerUrl.searchParams.set("embed", "1");
|
|
20
|
+
element.src = viewerUrl.href;
|
|
21
|
+
element.allowFullscreen = true;
|
|
22
|
+
element.style.cssText = "display:block;width:100%;height:100%;border:0";
|
|
23
|
+
container.append(element);
|
|
24
|
+
return { element, destroy() { element.remove(); } };
|
|
25
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createGeographicGround } from "./models/skyline-geography.js";
|
|
2
|
+
import type { BuildingModel, Vec3 } from "./models/building-kit.js";
|
|
3
|
+
import type { FitBox, PlatformOptions, ShadowCameraOptions, StudyLabel, StudyLayout, StudyView } from "./study-types.js";
|
|
4
|
+
export interface GeographicSkyline {
|
|
5
|
+
models: BuildingModel[];
|
|
6
|
+
ground: ReturnType<typeof createGeographicGround>;
|
|
7
|
+
center: Vec3;
|
|
8
|
+
settings: {
|
|
9
|
+
target: Vec3;
|
|
10
|
+
fit: FitBox;
|
|
11
|
+
platform: PlatformOptions & {
|
|
12
|
+
x: number;
|
|
13
|
+
z: number;
|
|
14
|
+
};
|
|
15
|
+
clippingMargin: number;
|
|
16
|
+
lightPosition: Vec3;
|
|
17
|
+
shadowCamera: ShadowCameraOptions;
|
|
18
|
+
};
|
|
19
|
+
layout: StudyLayout;
|
|
20
|
+
drawingView: StudyView;
|
|
21
|
+
comparisonViews: Record<string, StudyView>;
|
|
22
|
+
labels: StudyLabel[];
|
|
23
|
+
}
|
|
24
|
+
export declare const skylineAzimuth: number;
|
|
25
|
+
export declare function createGeographicSkyline(anchor?: Vec3): GeographicSkyline;
|