@a2f0/skyline 0.2.18 → 0.2.20
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/lib/models/blue-cross-geographic.js +99 -13
- package/lib/models/colour-materials.js +5 -3
- package/lib/models/colour-palette.js +6 -0
- package/package.json +1 -1
- package/site/models/blue-cross-geographic.js +99 -13
- package/site/models/colour-materials.js +5 -3
- package/site/models/colour-palette.js +6 -0
- package/site/skyline-3d.html +1 -1
|
@@ -38,6 +38,8 @@ const messageTopFloors = zones.map(([, , count], zone) => h.floors.slice(0, zone
|
|
|
38
38
|
+ Math.floor((count + 5) / 2) - 1).reverse().slice(0, 3);
|
|
39
39
|
export const blueCrossFloors = [...zones.flatMap(([from, to, count]) => Array.from({ length: count }, (_, i) => from + (to - from) * i / count)), h.occupied];
|
|
40
40
|
const spandrelBelow = 0.3, spandrelAbove = 0.9;
|
|
41
|
+
const infillFront = 0.52, emblemClearance = 0.1;
|
|
42
|
+
const transom = { height: 0.18, back: 0.17, front: 0.22, cornerClearance: 0.25 };
|
|
41
43
|
const color = (hex) => new THREE.Color(hex);
|
|
42
44
|
const glassTones = [0x414141, 0x464646, 0x3c3c3c, 0x4a4a4a].map(color);
|
|
43
45
|
const litGlass = color(0x8a8a8a), dimGlass = color(0x5c5c5c);
|
|
@@ -75,12 +77,30 @@ const rows = [];
|
|
|
75
77
|
push(h.roof, h.top, "screen", -1);
|
|
76
78
|
}
|
|
77
79
|
const rowsBetween = (lo, hi) => rows.filter((row) => row.lo >= lo - 1e-6 && row.hi <= hi + 1e-6);
|
|
80
|
+
// The end wings carry their curtain wall through the lower and upper bands.
|
|
81
|
+
// Only the middle level is an open setback. Subdivide the old band backing into
|
|
82
|
+
// normal glazing/spandrel courses on the wings and exposed short block returns.
|
|
83
|
+
// FID-BCBS-005: 2012 oblique photographs, docs/blue-cross-reference.md.
|
|
84
|
+
const glazedBandRows = (cuts) => cuts.flatMap((row) => {
|
|
85
|
+
if (row.kind !== "band")
|
|
86
|
+
return [row];
|
|
87
|
+
const band = h.bands.find(([, hi]) => hi === row.hi);
|
|
88
|
+
const count = band === h.bands[1] ? 3 : 2;
|
|
89
|
+
return Array.from({ length: count }, (_, i) => {
|
|
90
|
+
const lo = row.lo + (row.hi - row.lo) * i / count;
|
|
91
|
+
const hi = row.lo + (row.hi - row.lo) * (i + 1) / count;
|
|
92
|
+
return [{ lo, hi: lo + 0.8, kind: "spandrel", floor: -1 }, { lo: lo + 0.8, hi, kind: "glass", floor: -1 }];
|
|
93
|
+
}).flat();
|
|
94
|
+
});
|
|
78
95
|
export function createBlueCrossGeographicBuilding(record, projectPlan, offset) {
|
|
79
96
|
const kit = createBuilder(record.name, record.id);
|
|
80
97
|
const shell = kit.batch("Blue Cross · shell", kit.material(0xffffff, { vertexColors: true }));
|
|
81
98
|
const wall = kit.batch("Blue Cross · glass, spandrels and bands", kit.material(0xffffff, { vertexColors: true }));
|
|
82
99
|
const mullions = kit.batch("Blue Cross · mullions", kit.material(0x2c2c2c));
|
|
83
100
|
const bars = kit.batch("Blue Cross · band columns and emblems", kit.material(0xb4b4b4));
|
|
101
|
+
const infill = kit.batch("Blue Cross · band infill", kit.material(0xffffff, { vertexColors: true }));
|
|
102
|
+
const transoms = kit.batch("Blue Cross · transoms", kit.material(0xb4b4b4));
|
|
103
|
+
const aluminium = color(0xb4b4b4);
|
|
84
104
|
const windows = [];
|
|
85
105
|
let windowColumns = 0;
|
|
86
106
|
const outline = (way) => orient(polygonOf(projectPlan(record.parts.find((part) => part.way === way).coordinates)));
|
|
@@ -130,10 +150,13 @@ export function createBlueCrossGeographicBuilding(record, projectPlan, offset) {
|
|
|
130
150
|
// every row, and mullions one module apart on the office floors. Each unbroken stretch of
|
|
131
151
|
// kept runs along a chain is one box, held 2 cm clear of its ends.
|
|
132
152
|
const skin = (corners, lo, hi, seed, keep, lift = 0) => {
|
|
133
|
-
const plan = planOf(corners)
|
|
134
|
-
const heights = [cuts[0].lo + lift, ...cuts.map((row) => row.hi)];
|
|
153
|
+
const plan = planOf(corners);
|
|
135
154
|
const top = Math.min(hi, h.roof) - 0.05;
|
|
136
155
|
chainsOf(plan).forEach((chain, index) => {
|
|
156
|
+
const shortFace = Math.abs(plan[chain.runs[0]].normal(0)[0]) > 0.9;
|
|
157
|
+
const sideGlazing = seed !== 0 || shortFace;
|
|
158
|
+
const cuts = sideGlazing ? glazedBandRows(rowsBetween(lo, hi)) : rowsBetween(lo, hi);
|
|
159
|
+
const heights = [cuts[0].lo + lift, ...cuts.map((row) => row.hi)];
|
|
137
160
|
const count = Math.max(1, Math.round(chain.length / h.module));
|
|
138
161
|
const stations = Array.from({ length: count + 1 }, (_, i) => chain.length * i / count);
|
|
139
162
|
const messageFace = seed === 0 && plan[chain.runs[0]].normal(0)[1] > 0.9 && chain.length > 50;
|
|
@@ -160,6 +183,21 @@ export function createBlueCrossGeographicBuilding(record, projectPlan, offset) {
|
|
|
160
183
|
return screen;
|
|
161
184
|
return spandrel;
|
|
162
185
|
}, spandrel);
|
|
186
|
+
if (sideGlazing) {
|
|
187
|
+
// Close each rail as a thin box following the mapped chain. Keep rails
|
|
188
|
+
// off the skin's top/bottom planes and ahead of its vertical mullions.
|
|
189
|
+
// Leave corner clearance for rails on adjoining/reentrant faces.
|
|
190
|
+
const railStart = s0 + transom.cornerClearance, railEnd = s1 - transom.cornerClearance;
|
|
191
|
+
const railK0 = chain.starts.findLastIndex((s) => s <= railStart);
|
|
192
|
+
const railK1 = chain.starts.findLastIndex((s) => s < railEnd);
|
|
193
|
+
for (const row of cuts.filter(({ kind }) => kind === "glass")) {
|
|
194
|
+
for (const [bottom, top] of [[row.lo - transom.height, row.lo], [row.hi, row.hi + transom.height]]) {
|
|
195
|
+
if (railEnd <= railStart || bottom <= heights[0] || top >= hi)
|
|
196
|
+
continue;
|
|
197
|
+
chainSkin(kit, transoms, plan, chain, railK0, railK1, railStart, railEnd, [], [bottom, top], transom.back, transom.front, () => aluminium, aluminium);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
163
201
|
// Mullions on each office run, whole on one run and held clear of its ends. Their
|
|
164
202
|
// backs stand a centimetre off the wall: on the block's side walls the middle band's
|
|
165
203
|
// mullions start just inside the end bays, whose closing walls lie in that plane and
|
|
@@ -197,20 +235,22 @@ export function createBlueCrossGeographicBuilding(record, projectPlan, offset) {
|
|
|
197
235
|
skin(bay, h.lobbyTop, middle[0], i + 1, (run) => !lies(run, block));
|
|
198
236
|
skin(bay, middle[1], h.wings, i + 1, (run) => !lies(run, block));
|
|
199
237
|
});
|
|
200
|
-
// An emblem: a plate standing
|
|
238
|
+
// An emblem: a plate standing ahead of the screen infill, cut to an outline given across and
|
|
201
239
|
// up from its centre. Its faces fan from the centre, which sees the whole outline, and its
|
|
202
240
|
// sides follow the outline's edges.
|
|
203
241
|
const emblem = (centre, t, n, y, shape) => {
|
|
242
|
+
// The corrected screen panels stand ahead of the old backing plane.
|
|
204
243
|
const point = ([u, v], depth) => [centre[0] + t[0] * u + n[0] * depth, y + v, centre[1] + t[1] * u + n[1] * depth];
|
|
244
|
+
const backDepth = infillFront + emblemClearance, frontDepth = backDepth + 0.23;
|
|
205
245
|
const out = [n[0], 0, n[1]], back = [-n[0], 0, -n[1]];
|
|
206
246
|
shape.forEach((p, i) => {
|
|
207
247
|
const q = shape[(i + 1) % shape.length];
|
|
208
|
-
kit.triangle(bars, [point([0, 0],
|
|
209
|
-
kit.triangle(bars, [point([0, 0],
|
|
248
|
+
kit.triangle(bars, [point([0, 0], frontDepth), point(p, frontDepth), point(q, frontDepth)], [out, out, out]);
|
|
249
|
+
kit.triangle(bars, [point([0, 0], backDepth), point(q, backDepth), point(p, backDepth)], [back, back, back]);
|
|
210
250
|
// The edge's side faces away from the centre.
|
|
211
251
|
const sign = Math.sign((q[1] - p[1]) * (p[0] + q[0]) - (q[0] - p[0]) * (p[1] + q[1])), length = Math.hypot(q[0] - p[0], q[1] - p[1]);
|
|
212
252
|
const [across, up] = [sign * (q[1] - p[1]) / length, -sign * (q[0] - p[0]) / length];
|
|
213
|
-
kit.quad(bars, [point(p,
|
|
253
|
+
kit.quad(bars, [point(p, backDepth), point(q, backDepth), point(q, frontDepth), point(p, frontDepth)], [[t[0] * across, up, t[1] * across]]);
|
|
214
254
|
});
|
|
215
255
|
};
|
|
216
256
|
// The company's emblems, 4.2 m tall: a cross, its arms a third of its span, and a shield,
|
|
@@ -218,8 +258,8 @@ export function createBlueCrossGeographicBuilding(record, projectPlan, offset) {
|
|
|
218
258
|
const cross = [[0.7, 2.1], [0.7, 0.7], [2.1, 0.7], [2.1, -0.7], [0.7, -0.7], [0.7, -2.1],
|
|
219
259
|
[-0.7, -2.1], [-0.7, -0.7], [-2.1, -0.7], [-2.1, 0.7], [-0.7, 0.7], [-0.7, 2.1]];
|
|
220
260
|
const shield = [[-1.8, 2.1], [1.8, 2.1], [1.8, 0.3], [1.6, -0.7], [1.0, -1.5], [0, -2.1], [-1.0, -1.5], [-1.6, -0.7], [-1.8, 0.3]];
|
|
221
|
-
// The block's columns, exposed in each band and in the screen on its long faces:
|
|
222
|
-
//
|
|
261
|
+
// The block's columns, exposed in each band and in the screen on its long faces:
|
|
262
|
+
// eight south, six north. The emblems stand in the screen's first two south
|
|
223
263
|
// bays at the south face's west end, the cross west of the shield.
|
|
224
264
|
const faces = chainsOf(planOf(block)).filter(({ length }) => length > 50);
|
|
225
265
|
faces.forEach((chain) => {
|
|
@@ -227,14 +267,60 @@ export function createBlueCrossGeographicBuilding(record, projectPlan, offset) {
|
|
|
227
267
|
const a = run.at(0), b = last.at(last.length), n = run.normal(0), length = Math.hypot(b[0] - a[0], b[1] - a[1]);
|
|
228
268
|
const t = [(b[0] - a[0]) / length, (b[1] - a[1]) / length];
|
|
229
269
|
const at = (s) => [a[0] + t[0] * s, a[1] + t[1] * s];
|
|
230
|
-
const pitch = length /
|
|
231
|
-
for (let i = 0; i <
|
|
270
|
+
const slotCount = n[1] > 0.9 ? 8 : 6, pitch = length / slotCount;
|
|
271
|
+
for (let i = 0; i < slotCount; i += 1) {
|
|
232
272
|
for (const [lo, hi] of [...h.bands, [h.roof + 0.4, h.top - 0.3]])
|
|
233
273
|
kit.box(bars, at((i + 0.5) * pitch), n, 0.45, 0.07, 0.4, lo + 0.1, hi - 0.1);
|
|
234
274
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
275
|
+
if (Math.abs(n[1]) > 0.9) {
|
|
276
|
+
// FID-BCBS-002/005: the long elevations have narrow column slots,
|
|
277
|
+
// with curtain-wall panels between them, not an uninterrupted dark band.
|
|
278
|
+
// The northeast photograph resolves six on the north crown. The five
|
|
279
|
+
// internal north atrium bays alone do not establish facade spacing.
|
|
280
|
+
const panel = (target, s0, s1, levels, columns, back, front, paint, sides) => {
|
|
281
|
+
const k0 = chain.starts.findLastIndex((s) => s <= s0);
|
|
282
|
+
const k1 = chain.starts.findLastIndex((s) => s < s1);
|
|
283
|
+
chainSkin(kit, target, plan, chain, k0, k1, s0, s1, columns, levels, back, front, paint, sides);
|
|
284
|
+
};
|
|
285
|
+
// Two, three and two glazed rows in the mechanical bands; nine shallow
|
|
286
|
+
// courses in the crown. The 1.9 m slots and thin framing are photo ratios,
|
|
287
|
+
// not surveyed dimensions (docs/blue-cross-reference.md).
|
|
288
|
+
const bands = [
|
|
289
|
+
...h.bands.map(([lo, hi], i) => [lo, hi, i === 1 ? 3 : 2, false]),
|
|
290
|
+
[h.roof, h.top, 9, true],
|
|
291
|
+
];
|
|
292
|
+
for (const [lo, hi, count, crown] of bands) {
|
|
293
|
+
const levels = [lo + 0.02], paints = [];
|
|
294
|
+
const append = (height, paint) => { levels.push(height); paints.push(paint); };
|
|
295
|
+
append(lo + 0.18, aluminium);
|
|
296
|
+
for (let row = 1; row < count; row += 1) {
|
|
297
|
+
const y = lo + (hi - lo) * row / count;
|
|
298
|
+
append(y - (crown ? 0.08 : 0.55), crown ? screen : glassTones[1]);
|
|
299
|
+
if (!crown) {
|
|
300
|
+
append(y - 0.4, aluminium);
|
|
301
|
+
append(y + 0.4, spandrel);
|
|
302
|
+
}
|
|
303
|
+
append(y + (crown ? 0.08 : 0.55), aluminium);
|
|
304
|
+
}
|
|
305
|
+
append(hi - 0.18, crown ? screen : glassTones[1]);
|
|
306
|
+
append(hi - 0.02, aluminium);
|
|
307
|
+
for (let bay = 0; bay <= slotCount; bay += 1) {
|
|
308
|
+
const s0 = bay === 0 ? 0.02 : (bay - 0.5) * pitch + 0.95;
|
|
309
|
+
const s1 = bay === slotCount ? chain.length - 0.02 : (bay + 0.5) * pitch - 0.95;
|
|
310
|
+
panel(infill, s0, s1, levels, [s0, s0 + 0.12, s1 - 0.12, s1], 0.2, infillFront, (cell, row) => cell === 1 || cell === 3 ? aluminium : paints[row], aluminium);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
// Pale rails frame the office ribbons. They sit just ahead of the vertical
|
|
314
|
+
// mullions and never cross the exposed column slots or celebration panes.
|
|
315
|
+
for (const row of rows.filter(({ kind }) => kind === "glass")) {
|
|
316
|
+
for (const [lo, hi] of [[row.lo - transom.height, row.lo], [row.hi, row.hi + transom.height]]) {
|
|
317
|
+
panel(transoms, 0.02, chain.length - 0.02, [lo, hi], [], transom.back, transom.front, () => aluminium, aluminium);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
// The south face runs west to east; the emblems stand in its first two bays.
|
|
321
|
+
if (n[1] > 0.9)
|
|
322
|
+
[cross, shield].forEach((shape, i) => emblem(at((i + 1) * pitch), t, n, h.roof + 6.7, shape));
|
|
323
|
+
}
|
|
238
324
|
});
|
|
239
325
|
const model = kit.finish({ height: h.top, outlines: [shell, mullions], opacity: 0.16 });
|
|
240
326
|
const glass = model.building.getObjectByName(wall.name);
|
|
@@ -113,10 +113,12 @@ export const colourMaterials = {
|
|
|
113
113
|
"antenna mast": ["stainless"],
|
|
114
114
|
},
|
|
115
115
|
"building-blue-cross-blue-shield": {
|
|
116
|
-
shell: ["glass", { 0x3a3a3a: "neutral" }],
|
|
117
|
-
"glass, spandrels and bands": ["glass", { ...windows(0x8a8a8a, 0x5c5c5c), 0x2a2a2a: "neutral", 0x3e3e3e: "screen" }],
|
|
116
|
+
shell: ["glass", { 0x3a3a3a: "neutral", 0x383838: "opaque spandrel" }],
|
|
117
|
+
"glass, spandrels and bands": ["glass", { ...windows(0x8a8a8a, 0x5c5c5c), 0x383838: "opaque spandrel", 0x2a2a2a: "neutral", 0x3e3e3e: "screen" }],
|
|
118
118
|
mullions: ["dark metal"],
|
|
119
119
|
"band columns and emblems": ["band lights"],
|
|
120
|
+
"band infill": ["glass", { 0x383838: "opaque spandrel", 0x3e3e3e: "screen", 0xb4b4b4: "aluminium" }],
|
|
121
|
+
transoms: ["aluminium"],
|
|
120
122
|
},
|
|
121
123
|
// Blue-green glass, and the south face's white concrete frame.
|
|
122
124
|
"building-340-on-the-park": {
|
|
@@ -271,7 +273,7 @@ export function batchOf(buildingId, meshName) {
|
|
|
271
273
|
}
|
|
272
274
|
// A grey's tone: the hex the model wrote it as, from its linear channel.
|
|
273
275
|
export const toneOf = (grey) => new THREE.Color(grey, grey, grey).getHex();
|
|
274
|
-
// Historical comparisons below predate sky reflections
|
|
276
|
+
// Historical comparisons below predate sky reflections, FID-KEM-002/003 and FID-BCBS-003.
|
|
275
277
|
// The measured colours scale to the scene by this factor in linear light, so the skyline view's
|
|
276
278
|
// faces show about the photograph's brightness under the day's lights. At 1.4 the skyline view's
|
|
277
279
|
// 1600 x 900 render, its pixels chosen by each photograph sample's rule, reaches 0.97 (Two
|
|
@@ -62,8 +62,14 @@ export const measuredColours = {
|
|
|
62
62
|
// docs/blue-cross-reference.md, 2026-10-09 — Daytime colours from Chicago.jpg and a close-up.
|
|
63
63
|
"building-blue-cross-blue-shield": {
|
|
64
64
|
...glass(0x7d8992),
|
|
65
|
+
// FID-BCBS-003: the panorama mixes panes and opaque spandrels. Retain its
|
|
66
|
+
// measured hue provisionally for both, keeping their grey contrast and
|
|
67
|
+
// separate reflection masks through the shared glass swatch.
|
|
68
|
+
"opaque spandrel": "glass",
|
|
65
69
|
screen: 0x334a60,
|
|
66
70
|
"band lights": 0xb0bdcd,
|
|
71
|
+
// Inferred same pale metal as the columns; independent calibration is open.
|
|
72
|
+
aluminium: "band lights",
|
|
67
73
|
"dark metal": null,
|
|
68
74
|
neutral: null,
|
|
69
75
|
},
|
package/package.json
CHANGED
|
@@ -38,6 +38,8 @@ const messageTopFloors = zones.map(([, , count], zone) => h.floors.slice(0, zone
|
|
|
38
38
|
+ Math.floor((count + 5) / 2) - 1).reverse().slice(0, 3);
|
|
39
39
|
export const blueCrossFloors = [...zones.flatMap(([from, to, count]) => Array.from({ length: count }, (_, i) => from + (to - from) * i / count)), h.occupied];
|
|
40
40
|
const spandrelBelow = 0.3, spandrelAbove = 0.9;
|
|
41
|
+
const infillFront = 0.52, emblemClearance = 0.1;
|
|
42
|
+
const transom = { height: 0.18, back: 0.17, front: 0.22, cornerClearance: 0.25 };
|
|
41
43
|
const color = (hex) => new THREE.Color(hex);
|
|
42
44
|
const glassTones = [0x414141, 0x464646, 0x3c3c3c, 0x4a4a4a].map(color);
|
|
43
45
|
const litGlass = color(0x8a8a8a), dimGlass = color(0x5c5c5c);
|
|
@@ -75,12 +77,30 @@ const rows = [];
|
|
|
75
77
|
push(h.roof, h.top, "screen", -1);
|
|
76
78
|
}
|
|
77
79
|
const rowsBetween = (lo, hi) => rows.filter((row) => row.lo >= lo - 1e-6 && row.hi <= hi + 1e-6);
|
|
80
|
+
// The end wings carry their curtain wall through the lower and upper bands.
|
|
81
|
+
// Only the middle level is an open setback. Subdivide the old band backing into
|
|
82
|
+
// normal glazing/spandrel courses on the wings and exposed short block returns.
|
|
83
|
+
// FID-BCBS-005: 2012 oblique photographs, docs/blue-cross-reference.md.
|
|
84
|
+
const glazedBandRows = (cuts) => cuts.flatMap((row) => {
|
|
85
|
+
if (row.kind !== "band")
|
|
86
|
+
return [row];
|
|
87
|
+
const band = h.bands.find(([, hi]) => hi === row.hi);
|
|
88
|
+
const count = band === h.bands[1] ? 3 : 2;
|
|
89
|
+
return Array.from({ length: count }, (_, i) => {
|
|
90
|
+
const lo = row.lo + (row.hi - row.lo) * i / count;
|
|
91
|
+
const hi = row.lo + (row.hi - row.lo) * (i + 1) / count;
|
|
92
|
+
return [{ lo, hi: lo + 0.8, kind: "spandrel", floor: -1 }, { lo: lo + 0.8, hi, kind: "glass", floor: -1 }];
|
|
93
|
+
}).flat();
|
|
94
|
+
});
|
|
78
95
|
export function createBlueCrossGeographicBuilding(record, projectPlan, offset) {
|
|
79
96
|
const kit = createBuilder(record.name, record.id);
|
|
80
97
|
const shell = kit.batch("Blue Cross · shell", kit.material(0xffffff, { vertexColors: true }));
|
|
81
98
|
const wall = kit.batch("Blue Cross · glass, spandrels and bands", kit.material(0xffffff, { vertexColors: true }));
|
|
82
99
|
const mullions = kit.batch("Blue Cross · mullions", kit.material(0x2c2c2c));
|
|
83
100
|
const bars = kit.batch("Blue Cross · band columns and emblems", kit.material(0xb4b4b4));
|
|
101
|
+
const infill = kit.batch("Blue Cross · band infill", kit.material(0xffffff, { vertexColors: true }));
|
|
102
|
+
const transoms = kit.batch("Blue Cross · transoms", kit.material(0xb4b4b4));
|
|
103
|
+
const aluminium = color(0xb4b4b4);
|
|
84
104
|
const windows = [];
|
|
85
105
|
let windowColumns = 0;
|
|
86
106
|
const outline = (way) => orient(polygonOf(projectPlan(record.parts.find((part) => part.way === way).coordinates)));
|
|
@@ -130,10 +150,13 @@ export function createBlueCrossGeographicBuilding(record, projectPlan, offset) {
|
|
|
130
150
|
// every row, and mullions one module apart on the office floors. Each unbroken stretch of
|
|
131
151
|
// kept runs along a chain is one box, held 2 cm clear of its ends.
|
|
132
152
|
const skin = (corners, lo, hi, seed, keep, lift = 0) => {
|
|
133
|
-
const plan = planOf(corners)
|
|
134
|
-
const heights = [cuts[0].lo + lift, ...cuts.map((row) => row.hi)];
|
|
153
|
+
const plan = planOf(corners);
|
|
135
154
|
const top = Math.min(hi, h.roof) - 0.05;
|
|
136
155
|
chainsOf(plan).forEach((chain, index) => {
|
|
156
|
+
const shortFace = Math.abs(plan[chain.runs[0]].normal(0)[0]) > 0.9;
|
|
157
|
+
const sideGlazing = seed !== 0 || shortFace;
|
|
158
|
+
const cuts = sideGlazing ? glazedBandRows(rowsBetween(lo, hi)) : rowsBetween(lo, hi);
|
|
159
|
+
const heights = [cuts[0].lo + lift, ...cuts.map((row) => row.hi)];
|
|
137
160
|
const count = Math.max(1, Math.round(chain.length / h.module));
|
|
138
161
|
const stations = Array.from({ length: count + 1 }, (_, i) => chain.length * i / count);
|
|
139
162
|
const messageFace = seed === 0 && plan[chain.runs[0]].normal(0)[1] > 0.9 && chain.length > 50;
|
|
@@ -160,6 +183,21 @@ export function createBlueCrossGeographicBuilding(record, projectPlan, offset) {
|
|
|
160
183
|
return screen;
|
|
161
184
|
return spandrel;
|
|
162
185
|
}, spandrel);
|
|
186
|
+
if (sideGlazing) {
|
|
187
|
+
// Close each rail as a thin box following the mapped chain. Keep rails
|
|
188
|
+
// off the skin's top/bottom planes and ahead of its vertical mullions.
|
|
189
|
+
// Leave corner clearance for rails on adjoining/reentrant faces.
|
|
190
|
+
const railStart = s0 + transom.cornerClearance, railEnd = s1 - transom.cornerClearance;
|
|
191
|
+
const railK0 = chain.starts.findLastIndex((s) => s <= railStart);
|
|
192
|
+
const railK1 = chain.starts.findLastIndex((s) => s < railEnd);
|
|
193
|
+
for (const row of cuts.filter(({ kind }) => kind === "glass")) {
|
|
194
|
+
for (const [bottom, top] of [[row.lo - transom.height, row.lo], [row.hi, row.hi + transom.height]]) {
|
|
195
|
+
if (railEnd <= railStart || bottom <= heights[0] || top >= hi)
|
|
196
|
+
continue;
|
|
197
|
+
chainSkin(kit, transoms, plan, chain, railK0, railK1, railStart, railEnd, [], [bottom, top], transom.back, transom.front, () => aluminium, aluminium);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
163
201
|
// Mullions on each office run, whole on one run and held clear of its ends. Their
|
|
164
202
|
// backs stand a centimetre off the wall: on the block's side walls the middle band's
|
|
165
203
|
// mullions start just inside the end bays, whose closing walls lie in that plane and
|
|
@@ -197,20 +235,22 @@ export function createBlueCrossGeographicBuilding(record, projectPlan, offset) {
|
|
|
197
235
|
skin(bay, h.lobbyTop, middle[0], i + 1, (run) => !lies(run, block));
|
|
198
236
|
skin(bay, middle[1], h.wings, i + 1, (run) => !lies(run, block));
|
|
199
237
|
});
|
|
200
|
-
// An emblem: a plate standing
|
|
238
|
+
// An emblem: a plate standing ahead of the screen infill, cut to an outline given across and
|
|
201
239
|
// up from its centre. Its faces fan from the centre, which sees the whole outline, and its
|
|
202
240
|
// sides follow the outline's edges.
|
|
203
241
|
const emblem = (centre, t, n, y, shape) => {
|
|
242
|
+
// The corrected screen panels stand ahead of the old backing plane.
|
|
204
243
|
const point = ([u, v], depth) => [centre[0] + t[0] * u + n[0] * depth, y + v, centre[1] + t[1] * u + n[1] * depth];
|
|
244
|
+
const backDepth = infillFront + emblemClearance, frontDepth = backDepth + 0.23;
|
|
205
245
|
const out = [n[0], 0, n[1]], back = [-n[0], 0, -n[1]];
|
|
206
246
|
shape.forEach((p, i) => {
|
|
207
247
|
const q = shape[(i + 1) % shape.length];
|
|
208
|
-
kit.triangle(bars, [point([0, 0],
|
|
209
|
-
kit.triangle(bars, [point([0, 0],
|
|
248
|
+
kit.triangle(bars, [point([0, 0], frontDepth), point(p, frontDepth), point(q, frontDepth)], [out, out, out]);
|
|
249
|
+
kit.triangle(bars, [point([0, 0], backDepth), point(q, backDepth), point(p, backDepth)], [back, back, back]);
|
|
210
250
|
// The edge's side faces away from the centre.
|
|
211
251
|
const sign = Math.sign((q[1] - p[1]) * (p[0] + q[0]) - (q[0] - p[0]) * (p[1] + q[1])), length = Math.hypot(q[0] - p[0], q[1] - p[1]);
|
|
212
252
|
const [across, up] = [sign * (q[1] - p[1]) / length, -sign * (q[0] - p[0]) / length];
|
|
213
|
-
kit.quad(bars, [point(p,
|
|
253
|
+
kit.quad(bars, [point(p, backDepth), point(q, backDepth), point(q, frontDepth), point(p, frontDepth)], [[t[0] * across, up, t[1] * across]]);
|
|
214
254
|
});
|
|
215
255
|
};
|
|
216
256
|
// The company's emblems, 4.2 m tall: a cross, its arms a third of its span, and a shield,
|
|
@@ -218,8 +258,8 @@ export function createBlueCrossGeographicBuilding(record, projectPlan, offset) {
|
|
|
218
258
|
const cross = [[0.7, 2.1], [0.7, 0.7], [2.1, 0.7], [2.1, -0.7], [0.7, -0.7], [0.7, -2.1],
|
|
219
259
|
[-0.7, -2.1], [-0.7, -0.7], [-2.1, -0.7], [-2.1, 0.7], [-0.7, 0.7], [-0.7, 2.1]];
|
|
220
260
|
const shield = [[-1.8, 2.1], [1.8, 2.1], [1.8, 0.3], [1.6, -0.7], [1.0, -1.5], [0, -2.1], [-1.0, -1.5], [-1.6, -0.7], [-1.8, 0.3]];
|
|
221
|
-
// The block's columns, exposed in each band and in the screen on its long faces:
|
|
222
|
-
//
|
|
261
|
+
// The block's columns, exposed in each band and in the screen on its long faces:
|
|
262
|
+
// eight south, six north. The emblems stand in the screen's first two south
|
|
223
263
|
// bays at the south face's west end, the cross west of the shield.
|
|
224
264
|
const faces = chainsOf(planOf(block)).filter(({ length }) => length > 50);
|
|
225
265
|
faces.forEach((chain) => {
|
|
@@ -227,14 +267,60 @@ export function createBlueCrossGeographicBuilding(record, projectPlan, offset) {
|
|
|
227
267
|
const a = run.at(0), b = last.at(last.length), n = run.normal(0), length = Math.hypot(b[0] - a[0], b[1] - a[1]);
|
|
228
268
|
const t = [(b[0] - a[0]) / length, (b[1] - a[1]) / length];
|
|
229
269
|
const at = (s) => [a[0] + t[0] * s, a[1] + t[1] * s];
|
|
230
|
-
const pitch = length /
|
|
231
|
-
for (let i = 0; i <
|
|
270
|
+
const slotCount = n[1] > 0.9 ? 8 : 6, pitch = length / slotCount;
|
|
271
|
+
for (let i = 0; i < slotCount; i += 1) {
|
|
232
272
|
for (const [lo, hi] of [...h.bands, [h.roof + 0.4, h.top - 0.3]])
|
|
233
273
|
kit.box(bars, at((i + 0.5) * pitch), n, 0.45, 0.07, 0.4, lo + 0.1, hi - 0.1);
|
|
234
274
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
275
|
+
if (Math.abs(n[1]) > 0.9) {
|
|
276
|
+
// FID-BCBS-002/005: the long elevations have narrow column slots,
|
|
277
|
+
// with curtain-wall panels between them, not an uninterrupted dark band.
|
|
278
|
+
// The northeast photograph resolves six on the north crown. The five
|
|
279
|
+
// internal north atrium bays alone do not establish facade spacing.
|
|
280
|
+
const panel = (target, s0, s1, levels, columns, back, front, paint, sides) => {
|
|
281
|
+
const k0 = chain.starts.findLastIndex((s) => s <= s0);
|
|
282
|
+
const k1 = chain.starts.findLastIndex((s) => s < s1);
|
|
283
|
+
chainSkin(kit, target, plan, chain, k0, k1, s0, s1, columns, levels, back, front, paint, sides);
|
|
284
|
+
};
|
|
285
|
+
// Two, three and two glazed rows in the mechanical bands; nine shallow
|
|
286
|
+
// courses in the crown. The 1.9 m slots and thin framing are photo ratios,
|
|
287
|
+
// not surveyed dimensions (docs/blue-cross-reference.md).
|
|
288
|
+
const bands = [
|
|
289
|
+
...h.bands.map(([lo, hi], i) => [lo, hi, i === 1 ? 3 : 2, false]),
|
|
290
|
+
[h.roof, h.top, 9, true],
|
|
291
|
+
];
|
|
292
|
+
for (const [lo, hi, count, crown] of bands) {
|
|
293
|
+
const levels = [lo + 0.02], paints = [];
|
|
294
|
+
const append = (height, paint) => { levels.push(height); paints.push(paint); };
|
|
295
|
+
append(lo + 0.18, aluminium);
|
|
296
|
+
for (let row = 1; row < count; row += 1) {
|
|
297
|
+
const y = lo + (hi - lo) * row / count;
|
|
298
|
+
append(y - (crown ? 0.08 : 0.55), crown ? screen : glassTones[1]);
|
|
299
|
+
if (!crown) {
|
|
300
|
+
append(y - 0.4, aluminium);
|
|
301
|
+
append(y + 0.4, spandrel);
|
|
302
|
+
}
|
|
303
|
+
append(y + (crown ? 0.08 : 0.55), aluminium);
|
|
304
|
+
}
|
|
305
|
+
append(hi - 0.18, crown ? screen : glassTones[1]);
|
|
306
|
+
append(hi - 0.02, aluminium);
|
|
307
|
+
for (let bay = 0; bay <= slotCount; bay += 1) {
|
|
308
|
+
const s0 = bay === 0 ? 0.02 : (bay - 0.5) * pitch + 0.95;
|
|
309
|
+
const s1 = bay === slotCount ? chain.length - 0.02 : (bay + 0.5) * pitch - 0.95;
|
|
310
|
+
panel(infill, s0, s1, levels, [s0, s0 + 0.12, s1 - 0.12, s1], 0.2, infillFront, (cell, row) => cell === 1 || cell === 3 ? aluminium : paints[row], aluminium);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
// Pale rails frame the office ribbons. They sit just ahead of the vertical
|
|
314
|
+
// mullions and never cross the exposed column slots or celebration panes.
|
|
315
|
+
for (const row of rows.filter(({ kind }) => kind === "glass")) {
|
|
316
|
+
for (const [lo, hi] of [[row.lo - transom.height, row.lo], [row.hi, row.hi + transom.height]]) {
|
|
317
|
+
panel(transoms, 0.02, chain.length - 0.02, [lo, hi], [], transom.back, transom.front, () => aluminium, aluminium);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
// The south face runs west to east; the emblems stand in its first two bays.
|
|
321
|
+
if (n[1] > 0.9)
|
|
322
|
+
[cross, shield].forEach((shape, i) => emblem(at((i + 1) * pitch), t, n, h.roof + 6.7, shape));
|
|
323
|
+
}
|
|
238
324
|
});
|
|
239
325
|
const model = kit.finish({ height: h.top, outlines: [shell, mullions], opacity: 0.16 });
|
|
240
326
|
const glass = model.building.getObjectByName(wall.name);
|
|
@@ -113,10 +113,12 @@ export const colourMaterials = {
|
|
|
113
113
|
"antenna mast": ["stainless"],
|
|
114
114
|
},
|
|
115
115
|
"building-blue-cross-blue-shield": {
|
|
116
|
-
shell: ["glass", { 0x3a3a3a: "neutral" }],
|
|
117
|
-
"glass, spandrels and bands": ["glass", { ...windows(0x8a8a8a, 0x5c5c5c), 0x2a2a2a: "neutral", 0x3e3e3e: "screen" }],
|
|
116
|
+
shell: ["glass", { 0x3a3a3a: "neutral", 0x383838: "opaque spandrel" }],
|
|
117
|
+
"glass, spandrels and bands": ["glass", { ...windows(0x8a8a8a, 0x5c5c5c), 0x383838: "opaque spandrel", 0x2a2a2a: "neutral", 0x3e3e3e: "screen" }],
|
|
118
118
|
mullions: ["dark metal"],
|
|
119
119
|
"band columns and emblems": ["band lights"],
|
|
120
|
+
"band infill": ["glass", { 0x383838: "opaque spandrel", 0x3e3e3e: "screen", 0xb4b4b4: "aluminium" }],
|
|
121
|
+
transoms: ["aluminium"],
|
|
120
122
|
},
|
|
121
123
|
// Blue-green glass, and the south face's white concrete frame.
|
|
122
124
|
"building-340-on-the-park": {
|
|
@@ -271,7 +273,7 @@ export function batchOf(buildingId, meshName) {
|
|
|
271
273
|
}
|
|
272
274
|
// A grey's tone: the hex the model wrote it as, from its linear channel.
|
|
273
275
|
export const toneOf = (grey) => new THREE.Color(grey, grey, grey).getHex();
|
|
274
|
-
// Historical comparisons below predate sky reflections
|
|
276
|
+
// Historical comparisons below predate sky reflections, FID-KEM-002/003 and FID-BCBS-003.
|
|
275
277
|
// The measured colours scale to the scene by this factor in linear light, so the skyline view's
|
|
276
278
|
// faces show about the photograph's brightness under the day's lights. At 1.4 the skyline view's
|
|
277
279
|
// 1600 x 900 render, its pixels chosen by each photograph sample's rule, reaches 0.97 (Two
|
|
@@ -62,8 +62,14 @@ export const measuredColours = {
|
|
|
62
62
|
// docs/blue-cross-reference.md, 2026-10-09 — Daytime colours from Chicago.jpg and a close-up.
|
|
63
63
|
"building-blue-cross-blue-shield": {
|
|
64
64
|
...glass(0x7d8992),
|
|
65
|
+
// FID-BCBS-003: the panorama mixes panes and opaque spandrels. Retain its
|
|
66
|
+
// measured hue provisionally for both, keeping their grey contrast and
|
|
67
|
+
// separate reflection masks through the shared glass swatch.
|
|
68
|
+
"opaque spandrel": "glass",
|
|
65
69
|
screen: 0x334a60,
|
|
66
70
|
"band lights": 0xb0bdcd,
|
|
71
|
+
// Inferred same pale metal as the columns; independent calibration is open.
|
|
72
|
+
aluminium: "band lights",
|
|
67
73
|
"dark metal": null,
|
|
68
74
|
neutral: null,
|
|
69
75
|
},
|
package/site/skyline-3d.html
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
<span class="visually-hidden">Preparing the skyline…</span>
|
|
17
17
|
<!-- skyline-loading:start -->
|
|
18
18
|
<svg class="skyline-trace" viewBox="0 0 1000 390.01" aria-hidden="true" focusable="false">
|
|
19
|
-
<defs><path id="skyline-loading-outline" pathLength="1" d="M0 390.01 L24 390.01 L24 326.71 L36.78 320.28 L76.29 320.28 L78.85 321.63 L79.08 320.54 L87.68 320.54 L87.92 316.21 L133.24 316.74 L133.47 317.75 L133.94 317.75 L134.17 316.74 L164.38 316.74 L164.85 317.75 L165.08 321.6 L181.12 321.6 L181.35 329.03 L189.48 329.03 L190.41 330.18 L207.85 330.18 L208.08 328.36 L208.54 328.36 L209.01 331.14 L212.26 338.16 L212.49 334.77 L213.89 334.77 L214.12 344.27 L216.21 344.27 L216.45 348.52 L234.57 348.52 L234.81 318.86 L238.53 318.86 L238.76 308.7 L242.48 308.7 L242.71 280.86 L244.57 280.86 L244.8 273.62 L245.03 272.91 L245.73 272.91 L245.96 272.02 L249.68 272.02 L249.91 273.17 L251.54 273.17 L251.77 272.02 L253.87 272.02 L254.1 273.17 L255.26 272.91 L255.49 272.02 L257.58 272.02 L257.82 272.91 L258.75 272.91 L258.98 280.86 L260.84 280.86 L261.07 318.86 L261.3 316.21 L263.63 316.21 L263.86 318.86 L265.02 318.86 L265.25 316.21 L268.04 316.21 L268.28 333.8 L272.69 333.8 L272.92 321.07 L274.55 321.07 L274.78 314 L288.96 314 L289.19 317.54 L300.58 317.54 L300.81 310.9 L311.51 310.9 L311.74 229.87 L323.13 229.87 L323.36 222.27 L330.8 222.27 L331.03 219.97 L349.39 220.16 L349.62 229.87 L352.41 229.87 L352.64 249.49 L382.16 249.49 L382.39 383.11 L386.81 383.11 L387.04 262.54 L406.57 233.23 L407.26 233.23 L410.05 252.75 L410.52 252.78 L410.75 233.23 L411.21 233.23 L439.11 263.31 L439.34 313.74 L444.22 313.74 L444.45 202.79 L513.71 202.79 L513.95 228.1 L528.36 228.1 L528.59 76.89 L548.34 76.8 L548.58 67.61 L561.59 67.52 L562.29 16 L563.68 16 L564.61 67.52 L571.82 67.61 L572.05 83.08 L572.98 83.08 L573.21 152.98 L579.49 143.92 L580.88 121.96 L582.04 143.91 L595.53 163.38 L598.55 173.04 L598.78 176.62 L600.41 176.62 L600.64 180.12 L602.03 180.12 L602.27 183.62 L603.66 183.62 L603.89 187.12 L605.52 187.12 L605.75 336.98 L606.22 379.83 L608.77 379.83 L609.01 89.53 L622.72 89.53 L622.95 83.96 L641.78 83.96 L642.01 69.64 L642.71 69.64 L642.94 83.96 L661.77 83.96 L662 89.53 L675.25 89.53 L675.48 202.65 L675.
|
|
19
|
+
<defs><path id="skyline-loading-outline" pathLength="1" d="M0 390.01 L24 390.01 L24 326.71 L36.78 320.28 L76.29 320.28 L78.85 321.63 L79.08 320.54 L87.68 320.54 L87.92 316.21 L133.24 316.74 L133.47 317.75 L133.94 317.75 L134.17 316.74 L164.38 316.74 L164.85 317.75 L165.08 321.6 L181.12 321.6 L181.35 329.03 L189.48 329.03 L190.41 330.18 L207.85 330.18 L208.08 328.36 L208.54 328.36 L209.01 331.14 L212.26 338.16 L212.49 334.77 L213.89 334.77 L214.12 344.27 L216.21 344.27 L216.45 348.52 L234.57 348.52 L234.81 318.86 L238.53 318.86 L238.76 308.7 L242.48 308.7 L242.71 280.86 L244.57 280.86 L244.8 273.62 L245.03 272.91 L245.73 272.91 L245.96 272.02 L249.68 272.02 L249.91 273.17 L251.54 273.17 L251.77 272.02 L253.87 272.02 L254.1 273.17 L255.26 272.91 L255.49 272.02 L257.58 272.02 L257.82 272.91 L258.75 272.91 L258.98 280.86 L260.84 280.86 L261.07 318.86 L261.3 316.21 L263.63 316.21 L263.86 318.86 L265.02 318.86 L265.25 316.21 L268.04 316.21 L268.28 333.8 L272.69 333.8 L272.92 321.07 L274.55 321.07 L274.78 314 L288.96 314 L289.19 317.54 L300.58 317.54 L300.81 310.9 L311.51 310.9 L311.74 229.87 L323.13 229.87 L323.36 222.27 L330.8 222.27 L331.03 219.97 L349.39 220.16 L349.62 229.87 L352.41 229.87 L352.64 249.49 L382.16 249.49 L382.39 383.11 L386.81 383.11 L387.04 262.54 L406.57 233.23 L407.26 233.23 L410.05 252.75 L410.52 252.78 L410.75 233.23 L411.21 233.23 L439.11 263.31 L439.34 313.74 L444.22 313.74 L444.45 202.79 L513.71 202.79 L513.95 228.1 L528.36 228.1 L528.59 76.89 L548.34 76.8 L548.58 67.61 L561.59 67.52 L562.29 16 L563.68 16 L564.61 67.52 L571.82 67.61 L572.05 83.08 L572.98 83.08 L573.21 152.98 L579.49 143.92 L580.88 121.96 L582.04 143.91 L595.53 163.38 L598.55 173.04 L598.78 176.62 L600.41 176.62 L600.64 180.12 L602.03 180.12 L602.27 183.62 L603.66 183.62 L603.89 187.12 L605.52 187.12 L605.75 336.98 L606.22 379.83 L608.77 379.83 L609.01 89.53 L622.72 89.53 L622.95 83.96 L641.78 83.96 L642.01 69.64 L642.71 69.64 L642.94 83.96 L661.77 83.96 L662 89.53 L675.25 89.53 L675.48 202.65 L675.71 189.68 L748.46 189.66 L748.69 202.65 L755.43 202.65 L755.66 208.92 L800.75 208.92 L801.22 295.71 L806.33 295.71 L806.56 284.84 L818.42 284.84 L818.65 282.28 L837.48 282.28 L837.71 284.84 L848.17 284.84 L848.4 291.4 L848.87 291.4 L849.1 295.71 L854.91 295.71 L855.14 266.9 L894.19 266.9 L894.42 390.01 L937.42 390.01 L937.65 306.26 L939.51 306.26 L939.74 290.76 L953.69 290.76 L953.92 306.26 L958.57 306.26 L958.8 295.48 L974.14 295.48 L974.37 306.26 L976 306.26 L976 390.01 L1000 390.01" /></defs>
|
|
20
20
|
<use class="skyline-trace-rail" href="#skyline-loading-outline" />
|
|
21
21
|
<use class="skyline-trace-line" href="#skyline-loading-outline" />
|
|
22
22
|
<use class="skyline-trace-tip" href="#skyline-loading-outline" />
|