@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,53 @@
|
|
|
1
|
+
import type { BuildingModel, Plan, Vec2 } from "./building-kit.js";
|
|
2
|
+
export declare const twoPrudentialLevels: Readonly<{
|
|
3
|
+
pitch: 3.96;
|
|
4
|
+
lobbyTop: 11.52;
|
|
5
|
+
sill: 0.9;
|
|
6
|
+
head: 2.9;
|
|
7
|
+
lowerShoulder: 162;
|
|
8
|
+
lowerPeak: number;
|
|
9
|
+
middleShoulder: 193.68;
|
|
10
|
+
middlePeak: number;
|
|
11
|
+
eave: 229.32;
|
|
12
|
+
peak: 256;
|
|
13
|
+
apex: 280.2;
|
|
14
|
+
shaftTop: 295.5;
|
|
15
|
+
tip: 303.3;
|
|
16
|
+
floors: 64;
|
|
17
|
+
}>;
|
|
18
|
+
export declare const floorLine: (floor: number) => number;
|
|
19
|
+
export interface Gable {
|
|
20
|
+
strip: number;
|
|
21
|
+
bays: number;
|
|
22
|
+
bay: number;
|
|
23
|
+
half: number;
|
|
24
|
+
shoulder: number;
|
|
25
|
+
peak: number;
|
|
26
|
+
}
|
|
27
|
+
export declare const twoPrudentialGables: Readonly<{
|
|
28
|
+
front: Gable;
|
|
29
|
+
side: Gable;
|
|
30
|
+
middle: Gable;
|
|
31
|
+
lower: Gable;
|
|
32
|
+
}>;
|
|
33
|
+
export declare const twoPrudentialCore: Vec2;
|
|
34
|
+
export declare const stripShoulder: (gable: Gable) => number;
|
|
35
|
+
export declare function wallTop(gable: Gable, x: number): number;
|
|
36
|
+
export declare function wallExtent(gable: Gable, y: number): number;
|
|
37
|
+
export declare const pierStations: (gable: Gable) => number[];
|
|
38
|
+
export declare function crownSection(y: number): Vec2[];
|
|
39
|
+
export declare const crownSteps: number;
|
|
40
|
+
export interface TwoPrudentialForm {
|
|
41
|
+
name: string;
|
|
42
|
+
id: string;
|
|
43
|
+
centre: Vec2;
|
|
44
|
+
along: Vec2;
|
|
45
|
+
fronts: [number, number];
|
|
46
|
+
base: number;
|
|
47
|
+
lobby?: Plan;
|
|
48
|
+
tierPeaks?: {
|
|
49
|
+
lower: number;
|
|
50
|
+
middle: number;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export declare function buildTwoPrudentialTower(form: TwoPrudentialForm): BuildingModel;
|
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import * as THREE from "../vendor/three-r186.js";
|
|
2
|
+
import { createBuilder, line } from "./building-kit.js";
|
|
3
|
+
import { mitredBox } from "./facade-grid.js";
|
|
4
|
+
// Two Prudential Plaza as built, for both of the skyline study's layouts: the geographic
|
|
5
|
+
// model stands it on the mapped outline, and the original layout's copy on a clean plan cut
|
|
6
|
+
// at the drawing's datum. A 40.8 x 37.5 m limestone core rises to 229.3 m at its corners.
|
|
7
|
+
// Each face ends in a gable that steps up floor by floor to a pointed glass strip at its
|
|
8
|
+
// middle, 256 m, and a stepped pyramid turned 45° to the plan rises from those four points
|
|
9
|
+
// to 280.2 m, where the spire takes over to the published 303.3 m. A broad rib runs up each
|
|
10
|
+
// ridge from its strip's head to the apex, and the pyramid's steps end on the ribs. On the
|
|
11
|
+
// north and south faces two gabled tiers stand forward of the core, the lower in front of
|
|
12
|
+
// the middle one, and fill the rest of the mapped depth. Heights are measured on the
|
|
13
|
+
// photographs, down from the published tip; the plan on the photograph against the mapped
|
|
14
|
+
// outline. See docs/two-prudential-reference.md. Units are meters; +x is east and +z is south.
|
|
15
|
+
const pitch = 3.96;
|
|
16
|
+
// A tier's point stands on the line through its steps' corners: its glass head rises a floor
|
|
17
|
+
// for every 3.16 m bay, as the steps beside it do, from its 4.6 m half-strip's shoulder.
|
|
18
|
+
const tierPeak = (shoulder, bays) => shoulder + (bays + 4.6 / 3.16) * pitch;
|
|
19
|
+
export const twoPrudentialLevels = Object.freeze({
|
|
20
|
+
pitch, // the photograph's window rows
|
|
21
|
+
lobbyTop: 11.52, // the first office floor, a whole number of floors below the tiers
|
|
22
|
+
sill: 0.9,
|
|
23
|
+
head: 2.9, // a window's sill and head above its floor
|
|
24
|
+
lowerShoulder: 162,
|
|
25
|
+
lowerPeak: tierPeak(162, 3), // 179.64
|
|
26
|
+
middleShoulder: 193.68,
|
|
27
|
+
middlePeak: tierPeak(193.68, 4), // 215.28
|
|
28
|
+
eave: 229.32, // the core's corners
|
|
29
|
+
peak: 256, // each face's gable
|
|
30
|
+
apex: 280.2, // the pyramid
|
|
31
|
+
shaftTop: 295.5, // the spire's shaft, under its needle
|
|
32
|
+
tip: 303.3, // published architectural height
|
|
33
|
+
floors: 64,
|
|
34
|
+
});
|
|
35
|
+
const h = twoPrudentialLevels;
|
|
36
|
+
export const floorLine = (floor) => h.lobbyTop + floor * h.pitch;
|
|
37
|
+
const gableOf = (strip, bays, bay, shoulder, peak) => ({ strip, bays, bay, half: strip + bays * bay, shoulder, peak });
|
|
38
|
+
export const twoPrudentialGables = Object.freeze({
|
|
39
|
+
// The core's north and south faces, five bays either side of the strip.
|
|
40
|
+
front: gableOf(4.6, 5, 3.16, h.eave, h.peak),
|
|
41
|
+
// The east and west faces, four wider bays either side of a wider strip.
|
|
42
|
+
side: gableOf(5.7, 4, 3.2625, h.eave, h.peak),
|
|
43
|
+
// The tiers keep the front's bays, four and three either side of its strip.
|
|
44
|
+
middle: gableOf(4.6, 4, 3.16, h.middleShoulder, h.middlePeak),
|
|
45
|
+
lower: gableOf(4.6, 3, 3.16, h.lowerShoulder, h.lowerPeak),
|
|
46
|
+
});
|
|
47
|
+
const g = twoPrudentialGables;
|
|
48
|
+
// The core's half-widths, east-west and north-south.
|
|
49
|
+
export const twoPrudentialCore = [g.front.half, g.side.half];
|
|
50
|
+
export const stripShoulder = (gable) => gable.shoulder + gable.bays * h.pitch;
|
|
51
|
+
// A wall's top at x from its middle.
|
|
52
|
+
export function wallTop(gable, x) {
|
|
53
|
+
const r = Math.abs(x);
|
|
54
|
+
if (r >= gable.strip)
|
|
55
|
+
return gable.shoulder + Math.min(gable.bays - 1, Math.floor((gable.half - r) / gable.bay + 1e-9)) * h.pitch;
|
|
56
|
+
return stripShoulder(gable) + (gable.peak - stripShoulder(gable)) * (1 - r / gable.strip);
|
|
57
|
+
}
|
|
58
|
+
// The half-width of the part of a wall that reaches a height.
|
|
59
|
+
export function wallExtent(gable, y) {
|
|
60
|
+
if (y <= gable.shoulder + 1e-9)
|
|
61
|
+
return gable.half;
|
|
62
|
+
const steps = Math.ceil((y - gable.shoulder) / h.pitch - 1e-9);
|
|
63
|
+
if (steps < gable.bays)
|
|
64
|
+
return gable.half - steps * gable.bay;
|
|
65
|
+
if (y <= stripShoulder(gable) + 1e-9)
|
|
66
|
+
return gable.strip;
|
|
67
|
+
return Math.max(0, gable.strip * (gable.peak - y) / (gable.peak - stripShoulder(gable)));
|
|
68
|
+
}
|
|
69
|
+
// The piers stand on every bay's edges: its corner pier's centre, then the joints inward to
|
|
70
|
+
// the strip's edge. Windows are centred in their bays.
|
|
71
|
+
const pierWidth = 0.9, pierDepth = 0.35, windowHalf = 0.8;
|
|
72
|
+
export const pierStations = (gable) => Array.from({ length: gable.bays + 1 }, (_, i) => (i ? gable.half - i * gable.bay : gable.half - pierWidth / 2));
|
|
73
|
+
// Where a face's part of the pyramid ends at a height, as its half-width along the face and
|
|
74
|
+
// its reach from the core's centre: the face's wall to where it reaches, up to its strip's
|
|
75
|
+
// shoulder, and above that the inner edge of its ridge's rib, the straight line from the
|
|
76
|
+
// shoulder to the apex, so each floor's step ends a fixed way further up that line.
|
|
77
|
+
function crownEnd(gable, reach, y) {
|
|
78
|
+
const shoulder = stripShoulder(gable);
|
|
79
|
+
if (y <= shoulder + 1e-9)
|
|
80
|
+
return [wallExtent(gable, y), reach];
|
|
81
|
+
const s = (h.apex - y) / (h.apex - shoulder);
|
|
82
|
+
return [gable.strip * s, reach * s];
|
|
83
|
+
}
|
|
84
|
+
// The pyramid's horizontal section at a height above the eave, in the core's frame: each
|
|
85
|
+
// face's end, joined across each corner by the step's riser.
|
|
86
|
+
export function crownSection(y) {
|
|
87
|
+
const [a, b] = twoPrudentialCore, [front, south] = crownEnd(g.front, b, y), [side, east] = crownEnd(g.side, a, y);
|
|
88
|
+
return [[-front, south], [front, south], [east, side], [east, -side], [front, -south], [-front, -south], [-east, -side], [-east, side]];
|
|
89
|
+
}
|
|
90
|
+
// The crown's steps stop a floor under the apex, where a pointed cap and the spire take over.
|
|
91
|
+
export const crownSteps = Math.floor((h.apex - h.eave) / h.pitch - 0.25);
|
|
92
|
+
const color = (hex) => new THREE.Color(hex);
|
|
93
|
+
const stone = color(0x8a8a8a), stripSpandrel = color(0x4a4a4a), riserGlass = color(0x565656), band = color(0xcdcdcd);
|
|
94
|
+
const tread = color(0x7a7a7a), soffit = color(0x6a6a6a), roofGlass = color(0x3f3f3f);
|
|
95
|
+
const glassTones = [0x3a3a3a, 0x404040, 0x353535, 0x454545].map(color);
|
|
96
|
+
const litGlass = color(0x8e8e8e), dimGlass = color(0x5e5e5e);
|
|
97
|
+
function paneColor(floor, bay, wall) {
|
|
98
|
+
const hash = (Math.imul(floor + 7, 0x9e3779b1) ^ Math.imul(bay + 43, 0x85ebca77) ^ Math.imul(wall + 2, 0xc2b2ae3d)) >>> 0;
|
|
99
|
+
const value = hash % 97;
|
|
100
|
+
if (value < 3)
|
|
101
|
+
return litGlass;
|
|
102
|
+
if (value < 6)
|
|
103
|
+
return dimGlass;
|
|
104
|
+
return glassTones[value % glassTones.length];
|
|
105
|
+
}
|
|
106
|
+
export function buildTwoPrudentialTower(form) {
|
|
107
|
+
const kit = createBuilder(form.name, form.id);
|
|
108
|
+
const body = kit.batch("Two Prudential · limestone, glass and crown", kit.material(0xffffff, { vertexColors: true }));
|
|
109
|
+
const piers = kit.batch("Two Prudential · piers", kit.material(0x9c9c9c));
|
|
110
|
+
const copings = kit.batch("Two Prudential · copings", kit.material(0xc6c6c6));
|
|
111
|
+
const mullions = kit.batch("Two Prudential · strip mullions", kit.material(0x707070));
|
|
112
|
+
const ribs = kit.batch("Two Prudential · crown ribs", kit.material(0xd8d8d8));
|
|
113
|
+
const spire = kit.batch("Two Prudential · spire", kit.material(0xe9e9e9));
|
|
114
|
+
const spirePanels = kit.batch("Two Prudential · spire inset panels", kit.material(0x575757));
|
|
115
|
+
const y = (real) => real - form.base;
|
|
116
|
+
const [coreA, coreB] = twoPrudentialCore;
|
|
117
|
+
const middle = form.tierPeaks ? { ...g.middle, peak: form.tierPeaks.middle } : g.middle;
|
|
118
|
+
const lower = form.tierPeaks ? { ...g.lower, peak: form.tierPeaks.lower } : g.lower;
|
|
119
|
+
// The core's frame, and the same turned half a turn: the north and west halves are the
|
|
120
|
+
// south and east ones seen from the other side, so one description builds both.
|
|
121
|
+
// Frames 2 and 3 face east and west: their south axis is the side face's outward normal.
|
|
122
|
+
const frames = [form.along, [-form.along[0], -form.along[1]]].map((a) => ({ a, s: [-a[1], a[0]] }));
|
|
123
|
+
frames.push(...frames.map(({ a, s }) => ({ a: [-s[0], -s[1]], s: a })));
|
|
124
|
+
const at = (frame, u, v) => {
|
|
125
|
+
const { a, s } = frames[frame];
|
|
126
|
+
return [form.centre[0] + a[0] * u + s[0] * v, form.centre[1] + a[1] * u + s[1] * v];
|
|
127
|
+
};
|
|
128
|
+
const lift = ([x, z], real) => [x, y(real), z];
|
|
129
|
+
// Heights a solid spans, cut at the datum: none when it lies wholly below.
|
|
130
|
+
const above = (rows) => {
|
|
131
|
+
if (rows.at(-1) <= form.base + 1e-6)
|
|
132
|
+
return null;
|
|
133
|
+
return rows[0] >= form.base ? rows : [form.base, ...rows.filter((r) => r > form.base + 1e-6)];
|
|
134
|
+
};
|
|
135
|
+
// Sills and heads strictly between two heights, plus any extra offsets within a floor:
|
|
136
|
+
// the floor lines themselves divide stone from stone, so they need no cut.
|
|
137
|
+
const rowsBetween = (lo, hi, extra = []) => {
|
|
138
|
+
const cuts = new Set([lo, hi]);
|
|
139
|
+
for (let floor = Math.floor((lo - h.lobbyTop) / h.pitch) - 1; floorLine(floor) < hi; floor += 1) {
|
|
140
|
+
for (const offset of [h.sill, h.head, ...extra]) {
|
|
141
|
+
const r = Math.round((floorLine(floor) + offset) * 1e6) / 1e6;
|
|
142
|
+
if (r > lo + 1e-6 && r < hi - 1e-6)
|
|
143
|
+
cuts.add(r);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return [...cuts].sort((p, q) => p - q);
|
|
147
|
+
};
|
|
148
|
+
const floorOf = (real) => Math.floor((real - h.lobbyTop) / h.pitch + 1e-9);
|
|
149
|
+
const vision = (real) => { const o = real - floorLine(floorOf(real)); return o > h.sill && o < h.head; };
|
|
150
|
+
// A gabled wall's cuts: every window's edges and the strip's. A bay's edge divides stone
|
|
151
|
+
// from stone, under a pier.
|
|
152
|
+
const wallCuts = (gable) => {
|
|
153
|
+
const radii = [gable.strip];
|
|
154
|
+
for (let i = 0; i < gable.bays; i += 1) {
|
|
155
|
+
const centre = gable.half - (i + 0.5) * gable.bay;
|
|
156
|
+
radii.push(centre - windowHalf, centre + windowHalf);
|
|
157
|
+
}
|
|
158
|
+
return [...new Set(radii.flatMap((r) => [-r, r]))].sort((p, q) => p - q);
|
|
159
|
+
};
|
|
160
|
+
const paintWall = (wall, x, real) => {
|
|
161
|
+
const r = Math.abs(x), gable = wall.gable;
|
|
162
|
+
if (r < gable.strip)
|
|
163
|
+
return vision(real) ? paneColor(floorOf(real), Math.floor((x + gable.strip) / 1.8), wall.seed + 50) : stripSpandrel;
|
|
164
|
+
const bay = Math.min(gable.bays - 1, Math.floor((gable.half - r) / gable.bay));
|
|
165
|
+
const centre = gable.half - (bay + 0.5) * gable.bay;
|
|
166
|
+
return vision(real) && Math.abs(r - centre) < windowHalf ? paneColor(floorOf(real), Math.sign(x) * (bay + 1), wall.seed) : stone;
|
|
167
|
+
};
|
|
168
|
+
// A closed prism on a convex ring, counterclockwise from above, whose walls are cut into
|
|
169
|
+
// painted cells along each edge and at every row. Its top and bottom fan out from its
|
|
170
|
+
// centre through every cut, so each cell's edge meets its partner.
|
|
171
|
+
const prism = (ring, rowsIn, edges, top = tread, bottom = soffit) => {
|
|
172
|
+
const rows = above(rowsIn);
|
|
173
|
+
if (!rows)
|
|
174
|
+
return;
|
|
175
|
+
const loop = [];
|
|
176
|
+
ring.forEach((a, i) => {
|
|
177
|
+
const b = ring[(i + 1) % ring.length], length = Math.hypot(b[0] - a[0], b[1] - a[1]);
|
|
178
|
+
const t = [(b[0] - a[0]) / length, (b[1] - a[1]) / length], out = [-t[1], 0, t[0]], edge = edges[i];
|
|
179
|
+
let cuts = [];
|
|
180
|
+
if ("wall" in edge)
|
|
181
|
+
cuts = wallCuts(edge.wall.gable).map((x) => x - edge.from);
|
|
182
|
+
else if ("ribbon" in edge)
|
|
183
|
+
cuts = [pierWidth + 0.05, length - 0.35];
|
|
184
|
+
const stations = [0, ...cuts.filter((s) => s > 1e-3 && s < length - 1e-3), length];
|
|
185
|
+
const points = stations.map((s, k) => (k === 0 ? a : k === stations.length - 1 ? b : [a[0] + t[0] * s, a[1] + t[1] * s]));
|
|
186
|
+
loop.push(...points.slice(0, -1));
|
|
187
|
+
for (let k = 0; k + 1 < points.length; k += 1) {
|
|
188
|
+
const middle = (stations[k] + stations[k + 1]) / 2;
|
|
189
|
+
for (let r = 0; r + 1 < rows.length; r += 1) {
|
|
190
|
+
const [lo, hi] = [rows[r], rows[r + 1]], real = (lo + hi) / 2;
|
|
191
|
+
let paint = stone;
|
|
192
|
+
if ("wall" in edge)
|
|
193
|
+
paint = paintWall(edge.wall, edge.from + middle, real);
|
|
194
|
+
else if ("ribbon" in edge)
|
|
195
|
+
paint = k === 1 && vision(real) ? paneColor(floorOf(real), 90 + i, 7) : stone;
|
|
196
|
+
else if ("riser" in edge)
|
|
197
|
+
paint = real - floorLine(floorOf(real)) < 2.5 ? riserGlass : band;
|
|
198
|
+
else
|
|
199
|
+
paint = edge.plain;
|
|
200
|
+
const [p, q] = [points[k], points[k + 1]];
|
|
201
|
+
kit.quad(body, [lift(p, lo), lift(q, lo), lift(q, hi), lift(p, hi)], [out], paint);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
const centre = [ring.reduce((sum, p) => sum + p[0], 0) / ring.length, ring.reduce((sum, p) => sum + p[1], 0) / ring.length];
|
|
206
|
+
const [lo, hi] = [rows[0], rows.at(-1)];
|
|
207
|
+
loop.forEach((p, i) => {
|
|
208
|
+
const q = loop[(i + 1) % loop.length];
|
|
209
|
+
kit.triangle(body, [lift(centre, hi), lift(p, hi), lift(q, hi)], [[0, 1, 0], [0, 1, 0], [0, 1, 0]], top);
|
|
210
|
+
kit.triangle(body, [lift(centre, lo), lift(p, lo), lift(q, lo)], [[0, -1, 0], [0, -1, 0], [0, -1, 0]], bottom);
|
|
211
|
+
});
|
|
212
|
+
};
|
|
213
|
+
const ringOf = (frame, corners) => corners.map(([u, v]) => at(frame, u, v));
|
|
214
|
+
// A closed solid extruded along a frame's south axis from a convex profile in (u, height):
|
|
215
|
+
// a strip's pointed head, or a tier's strip with its gabled roof.
|
|
216
|
+
const extrude = (target, frame, profile, v0, v1, front, sides) => {
|
|
217
|
+
const rows = profile.map(([, real]) => real);
|
|
218
|
+
if (Math.max(...rows) <= form.base)
|
|
219
|
+
return;
|
|
220
|
+
const cut = profile.map(([u, real]) => [u, Math.max(real, form.base)]);
|
|
221
|
+
const { a, s } = frames[frame], point = (u, real, v) => lift(at(frame, u, v), real);
|
|
222
|
+
const centroid = [cut.reduce((sum, p) => sum + p[0], 0) / cut.length, cut.reduce((sum, p) => sum + p[1], 0) / cut.length];
|
|
223
|
+
const fan = (v, normal, paint) => cut.forEach((p, i) => {
|
|
224
|
+
const q = cut[(i + 1) % cut.length];
|
|
225
|
+
kit.triangle(target, [point(centroid[0], centroid[1], v), point(p[0], p[1], v), point(q[0], q[1], v)], [normal, normal, normal], paint);
|
|
226
|
+
});
|
|
227
|
+
fan(v1, [s[0], 0, s[1]], front);
|
|
228
|
+
fan(v0, [-s[0], 0, -s[1]], sides);
|
|
229
|
+
cut.forEach((p, i) => {
|
|
230
|
+
const q = cut[(i + 1) % cut.length], du = q[0] - p[0], dy = q[1] - p[1], length = Math.hypot(du, dy);
|
|
231
|
+
if (length < 1e-6)
|
|
232
|
+
return;
|
|
233
|
+
let [nu, ny] = [dy / length, -du / length];
|
|
234
|
+
if (nu * ((p[0] + q[0]) / 2 - centroid[0]) + ny * ((p[1] + q[1]) / 2 - centroid[1]) < 0)
|
|
235
|
+
[nu, ny] = [-nu, -ny];
|
|
236
|
+
kit.quad(target, [point(p[0], p[1], v0), point(q[0], q[1], v0), point(q[0], q[1], v1), point(p[0], p[1], v1)], [[a[0] * nu, ny, a[1] * nu]], sides);
|
|
237
|
+
});
|
|
238
|
+
};
|
|
239
|
+
// The lobby: the mapped outline from grade to the first office floor.
|
|
240
|
+
if (form.lobby && form.base < h.lobbyTop)
|
|
241
|
+
kit.prism(kit.batch("Two Prudential · lobby", kit.material(0x5a5a5a)), form.lobby, [y(Math.max(0, form.base)), y(h.lobbyTop)]);
|
|
242
|
+
// The core: its shaft to the eave, in one piece, so no seam crosses its faces where the
|
|
243
|
+
// tiers stop covering them.
|
|
244
|
+
const walls = (frame) => ({ front: { gable: g.front, seed: frame * 4 }, side: { gable: g.side, seed: frame * 4 + 1 } });
|
|
245
|
+
prism(ringOf(0, [[-coreA, coreB], [coreA, coreB], [coreA, -coreB], [-coreA, -coreB]]), rowsBetween(h.lobbyTop, h.eave), [0, 1].flatMap((frame) => [{ wall: walls(frame).front, from: -coreA }, { wall: walls(frame).side, from: -coreB }]));
|
|
246
|
+
// The crown: one floor's step at a time, each on the pyramid's section at its top, so
|
|
247
|
+
// every step's nosing lies on the pyramid. Up to a strip's shoulder a face's end is its
|
|
248
|
+
// gable's stepped bays; above it the end stands back under the ridge's rib, plain stone.
|
|
249
|
+
let crownTop = h.eave;
|
|
250
|
+
for (let step = 1; step <= crownSteps; step += 1) {
|
|
251
|
+
const lo = h.eave + (step - 1) * h.pitch, hi = lo + h.pitch, section = crownSection(hi);
|
|
252
|
+
const end = (wall) => (hi <= stripShoulder(wall.gable) + 1e-9 ? { wall, from: -wallExtent(wall.gable, hi) } : { plain: stone });
|
|
253
|
+
const edges = [0, 1].flatMap((frame) => [end(walls(frame).front), { riser: true }, end(walls(frame).side), { riser: true }]);
|
|
254
|
+
prism(ringOf(0, section), rowsBetween(lo, hi, [2.5]), edges);
|
|
255
|
+
crownTop = hi;
|
|
256
|
+
}
|
|
257
|
+
// The cap: from the last step's section to the apex.
|
|
258
|
+
{
|
|
259
|
+
const section = ringOf(0, crownSection(crownTop)), apex = lift(form.centre, h.apex);
|
|
260
|
+
section.forEach((p, i) => {
|
|
261
|
+
const q = section[(i + 1) % section.length], [pp, qq] = [lift(p, crownTop), lift(q, crownTop)];
|
|
262
|
+
const u = pp.map((value, k) => value - apex[k]), w = qq.map((value, k) => value - apex[k]);
|
|
263
|
+
const n = [u[1] * w[2] - u[2] * w[1], u[2] * w[0] - u[0] * w[2], u[0] * w[1] - u[1] * w[0]];
|
|
264
|
+
kit.triangle(body, [apex, pp, qq], [n, n, n].map((v) => (v[1] < 0 ? v.map((c) => -c) : v)), band);
|
|
265
|
+
});
|
|
266
|
+
for (let i = 1; i + 1 < section.length; i += 1) {
|
|
267
|
+
kit.triangle(body, [lift(section[0], crownTop), lift(section[i], crownTop), lift(section[i + 1], crownTop)], [[0, -1, 0], [0, -1, 0], [0, -1, 0]], soffit);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
// The tiers, on the north and south faces: each a block to its shoulder and a step per
|
|
271
|
+
// bay inward, with its strip's gabled head over the last.
|
|
272
|
+
const tiers = [0, 1].flatMap((frame) => {
|
|
273
|
+
const depth = (form.fronts[frame] - coreB) / 2;
|
|
274
|
+
return [
|
|
275
|
+
{ frame, gable: middle, back: coreB, front: coreB + depth, seed: 10 + frame * 2 },
|
|
276
|
+
{ frame, gable: lower, back: coreB + depth, front: form.fronts[frame], seed: 11 + frame * 2 },
|
|
277
|
+
];
|
|
278
|
+
});
|
|
279
|
+
for (const tier of tiers) {
|
|
280
|
+
const { frame, gable, back, front } = tier, wall = { gable, seed: tier.seed };
|
|
281
|
+
const block = (half) => ringOf(frame, [[-half, front], [half, front], [half, back], [-half, back]]);
|
|
282
|
+
const sides = (half, ribbon = true) => [{ wall, from: -half }, ribbon ? { ribbon: front - back } : { plain: stone }, { plain: stone }, ribbon ? { ribbon: front - back } : { plain: stone }];
|
|
283
|
+
prism(block(gable.half), rowsBetween(h.lobbyTop, gable.shoulder), sides(gable.half));
|
|
284
|
+
for (let step = 1; step < gable.bays; step += 1) {
|
|
285
|
+
const half = gable.half - step * gable.bay, lo = gable.shoulder + (step - 1) * h.pitch;
|
|
286
|
+
prism(block(half), rowsBetween(lo, lo + h.pitch), sides(half, false));
|
|
287
|
+
}
|
|
288
|
+
const foot = gable.shoulder + (gable.bays - 1) * h.pitch;
|
|
289
|
+
extrude(body, frame, [[-gable.strip, foot], [gable.strip, foot], [gable.strip, stripShoulder(gable)], [0, gable.peak], [-gable.strip, stripShoulder(gable)]], back, front, roofGlass, stone);
|
|
290
|
+
}
|
|
291
|
+
// The core's strips' pointed heads, 3 cm proud of the stepped walls behind them. Each
|
|
292
|
+
// starts in the spandrel under its shoulder, so its underside shares no plane with a
|
|
293
|
+
// step's.
|
|
294
|
+
for (const frame of [0, 1]) {
|
|
295
|
+
for (const [gable, local, reach] of [[g.front, frame, coreB], [g.side, frame + 2, coreA]]) {
|
|
296
|
+
const shoulder = stripShoulder(gable);
|
|
297
|
+
extrude(body, local, [[-gable.strip, shoulder - 0.3], [gable.strip, shoulder - 0.3], [gable.strip, shoulder], [0, gable.peak], [-gable.strip, shoulder]], reach - 0.5, reach + 0.03, roofGlass, roofGlass);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
// Every face's piers, copings and strip mullions. A face is a run along its wall with
|
|
301
|
+
// its outward normal, its gable, and what covers its foot.
|
|
302
|
+
const faces = [0, 1].flatMap((frame) => {
|
|
303
|
+
const depth = (form.fronts[frame] - coreB) / 2;
|
|
304
|
+
return [
|
|
305
|
+
{ run: line(at(frame, -coreA, coreB), at(frame, coreA, coreB)), gable: g.front, cover: (x) => (Math.abs(x) <= middle.half ? wallTop(middle, x) : h.lobbyTop), tier: false },
|
|
306
|
+
{ run: line(at(frame, coreA, coreB), at(frame, coreA, -coreB)), gable: g.side, cover: () => h.lobbyTop, tier: false },
|
|
307
|
+
{ run: line(at(frame, -middle.half, coreB + depth), at(frame, middle.half, coreB + depth)), gable: middle, cover: (x) => (Math.abs(x) <= lower.half ? wallTop(lower, x) : h.lobbyTop), tier: true },
|
|
308
|
+
{ run: line(at(frame, -lower.half, form.fronts[frame]), at(frame, lower.half, form.fronts[frame])), gable: lower, cover: () => h.lobbyTop, tier: true },
|
|
309
|
+
];
|
|
310
|
+
});
|
|
311
|
+
const rightAngle = -Math.PI / 2;
|
|
312
|
+
const box = (target, run, x0, x1, before, after, width, lo, hi, half) => {
|
|
313
|
+
const bottom = Math.max(lo, form.base);
|
|
314
|
+
if (hi - bottom < 0.05)
|
|
315
|
+
return;
|
|
316
|
+
mitredBox(kit, target, run, x0 + half, x1 + half, before, after, width, bottom, hi, y);
|
|
317
|
+
};
|
|
318
|
+
for (const { run, gable, cover, tier } of faces) {
|
|
319
|
+
const half = gable.half, span = (x0, x1, f, pick) => pick(f(x0 + 1e-4), f(x1 - 1e-4));
|
|
320
|
+
for (const sign of [-1, 1]) {
|
|
321
|
+
pierStations(gable).forEach((r, i) => {
|
|
322
|
+
const [lo, hi] = i ? [r - pierWidth / 2, r + pierWidth / 2] : [half - pierWidth, half];
|
|
323
|
+
const [x0, x1] = sign < 0 ? [-hi, -lo] : [lo, hi];
|
|
324
|
+
// A pier over a tier stands on the coping along that tier's step: every step's edge
|
|
325
|
+
// falls on a joint of the wall behind it.
|
|
326
|
+
const covered = span(x0, x1, cover, Math.max), bottom = covered > h.lobbyTop + 1e-6 ? covered + 0.26 : covered;
|
|
327
|
+
const top = span(x0, x1, (x) => wallTop(gable, x), Math.min) - 0.7;
|
|
328
|
+
const corner = i === 0 ? rightAngle : undefined;
|
|
329
|
+
box(piers, run, x0, x1, sign < 0 ? corner : undefined, sign > 0 ? corner : undefined, pierDepth, bottom, top, half);
|
|
330
|
+
});
|
|
331
|
+
for (let bay = 0; bay < gable.bays; bay += 1) {
|
|
332
|
+
const top = gable.shoulder + bay * h.pitch, outer = half - bay * gable.bay, inner = outer - gable.bay;
|
|
333
|
+
const [x0, x1] = sign < 0 ? [-outer, -inner] : [inner, outer];
|
|
334
|
+
// Each coping meets the side's at a convex corner: the core's corner, and every step
|
|
335
|
+
// of a tier. The core's steps above its eave end against the pyramid's risers.
|
|
336
|
+
const corner = bay === 0 || tier ? rightAngle : undefined;
|
|
337
|
+
box(copings, run, x0, x1, sign < 0 ? corner : undefined, sign > 0 ? corner : undefined, 0.45, top - 0.7, top + 0.25, half);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
// Mullions between the strip's lites, from what covers the strip to under its head.
|
|
341
|
+
const lites = Math.round(2 * gable.strip / 1.85), lite = 2 * gable.strip / lites, n = run.normal(0);
|
|
342
|
+
for (let k = 1; k < lites; k += 1) {
|
|
343
|
+
const x = -gable.strip + k * lite, bottom = Math.max(cover(x - 0.06), cover(x + 0.06), form.base) + 0.05;
|
|
344
|
+
const top = Math.min(wallTop(gable, x - 0.06), wallTop(gable, x + 0.06)) - 0.35;
|
|
345
|
+
if (top - bottom > 0.5)
|
|
346
|
+
kit.box(mullions, run.at(x + half), n, 0.06, -0.02, 0.14, y(bottom), y(top));
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
// The tiers' sides: a corner pier meeting the front's, and a coping along each step's
|
|
350
|
+
// top from the front back to the wall behind.
|
|
351
|
+
for (const tier of tiers) {
|
|
352
|
+
const { frame, gable, back, front } = tier;
|
|
353
|
+
for (let step = 0; step < gable.bays; step += 1) {
|
|
354
|
+
const half = gable.half - step * gable.bay, top = gable.shoulder + step * h.pitch;
|
|
355
|
+
const east = line(at(frame, half, front), at(frame, half, back)), west = line(at(frame, -half, back), at(frame, -half, front));
|
|
356
|
+
const length = front - back;
|
|
357
|
+
box(copings, east, -length / 2, length / 2, rightAngle, undefined, 0.45, top - 0.7, top + 0.25, length / 2);
|
|
358
|
+
box(copings, west, -length / 2, length / 2, undefined, rightAngle, 0.45, top - 0.7, top + 0.25, length / 2);
|
|
359
|
+
if (step === 0) {
|
|
360
|
+
const bottom = h.lobbyTop;
|
|
361
|
+
box(piers, east, -length / 2, -length / 2 + pierWidth, rightAngle, undefined, pierDepth, bottom, top - 0.7, length / 2);
|
|
362
|
+
box(piers, west, length / 2 - pierWidth, length / 2, undefined, rightAngle, pierDepth, bottom, top - 0.7, length / 2);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
// A broad rib on each ridge: a hipped plate whose front edges follow its strip's pointed
|
|
367
|
+
// head, from the head's shoulders to its point, and whose ridge and inner edges run on
|
|
368
|
+
// straight to the apex. The pyramid's steps end on its inner edges, and under it each
|
|
369
|
+
// step stands back, so the rib stands proud of the steps beside it.
|
|
370
|
+
const ribDepth = 0.35;
|
|
371
|
+
for (const frame of [0, 1, 2, 3]) {
|
|
372
|
+
const gable = frame < 2 ? g.front : g.side, reach = frame < 2 ? coreB : coreA, shoulder = stripShoulder(gable);
|
|
373
|
+
const corner = (u, v, real) => lift(at(frame, u, v), real);
|
|
374
|
+
// Its left shoulder seen from outside, the head's point, its right shoulder and the apex,
|
|
375
|
+
// then the same raised.
|
|
376
|
+
const low = [corner(-gable.strip, reach, shoulder), corner(0, reach, gable.peak), corner(gable.strip, reach, shoulder), corner(0, 0, h.apex)];
|
|
377
|
+
const high = low.map(([x, real, z]) => [x, real + ribDepth, z]);
|
|
378
|
+
// A point inside each half of the plate, to turn every face's normal outward: the plate
|
|
379
|
+
// is folded along its ridge, so no one centre lies inside it.
|
|
380
|
+
const inside = (a, b, c) => [0, 1, 2].map((k) => (a[k] + b[k] + c[k]) / 3 + (k === 1 ? ribDepth / 2 : 0));
|
|
381
|
+
const left = inside(low[0], low[1], low[3]), right = inside(low[1], low[2], low[3]);
|
|
382
|
+
const face = (points, within) => {
|
|
383
|
+
const u = points[1].map((value, k) => value - points[0][k]), w = points[2].map((value, k) => value - points[0][k]);
|
|
384
|
+
let n = [u[1] * w[2] - u[2] * w[1], u[2] * w[0] - u[0] * w[2], u[0] * w[1] - u[1] * w[0]];
|
|
385
|
+
const centre = points.reduce((sum, p) => sum.map((value, k) => value + p[k] / points.length), [0, 0, 0]);
|
|
386
|
+
if (n[0] * (centre[0] - within[0]) + n[1] * (centre[1] - within[1]) + n[2] * (centre[2] - within[2]) < 0)
|
|
387
|
+
n = n.map((value) => -value);
|
|
388
|
+
if (points.length === 3)
|
|
389
|
+
kit.triangle(ribs, points, [n, n, n]);
|
|
390
|
+
else
|
|
391
|
+
kit.quad(ribs, points, [n]);
|
|
392
|
+
};
|
|
393
|
+
face([high[0], high[1], high[3]], left);
|
|
394
|
+
face([high[1], high[2], high[3]], right);
|
|
395
|
+
face([low[0], low[1], low[3]], left);
|
|
396
|
+
face([low[1], low[2], low[3]], right);
|
|
397
|
+
// Its front edges over the head, then its inner edges back from the apex.
|
|
398
|
+
for (const [i, j, within] of [[0, 1, left], [1, 2, right], [2, 3, right], [3, 0, left]])
|
|
399
|
+
face([low[i], low[j], high[j], high[i]], within);
|
|
400
|
+
}
|
|
401
|
+
// The spire: a shaft turned with the pyramid, its corners on the ridges, tapering from its
|
|
402
|
+
// foot in the cap to a needle.
|
|
403
|
+
const spireAt = (r, real) => [[0, r], [r, 0], [0, -r], [-r, 0]].map(([u, v]) => lift(at(0, u, v), real));
|
|
404
|
+
const faceNormal = (p, q, r) => {
|
|
405
|
+
const u = q.map((value, k) => value - p[k]), w = r.map((value, k) => value - p[k]);
|
|
406
|
+
const n = [u[1] * w[2] - u[2] * w[1], u[2] * w[0] - u[0] * w[2], u[0] * w[1] - u[1] * w[0]];
|
|
407
|
+
const centre = lift(form.centre, p[1] + form.base);
|
|
408
|
+
return (n[0] * (p[0] - centre[0]) + n[2] * (p[2] - centre[2]) < 0 ? n.map((value) => -value) : n);
|
|
409
|
+
};
|
|
410
|
+
const footY = h.apex - 3.9, foot = spireAt(1.6, footY), shaft = spireAt(0.85, h.shaftTop), needle = spireAt(0.32, h.shaftTop), tip = lift(form.centre, h.tip);
|
|
411
|
+
for (let i = 0; i < 4; i += 1) {
|
|
412
|
+
const j = (i + 1) % 4, n = faceNormal(foot[i], foot[j], shaft[i]);
|
|
413
|
+
kit.quad(spire, [foot[i], foot[j], shaft[j], shaft[i]], [n]);
|
|
414
|
+
const m = faceNormal(needle[i], needle[j], tip);
|
|
415
|
+
kit.triangle(spire, [needle[i], needle[j], tip], [m, m, m]);
|
|
416
|
+
// A dark inset panel down each face, in two lengths between bright folded edges.
|
|
417
|
+
const unit = n.map((value) => value / Math.hypot(...n));
|
|
418
|
+
const radius = (real) => 1.6 + (0.85 - 1.6) * (real - footY) / (h.shaftTop - footY);
|
|
419
|
+
for (const [lo, hi] of [[h.apex + 0.6, h.apex + 7.4], [h.apex + 7.9, h.shaftTop - 1.2]]) {
|
|
420
|
+
// Half the face's width, about its middle.
|
|
421
|
+
const corners = [[lo, -1], [lo, 1], [hi, 1], [hi, -1]].map(([real, side]) => {
|
|
422
|
+
const ring = spireAt(radius(real), real), [p, q] = [ring[i], ring[j]];
|
|
423
|
+
return p.map((value, k) => (value + q[k]) / 2 + (q[k] - value) / 4 * side);
|
|
424
|
+
});
|
|
425
|
+
const moved = (distance) => corners.map((p) => p.map((value, k) => value + unit[k] * distance));
|
|
426
|
+
const [outer, inner] = [moved(0.03), moved(-0.02)];
|
|
427
|
+
kit.quad(spirePanels, [outer[0], outer[1], outer[2], outer[3]], [unit]);
|
|
428
|
+
kit.quad(spirePanels, [inner[0], inner[1], inner[2], inner[3]], [unit.map((value) => -value)]);
|
|
429
|
+
for (let k = 0; k < 4; k += 1) {
|
|
430
|
+
const l = (k + 1) % 4, quad = [inner[k], inner[l], outer[l], outer[k]];
|
|
431
|
+
const edge = outer[l].map((value, c) => value - outer[k][c]), side = [edge[1] * unit[2] - edge[2] * unit[1], edge[2] * unit[0] - edge[0] * unit[2], edge[0] * unit[1] - edge[1] * unit[0]];
|
|
432
|
+
const centre = outer.reduce((sum, v) => sum.map((value, c) => value + v[c] / 4), [0, 0, 0]), mid = outer[k].map((value, c) => (value + outer[l][c]) / 2);
|
|
433
|
+
kit.quad(spirePanels, quad, [side[0] * (mid[0] - centre[0]) + side[1] * (mid[1] - centre[1]) + side[2] * (mid[2] - centre[2]) < 0 ? side.map((value) => -value) : side]);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
kit.quad(spire, [foot[0], foot[1], foot[2], foot[3]], [[0, -1, 0]]);
|
|
438
|
+
kit.quad(spire, [shaft[0], shaft[1], shaft[2], shaft[3]], [[0, 1, 0]]);
|
|
439
|
+
kit.quad(spire, [needle[0], needle[1], needle[2], needle[3]], [[0, -1, 0]]);
|
|
440
|
+
return kit.finish({ height: y(h.tip), outlines: [body, spire], opacity: 0.18 });
|
|
441
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { BuildingModel, Plan } from "./building-kit.js";
|
|
2
|
+
import type { GeoBuilding } from "./skyline-geography-data.js";
|
|
3
|
+
export declare const universityClubLevels: Readonly<{
|
|
4
|
+
band: 46.2;
|
|
5
|
+
parapet: 48.95;
|
|
6
|
+
walls: 51;
|
|
7
|
+
merlons: 51.75;
|
|
8
|
+
eaves: 56.4;
|
|
9
|
+
pinnacles: 62.5;
|
|
10
|
+
ridge: 67.7;
|
|
11
|
+
cross: 69.75;
|
|
12
|
+
dormer: 59.3;
|
|
13
|
+
dormerCross: 60.4;
|
|
14
|
+
back: 3.3;
|
|
15
|
+
sides: 1.3;
|
|
16
|
+
}>;
|
|
17
|
+
export declare const universityClubPalette: Readonly<{
|
|
18
|
+
glass: number[];
|
|
19
|
+
lit: 9211020;
|
|
20
|
+
dim: 4473924;
|
|
21
|
+
stone: 10790052;
|
|
22
|
+
band: 11842740;
|
|
23
|
+
}>;
|
|
24
|
+
export declare function createUniversityClubGeographicBuilding(record: GeoBuilding, projectPlan: (coordinates: [number, number][]) => Plan, offset: [number, number]): BuildingModel;
|