@a2f0/skyline 0.2.31 → 0.2.33
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/docs/package.md +0 -3
- package/lib/models/aon-geographic.js +5 -4
- package/lib/models/aon-tower.d.ts +1 -0
- package/lib/models/aon-tower.js +8 -6
- package/lib/models/colour-materials.js +1 -1
- package/lib/models/colour-palette.js +1 -0
- package/lib/models/river-plaza-geographic.d.ts +7 -0
- package/lib/models/river-plaza-geographic.js +47 -12
- package/package.json +20 -13
- package/site/models/aon-geographic.js +5 -4
- package/site/models/aon-tower.js +8 -6
- package/site/models/colour-materials.js +1 -1
- package/site/models/colour-palette.js +1 -0
- package/site/models/river-plaza-geographic.js +47 -12
package/docs/package.md
CHANGED
|
@@ -382,9 +382,6 @@ Then, in the package's settings on npmjs.com, add a GitHub Actions trusted publi
|
|
|
382
382
|
repository `a2f0/skyline`, workflow `npm-publish.yml`, and environment `npm`, and re-run
|
|
383
383
|
the workflow; it reports the version as already published.
|
|
384
384
|
|
|
385
|
-
The Sharp override in the tarball is repository development tooling policy; npm
|
|
386
|
-
and Bun consumers use their own root overrides, so it does not pin their dependencies.
|
|
387
|
-
|
|
388
385
|
The package is ESM only, with no CommonJS entrypoint. Building requires this repository's
|
|
389
386
|
Bun/TypeScript tools; consuming the published package requires neither Bun nor build
|
|
390
387
|
scripts. Metadata uses `UNLICENSED` and preserves the third-party notices in
|
|
@@ -4,9 +4,10 @@ import { aonLevels, buildAonTower } from "./aon-tower.js";
|
|
|
4
4
|
// notched corners, the rooftop enclosure part 284775635, and the published heights. The
|
|
5
5
|
// tower is the shared generator in aon-tower.ts, the same one the original layout's copy
|
|
6
6
|
// uses. Each mapped face carries the source plan's fifteen window bays, distributed
|
|
7
|
-
//
|
|
8
|
-
// at the enclosure's centre, reaching the
|
|
9
|
-
// geometry. See docs/aon-reference.md.
|
|
7
|
+
// uniformly between inset end piers; the mapped notches are deeper, so absolute spacing
|
|
8
|
+
// remains approximate. The antenna stands at the enclosure's centre, reaching the
|
|
9
|
+
// published 362.5 m tip; the map gives no antenna geometry. See docs/aon-reference.md.
|
|
10
|
+
// Units are meters; +x is east, +z is south.
|
|
10
11
|
export const aonGeographicLevels = Object.freeze({
|
|
11
12
|
shaftTop: aonLevels.roof, // OSM shaft part top
|
|
12
13
|
enclosureTop: aonLevels.enclosureTop, // OSM part 284775635
|
|
@@ -27,7 +28,7 @@ export function createAonGeographicBuilding(record, projectPlan, offset) {
|
|
|
27
28
|
cz += (z + z2) * cross;
|
|
28
29
|
});
|
|
29
30
|
const mast = [cx / (3 * twice), cz / (3 * twice)];
|
|
30
|
-
const model = buildAonTower({ name: record.name, id: record.id, outline: polygonOf(projectPlan(record.footprint.coordinates)), enclosure, mast, base: 0 });
|
|
31
|
+
const model = buildAonTower({ name: record.name, id: record.id, outline: polygonOf(projectPlan(record.footprint.coordinates)), enclosure, mast, base: 0, uniformPierSpacing: true });
|
|
31
32
|
model.building.position.set(offset[0], 0, offset[1]);
|
|
32
33
|
model.building.userData["geography"] = record;
|
|
33
34
|
model.building.userData["geographicDetail"] = { levels: h, floors: h.floors, source: "docs/aon-reference.md" };
|
package/lib/models/aon-tower.js
CHANGED
|
@@ -15,7 +15,7 @@ export const aonLevels = Object.freeze({
|
|
|
15
15
|
head: 3.1,
|
|
16
16
|
enclosureTop: 346.3,
|
|
17
17
|
tip: 362.5,
|
|
18
|
-
bay: 3.048, // nominal clean-plan module
|
|
18
|
+
bay: 3.048, // nominal clean-plan module
|
|
19
19
|
baysPerFace: 15, // tenant portal's Suite 1300 plan: sixteen piers, fifteen openings
|
|
20
20
|
faceLength: 20,
|
|
21
21
|
});
|
|
@@ -60,13 +60,15 @@ export function buildAonTower(form) {
|
|
|
60
60
|
paintedSlab(kit, shell, outline, 0, false, roofing);
|
|
61
61
|
paintedSlab(kit, shell, outline, y(h.roof), true, roofing);
|
|
62
62
|
// The source plan fixes fifteen openings per face, independent of an OSM trace's
|
|
63
|
-
// corner depth.
|
|
64
|
-
//
|
|
63
|
+
// corner depth. In geographic mode distribute pier centres between the inset
|
|
64
|
+
// ends, so the terminal openings match the others. Absolute mapped spacing
|
|
65
|
+
// remains approximate (docs/aon-reference.md, FID-AON-002/003).
|
|
66
|
+
const pierHalf = 0.65, pierPoint = 0.7;
|
|
65
67
|
const faceBays = (run) => {
|
|
66
|
-
const count = h.baysPerFace,
|
|
67
|
-
|
|
68
|
+
const count = h.baysPerFace, inset = form.uniformPierSpacing ? pierHalf : 0;
|
|
69
|
+
const bay = (run.length - 2 * inset) / count;
|
|
70
|
+
return Array.from({ length: count + 1 }, (_, i) => inset + i * bay);
|
|
68
71
|
};
|
|
69
|
-
const pierHalf = 0.65, pierPoint = 0.7;
|
|
70
72
|
const floors = [];
|
|
71
73
|
for (let level = h.lobbyTop; level + h.pitch <= h.capBottom + 1e-6; level += h.pitch)
|
|
72
74
|
floors.push(level);
|
|
@@ -242,7 +242,7 @@ export const colourMaterials = {
|
|
|
242
242
|
// White concrete.
|
|
243
243
|
"building-river-plaza": {
|
|
244
244
|
shell: ["concrete"],
|
|
245
|
-
"concrete and windows": ["glass", { ...windows(0x8a8a8a, 0x505050), 0x9a9a9a: "concrete" }],
|
|
245
|
+
"concrete and windows": ["glass", { ...windows(0x8a8a8a, 0x505050), 0x9a9a9a: "concrete", 0x424242: "aluminium" }],
|
|
246
246
|
"rooftop box": ["concrete"],
|
|
247
247
|
},
|
|
248
248
|
// Dark orange brick and bronze glass.
|
|
@@ -5,12 +5,19 @@ export declare const riverPlazaLevels: Readonly<{
|
|
|
5
5
|
floors: 56;
|
|
6
6
|
roof: 156;
|
|
7
7
|
box: 159.7;
|
|
8
|
+
/** @deprecated Historical generic spacing, retained for package consumers; the model now uses counted groups. */
|
|
8
9
|
bay: 1.6;
|
|
10
|
+
pier: 0.45;
|
|
11
|
+
sill: 0.35;
|
|
12
|
+
window: 2.1;
|
|
13
|
+
transom: 0.6;
|
|
14
|
+
frame: 0.06;
|
|
9
15
|
}>;
|
|
10
16
|
export declare const riverPlazaPalette: Readonly<{
|
|
11
17
|
glass: number[];
|
|
12
18
|
lit: 9079434;
|
|
13
19
|
dim: 5263440;
|
|
14
20
|
concrete: 10132122;
|
|
21
|
+
frame: 4342338;
|
|
15
22
|
}>;
|
|
16
23
|
export declare function createRiverPlazaGeographicBuilding(record: GeoBuilding, projectPlan: (coordinates: [number, number][]) => Plan, offset: [number, number]): BuildingModel;
|
|
@@ -17,17 +17,28 @@ export const riverPlazaLevels = Object.freeze({
|
|
|
17
17
|
floors: 56,
|
|
18
18
|
roof: 156, // the slab's roof, OpenStreetMap's
|
|
19
19
|
box: 159.7, // the rooftop box's top, the published height and tip, the Skyscraper Center's 524 ft
|
|
20
|
-
|
|
20
|
+
/** @deprecated Historical generic spacing, retained for package consumers; the model now uses counted groups. */
|
|
21
|
+
bay: 1.6,
|
|
22
|
+
pier: 0.45, // schematic concrete width; group counts come from dated photographs
|
|
23
|
+
sill: 0.35, // inferred floor datum; photographed opening is about 75% of the pitch
|
|
24
|
+
window: 2.1,
|
|
25
|
+
transom: 0.6, // lower light about 30% of the opening; section width is schematic
|
|
26
|
+
frame: 0.06,
|
|
21
27
|
});
|
|
28
|
+
// Starting at the south-east end, follow the mapped outline counter-clockwise.
|
|
29
|
+
// The long elevations have 3 / recess / 4 / recess / 3 groups; the ends have
|
|
30
|
+
// 2 / chamfer / 1 / chamfer / 2. Each outer corner is glazed as one group.
|
|
31
|
+
// See the 2007/2008/2012 counts in the reference audit; west-end symmetry is inferred.
|
|
32
|
+
const groupsPerChain = [2, 1, 1, 1, 2, 1, 3, 1, 4, 1, 3, 1, 2, 1, 1, 1, 2, 1, 3, 1, 4, 1, 3, 1];
|
|
22
33
|
const h = riverPlazaLevels;
|
|
23
34
|
const pitch = h.roof / h.floors;
|
|
24
35
|
// The skins' colours: the window tones, and the concrete, which no window shares.
|
|
25
|
-
export const riverPlazaPalette = Object.freeze({ glass: [0x2c2c2c, 0x303030, 0x343434, 0x282828], lit: 0x8a8a8a, dim: 0x505050, concrete: 0x9a9a9a });
|
|
36
|
+
export const riverPlazaPalette = Object.freeze({ glass: [0x2c2c2c, 0x303030, 0x343434, 0x282828], lit: 0x8a8a8a, dim: 0x505050, concrete: 0x9a9a9a, frame: 0x424242 });
|
|
26
37
|
const palette = riverPlazaPalette;
|
|
27
38
|
const color = (hex) => new THREE.Color(hex);
|
|
28
39
|
const glassTones = palette.glass.map(color);
|
|
29
40
|
const litGlass = color(palette.lit), dimGlass = color(palette.dim), concrete = color(palette.concrete);
|
|
30
|
-
const core = color(0x6a6a6a), roofing = color(0x8a8a8a);
|
|
41
|
+
const core = color(0x6a6a6a), roofing = color(0x8a8a8a), frame = color(palette.frame);
|
|
31
42
|
function paneColor(row, bay, wall) {
|
|
32
43
|
const hash = (Math.imul(row + 19, 0x9e3779b1) ^ Math.imul(bay + 37, 0x85ebca77) ^ Math.imul(wall + 29, 0xc2b2ae3d)) >>> 0;
|
|
33
44
|
const value = hash % 101;
|
|
@@ -55,8 +66,9 @@ export function createRiverPlazaGeographicBuilding(record, projectPlan, offset)
|
|
|
55
66
|
// The box on the roof, on its mapped part.
|
|
56
67
|
kit.prism(box, planOf(top), [h.roof, h.box]);
|
|
57
68
|
// The slab's skin, one box to a run of a chain, held 2 cm clear of the chain's ends or 9 cm
|
|
58
|
-
// where the outline turns in:
|
|
59
|
-
//
|
|
69
|
+
// where the outline turns in: taller windows with a narrow lower transom follow
|
|
70
|
+
// dated photographs. Broad structural groups continue into the glazed
|
|
71
|
+
// recesses and corners. A run the podium stands against, its
|
|
60
72
|
// two ends on the podium's outline, starts over the podium's roof; a chain whose runs differ
|
|
61
73
|
// is skinned in parts, which meet at their joint.
|
|
62
74
|
const plan = planOf(tower);
|
|
@@ -66,11 +78,17 @@ export function createRiverPlazaGeographicBuilding(record, projectPlan, offset)
|
|
|
66
78
|
return Math.hypot(p[0] - a[0] - t * (b[0] - a[0]), p[1] - a[1] - t * (b[1] - a[1])) < 0.05;
|
|
67
79
|
});
|
|
68
80
|
const covered = (run) => onEdge(run.at(0)) && onEdge(run.at(run.length));
|
|
69
|
-
chainsOf(plan)
|
|
81
|
+
const chains = chainsOf(plan);
|
|
82
|
+
if (chains.length !== groupsPerChain.length)
|
|
83
|
+
throw new Error("River Plaza mapped facade chains changed; recheck photographed groups");
|
|
84
|
+
chains.forEach((chain, index) => {
|
|
70
85
|
const first = chain.runs[0], last = chain.runs.at(-1);
|
|
71
86
|
const s0 = turnsAt(plan, first)[0] > 0 ? 0.09 : 0.02, s1 = chain.length - (turnsAt(plan, last)[1] > 0 ? 0.09 : 0.02);
|
|
72
|
-
const count =
|
|
73
|
-
const
|
|
87
|
+
const count = groupsPerChain[index];
|
|
88
|
+
const module = chain.length / count, halfWindow = (module - h.pier) / 2;
|
|
89
|
+
if (halfWindow <= 0)
|
|
90
|
+
throw new Error("River Plaza facade group is narrower than its concrete pier");
|
|
91
|
+
const centres = Array.from({ length: count }, (_, k) => (k + 0.5) * module);
|
|
74
92
|
// The chain's parts, each a span of runs the podium covers alike.
|
|
75
93
|
const parts = [];
|
|
76
94
|
chain.runs.forEach((run, k) => { const part = parts.at(-1); if (part && covered(plan[run]) === covered(plan[chain.runs[part[0]]]))
|
|
@@ -82,18 +100,35 @@ export function createRiverPlazaGeographicBuilding(record, projectPlan, offset)
|
|
|
82
100
|
const from = k0 === 0 ? s0 : chain.starts[k0], to = k1 === chain.runs.length - 1 ? s1 : chain.starts[k1] + plan[chain.runs[k1]].length;
|
|
83
101
|
const heights = [base];
|
|
84
102
|
for (let n = Math.ceil(base / pitch); n < h.floors; n += 1) {
|
|
85
|
-
const sill = n * pitch +
|
|
103
|
+
const sill = n * pitch + h.sill;
|
|
86
104
|
if (sill > base + 0.2)
|
|
87
|
-
heights.push(sill, sill +
|
|
105
|
+
heights.push(sill, sill + h.window);
|
|
88
106
|
}
|
|
89
107
|
heights.push(h.roof);
|
|
90
|
-
const cuts = centres.flatMap((c) => [c -
|
|
108
|
+
const cuts = centres.flatMap((c) => [c - halfWindow, c + halfWindow]).filter((s) => s > from + 0.01 && s < to - 0.01);
|
|
91
109
|
const columns = [from, ...cuts, to];
|
|
92
110
|
chainSkin(kit, wall, plan, chain, k0, k1, from, to, columns, heights, 0.02, 0.07, (bay, r) => {
|
|
93
111
|
const middle = (columns[bay - 1] + columns[bay]) / 2;
|
|
94
|
-
const glazed = r % 2 === 1 && centres.some((c) => Math.abs(middle - c) <
|
|
112
|
+
const glazed = r % 2 === 1 && centres.some((c) => Math.abs(middle - c) < halfWindow);
|
|
95
113
|
return glazed ? paneColor(r, bay, index) : concrete;
|
|
96
114
|
}, concrete);
|
|
115
|
+
// A closed metal bar sits 5 mm clear of each full-height pane. Keeping
|
|
116
|
+
// the glass quad whole gives its upper and lower lights one night seed.
|
|
117
|
+
for (const centre of centres) {
|
|
118
|
+
const left = Math.max(from, centre - halfWindow), right = Math.min(to, centre + halfWindow);
|
|
119
|
+
if (right - left < 0.01)
|
|
120
|
+
continue;
|
|
121
|
+
const firstRun = chain.runs.findIndex((run, k) => k >= k0 && k <= k1 && chain.starts[k] + plan[run].length > left + 1e-6);
|
|
122
|
+
if (firstRun < 0)
|
|
123
|
+
throw new Error("River Plaza transom falls outside its facade section");
|
|
124
|
+
let lastRun = firstRun;
|
|
125
|
+
while (lastRun < k1 && chain.starts[lastRun + 1] < right - 1e-6)
|
|
126
|
+
lastRun += 1;
|
|
127
|
+
for (let row = 1; row + 1 < heights.length; row += 2) {
|
|
128
|
+
const lower = heights[row] + h.transom;
|
|
129
|
+
chainSkin(kit, wall, plan, chain, firstRun, lastRun, left, right, [left, right], [lower, lower + h.frame], 0.075, 0.09, () => frame, frame);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
97
132
|
}
|
|
98
133
|
});
|
|
99
134
|
const model = kit.finish({ height: h.box, outlines: [shell, box], opacity: 0.16 });
|
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@a2f0/skyline",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.33",
|
|
4
4
|
"description": "Interactive Chicago skyline viewer and reusable Three.js building models",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "git+https://github.com/a2f0/skyline.git"
|
|
9
9
|
},
|
|
10
|
-
"publishConfig": {
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public"
|
|
12
|
+
},
|
|
11
13
|
"type": "module",
|
|
12
14
|
"sideEffects": ["./site/**/*.js"],
|
|
13
15
|
"files": ["lib/", "site/", "NOTICE.md", "docs/package.md", "docs/skyline-geography.md"],
|
|
@@ -37,15 +39,20 @@
|
|
|
37
39
|
"./assets/*": "./site/*",
|
|
38
40
|
"./package.json": "./package.json"
|
|
39
41
|
},
|
|
40
|
-
"engines": {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
"
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=22"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"three": "~0.186.0"
|
|
47
|
+
},
|
|
48
|
+
"peerDependenciesMeta": {
|
|
49
|
+
"three": {
|
|
50
|
+
"optional": true
|
|
47
51
|
}
|
|
48
52
|
},
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"@types/three": "0.186.0"
|
|
55
|
+
},
|
|
49
56
|
"scripts": {
|
|
50
57
|
"agent-tool": "agent-tool",
|
|
51
58
|
"agent:run": "agent-tool run",
|
|
@@ -66,13 +73,13 @@
|
|
|
66
73
|
"verify:deploy": "bun scripts/verify-deploy.ts"
|
|
67
74
|
},
|
|
68
75
|
"devDependencies": {
|
|
69
|
-
"@a2f0/agent-tool": "0.1.
|
|
76
|
+
"@a2f0/agent-tool": "0.1.15",
|
|
70
77
|
"@tsconfig/strictest": "^2.0.8",
|
|
71
|
-
"@types/bun": "^1.4.
|
|
78
|
+
"@types/bun": "^1.4.3",
|
|
72
79
|
"esbuild": "0.28.2",
|
|
73
80
|
"three": "0.186.1",
|
|
74
|
-
"playwright": "1.
|
|
81
|
+
"playwright": "1.64.1",
|
|
75
82
|
"typescript": "^7.0.2",
|
|
76
|
-
"wrangler": "4.
|
|
83
|
+
"wrangler": "4.151.1"
|
|
77
84
|
}
|
|
78
85
|
}
|
|
@@ -4,9 +4,10 @@ import { aonLevels, buildAonTower } from "./aon-tower.js";
|
|
|
4
4
|
// notched corners, the rooftop enclosure part 284775635, and the published heights. The
|
|
5
5
|
// tower is the shared generator in aon-tower.ts, the same one the original layout's copy
|
|
6
6
|
// uses. Each mapped face carries the source plan's fifteen window bays, distributed
|
|
7
|
-
//
|
|
8
|
-
// at the enclosure's centre, reaching the
|
|
9
|
-
// geometry. See docs/aon-reference.md.
|
|
7
|
+
// uniformly between inset end piers; the mapped notches are deeper, so absolute spacing
|
|
8
|
+
// remains approximate. The antenna stands at the enclosure's centre, reaching the
|
|
9
|
+
// published 362.5 m tip; the map gives no antenna geometry. See docs/aon-reference.md.
|
|
10
|
+
// Units are meters; +x is east, +z is south.
|
|
10
11
|
export const aonGeographicLevels = Object.freeze({
|
|
11
12
|
shaftTop: aonLevels.roof, // OSM shaft part top
|
|
12
13
|
enclosureTop: aonLevels.enclosureTop, // OSM part 284775635
|
|
@@ -27,7 +28,7 @@ export function createAonGeographicBuilding(record, projectPlan, offset) {
|
|
|
27
28
|
cz += (z + z2) * cross;
|
|
28
29
|
});
|
|
29
30
|
const mast = [cx / (3 * twice), cz / (3 * twice)];
|
|
30
|
-
const model = buildAonTower({ name: record.name, id: record.id, outline: polygonOf(projectPlan(record.footprint.coordinates)), enclosure, mast, base: 0 });
|
|
31
|
+
const model = buildAonTower({ name: record.name, id: record.id, outline: polygonOf(projectPlan(record.footprint.coordinates)), enclosure, mast, base: 0, uniformPierSpacing: true });
|
|
31
32
|
model.building.position.set(offset[0], 0, offset[1]);
|
|
32
33
|
model.building.userData["geography"] = record;
|
|
33
34
|
model.building.userData["geographicDetail"] = { levels: h, floors: h.floors, source: "docs/aon-reference.md" };
|
package/site/models/aon-tower.js
CHANGED
|
@@ -15,7 +15,7 @@ export const aonLevels = Object.freeze({
|
|
|
15
15
|
head: 3.1,
|
|
16
16
|
enclosureTop: 346.3,
|
|
17
17
|
tip: 362.5,
|
|
18
|
-
bay: 3.048, // nominal clean-plan module
|
|
18
|
+
bay: 3.048, // nominal clean-plan module
|
|
19
19
|
baysPerFace: 15, // tenant portal's Suite 1300 plan: sixteen piers, fifteen openings
|
|
20
20
|
faceLength: 20,
|
|
21
21
|
});
|
|
@@ -60,13 +60,15 @@ export function buildAonTower(form) {
|
|
|
60
60
|
paintedSlab(kit, shell, outline, 0, false, roofing);
|
|
61
61
|
paintedSlab(kit, shell, outline, y(h.roof), true, roofing);
|
|
62
62
|
// The source plan fixes fifteen openings per face, independent of an OSM trace's
|
|
63
|
-
// corner depth.
|
|
64
|
-
//
|
|
63
|
+
// corner depth. In geographic mode distribute pier centres between the inset
|
|
64
|
+
// ends, so the terminal openings match the others. Absolute mapped spacing
|
|
65
|
+
// remains approximate (docs/aon-reference.md, FID-AON-002/003).
|
|
66
|
+
const pierHalf = 0.65, pierPoint = 0.7;
|
|
65
67
|
const faceBays = (run) => {
|
|
66
|
-
const count = h.baysPerFace,
|
|
67
|
-
|
|
68
|
+
const count = h.baysPerFace, inset = form.uniformPierSpacing ? pierHalf : 0;
|
|
69
|
+
const bay = (run.length - 2 * inset) / count;
|
|
70
|
+
return Array.from({ length: count + 1 }, (_, i) => inset + i * bay);
|
|
68
71
|
};
|
|
69
|
-
const pierHalf = 0.65, pierPoint = 0.7;
|
|
70
72
|
const floors = [];
|
|
71
73
|
for (let level = h.lobbyTop; level + h.pitch <= h.capBottom + 1e-6; level += h.pitch)
|
|
72
74
|
floors.push(level);
|
|
@@ -242,7 +242,7 @@ export const colourMaterials = {
|
|
|
242
242
|
// White concrete.
|
|
243
243
|
"building-river-plaza": {
|
|
244
244
|
shell: ["concrete"],
|
|
245
|
-
"concrete and windows": ["glass", { ...windows(0x8a8a8a, 0x505050), 0x9a9a9a: "concrete" }],
|
|
245
|
+
"concrete and windows": ["glass", { ...windows(0x8a8a8a, 0x505050), 0x9a9a9a: "concrete", 0x424242: "aluminium" }],
|
|
246
246
|
"rooftop box": ["concrete"],
|
|
247
247
|
},
|
|
248
248
|
// Dark orange brick and bronze glass.
|
|
@@ -17,17 +17,28 @@ export const riverPlazaLevels = Object.freeze({
|
|
|
17
17
|
floors: 56,
|
|
18
18
|
roof: 156, // the slab's roof, OpenStreetMap's
|
|
19
19
|
box: 159.7, // the rooftop box's top, the published height and tip, the Skyscraper Center's 524 ft
|
|
20
|
-
|
|
20
|
+
/** @deprecated Historical generic spacing, retained for package consumers; the model now uses counted groups. */
|
|
21
|
+
bay: 1.6,
|
|
22
|
+
pier: 0.45, // schematic concrete width; group counts come from dated photographs
|
|
23
|
+
sill: 0.35, // inferred floor datum; photographed opening is about 75% of the pitch
|
|
24
|
+
window: 2.1,
|
|
25
|
+
transom: 0.6, // lower light about 30% of the opening; section width is schematic
|
|
26
|
+
frame: 0.06,
|
|
21
27
|
});
|
|
28
|
+
// Starting at the south-east end, follow the mapped outline counter-clockwise.
|
|
29
|
+
// The long elevations have 3 / recess / 4 / recess / 3 groups; the ends have
|
|
30
|
+
// 2 / chamfer / 1 / chamfer / 2. Each outer corner is glazed as one group.
|
|
31
|
+
// See the 2007/2008/2012 counts in the reference audit; west-end symmetry is inferred.
|
|
32
|
+
const groupsPerChain = [2, 1, 1, 1, 2, 1, 3, 1, 4, 1, 3, 1, 2, 1, 1, 1, 2, 1, 3, 1, 4, 1, 3, 1];
|
|
22
33
|
const h = riverPlazaLevels;
|
|
23
34
|
const pitch = h.roof / h.floors;
|
|
24
35
|
// The skins' colours: the window tones, and the concrete, which no window shares.
|
|
25
|
-
export const riverPlazaPalette = Object.freeze({ glass: [0x2c2c2c, 0x303030, 0x343434, 0x282828], lit: 0x8a8a8a, dim: 0x505050, concrete: 0x9a9a9a });
|
|
36
|
+
export const riverPlazaPalette = Object.freeze({ glass: [0x2c2c2c, 0x303030, 0x343434, 0x282828], lit: 0x8a8a8a, dim: 0x505050, concrete: 0x9a9a9a, frame: 0x424242 });
|
|
26
37
|
const palette = riverPlazaPalette;
|
|
27
38
|
const color = (hex) => new THREE.Color(hex);
|
|
28
39
|
const glassTones = palette.glass.map(color);
|
|
29
40
|
const litGlass = color(palette.lit), dimGlass = color(palette.dim), concrete = color(palette.concrete);
|
|
30
|
-
const core = color(0x6a6a6a), roofing = color(0x8a8a8a);
|
|
41
|
+
const core = color(0x6a6a6a), roofing = color(0x8a8a8a), frame = color(palette.frame);
|
|
31
42
|
function paneColor(row, bay, wall) {
|
|
32
43
|
const hash = (Math.imul(row + 19, 0x9e3779b1) ^ Math.imul(bay + 37, 0x85ebca77) ^ Math.imul(wall + 29, 0xc2b2ae3d)) >>> 0;
|
|
33
44
|
const value = hash % 101;
|
|
@@ -55,8 +66,9 @@ export function createRiverPlazaGeographicBuilding(record, projectPlan, offset)
|
|
|
55
66
|
// The box on the roof, on its mapped part.
|
|
56
67
|
kit.prism(box, planOf(top), [h.roof, h.box]);
|
|
57
68
|
// The slab's skin, one box to a run of a chain, held 2 cm clear of the chain's ends or 9 cm
|
|
58
|
-
// where the outline turns in:
|
|
59
|
-
//
|
|
69
|
+
// where the outline turns in: taller windows with a narrow lower transom follow
|
|
70
|
+
// dated photographs. Broad structural groups continue into the glazed
|
|
71
|
+
// recesses and corners. A run the podium stands against, its
|
|
60
72
|
// two ends on the podium's outline, starts over the podium's roof; a chain whose runs differ
|
|
61
73
|
// is skinned in parts, which meet at their joint.
|
|
62
74
|
const plan = planOf(tower);
|
|
@@ -66,11 +78,17 @@ export function createRiverPlazaGeographicBuilding(record, projectPlan, offset)
|
|
|
66
78
|
return Math.hypot(p[0] - a[0] - t * (b[0] - a[0]), p[1] - a[1] - t * (b[1] - a[1])) < 0.05;
|
|
67
79
|
});
|
|
68
80
|
const covered = (run) => onEdge(run.at(0)) && onEdge(run.at(run.length));
|
|
69
|
-
chainsOf(plan)
|
|
81
|
+
const chains = chainsOf(plan);
|
|
82
|
+
if (chains.length !== groupsPerChain.length)
|
|
83
|
+
throw new Error("River Plaza mapped facade chains changed; recheck photographed groups");
|
|
84
|
+
chains.forEach((chain, index) => {
|
|
70
85
|
const first = chain.runs[0], last = chain.runs.at(-1);
|
|
71
86
|
const s0 = turnsAt(plan, first)[0] > 0 ? 0.09 : 0.02, s1 = chain.length - (turnsAt(plan, last)[1] > 0 ? 0.09 : 0.02);
|
|
72
|
-
const count =
|
|
73
|
-
const
|
|
87
|
+
const count = groupsPerChain[index];
|
|
88
|
+
const module = chain.length / count, halfWindow = (module - h.pier) / 2;
|
|
89
|
+
if (halfWindow <= 0)
|
|
90
|
+
throw new Error("River Plaza facade group is narrower than its concrete pier");
|
|
91
|
+
const centres = Array.from({ length: count }, (_, k) => (k + 0.5) * module);
|
|
74
92
|
// The chain's parts, each a span of runs the podium covers alike.
|
|
75
93
|
const parts = [];
|
|
76
94
|
chain.runs.forEach((run, k) => { const part = parts.at(-1); if (part && covered(plan[run]) === covered(plan[chain.runs[part[0]]]))
|
|
@@ -82,18 +100,35 @@ export function createRiverPlazaGeographicBuilding(record, projectPlan, offset)
|
|
|
82
100
|
const from = k0 === 0 ? s0 : chain.starts[k0], to = k1 === chain.runs.length - 1 ? s1 : chain.starts[k1] + plan[chain.runs[k1]].length;
|
|
83
101
|
const heights = [base];
|
|
84
102
|
for (let n = Math.ceil(base / pitch); n < h.floors; n += 1) {
|
|
85
|
-
const sill = n * pitch +
|
|
103
|
+
const sill = n * pitch + h.sill;
|
|
86
104
|
if (sill > base + 0.2)
|
|
87
|
-
heights.push(sill, sill +
|
|
105
|
+
heights.push(sill, sill + h.window);
|
|
88
106
|
}
|
|
89
107
|
heights.push(h.roof);
|
|
90
|
-
const cuts = centres.flatMap((c) => [c -
|
|
108
|
+
const cuts = centres.flatMap((c) => [c - halfWindow, c + halfWindow]).filter((s) => s > from + 0.01 && s < to - 0.01);
|
|
91
109
|
const columns = [from, ...cuts, to];
|
|
92
110
|
chainSkin(kit, wall, plan, chain, k0, k1, from, to, columns, heights, 0.02, 0.07, (bay, r) => {
|
|
93
111
|
const middle = (columns[bay - 1] + columns[bay]) / 2;
|
|
94
|
-
const glazed = r % 2 === 1 && centres.some((c) => Math.abs(middle - c) <
|
|
112
|
+
const glazed = r % 2 === 1 && centres.some((c) => Math.abs(middle - c) < halfWindow);
|
|
95
113
|
return glazed ? paneColor(r, bay, index) : concrete;
|
|
96
114
|
}, concrete);
|
|
115
|
+
// A closed metal bar sits 5 mm clear of each full-height pane. Keeping
|
|
116
|
+
// the glass quad whole gives its upper and lower lights one night seed.
|
|
117
|
+
for (const centre of centres) {
|
|
118
|
+
const left = Math.max(from, centre - halfWindow), right = Math.min(to, centre + halfWindow);
|
|
119
|
+
if (right - left < 0.01)
|
|
120
|
+
continue;
|
|
121
|
+
const firstRun = chain.runs.findIndex((run, k) => k >= k0 && k <= k1 && chain.starts[k] + plan[run].length > left + 1e-6);
|
|
122
|
+
if (firstRun < 0)
|
|
123
|
+
throw new Error("River Plaza transom falls outside its facade section");
|
|
124
|
+
let lastRun = firstRun;
|
|
125
|
+
while (lastRun < k1 && chain.starts[lastRun + 1] < right - 1e-6)
|
|
126
|
+
lastRun += 1;
|
|
127
|
+
for (let row = 1; row + 1 < heights.length; row += 2) {
|
|
128
|
+
const lower = heights[row] + h.transom;
|
|
129
|
+
chainSkin(kit, wall, plan, chain, firstRun, lastRun, left, right, [left, right], [lower, lower + h.frame], 0.075, 0.09, () => frame, frame);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
97
132
|
}
|
|
98
133
|
});
|
|
99
134
|
const model = kit.finish({ height: h.box, outlines: [shell, box], opacity: 0.16 });
|