@a2f0/skyline 0.2.30 → 0.2.32

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.
@@ -362,7 +362,7 @@ Intermediate parts have less certainty than the overall height:
362
362
  geographic camera names; see the
363
363
  [building audit](building-labels.md#names-from-the-geographic-camera). A 90 m concrete
364
364
  slab stands to the published 121.9 m, with window strips in its narrow ends and punched
365
- windows in its long faces. Its floor levels are estimates; see the
365
+ windows in its long faces. Continuous south office piers and recessed panel joints follow dated photographs; joint dimensions and floor levels remain estimates; see the
366
366
  [Millennium Park Plaza reference audit](millennium-park-plaza-reference.md).
367
367
  - Willoughby Tower stands at Michigan and Madison, left of Six North Michigan in the
368
368
  drawing. Its limestone base fills the mapped L to the setback over the 23rd floor; a
@@ -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
- // over its chord; the mapped notches are deeper, so this spacing is approximate. The antenna stands
8
- // at the enclosure's centre, reaching the published 362.5 m tip; the map gives no antenna
9
- // geometry. See docs/aon-reference.md. Units are meters; +x is east, +z is south.
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" };
@@ -6,6 +6,7 @@ export interface AonForm {
6
6
  enclosure: Vec2[];
7
7
  mast: Vec2;
8
8
  base: number;
9
+ uniformPierSpacing?: boolean;
9
10
  }
10
11
  export declare const aonLevels: Readonly<{
11
12
  roof: 340;
@@ -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; mapped spacing follows each face's chord
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. Distribute them over each face's chord; mapped spacing remains an
64
- // approximation (docs/aon-reference.md, FID-AON-001). End piers stay on their face.
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, bay = run.length / count;
67
- return Array.from({ length: count + 1 }, (_, i) => i * bay);
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);
@@ -138,7 +138,8 @@ export const colourMaterials = {
138
138
  // A warm white precast slab with bronze-tinted glass.
139
139
  "building-michigan-plaza-front-tall": {
140
140
  shell: ["neutral"],
141
- "walls and windows": ["glass", { ...windows(0x727272, 0x484848), 0x666666: "concrete" }],
141
+ "walls and windows": ["glass", { ...windows(0x727272, 0x484848), 0x666666: "concrete", 0x5e5e5e: "concrete" }],
142
+ "south concrete panels": ["concrete"],
142
143
  },
143
144
  // Buff limestone over a granite base.
144
145
  "building-willoughby-tower": {
@@ -241,7 +242,7 @@ export const colourMaterials = {
241
242
  // White concrete.
242
243
  "building-river-plaza": {
243
244
  shell: ["concrete"],
244
- "concrete and windows": ["glass", { ...windows(0x8a8a8a, 0x505050), 0x9a9a9a: "concrete" }],
245
+ "concrete and windows": ["glass", { ...windows(0x8a8a8a, 0x505050), 0x9a9a9a: "concrete", 0x424242: "aluminium" }],
245
246
  "rooftop box": ["concrete"],
246
247
  },
247
248
  // Dark orange brick and bronze glass.
@@ -230,6 +230,7 @@ export const measuredColours = {
230
230
  },
231
231
  "building-river-plaza": {
232
232
  concrete: 0xe5dfd3,
233
+ aluminium: null, // photographed frames; no separately calibrated sunny sample
233
234
  glass: null,
234
235
  "lit window": null,
235
236
  "dim window": null,
@@ -24,7 +24,7 @@ const floor = (n) => (n <= 8 ? h.lobby + (n - 2) * h.office : h.lobby + 6 * h.of
24
24
  const color = (hex) => new THREE.Color(hex);
25
25
  const glassTones = [0x2e2e2e, 0x323232, 0x363636, 0x3a3a3a].map(color);
26
26
  const litGlass = color(0x727272), dimGlass = color(0x484848);
27
- const concrete = color(0x666666), strip = color(0x3e3e3e), lobby = color(0x3a3a3a), core = color(0x262626), roofing = color(0x3e3e3e);
27
+ const concrete = color(0x666666), joint = color(0x5e5e5e), strip = color(0x3e3e3e), lobby = color(0x3a3a3a), core = color(0x262626), roofing = color(0x3e3e3e);
28
28
  function paneColor(row, bay, wall) {
29
29
  const hash = (Math.imul(row + 23, 0x9e3779b1) ^ Math.imul(bay + 47, 0x85ebca77) ^ Math.imul(wall + 37, 0xc2b2ae3d)) >>> 0;
30
30
  const value = hash % 101;
@@ -45,6 +45,7 @@ export function createMillenniumParkPlazaGeographicBuilding(record, projectPlan,
45
45
  const kit = createBuilder(record.name, record.id);
46
46
  const shell = kit.batch("Millennium Park Plaza · shell", kit.material(0xffffff, { vertexColors: true }));
47
47
  const wall = kit.batch("Millennium Park Plaza · walls and windows", kit.material(0xffffff, { vertexColors: true }));
48
+ const panels = kit.batch("Millennium Park Plaza · south concrete panels", kit.material(0x666666));
48
49
  const corners = orient(polygonOf(projectPlan(record.footprint.coordinates)));
49
50
  // The slab, its walls the shell's dark core.
50
51
  planOf(corners).forEach((run) => {
@@ -56,13 +57,15 @@ export function createMillenniumParkPlazaGeographicBuilding(record, projectPlan,
56
57
  // Every wall's skin, one box a chain, held 2 cm clear of its ends. A narrow end carries
57
58
  // four window strips, 1.25 m wide and symmetrical about its middle, from the eighth floor,
58
59
  // the first of apartments, with the top floor's window standing apart over each; below,
59
- // the offices' windows run across it. A long face has a punched window every 3 m between
60
+ // the office glazing is bounded by outer piers on the south (the north remains
61
+ // the older full-width approximation). A long face has a window every 3 m between
60
62
  // 0.8 m piers. The chamfers are solid.
61
63
  const plan = planOf(corners);
62
64
  chainsOf(plan).forEach((chain, index) => {
63
65
  const first = chain.runs[0], last = chain.runs.at(-1);
64
66
  const s0 = turnsAt(plan, first)[0] > 0 ? 0.09 : 0.02, s1 = chain.length - (turnsAt(plan, last)[1] > 0 ? 0.09 : 0.02);
65
67
  const end = chain.length > 12 && chain.length < 30, long = chain.length >= 30;
68
+ const south = end && plan[first].normal(0)[1] > 0.9;
66
69
  let columns, isWindow;
67
70
  if (end) {
68
71
  const middle = chain.length / 2, centres = [-4.1, -1.6, 1.6, 4.1].map((d) => middle + d);
@@ -78,11 +81,15 @@ export function createMillenniumParkPlazaGeographicBuilding(record, projectPlan,
78
81
  columns = [0, chain.length];
79
82
  isWindow = () => false;
80
83
  }
81
- chainSkin(kit, wall, plan, chain, 0, chain.runs.length - 1, s0, s1, columns, heights, 0.02, 0.07, (bay, r) => {
84
+ chainSkin(kit, wall, plan, chain, 0, chain.runs.length - 1, s0, s1, columns, heights, 0.02, south ? 0.04 : 0.07, (bay, r) => {
82
85
  const row = rows[r];
83
86
  if (row.kind === "lobby")
84
87
  return lobby;
85
88
  const office = row.floor < 8;
89
+ // Dated south views show the outer concrete piers continuing past the
90
+ // office windows; the earlier model glazed the entire end below floor 8.
91
+ if (south && (bay === 1 || bay === columns.length - 1 || (!office && !isWindow(bay))))
92
+ return joint;
86
93
  if (end && !office && isWindow(bay)) {
87
94
  if (row.kind === "glass")
88
95
  return paneColor(row.floor, bay, index);
@@ -91,9 +98,25 @@ export function createMillenniumParkPlazaGeographicBuilding(record, projectPlan,
91
98
  if (row.kind === "spandrel")
92
99
  return concrete;
93
100
  if (end)
94
- return office ? paneColor(row.floor, bay, index) : concrete;
101
+ return office ? paneColor(row.floor, bay, index) : south ? joint : concrete;
95
102
  return isWindow(bay) ? paneColor(row.floor, bay, index) : concrete;
96
103
  }, concrete);
104
+ if (south) {
105
+ // 1980/2005/2008 photographs resolve horizontal joints on the solid end.
106
+ // Keep the existing estimated floor cadence; 5 cm joint width and 3 cm
107
+ // relief are schematic, not measured construction dimensions. Closed
108
+ // panels stand in front of the continuous, closed backing skin.
109
+ for (let bay = 1; bay < columns.length; bay += 2) {
110
+ const outer = bay === 1 || bay === columns.length - 1;
111
+ // End clearances keep panel caps off the backing skin's end-cap planes.
112
+ const from = Math.max(s0 + 0.01, columns[bay - 1]), to = Math.min(s1 - 0.01, columns[bay]);
113
+ for (let n = outer ? 2 : 8; n <= 41; n += 1) {
114
+ const lo = floor(n) - 0.3 + 0.025;
115
+ const hi = n === 41 ? h.top - 0.01 : floor(n + 1) - 0.3 - 0.025;
116
+ chainSkin(kit, panels, plan, chain, 0, chain.runs.length - 1, from, to, [from, to], [lo, hi], 0.035, 0.07, () => concrete, concrete);
117
+ }
118
+ }
119
+ }
97
120
  });
98
121
  const model = kit.finish({ height: h.top, outlines: [shell], opacity: 0.16 });
99
122
  model.building.position.set(offset[0], 0, offset[1]);
@@ -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
- bay: 1.6, // the frame's bays, an estimate from photographs
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: on each floor, a window 1.1 m wide and 1.5 m tall in each
59
- // 1.6 m bay of the frame, from the chain's middle out. A run the podium stands against, its
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).forEach((chain, index) => {
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 = Math.floor(chain.length / 2 / h.bay);
73
- const centres = Array.from({ length: 2 * count }, (_, k) => chain.length / 2 + (k - count + 0.5) * h.bay);
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 + 0.75;
103
+ const sill = n * pitch + h.sill;
86
104
  if (sill > base + 0.2)
87
- heights.push(sill, sill + 1.5);
105
+ heights.push(sill, sill + h.window);
88
106
  }
89
107
  heights.push(h.roof);
90
- const cuts = centres.flatMap((c) => [c - 0.55, c + 0.55]).filter((s) => s > from + 0.01 && s < to - 0.01);
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) < 0.55);
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 });
@@ -25,7 +25,7 @@ export const geographicBuildings = [
25
25
  { "id": "building-blue-cross-blue-shield", "name": "Blue Cross and Blue Shield Tower", "shortName": "Blue Cross", "height": 226.7, "tipHeight": 226.7, "heightSource": "https://www.skyscrapercenter.com/building/blue-cross-blue-shield-tower/1160", "note": "Detailed Blue Cross: the mapped outline and parts; a glass curtain wall on the 5 ft module, the central block standing forward of end bays open through at the middle mechanical band, three bands of mechanical floors with their columns, and the emblem screen to the published 226.7 m. The lobby's height is estimated.", "footprint": { "way": 95486960, "version": 16, "coordinates": [[-87.6204645, 41.8846925], [-87.620465, 41.8847182], [-87.6204697, 41.8849849], [-87.6203148, 41.8849886], [-87.6203086, 41.8850315], [-87.6201881, 41.8850329], [-87.6201798, 41.8850759], [-87.6201749, 41.8851015], [-87.6196594, 41.8851062], [-87.6196557, 41.8850788], [-87.6196503, 41.8850388], [-87.6195227, 41.8850407], [-87.6195157, 41.8850011], [-87.6193623, 41.8849997], [-87.6193587, 41.8849047], [-87.6193513, 41.8847032], [-87.6193817, 41.8847037], [-87.6194025, 41.8847041], [-87.6194595, 41.8847032], [-87.6194506, 41.8846598], [-87.6199021, 41.8846549], [-87.6203621, 41.8846501], [-87.6203607, 41.884669], [-87.6203595, 41.8846955]] }, "parts": [{ "way": 284779637, "version": 4, "coordinates": [[-87.6203086, 41.8850315], [-87.6203148, 41.8849886], [-87.6203559, 41.8847194], [-87.6203595, 41.8846955], [-87.6203607, 41.884669], [-87.6203621, 41.8846501], [-87.6202, 41.8846517], [-87.6199021, 41.8846549], [-87.6196193, 41.8846576], [-87.6194506, 41.8846598], [-87.6194595, 41.8847032], [-87.6195157, 41.8850011], [-87.6195227, 41.8850407], [-87.6196503, 41.8850388], [-87.6201881, 41.8850329]], "bottom": 0, "top": 226.7 }, { "way": 284779635, "version": 5, "coordinates": [[-87.6193623, 41.8849997], [-87.6195157, 41.8850011], [-87.6203148, 41.8849886], [-87.6204697, 41.8849849], [-87.620465, 41.8847182], [-87.6204645, 41.8846925], [-87.6203595, 41.8846955], [-87.6194595, 41.8847032], [-87.6194025, 41.8847041], [-87.6193817, 41.8847037], [-87.6193513, 41.8847032], [-87.6193587, 41.8849047]], "bottom": 0, "top": 212 }] },
26
26
  { "id": "building-340-on-the-park", "name": "340 on the Park", "shortName": "340 on the Park", "height": 204.9, "tipHeight": 204.9, "heightSource": "https://www.skyscrapercenter.com/building/340-on-the-park/1583", "note": "Detailed 340 on the Park: the mapped outline and parts; glass curtain walls on a straight south face, a diagonal south-east face and a curved north face, with the south face's white concrete frame: piers, a beam every fifth floor, a parapet band over four penthouse floors, cantilevered and recessed balconies, and the winter garden's tall bay. The corner block's roof is measured on photographs at 51.7 m, over the map's 40 m. Floor levels are measured on the drawing; the lobby's height is estimated.", "footprint": { "way": 95486949, "version": 12, "coordinates": [[-87.619168, 41.884674], [-87.6191536, 41.884912], [-87.6191146, 41.8849133], [-87.6190581, 41.8849113], [-87.6188847, 41.8849048], [-87.6187278, 41.8848896], [-87.6186525, 41.8848776], [-87.6185999, 41.8848701], [-87.6184438, 41.8848387], [-87.61849, 41.8848042], [-87.6184907, 41.8847024], [-87.6185294, 41.8847019], [-87.6186088, 41.8847009], [-87.6186482, 41.8847001], [-87.6186465, 41.8846785]] }, "parts": [{ "way": 284789056, "version": 4, "coordinates": [[-87.6191536, 41.884912], [-87.619168, 41.884674], [-87.6186465, 41.8846785], [-87.6186482, 41.8847001], [-87.6186088, 41.8847009], [-87.61849, 41.8848042], [-87.6184438, 41.8848387], [-87.6185999, 41.8848701], [-87.6186525, 41.8848776], [-87.6187278, 41.8848896], [-87.6188847, 41.8849048], [-87.6190581, 41.8849113], [-87.6191146, 41.8849133]], "bottom": 0, "top": 204.9 }, { "way": 284789058, "version": 4, "coordinates": [[-87.6186088, 41.8847009], [-87.6185294, 41.8847019], [-87.6184907, 41.8847024], [-87.61849, 41.8848042]], "bottom": 0, "top": 51.7, "estimatedHeight": true }] },
27
27
  { "id": "building-the-buckingham", "name": "The Buckingham", "shortName": "Buckingham", "height": 121.9, "tipHeight": 121.9, "heightSource": "https://www.skyscrapercenter.com/building/buckingham-plaza/10315", "note": "Detailed Buckingham: the mapped outline and rooftop part; a concrete frame of five bays a face, a band at every floor over bronze ribbon windows, the end bays standing forward of the middle three on the south, east and north faces, balconies stacked in the two notched corners, the top floor's tall openings under a cap at the mapped 119 m, and the rooftop enclosure to the published 121.9 m. Floor levels are measured on the drawing; the lobby's height is estimated.", "footprint": { "way": 95486940, "version": 11, "coordinates": [[-87.6181599, 41.8846507], [-87.6181604, 41.8847282], [-87.618162, 41.8847796], [-87.6181638, 41.8849686], [-87.6180927, 41.8849698], [-87.618091, 41.8849586], [-87.6178554, 41.8849597], [-87.6178564, 41.8849736], [-87.6177989, 41.8849746], [-87.6177849, 41.8849748], [-87.6177824, 41.8849587], [-87.6177569, 41.8849571], [-87.6177552, 41.8849011], [-87.617777, 41.8849005], [-87.6177734, 41.8847266], [-87.6177501, 41.8847273], [-87.6177485, 41.884672], [-87.6177809, 41.8846702], [-87.6177797, 41.8846527], [-87.617852, 41.8846523], [-87.617852, 41.8846679], [-87.618083, 41.8846651], [-87.618083, 41.8846511]] }, "parts": [{ "way": 95486940, "version": 11, "coordinates": [[-87.6181599, 41.8846507], [-87.6181604, 41.8847282], [-87.618162, 41.8847796], [-87.6181638, 41.8849686], [-87.6180927, 41.8849698], [-87.618091, 41.8849586], [-87.6178554, 41.8849597], [-87.6178564, 41.8849736], [-87.6177989, 41.8849746], [-87.6177849, 41.8849748], [-87.6177824, 41.8849587], [-87.6177569, 41.8849571], [-87.6177552, 41.8849011], [-87.617777, 41.8849005], [-87.6177734, 41.8847266], [-87.6177501, 41.8847273], [-87.6177485, 41.884672], [-87.6177809, 41.8846702], [-87.6177797, 41.8846527], [-87.617852, 41.8846523], [-87.617852, 41.8846679], [-87.618083, 41.8846651], [-87.618083, 41.8846511]], "bottom": 0, "top": 119 }, { "way": 284790189, "version": 1, "coordinates": [[-87.6180734, 41.8848536], [-87.6178346, 41.8848542], [-87.6178342, 41.8847737], [-87.6180731, 41.8847732]], "bottom": 119, "top": 121.9 }] },
28
- { "id": "building-michigan-plaza-front-tall", "name": "Millennium Park Plaza", "shortName": "Millennium Park Plaza", "height": 121.9, "tipHeight": 121.9, "heightSource": "https://www.skyscrapercenter.com/building/millennium-park-plaza/10316", "note": "Detailed Millennium Park Plaza: the mapped outline, named through the geographic camera; a 90 m concrete slab with chamfered south corners, offices on the first seven floors and apartments above, solid narrow ends with four slender window strips, and punched windows between piers on the long faces, to the published 121.9 m. Floor levels are estimated from the published 40 storeys.", "footprint": { "way": 127107026, "version": 7, "coordinates": [[-87.6240359, 41.8856557], [-87.624011, 41.8848485], [-87.6240357, 41.8848351], [-87.624242, 41.8848314], [-87.6242648, 41.8848508], [-87.6242799, 41.8856518]] }, "parts": [{ "way": 127107026, "version": 7, "coordinates": [[-87.6240359, 41.8856557], [-87.624011, 41.8848485], [-87.6240357, 41.8848351], [-87.624242, 41.8848314], [-87.6242648, 41.8848508], [-87.6242799, 41.8856518]], "bottom": 0, "top": 121.9 }] },
28
+ { "id": "building-michigan-plaza-front-tall", "name": "Millennium Park Plaza", "shortName": "Millennium Park Plaza", "height": 121.9, "tipHeight": 121.9, "heightSource": "https://www.skyscrapercenter.com/building/millennium-park-plaza/10316", "note": "Detailed Millennium Park Plaza: the mapped outline, named through the geographic camera; a 90 m concrete slab with chamfered south corners, offices on the first seven floors and apartments above, solid narrow ends with four slender window strips, and punched windows between piers on the long faces, to the published 121.9 m. South office glazing sits between continuous outer concrete piers; recessed south-panel joints follow the existing estimated floors. Floor levels, window widths and joint relief remain approximations.", "footprint": { "way": 127107026, "version": 7, "coordinates": [[-87.6240359, 41.8856557], [-87.624011, 41.8848485], [-87.6240357, 41.8848351], [-87.624242, 41.8848314], [-87.6242648, 41.8848508], [-87.6242799, 41.8856518]] }, "parts": [{ "way": 127107026, "version": 7, "coordinates": [[-87.6240359, 41.8856557], [-87.624011, 41.8848485], [-87.6240357, 41.8848351], [-87.624242, 41.8848314], [-87.6242648, 41.8848508], [-87.6242799, 41.8856518]], "bottom": 0, "top": 121.9 }] },
29
29
  { "id": "building-willoughby-tower", "name": "Willoughby Tower", "shortName": "Willoughby", "height": 133.5, "tipHeight": 133.5, "heightSource": "https://www.skyscrapercenter.com/building/willoughby-tower/9854", "note": "Detailed Willoughby Tower: the mapped L-shaped outline; a limestone Gothic base to the setback over the 23rd floor under a parapet of pinnacles, a shaft at the lot's south-east corner with a shoulder west of it, and a crown of arched windows under pinnacles and gables, its tallest pinnacles at the published 133.5 m. Levels are measured on the drawing; the shaft's plan is read from the drawing and photographs.", "footprint": { "way": 124873939, "version": 10, "coordinates": [[-87.6250765, 41.882002], [-87.6249763, 41.8820043], [-87.6249007, 41.8820051], [-87.624702, 41.8820073], [-87.6246761, 41.8820076], [-87.624674, 41.8819841], [-87.6246656, 41.8818144], [-87.6246642, 41.8817867], [-87.6247865, 41.881785], [-87.6249397, 41.8817829], [-87.624944, 41.8818519], [-87.6250739, 41.8818499]] }, "parts": [{ "way": 124873939, "version": 10, "coordinates": [[-87.6250765, 41.882002], [-87.6249763, 41.8820043], [-87.6249007, 41.8820051], [-87.624702, 41.8820073], [-87.6246761, 41.8820076], [-87.624674, 41.8819841], [-87.6246656, 41.8818144], [-87.6246642, 41.8817867], [-87.6247865, 41.881785], [-87.6249397, 41.8817829], [-87.624944, 41.8818519], [-87.6250739, 41.8818499]], "bottom": 0, "top": 133.5 }] },
30
30
  { "id": "building-six-north-michigan", "name": "Six North Michigan", "shortName": "Six North", "height": 86, "tipHeight": 86, "heightSource": "https://chicagology.com/goldenage/goldenage015/", "note": "Detailed Six North Michigan, the Montgomery Ward Building: the mapped outline; a sixteen-storey block under a projecting cornice, and the tower in the middle of its Michigan front, with a band, an arched stage of three tall windows to a face under a cornice, and a panelled top stage under a cap at the published 86 m, where the pyramid came off in 1947. Levels are measured on the drawing; the tower's plan is read from the drawing.", "footprint": { "way": 126982631, "version": 12, "coordinates": [[-87.6246862, 41.8824645], [-87.6246875, 41.8824613], [-87.6246872, 41.8824178], [-87.6246863, 41.8823638], [-87.624686, 41.8823412], [-87.6246848, 41.8822497], [-87.6246844, 41.882217], [-87.6249101, 41.8822147], [-87.6249772, 41.8822141], [-87.6252824, 41.8822112], [-87.6252834, 41.8823395], [-87.6252861, 41.8824613]] }, "parts": [{ "way": 126982631, "version": 12, "coordinates": [[-87.6246862, 41.8824645], [-87.6246875, 41.8824613], [-87.6246872, 41.8824178], [-87.6246863, 41.8823638], [-87.624686, 41.8823412], [-87.6246848, 41.8822497], [-87.6246844, 41.882217], [-87.6249101, 41.8822147], [-87.6249772, 41.8822141], [-87.6252824, 41.8822112], [-87.6252834, 41.8823395], [-87.6252861, 41.8824613]], "bottom": 0, "top": 86 }] },
31
31
  { "id": "building-six-north-far-east", "name": "Michigan Boulevard Building", "shortName": "Michigan Boulevard", "height": 82, "tipHeight": 83.3, "heightSource": "https://web.archive.org/web/20070216134331/http://www.emporis.com/en/wm/bu/?id=michiganboulevardbuilding-chicago-il-usa", "note": "Detailed Michigan Boulevard Building: the mapped outline, named through the geographic camera; a terracotta Michigan front of five bays, a single window and four pairs to a floor, over twenty-one storeys, with a belt course over the thirteenth and an attic of panels under the parapet at the published 82 m, its south bay's parapet raised to 83.3 m. The terracotta turns one bay onto the south wall, common brick beyond, blank above 20 North Michigan to the eighteenth floor. Levels are measured on the drawing.", "footprint": { "way": 126982630, "version": 8, "coordinates": [[-87.6246899, 41.8828465], [-87.6250574, 41.8828445], [-87.6250579, 41.8828698], [-87.6252905, 41.8828713], [-87.625293, 41.8831223], [-87.6251914, 41.8831227], [-87.6251166, 41.883123], [-87.6250744, 41.8831231], [-87.6249196, 41.8831237], [-87.6246922, 41.8831245], [-87.6246919, 41.8830972], [-87.6246898, 41.8828903], [-87.6246895, 41.8828674], [-87.6246898, 41.8828566]] }, "parts": [{ "way": 126982630, "version": 8, "coordinates": [[-87.6246899, 41.8828465], [-87.6250574, 41.8828445], [-87.6250579, 41.8828698], [-87.6252905, 41.8828713], [-87.625293, 41.8831223], [-87.6251914, 41.8831227], [-87.6251166, 41.883123], [-87.6250744, 41.8831231], [-87.6249196, 41.8831237], [-87.6246922, 41.8831245], [-87.6246919, 41.8830972], [-87.6246898, 41.8828903], [-87.6246895, 41.8828674], [-87.6246898, 41.8828566]], "bottom": 0, "top": 83.3 }] },
@@ -6,7 +6,7 @@ import { chainSkin, chainsOf, orient, paintedSlab, planOf, turnsAt } from "./fac
6
6
  // the photograph's camera it stands behind Three Illinois Center and The Buckingham, and fills
7
7
  // the seam where their corners meet.
8
8
  //
9
- // A triangular tower of reflective glass: a flush grid of thin mullions about 1.5 m apart and
9
+ // A triangular tower of reflective glass and opaque bands: thin mullions about 1.5 m apart and
10
10
  // paired opaque spandrel / vision panels at each floor, under a flat roof at the published 139.3 m. The
11
11
  // floors divide that height by OpenStreetMap's 45 levels. See docs/swissotel-reference.md.
12
12
  // Units are meters; +x is east, +z is south.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a2f0/skyline",
3
- "version": "0.2.30",
3
+ "version": "0.2.32",
4
4
  "description": "Interactive Chicago skyline viewer and reusable Three.js building models",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -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
- // over its chord; the mapped notches are deeper, so this spacing is approximate. The antenna stands
8
- // at the enclosure's centre, reaching the published 362.5 m tip; the map gives no antenna
9
- // geometry. See docs/aon-reference.md. Units are meters; +x is east, +z is south.
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" };
@@ -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; mapped spacing follows each face's chord
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. Distribute them over each face's chord; mapped spacing remains an
64
- // approximation (docs/aon-reference.md, FID-AON-001). End piers stay on their face.
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, bay = run.length / count;
67
- return Array.from({ length: count + 1 }, (_, i) => i * bay);
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);
@@ -138,7 +138,8 @@ export const colourMaterials = {
138
138
  // A warm white precast slab with bronze-tinted glass.
139
139
  "building-michigan-plaza-front-tall": {
140
140
  shell: ["neutral"],
141
- "walls and windows": ["glass", { ...windows(0x727272, 0x484848), 0x666666: "concrete" }],
141
+ "walls and windows": ["glass", { ...windows(0x727272, 0x484848), 0x666666: "concrete", 0x5e5e5e: "concrete" }],
142
+ "south concrete panels": ["concrete"],
142
143
  },
143
144
  // Buff limestone over a granite base.
144
145
  "building-willoughby-tower": {
@@ -241,7 +242,7 @@ export const colourMaterials = {
241
242
  // White concrete.
242
243
  "building-river-plaza": {
243
244
  shell: ["concrete"],
244
- "concrete and windows": ["glass", { ...windows(0x8a8a8a, 0x505050), 0x9a9a9a: "concrete" }],
245
+ "concrete and windows": ["glass", { ...windows(0x8a8a8a, 0x505050), 0x9a9a9a: "concrete", 0x424242: "aluminium" }],
245
246
  "rooftop box": ["concrete"],
246
247
  },
247
248
  // Dark orange brick and bronze glass.
@@ -230,6 +230,7 @@ export const measuredColours = {
230
230
  },
231
231
  "building-river-plaza": {
232
232
  concrete: 0xe5dfd3,
233
+ aluminium: null, // photographed frames; no separately calibrated sunny sample
233
234
  glass: null,
234
235
  "lit window": null,
235
236
  "dim window": null,
@@ -24,7 +24,7 @@ const floor = (n) => (n <= 8 ? h.lobby + (n - 2) * h.office : h.lobby + 6 * h.of
24
24
  const color = (hex) => new THREE.Color(hex);
25
25
  const glassTones = [0x2e2e2e, 0x323232, 0x363636, 0x3a3a3a].map(color);
26
26
  const litGlass = color(0x727272), dimGlass = color(0x484848);
27
- const concrete = color(0x666666), strip = color(0x3e3e3e), lobby = color(0x3a3a3a), core = color(0x262626), roofing = color(0x3e3e3e);
27
+ const concrete = color(0x666666), joint = color(0x5e5e5e), strip = color(0x3e3e3e), lobby = color(0x3a3a3a), core = color(0x262626), roofing = color(0x3e3e3e);
28
28
  function paneColor(row, bay, wall) {
29
29
  const hash = (Math.imul(row + 23, 0x9e3779b1) ^ Math.imul(bay + 47, 0x85ebca77) ^ Math.imul(wall + 37, 0xc2b2ae3d)) >>> 0;
30
30
  const value = hash % 101;
@@ -45,6 +45,7 @@ export function createMillenniumParkPlazaGeographicBuilding(record, projectPlan,
45
45
  const kit = createBuilder(record.name, record.id);
46
46
  const shell = kit.batch("Millennium Park Plaza · shell", kit.material(0xffffff, { vertexColors: true }));
47
47
  const wall = kit.batch("Millennium Park Plaza · walls and windows", kit.material(0xffffff, { vertexColors: true }));
48
+ const panels = kit.batch("Millennium Park Plaza · south concrete panels", kit.material(0x666666));
48
49
  const corners = orient(polygonOf(projectPlan(record.footprint.coordinates)));
49
50
  // The slab, its walls the shell's dark core.
50
51
  planOf(corners).forEach((run) => {
@@ -56,13 +57,15 @@ export function createMillenniumParkPlazaGeographicBuilding(record, projectPlan,
56
57
  // Every wall's skin, one box a chain, held 2 cm clear of its ends. A narrow end carries
57
58
  // four window strips, 1.25 m wide and symmetrical about its middle, from the eighth floor,
58
59
  // the first of apartments, with the top floor's window standing apart over each; below,
59
- // the offices' windows run across it. A long face has a punched window every 3 m between
60
+ // the office glazing is bounded by outer piers on the south (the north remains
61
+ // the older full-width approximation). A long face has a window every 3 m between
60
62
  // 0.8 m piers. The chamfers are solid.
61
63
  const plan = planOf(corners);
62
64
  chainsOf(plan).forEach((chain, index) => {
63
65
  const first = chain.runs[0], last = chain.runs.at(-1);
64
66
  const s0 = turnsAt(plan, first)[0] > 0 ? 0.09 : 0.02, s1 = chain.length - (turnsAt(plan, last)[1] > 0 ? 0.09 : 0.02);
65
67
  const end = chain.length > 12 && chain.length < 30, long = chain.length >= 30;
68
+ const south = end && plan[first].normal(0)[1] > 0.9;
66
69
  let columns, isWindow;
67
70
  if (end) {
68
71
  const middle = chain.length / 2, centres = [-4.1, -1.6, 1.6, 4.1].map((d) => middle + d);
@@ -78,11 +81,15 @@ export function createMillenniumParkPlazaGeographicBuilding(record, projectPlan,
78
81
  columns = [0, chain.length];
79
82
  isWindow = () => false;
80
83
  }
81
- chainSkin(kit, wall, plan, chain, 0, chain.runs.length - 1, s0, s1, columns, heights, 0.02, 0.07, (bay, r) => {
84
+ chainSkin(kit, wall, plan, chain, 0, chain.runs.length - 1, s0, s1, columns, heights, 0.02, south ? 0.04 : 0.07, (bay, r) => {
82
85
  const row = rows[r];
83
86
  if (row.kind === "lobby")
84
87
  return lobby;
85
88
  const office = row.floor < 8;
89
+ // Dated south views show the outer concrete piers continuing past the
90
+ // office windows; the earlier model glazed the entire end below floor 8.
91
+ if (south && (bay === 1 || bay === columns.length - 1 || (!office && !isWindow(bay))))
92
+ return joint;
86
93
  if (end && !office && isWindow(bay)) {
87
94
  if (row.kind === "glass")
88
95
  return paneColor(row.floor, bay, index);
@@ -91,9 +98,25 @@ export function createMillenniumParkPlazaGeographicBuilding(record, projectPlan,
91
98
  if (row.kind === "spandrel")
92
99
  return concrete;
93
100
  if (end)
94
- return office ? paneColor(row.floor, bay, index) : concrete;
101
+ return office ? paneColor(row.floor, bay, index) : south ? joint : concrete;
95
102
  return isWindow(bay) ? paneColor(row.floor, bay, index) : concrete;
96
103
  }, concrete);
104
+ if (south) {
105
+ // 1980/2005/2008 photographs resolve horizontal joints on the solid end.
106
+ // Keep the existing estimated floor cadence; 5 cm joint width and 3 cm
107
+ // relief are schematic, not measured construction dimensions. Closed
108
+ // panels stand in front of the continuous, closed backing skin.
109
+ for (let bay = 1; bay < columns.length; bay += 2) {
110
+ const outer = bay === 1 || bay === columns.length - 1;
111
+ // End clearances keep panel caps off the backing skin's end-cap planes.
112
+ const from = Math.max(s0 + 0.01, columns[bay - 1]), to = Math.min(s1 - 0.01, columns[bay]);
113
+ for (let n = outer ? 2 : 8; n <= 41; n += 1) {
114
+ const lo = floor(n) - 0.3 + 0.025;
115
+ const hi = n === 41 ? h.top - 0.01 : floor(n + 1) - 0.3 - 0.025;
116
+ chainSkin(kit, panels, plan, chain, 0, chain.runs.length - 1, from, to, [from, to], [lo, hi], 0.035, 0.07, () => concrete, concrete);
117
+ }
118
+ }
119
+ }
97
120
  });
98
121
  const model = kit.finish({ height: h.top, outlines: [shell], opacity: 0.16 });
99
122
  model.building.position.set(offset[0], 0, offset[1]);
@@ -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
- bay: 1.6, // the frame's bays, an estimate from photographs
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: on each floor, a window 1.1 m wide and 1.5 m tall in each
59
- // 1.6 m bay of the frame, from the chain's middle out. A run the podium stands against, its
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).forEach((chain, index) => {
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 = Math.floor(chain.length / 2 / h.bay);
73
- const centres = Array.from({ length: 2 * count }, (_, k) => chain.length / 2 + (k - count + 0.5) * h.bay);
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 + 0.75;
103
+ const sill = n * pitch + h.sill;
86
104
  if (sill > base + 0.2)
87
- heights.push(sill, sill + 1.5);
105
+ heights.push(sill, sill + h.window);
88
106
  }
89
107
  heights.push(h.roof);
90
- const cuts = centres.flatMap((c) => [c - 0.55, c + 0.55]).filter((s) => s > from + 0.01 && s < to - 0.01);
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) < 0.55);
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 });
@@ -25,7 +25,7 @@ export const geographicBuildings = [
25
25
  { "id": "building-blue-cross-blue-shield", "name": "Blue Cross and Blue Shield Tower", "shortName": "Blue Cross", "height": 226.7, "tipHeight": 226.7, "heightSource": "https://www.skyscrapercenter.com/building/blue-cross-blue-shield-tower/1160", "note": "Detailed Blue Cross: the mapped outline and parts; a glass curtain wall on the 5 ft module, the central block standing forward of end bays open through at the middle mechanical band, three bands of mechanical floors with their columns, and the emblem screen to the published 226.7 m. The lobby's height is estimated.", "footprint": { "way": 95486960, "version": 16, "coordinates": [[-87.6204645, 41.8846925], [-87.620465, 41.8847182], [-87.6204697, 41.8849849], [-87.6203148, 41.8849886], [-87.6203086, 41.8850315], [-87.6201881, 41.8850329], [-87.6201798, 41.8850759], [-87.6201749, 41.8851015], [-87.6196594, 41.8851062], [-87.6196557, 41.8850788], [-87.6196503, 41.8850388], [-87.6195227, 41.8850407], [-87.6195157, 41.8850011], [-87.6193623, 41.8849997], [-87.6193587, 41.8849047], [-87.6193513, 41.8847032], [-87.6193817, 41.8847037], [-87.6194025, 41.8847041], [-87.6194595, 41.8847032], [-87.6194506, 41.8846598], [-87.6199021, 41.8846549], [-87.6203621, 41.8846501], [-87.6203607, 41.884669], [-87.6203595, 41.8846955]] }, "parts": [{ "way": 284779637, "version": 4, "coordinates": [[-87.6203086, 41.8850315], [-87.6203148, 41.8849886], [-87.6203559, 41.8847194], [-87.6203595, 41.8846955], [-87.6203607, 41.884669], [-87.6203621, 41.8846501], [-87.6202, 41.8846517], [-87.6199021, 41.8846549], [-87.6196193, 41.8846576], [-87.6194506, 41.8846598], [-87.6194595, 41.8847032], [-87.6195157, 41.8850011], [-87.6195227, 41.8850407], [-87.6196503, 41.8850388], [-87.6201881, 41.8850329]], "bottom": 0, "top": 226.7 }, { "way": 284779635, "version": 5, "coordinates": [[-87.6193623, 41.8849997], [-87.6195157, 41.8850011], [-87.6203148, 41.8849886], [-87.6204697, 41.8849849], [-87.620465, 41.8847182], [-87.6204645, 41.8846925], [-87.6203595, 41.8846955], [-87.6194595, 41.8847032], [-87.6194025, 41.8847041], [-87.6193817, 41.8847037], [-87.6193513, 41.8847032], [-87.6193587, 41.8849047]], "bottom": 0, "top": 212 }] },
26
26
  { "id": "building-340-on-the-park", "name": "340 on the Park", "shortName": "340 on the Park", "height": 204.9, "tipHeight": 204.9, "heightSource": "https://www.skyscrapercenter.com/building/340-on-the-park/1583", "note": "Detailed 340 on the Park: the mapped outline and parts; glass curtain walls on a straight south face, a diagonal south-east face and a curved north face, with the south face's white concrete frame: piers, a beam every fifth floor, a parapet band over four penthouse floors, cantilevered and recessed balconies, and the winter garden's tall bay. The corner block's roof is measured on photographs at 51.7 m, over the map's 40 m. Floor levels are measured on the drawing; the lobby's height is estimated.", "footprint": { "way": 95486949, "version": 12, "coordinates": [[-87.619168, 41.884674], [-87.6191536, 41.884912], [-87.6191146, 41.8849133], [-87.6190581, 41.8849113], [-87.6188847, 41.8849048], [-87.6187278, 41.8848896], [-87.6186525, 41.8848776], [-87.6185999, 41.8848701], [-87.6184438, 41.8848387], [-87.61849, 41.8848042], [-87.6184907, 41.8847024], [-87.6185294, 41.8847019], [-87.6186088, 41.8847009], [-87.6186482, 41.8847001], [-87.6186465, 41.8846785]] }, "parts": [{ "way": 284789056, "version": 4, "coordinates": [[-87.6191536, 41.884912], [-87.619168, 41.884674], [-87.6186465, 41.8846785], [-87.6186482, 41.8847001], [-87.6186088, 41.8847009], [-87.61849, 41.8848042], [-87.6184438, 41.8848387], [-87.6185999, 41.8848701], [-87.6186525, 41.8848776], [-87.6187278, 41.8848896], [-87.6188847, 41.8849048], [-87.6190581, 41.8849113], [-87.6191146, 41.8849133]], "bottom": 0, "top": 204.9 }, { "way": 284789058, "version": 4, "coordinates": [[-87.6186088, 41.8847009], [-87.6185294, 41.8847019], [-87.6184907, 41.8847024], [-87.61849, 41.8848042]], "bottom": 0, "top": 51.7, "estimatedHeight": true }] },
27
27
  { "id": "building-the-buckingham", "name": "The Buckingham", "shortName": "Buckingham", "height": 121.9, "tipHeight": 121.9, "heightSource": "https://www.skyscrapercenter.com/building/buckingham-plaza/10315", "note": "Detailed Buckingham: the mapped outline and rooftop part; a concrete frame of five bays a face, a band at every floor over bronze ribbon windows, the end bays standing forward of the middle three on the south, east and north faces, balconies stacked in the two notched corners, the top floor's tall openings under a cap at the mapped 119 m, and the rooftop enclosure to the published 121.9 m. Floor levels are measured on the drawing; the lobby's height is estimated.", "footprint": { "way": 95486940, "version": 11, "coordinates": [[-87.6181599, 41.8846507], [-87.6181604, 41.8847282], [-87.618162, 41.8847796], [-87.6181638, 41.8849686], [-87.6180927, 41.8849698], [-87.618091, 41.8849586], [-87.6178554, 41.8849597], [-87.6178564, 41.8849736], [-87.6177989, 41.8849746], [-87.6177849, 41.8849748], [-87.6177824, 41.8849587], [-87.6177569, 41.8849571], [-87.6177552, 41.8849011], [-87.617777, 41.8849005], [-87.6177734, 41.8847266], [-87.6177501, 41.8847273], [-87.6177485, 41.884672], [-87.6177809, 41.8846702], [-87.6177797, 41.8846527], [-87.617852, 41.8846523], [-87.617852, 41.8846679], [-87.618083, 41.8846651], [-87.618083, 41.8846511]] }, "parts": [{ "way": 95486940, "version": 11, "coordinates": [[-87.6181599, 41.8846507], [-87.6181604, 41.8847282], [-87.618162, 41.8847796], [-87.6181638, 41.8849686], [-87.6180927, 41.8849698], [-87.618091, 41.8849586], [-87.6178554, 41.8849597], [-87.6178564, 41.8849736], [-87.6177989, 41.8849746], [-87.6177849, 41.8849748], [-87.6177824, 41.8849587], [-87.6177569, 41.8849571], [-87.6177552, 41.8849011], [-87.617777, 41.8849005], [-87.6177734, 41.8847266], [-87.6177501, 41.8847273], [-87.6177485, 41.884672], [-87.6177809, 41.8846702], [-87.6177797, 41.8846527], [-87.617852, 41.8846523], [-87.617852, 41.8846679], [-87.618083, 41.8846651], [-87.618083, 41.8846511]], "bottom": 0, "top": 119 }, { "way": 284790189, "version": 1, "coordinates": [[-87.6180734, 41.8848536], [-87.6178346, 41.8848542], [-87.6178342, 41.8847737], [-87.6180731, 41.8847732]], "bottom": 119, "top": 121.9 }] },
28
- { "id": "building-michigan-plaza-front-tall", "name": "Millennium Park Plaza", "shortName": "Millennium Park Plaza", "height": 121.9, "tipHeight": 121.9, "heightSource": "https://www.skyscrapercenter.com/building/millennium-park-plaza/10316", "note": "Detailed Millennium Park Plaza: the mapped outline, named through the geographic camera; a 90 m concrete slab with chamfered south corners, offices on the first seven floors and apartments above, solid narrow ends with four slender window strips, and punched windows between piers on the long faces, to the published 121.9 m. Floor levels are estimated from the published 40 storeys.", "footprint": { "way": 127107026, "version": 7, "coordinates": [[-87.6240359, 41.8856557], [-87.624011, 41.8848485], [-87.6240357, 41.8848351], [-87.624242, 41.8848314], [-87.6242648, 41.8848508], [-87.6242799, 41.8856518]] }, "parts": [{ "way": 127107026, "version": 7, "coordinates": [[-87.6240359, 41.8856557], [-87.624011, 41.8848485], [-87.6240357, 41.8848351], [-87.624242, 41.8848314], [-87.6242648, 41.8848508], [-87.6242799, 41.8856518]], "bottom": 0, "top": 121.9 }] },
28
+ { "id": "building-michigan-plaza-front-tall", "name": "Millennium Park Plaza", "shortName": "Millennium Park Plaza", "height": 121.9, "tipHeight": 121.9, "heightSource": "https://www.skyscrapercenter.com/building/millennium-park-plaza/10316", "note": "Detailed Millennium Park Plaza: the mapped outline, named through the geographic camera; a 90 m concrete slab with chamfered south corners, offices on the first seven floors and apartments above, solid narrow ends with four slender window strips, and punched windows between piers on the long faces, to the published 121.9 m. South office glazing sits between continuous outer concrete piers; recessed south-panel joints follow the existing estimated floors. Floor levels, window widths and joint relief remain approximations.", "footprint": { "way": 127107026, "version": 7, "coordinates": [[-87.6240359, 41.8856557], [-87.624011, 41.8848485], [-87.6240357, 41.8848351], [-87.624242, 41.8848314], [-87.6242648, 41.8848508], [-87.6242799, 41.8856518]] }, "parts": [{ "way": 127107026, "version": 7, "coordinates": [[-87.6240359, 41.8856557], [-87.624011, 41.8848485], [-87.6240357, 41.8848351], [-87.624242, 41.8848314], [-87.6242648, 41.8848508], [-87.6242799, 41.8856518]], "bottom": 0, "top": 121.9 }] },
29
29
  { "id": "building-willoughby-tower", "name": "Willoughby Tower", "shortName": "Willoughby", "height": 133.5, "tipHeight": 133.5, "heightSource": "https://www.skyscrapercenter.com/building/willoughby-tower/9854", "note": "Detailed Willoughby Tower: the mapped L-shaped outline; a limestone Gothic base to the setback over the 23rd floor under a parapet of pinnacles, a shaft at the lot's south-east corner with a shoulder west of it, and a crown of arched windows under pinnacles and gables, its tallest pinnacles at the published 133.5 m. Levels are measured on the drawing; the shaft's plan is read from the drawing and photographs.", "footprint": { "way": 124873939, "version": 10, "coordinates": [[-87.6250765, 41.882002], [-87.6249763, 41.8820043], [-87.6249007, 41.8820051], [-87.624702, 41.8820073], [-87.6246761, 41.8820076], [-87.624674, 41.8819841], [-87.6246656, 41.8818144], [-87.6246642, 41.8817867], [-87.6247865, 41.881785], [-87.6249397, 41.8817829], [-87.624944, 41.8818519], [-87.6250739, 41.8818499]] }, "parts": [{ "way": 124873939, "version": 10, "coordinates": [[-87.6250765, 41.882002], [-87.6249763, 41.8820043], [-87.6249007, 41.8820051], [-87.624702, 41.8820073], [-87.6246761, 41.8820076], [-87.624674, 41.8819841], [-87.6246656, 41.8818144], [-87.6246642, 41.8817867], [-87.6247865, 41.881785], [-87.6249397, 41.8817829], [-87.624944, 41.8818519], [-87.6250739, 41.8818499]], "bottom": 0, "top": 133.5 }] },
30
30
  { "id": "building-six-north-michigan", "name": "Six North Michigan", "shortName": "Six North", "height": 86, "tipHeight": 86, "heightSource": "https://chicagology.com/goldenage/goldenage015/", "note": "Detailed Six North Michigan, the Montgomery Ward Building: the mapped outline; a sixteen-storey block under a projecting cornice, and the tower in the middle of its Michigan front, with a band, an arched stage of three tall windows to a face under a cornice, and a panelled top stage under a cap at the published 86 m, where the pyramid came off in 1947. Levels are measured on the drawing; the tower's plan is read from the drawing.", "footprint": { "way": 126982631, "version": 12, "coordinates": [[-87.6246862, 41.8824645], [-87.6246875, 41.8824613], [-87.6246872, 41.8824178], [-87.6246863, 41.8823638], [-87.624686, 41.8823412], [-87.6246848, 41.8822497], [-87.6246844, 41.882217], [-87.6249101, 41.8822147], [-87.6249772, 41.8822141], [-87.6252824, 41.8822112], [-87.6252834, 41.8823395], [-87.6252861, 41.8824613]] }, "parts": [{ "way": 126982631, "version": 12, "coordinates": [[-87.6246862, 41.8824645], [-87.6246875, 41.8824613], [-87.6246872, 41.8824178], [-87.6246863, 41.8823638], [-87.624686, 41.8823412], [-87.6246848, 41.8822497], [-87.6246844, 41.882217], [-87.6249101, 41.8822147], [-87.6249772, 41.8822141], [-87.6252824, 41.8822112], [-87.6252834, 41.8823395], [-87.6252861, 41.8824613]], "bottom": 0, "top": 86 }] },
31
31
  { "id": "building-six-north-far-east", "name": "Michigan Boulevard Building", "shortName": "Michigan Boulevard", "height": 82, "tipHeight": 83.3, "heightSource": "https://web.archive.org/web/20070216134331/http://www.emporis.com/en/wm/bu/?id=michiganboulevardbuilding-chicago-il-usa", "note": "Detailed Michigan Boulevard Building: the mapped outline, named through the geographic camera; a terracotta Michigan front of five bays, a single window and four pairs to a floor, over twenty-one storeys, with a belt course over the thirteenth and an attic of panels under the parapet at the published 82 m, its south bay's parapet raised to 83.3 m. The terracotta turns one bay onto the south wall, common brick beyond, blank above 20 North Michigan to the eighteenth floor. Levels are measured on the drawing.", "footprint": { "way": 126982630, "version": 8, "coordinates": [[-87.6246899, 41.8828465], [-87.6250574, 41.8828445], [-87.6250579, 41.8828698], [-87.6252905, 41.8828713], [-87.625293, 41.8831223], [-87.6251914, 41.8831227], [-87.6251166, 41.883123], [-87.6250744, 41.8831231], [-87.6249196, 41.8831237], [-87.6246922, 41.8831245], [-87.6246919, 41.8830972], [-87.6246898, 41.8828903], [-87.6246895, 41.8828674], [-87.6246898, 41.8828566]] }, "parts": [{ "way": 126982630, "version": 8, "coordinates": [[-87.6246899, 41.8828465], [-87.6250574, 41.8828445], [-87.6250579, 41.8828698], [-87.6252905, 41.8828713], [-87.625293, 41.8831223], [-87.6251914, 41.8831227], [-87.6251166, 41.883123], [-87.6250744, 41.8831231], [-87.6249196, 41.8831237], [-87.6246922, 41.8831245], [-87.6246919, 41.8830972], [-87.6246898, 41.8828903], [-87.6246895, 41.8828674], [-87.6246898, 41.8828566]], "bottom": 0, "top": 83.3 }] },
@@ -6,7 +6,7 @@ import { chainSkin, chainsOf, orient, paintedSlab, planOf, turnsAt } from "./fac
6
6
  // the photograph's camera it stands behind Three Illinois Center and The Buckingham, and fills
7
7
  // the seam where their corners meet.
8
8
  //
9
- // A triangular tower of reflective glass: a flush grid of thin mullions about 1.5 m apart and
9
+ // A triangular tower of reflective glass and opaque bands: thin mullions about 1.5 m apart and
10
10
  // paired opaque spandrel / vision panels at each floor, under a flat roof at the published 139.3 m. The
11
11
  // floors divide that height by OpenStreetMap's 45 levels. See docs/swissotel-reference.md.
12
12
  // Units are meters; +x is east, +z is south.