@a2f0/skyline 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE.md +20 -0
- package/README.md +52 -2
- package/docs/package.md +222 -0
- package/docs/skyline-geography.md +503 -0
- package/lib/models/aon-center.d.ts +4 -0
- package/lib/models/aon-center.js +60 -0
- package/lib/models/aon-geographic.d.ts +9 -0
- package/lib/models/aon-geographic.js +35 -0
- package/lib/models/aon-tower.d.ts +23 -0
- package/lib/models/aon-tower.js +130 -0
- package/lib/models/blue-cross-geographic.d.ts +14 -0
- package/lib/models/blue-cross-geographic.js +248 -0
- package/lib/models/borg-warner-geographic.d.ts +20 -0
- package/lib/models/borg-warner-geographic.js +143 -0
- package/lib/models/buckingham-geographic.d.ts +10 -0
- package/lib/models/buckingham-geographic.js +121 -0
- package/lib/models/building-kit.d.ts +84 -0
- package/lib/models/building-kit.js +358 -0
- package/lib/models/celebrations.d.ts +39 -0
- package/lib/models/celebrations.js +44 -0
- package/lib/models/chicago-athletic-association-geographic.d.ts +32 -0
- package/lib/models/chicago-athletic-association-geographic.js +173 -0
- package/lib/models/crain-communications.d.ts +5 -0
- package/lib/models/crain-communications.js +75 -0
- package/lib/models/crain-geographic.d.ts +10 -0
- package/lib/models/crain-geographic.js +69 -0
- package/lib/models/crain-tower.d.ts +32 -0
- package/lib/models/crain-tower.js +378 -0
- package/lib/models/facade-grid.d.ts +18 -0
- package/lib/models/facade-grid.js +176 -0
- package/lib/models/gage-geographic.d.ts +19 -0
- package/lib/models/gage-geographic.js +154 -0
- package/lib/models/heritage-at-millennium-park.d.ts +3 -0
- package/lib/models/heritage-at-millennium-park.js +284 -0
- package/lib/models/heritage-geographic.d.ts +11 -0
- package/lib/models/heritage-geographic.js +208 -0
- package/lib/models/hyatt-west-tower-geographic.d.ts +26 -0
- package/lib/models/hyatt-west-tower-geographic.js +86 -0
- package/lib/models/keith-ascher-geographic.d.ts +29 -0
- package/lib/models/keith-ascher-geographic.js +110 -0
- package/lib/models/kemper-geographic.d.ts +8 -0
- package/lib/models/kemper-geographic.js +153 -0
- package/lib/models/kemper.d.ts +2 -0
- package/lib/models/kemper.js +78 -0
- package/lib/models/lake-view-geographic.d.ts +15 -0
- package/lib/models/lake-view-geographic.js +134 -0
- package/lib/models/maclean-center-geographic.d.ts +20 -0
- package/lib/models/maclean-center-geographic.js +162 -0
- package/lib/models/michigan-boulevard-geographic.d.ts +12 -0
- package/lib/models/michigan-boulevard-geographic.js +157 -0
- package/lib/models/michigan-plaza-south-geographic.d.ts +7 -0
- package/lib/models/michigan-plaza-south-geographic.js +84 -0
- package/lib/models/michigan-plaza-south.d.ts +2 -0
- package/lib/models/michigan-plaza-south.js +68 -0
- package/lib/models/millennium-park-plaza-geographic.d.ts +9 -0
- package/lib/models/millennium-park-plaza-geographic.js +103 -0
- package/lib/models/monroe-geographic.d.ts +19 -0
- package/lib/models/monroe-geographic.js +177 -0
- package/lib/models/north-michigan-180-geographic.d.ts +14 -0
- package/lib/models/north-michigan-180-geographic.js +117 -0
- package/lib/models/north-wabash-geographic.d.ts +14 -0
- package/lib/models/north-wabash-geographic.js +122 -0
- package/lib/models/on-the-park-geographic.d.ts +11 -0
- package/lib/models/on-the-park-geographic.js +245 -0
- package/lib/models/one-prudential-geographic.d.ts +12 -0
- package/lib/models/one-prudential-geographic.js +55 -0
- package/lib/models/one-prudential-plaza.d.ts +5 -0
- package/lib/models/one-prudential-plaza.js +72 -0
- package/lib/models/one-prudential-tower.d.ts +45 -0
- package/lib/models/one-prudential-tower.js +246 -0
- package/lib/models/peoples-gas-geographic.d.ts +23 -0
- package/lib/models/peoples-gas-geographic.js +250 -0
- package/lib/models/railway-exchange-geographic.d.ts +18 -0
- package/lib/models/railway-exchange-geographic.js +176 -0
- package/lib/models/river-plaza-geographic.d.ts +16 -0
- package/lib/models/river-plaza-geographic.js +104 -0
- package/lib/models/sheraton-grand-geographic.d.ts +24 -0
- package/lib/models/sheraton-grand-geographic.js +112 -0
- package/lib/models/six-north-michigan-geographic.d.ts +16 -0
- package/lib/models/six-north-michigan-geographic.js +180 -0
- package/lib/models/skyline-geography-data.d.ts +44 -0
- package/lib/models/skyline-geography-data.js +607 -0
- package/lib/models/skyline-geography.d.ts +23 -0
- package/lib/models/skyline-geography.js +322 -0
- package/lib/models/swissotel-geographic.d.ts +24 -0
- package/lib/models/swissotel-geographic.js +79 -0
- package/lib/models/three-illinois-center-geographic.d.ts +26 -0
- package/lib/models/three-illinois-center-geographic.js +86 -0
- package/lib/models/trump-geographic.d.ts +14 -0
- package/lib/models/trump-geographic.js +51 -0
- package/lib/models/trump-international-tower.d.ts +4 -0
- package/lib/models/trump-international-tower.js +84 -0
- package/lib/models/trump-tower.d.ts +43 -0
- package/lib/models/trump-tower.js +229 -0
- package/lib/models/two-illinois-center-geographic.d.ts +26 -0
- package/lib/models/two-illinois-center-geographic.js +85 -0
- package/lib/models/two-prudential-geographic.d.ts +25 -0
- package/lib/models/two-prudential-geographic.js +47 -0
- package/lib/models/two-prudential-plaza.d.ts +8 -0
- package/lib/models/two-prudential-plaza.js +55 -0
- package/lib/models/two-prudential-tower.d.ts +53 -0
- package/lib/models/two-prudential-tower.js +441 -0
- package/lib/models/university-club-geographic.d.ts +24 -0
- package/lib/models/university-club-geographic.js +217 -0
- package/lib/models/willoughby-tower-geographic.d.ts +17 -0
- package/lib/models/willoughby-tower-geographic.js +195 -0
- package/lib/models/window-illumination.d.ts +14 -0
- package/lib/models/window-illumination.js +64 -0
- package/lib/package-assets.d.ts +5 -0
- package/lib/package-assets.js +9 -0
- package/lib/skyline-package.d.ts +23 -0
- package/lib/skyline-package.js +25 -0
- package/lib/skyline-scene.d.ts +25 -0
- package/lib/skyline-scene.js +86 -0
- package/lib/study-types.d.ts +49 -0
- package/lib/study-types.js +1 -0
- package/lib/vendor/three-r186.d.ts +1 -0
- package/lib/vendor/three-r186.js +1 -0
- package/package.json +64 -4
- package/site/building-detail.html +54 -0
- package/site/building-detail.js +84 -0
- package/site/building-study.html +58 -0
- package/site/building-study.js +3 -0
- package/site/index.html +407 -0
- package/site/models/aon-center.js +60 -0
- package/site/models/aon-geographic.js +35 -0
- package/site/models/aon-tower.js +130 -0
- package/site/models/blue-cross-geographic.js +248 -0
- package/site/models/borg-warner-geographic.js +143 -0
- package/site/models/buckingham-geographic.js +121 -0
- package/site/models/building-kit.js +358 -0
- package/site/models/celebrations.js +44 -0
- package/site/models/chicago-athletic-association-geographic.js +173 -0
- package/site/models/crain-communications.js +75 -0
- package/site/models/crain-geographic.js +69 -0
- package/site/models/crain-reference.svg +10 -0
- package/site/models/crain-tower.js +378 -0
- package/site/models/facade-grid.js +176 -0
- package/site/models/gage-geographic.js +154 -0
- package/site/models/heritage-at-millennium-park.js +284 -0
- package/site/models/heritage-geographic.js +208 -0
- package/site/models/hyatt-west-tower-geographic.js +86 -0
- package/site/models/keith-ascher-geographic.js +110 -0
- package/site/models/kemper-geographic.js +153 -0
- package/site/models/kemper.js +78 -0
- package/site/models/lake-view-geographic.js +134 -0
- package/site/models/maclean-center-geographic.js +162 -0
- package/site/models/michigan-boulevard-geographic.js +157 -0
- package/site/models/michigan-plaza-south-geographic.js +84 -0
- package/site/models/michigan-plaza-south.js +68 -0
- package/site/models/millennium-park-plaza-geographic.js +103 -0
- package/site/models/monroe-geographic.js +177 -0
- package/site/models/north-michigan-180-geographic.js +117 -0
- package/site/models/north-wabash-geographic.js +122 -0
- package/site/models/on-the-park-geographic.js +245 -0
- package/site/models/one-prudential-geographic.js +55 -0
- package/site/models/one-prudential-plaza.js +72 -0
- package/site/models/one-prudential-tower.js +246 -0
- package/site/models/peoples-gas-geographic.js +250 -0
- package/site/models/railway-exchange-geographic.js +176 -0
- package/site/models/river-plaza-geographic.js +104 -0
- package/site/models/sheraton-grand-geographic.js +112 -0
- package/site/models/six-north-michigan-geographic.js +180 -0
- package/site/models/skyline-geography-data.js +607 -0
- package/site/models/skyline-geography.js +322 -0
- package/site/models/skyline-panorama.svg +3491 -0
- package/site/models/skyline-reference.svg +3090 -0
- package/site/models/swissotel-geographic.js +79 -0
- package/site/models/three-illinois-center-geographic.js +86 -0
- package/site/models/trump-geographic.js +51 -0
- package/site/models/trump-international-tower.js +84 -0
- package/site/models/trump-tower.js +229 -0
- package/site/models/two-illinois-center-geographic.js +85 -0
- package/site/models/two-prudential-geographic.js +47 -0
- package/site/models/two-prudential-plaza.js +55 -0
- package/site/models/two-prudential-tower.js +441 -0
- package/site/models/university-club-geographic.js +217 -0
- package/site/models/willoughby-tower-geographic.js +195 -0
- package/site/models/window-illumination.js +64 -0
- package/site/skyline-3d.html +92 -0
- package/site/skyline-3d.js +333 -0
- package/site/skyline-animated.svg +18795 -0
- package/site/skyline-comparison.js +97 -0
- package/site/skyline-original-fit.svg +18312 -0
- package/site/skyline-scene.js +86 -0
- package/site/skyline-study.html +83 -0
- package/site/skyline-study.js +109 -0
- package/site/skyline-webgl.html +667 -0
- package/site/stars.svg +176 -0
- package/site/study-loader.js +20 -0
- package/site/study-types.js +1 -0
- package/site/study-viewer.js +605 -0
- package/site/study.css +204 -0
- package/site/vendor/THREE-LICENSE.txt +21 -0
- package/site/vendor/three-r186.js +4228 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import * as THREE from "../vendor/three-r186.js";
|
|
2
|
+
import { createBuilder, polygonOf } from "./building-kit.js";
|
|
3
|
+
import { chainSkin, chainsOf, orient, paintedSlab, planOf, turnAt, turnsAt } from "./facade-grid.js";
|
|
4
|
+
// The Buckingham, 360 East Randolph Street (Fujikawa Johnson & Associates, 1982): the
|
|
5
|
+
// geographic layout's model, on the mapped outline 95486940 and its rooftop part 284790189.
|
|
6
|
+
// The original layout has no model of it; the drawing shows it right of 340 on the Park,
|
|
7
|
+
// outside the excerpt it is fitted to.
|
|
8
|
+
//
|
|
9
|
+
// A concrete frame of five bays a face, a band at every floor over bronze ribbon windows. On
|
|
10
|
+
// the south, east and north faces the end bays stand forward of the middle three, and two
|
|
11
|
+
// corners are notched for stacked balconies. The top floor's tall openings sit under a cap
|
|
12
|
+
// at the mapped 119 m, and the rooftop enclosure rises to the published 121.9 m. Floor levels
|
|
13
|
+
// are measured on the drawing down from the mapped roof; see docs/buckingham-reference.md.
|
|
14
|
+
// Units are meters; +x is east, +z is south.
|
|
15
|
+
export const buckinghamLevels = Object.freeze({
|
|
16
|
+
lowestFloor: 18.94, // floor 7, the drawing's lowest floor line
|
|
17
|
+
pitch: 2.5456, // the drawn floor lines, 36 floors apart from floor 7's to floor 43's
|
|
18
|
+
capFoot: 117.4, // the top floor's openings' head, under the cap
|
|
19
|
+
roof: 119, // the cap's top, mapped
|
|
20
|
+
top: 121.9, // the rooftop enclosure's top, the published architectural height
|
|
21
|
+
});
|
|
22
|
+
const h = buckinghamLevels;
|
|
23
|
+
// Each floor's level, from the second, over the lobby, to the 44th.
|
|
24
|
+
const floor = (n) => h.lowestFloor + (n - 7) * h.pitch;
|
|
25
|
+
const color = (hex) => new THREE.Color(hex);
|
|
26
|
+
const glassTones = [0x303030, 0x343434, 0x383838, 0x3c3c3c].map(color);
|
|
27
|
+
const litGlass = color(0x6e6e6e), dimGlass = color(0x464646);
|
|
28
|
+
const concrete = color(0x4c4c4c), lobby = color(0x3a3a3a), core = color(0x262626), roofing = color(0x3a3a3a), enclosure = color(0x2e2e2e);
|
|
29
|
+
function paneColor(row, bay, wall) {
|
|
30
|
+
const hash = (Math.imul(row + 19, 0x9e3779b1) ^ Math.imul(bay + 43, 0x85ebca77) ^ Math.imul(wall + 31, 0xc2b2ae3d)) >>> 0;
|
|
31
|
+
const value = hash % 101;
|
|
32
|
+
if (value < 4)
|
|
33
|
+
return litGlass;
|
|
34
|
+
if (value < 8)
|
|
35
|
+
return dimGlass;
|
|
36
|
+
return glassTones[value % glassTones.length];
|
|
37
|
+
}
|
|
38
|
+
const rows = [{ lo: 0.25, hi: floor(2) - 0.3, kind: "lobby", floor: 1 }];
|
|
39
|
+
for (let n = 2; n <= 43; n += 1) {
|
|
40
|
+
rows.push({ lo: floor(n) - 0.3, hi: floor(n) + 0.55, kind: "band", floor: n });
|
|
41
|
+
rows.push({ lo: floor(n) + 0.55, hi: floor(n + 1) - 0.3, kind: "glass", floor: n });
|
|
42
|
+
}
|
|
43
|
+
rows.push({ lo: floor(44) - 0.3, hi: floor(44) + 1.4, kind: "band", floor: 44 });
|
|
44
|
+
rows.push({ lo: floor(44) + 1.4, hi: h.capFoot, kind: "glass", floor: 44 });
|
|
45
|
+
rows.push({ lo: h.capFoot, hi: h.roof, kind: "band", floor: 45 });
|
|
46
|
+
const heights = [rows[0].lo, ...rows.map((row) => row.hi)];
|
|
47
|
+
export function createBuckinghamGeographicBuilding(record, projectPlan, offset) {
|
|
48
|
+
const kit = createBuilder(record.name, record.id);
|
|
49
|
+
const shell = kit.batch("Buckingham · shell", kit.material(0xffffff, { vertexColors: true }));
|
|
50
|
+
const wall = kit.batch("Buckingham · frame and glass", kit.material(0xffffff, { vertexColors: true }));
|
|
51
|
+
const piers = kit.batch("Buckingham · piers", kit.material(0x565656));
|
|
52
|
+
const balconies = kit.batch("Buckingham · corner balconies", kit.material(0x5a5a5a));
|
|
53
|
+
const outline = (way) => orient(polygonOf(projectPlan(record.parts.find((part) => part.way === way).coordinates)));
|
|
54
|
+
const tower = outline(95486940), roofRoom = outline(284790189);
|
|
55
|
+
// A solid on an outline between two heights.
|
|
56
|
+
const solid = (corners, lo, hi, paint) => {
|
|
57
|
+
planOf(corners).forEach((run) => {
|
|
58
|
+
const a = run.at(0), b = run.at(run.length), n = run.normal(0);
|
|
59
|
+
kit.quad(shell, [[a[0], lo, a[1]], [b[0], lo, b[1]], [b[0], hi, b[1]], [a[0], hi, a[1]]], [[n[0], 0, n[1]]], paint);
|
|
60
|
+
});
|
|
61
|
+
paintedSlab(kit, shell, corners, lo, false, roofing);
|
|
62
|
+
paintedSlab(kit, shell, corners, hi, true, roofing);
|
|
63
|
+
};
|
|
64
|
+
solid(tower, 0, h.roof, core);
|
|
65
|
+
solid(roofRoom, h.roof, h.top, enclosure);
|
|
66
|
+
// Every wall's skin, one box a chain, held 2 cm clear of its ends, or 9 cm, past its own
|
|
67
|
+
// depth, where the outline turns in and the next wall's skin would otherwise cross it. A
|
|
68
|
+
// face's cells are a quarter bay wide, with its corner piers painted in its end cells.
|
|
69
|
+
const plan = planOf(tower), chains = chainsOf(plan);
|
|
70
|
+
chains.forEach((chain, index) => {
|
|
71
|
+
const first = chain.runs[0], last = chain.runs.at(-1);
|
|
72
|
+
const s0 = turnsAt(plan, first)[0] > 0 ? 0.09 : 0.02, s1 = chain.length - (turnsAt(plan, last)[1] > 0 ? 0.09 : 0.02);
|
|
73
|
+
const count = Math.max(1, Math.round((chain.length - 1.2) / 1.6));
|
|
74
|
+
const columns = chain.length > 4 ? [0, 0.6, ...Array.from({ length: count - 1 }, (_, i) => 0.6 + (chain.length - 1.2) * (i + 1) / count), chain.length - 0.6, chain.length] : [0, chain.length];
|
|
75
|
+
chainSkin(kit, wall, plan, chain, 0, chain.runs.length - 1, s0, s1, columns, heights, 0.02, 0.07, (bay, r) => {
|
|
76
|
+
const row = rows[r], pier = chain.length > 4 && (bay <= 1 || bay >= columns.length - 1);
|
|
77
|
+
if (row.kind === "lobby")
|
|
78
|
+
return lobby;
|
|
79
|
+
if (row.kind === "band" || pier)
|
|
80
|
+
return concrete;
|
|
81
|
+
return paneColor(row.floor, bay, index);
|
|
82
|
+
}, concrete);
|
|
83
|
+
// Piers stand proud between a wall's bays, about 6.4 m wide: three across a face's
|
|
84
|
+
// set-back middle and five across the flat west face; an end bay has none.
|
|
85
|
+
const bays = Math.round(chain.length / 6.5);
|
|
86
|
+
for (let k = 1; k < bays; k += 1) {
|
|
87
|
+
const s = chain.length * k / bays, j = chain.starts.findLastIndex((start) => start <= s), run = plan[chain.runs[j]];
|
|
88
|
+
kit.box(piers, run.at(s - chain.starts[j]), run.normal(0), 0.3, 0.05, 0.25, 0.3, h.roof - 0.1);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
// The notched corners: two short walls turning in between two faces. A balcony fills each
|
|
92
|
+
// notch on every floor to the 43rd, a slab held clear of the walls' skins under an L of
|
|
93
|
+
// railing along its open sides.
|
|
94
|
+
chains.forEach((chain, i) => {
|
|
95
|
+
const next = chains[(i + 1) % chains.length];
|
|
96
|
+
if (chain.length > 3 || next.length > 3 || turnAt(plan[chain.runs.at(-1)], plan[next.runs[0]]) <= 0)
|
|
97
|
+
return;
|
|
98
|
+
const a = plan[chain.runs[0]], b = plan[next.runs[0]];
|
|
99
|
+
const [p0, p1, p2] = [a.at(0), a.at(a.length), b.at(b.length)];
|
|
100
|
+
const [na, nb] = [a.normal(0), b.normal(0)];
|
|
101
|
+
const q = [p0[0] + p2[0] - p1[0], p0[1] + p2[1] - p1[1]];
|
|
102
|
+
const off = (p, n, d) => [p[0] + n[0] * d, p[1] + n[1] * d];
|
|
103
|
+
const a0 = off(p0, na, 0.07), b0 = off(p2, nb, 0.07), inner = off(off(p1, na, 0.07), nb, 0.07);
|
|
104
|
+
// Railing: the slab's open sides, 5 cm deep, turned in toward the notch's inner corner.
|
|
105
|
+
const inward = (from, to) => {
|
|
106
|
+
const [dx, dz] = [to[0] - from[0], to[1] - from[1]], l = Math.hypot(dx, dz), n = [-dz / l, dx / l];
|
|
107
|
+
return (inner[0] - from[0]) * n[0] + (inner[1] - from[1]) * n[1] > 0 ? n : [-n[0], -n[1]];
|
|
108
|
+
};
|
|
109
|
+
const [na0, nb0] = [inward(a0, q), inward(q, b0)];
|
|
110
|
+
const rail = [a0, q, b0, off(b0, nb0, 0.05), off(off(q, na0, 0.05), nb0, 0.05), off(a0, na0, 0.05)];
|
|
111
|
+
for (let n = 2; n <= 43; n += 1) {
|
|
112
|
+
kit.prism(balconies, planOf(orient([inner, a0, q, b0])), [floor(n) - 0.12, floor(n) + 0.12]);
|
|
113
|
+
kit.prism(balconies, planOf(orient(rail)), [floor(n) + 0.12, floor(n) + 1.0]);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
const model = kit.finish({ height: h.top, outlines: [shell, piers], opacity: 0.16 });
|
|
117
|
+
model.building.position.set(offset[0], 0, offset[1]);
|
|
118
|
+
model.building.userData["geography"] = record;
|
|
119
|
+
model.building.userData["geographicDetail"] = { levels: h, source: "docs/buckingham-reference.md" };
|
|
120
|
+
return model;
|
|
121
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import * as THREE from "../vendor/three-r186.js";
|
|
2
|
+
import type { WindowIllumination } from "./window-illumination.js";
|
|
3
|
+
export declare const deg: number;
|
|
4
|
+
export type Vec2 = [number, number];
|
|
5
|
+
export type Vec3 = [number, number, number];
|
|
6
|
+
export type Normal = Vec2;
|
|
7
|
+
export interface Run {
|
|
8
|
+
length: number;
|
|
9
|
+
pieces(s0?: number, s1?: number): number;
|
|
10
|
+
at(s: number, offset?: number): Vec2;
|
|
11
|
+
normal(s?: number): Normal;
|
|
12
|
+
}
|
|
13
|
+
export type Plan = Run[];
|
|
14
|
+
export declare function line(a: Vec2, b: Vec2): Run;
|
|
15
|
+
export declare function arc(center: Vec2, radius: number, from: number, to: number, concave?: boolean, step?: number): Run;
|
|
16
|
+
export declare function bulge(a: Vec2, b: Vec2, sagitta: number, step?: number): Run;
|
|
17
|
+
export declare function rectangle(west: number, east: number, north: number, south: number): Plan;
|
|
18
|
+
export declare const evenly: (length: number, count: number) => number[];
|
|
19
|
+
export declare const cornerNormal: (a: Vec2, b: Vec2) => Vec2;
|
|
20
|
+
export declare const station: (run: Run, s: number) => Station;
|
|
21
|
+
export declare const along: ({ at, normal }: Station, offset: number, side?: number) => Vec2;
|
|
22
|
+
export declare const point: ([x, z]: Vec2, y: number) => Vec3;
|
|
23
|
+
export interface Station {
|
|
24
|
+
at: Vec2;
|
|
25
|
+
normal: Normal;
|
|
26
|
+
}
|
|
27
|
+
export declare function polygonOf(runs: Plan): Vec2[];
|
|
28
|
+
export declare function inside(polygon: Vec2[], [x, z]: Vec2): boolean;
|
|
29
|
+
export interface BatchData {
|
|
30
|
+
name: string;
|
|
31
|
+
surface: THREE.MeshToonMaterial;
|
|
32
|
+
positions: number[];
|
|
33
|
+
normals: number[];
|
|
34
|
+
colors: number[];
|
|
35
|
+
}
|
|
36
|
+
export interface Omission {
|
|
37
|
+
batch: string;
|
|
38
|
+
corners: Vec3[];
|
|
39
|
+
normal: Vec3;
|
|
40
|
+
}
|
|
41
|
+
export interface BandOptions {
|
|
42
|
+
omit?: string[];
|
|
43
|
+
from?: number;
|
|
44
|
+
to?: number;
|
|
45
|
+
closed?: boolean;
|
|
46
|
+
visible?: (point: Vec2, y: number) => boolean;
|
|
47
|
+
}
|
|
48
|
+
export interface BoxOptions {
|
|
49
|
+
omit?: string[];
|
|
50
|
+
}
|
|
51
|
+
export interface PrismOptions {
|
|
52
|
+
omit?: string[];
|
|
53
|
+
}
|
|
54
|
+
export interface FinishOptions {
|
|
55
|
+
height: number;
|
|
56
|
+
outlines?: BatchData[];
|
|
57
|
+
threshold?: number;
|
|
58
|
+
opacity?: number;
|
|
59
|
+
}
|
|
60
|
+
export interface BuildingModel {
|
|
61
|
+
building: THREE.Group;
|
|
62
|
+
height: number;
|
|
63
|
+
triangleCount: number;
|
|
64
|
+
illumination?: WindowIllumination;
|
|
65
|
+
setHighlighted(highlighted: boolean): void;
|
|
66
|
+
setWireframe(enabled: boolean): void;
|
|
67
|
+
}
|
|
68
|
+
export interface Builder {
|
|
69
|
+
building: THREE.Group;
|
|
70
|
+
material(color: number, extras?: THREE.MeshToonMaterialParameters): THREE.MeshToonMaterial;
|
|
71
|
+
batch(name: string, surface: THREE.MeshToonMaterial): BatchData;
|
|
72
|
+
triangle(target: BatchData, points: Vec3[], normals: Vec3[], color?: THREE.Color): void;
|
|
73
|
+
quad(target: BatchData, points: [Vec3, Vec3, Vec3, Vec3], normals: Vec3[], color?: THREE.Color): void;
|
|
74
|
+
panel(target: BatchData, run: Run, s0: number, s1: number, y0: number, y1: number, offset: number, color?: THREE.Color): void;
|
|
75
|
+
ledge(target: BatchData, run: Run, s0: number, s1: number, y: number, inner: number, outer: number, up: boolean): void;
|
|
76
|
+
band(target: BatchData, runs: Plan, y0: number, y1: number, proud: number, options?: BandOptions): void;
|
|
77
|
+
box(target: BatchData, at: Vec2, normal: Normal, halfWidth: number, back: number, front: number, y0: number, y1: number, options?: BoxOptions): void;
|
|
78
|
+
slab(target: BatchData, polygon: Vec2[], y: number, up: boolean): void;
|
|
79
|
+
prism(target: BatchData, runs: Plan, heights: number[], options?: PrismOptions): Vec2[];
|
|
80
|
+
finish(options: FinishOptions): BuildingModel;
|
|
81
|
+
}
|
|
82
|
+
export declare function createBuilder(name: string, buildingId: string, { gradient }?: {
|
|
83
|
+
gradient?: number[];
|
|
84
|
+
}): Builder;
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import * as THREE from "../vendor/three-r186.js";
|
|
2
|
+
// Shared geometry for fitted skyline models. A plan is a list of runs, straight or
|
|
3
|
+
// arced, counterclockwise from above, so each run's outward normal is to its right.
|
|
4
|
+
// Units are meters; +x is east (right in the skyline view) and +z is south.
|
|
5
|
+
export const deg = Math.PI / 180;
|
|
6
|
+
export function line(a, b) {
|
|
7
|
+
const length = Math.hypot(b[0] - a[0], b[1] - a[1]);
|
|
8
|
+
const tangent = [(b[0] - a[0]) / length, (b[1] - a[1]) / length];
|
|
9
|
+
const normal = [-tangent[1], tangent[0]];
|
|
10
|
+
return {
|
|
11
|
+
length,
|
|
12
|
+
pieces: () => 1,
|
|
13
|
+
at: (s, offset = 0) => [a[0] + tangent[0] * s + normal[0] * offset, a[1] + tangent[1] * s + normal[1] * offset],
|
|
14
|
+
normal: () => normal,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
// Angles measure the outward normal from +z toward +x. A concave arc keeps its
|
|
18
|
+
// center outside the building, so the same angle gives the same normal. Facets stay
|
|
19
|
+
// within `step`; 2.4° keeps silhouettes round without faceted toon bands.
|
|
20
|
+
export function arc(center, radius, from, to, concave = false, step = 2.4 * deg) {
|
|
21
|
+
const length = radius * Math.abs(to - from);
|
|
22
|
+
const angle = (s) => from + (to - from) * s / length;
|
|
23
|
+
const side = concave ? -1 : 1;
|
|
24
|
+
return {
|
|
25
|
+
length,
|
|
26
|
+
pieces: (s0 = 0, s1 = length) => Math.max(1, Math.ceil(Math.abs(to - from) * (s1 - s0) / length / step - 1e-9)),
|
|
27
|
+
at(s, offset = 0) {
|
|
28
|
+
const a = angle(s), reach = side * radius + offset;
|
|
29
|
+
return [center[0] + Math.sin(a) * reach, center[1] + Math.cos(a) * reach];
|
|
30
|
+
},
|
|
31
|
+
normal: (s = 0) => [Math.sin(angle(s)), Math.cos(angle(s))],
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
// A gentle convex arc between two corners, bulging outward by its sagitta.
|
|
35
|
+
export function bulge(a, b, sagitta, step) {
|
|
36
|
+
const chord = Math.hypot(b[0] - a[0], b[1] - a[1]);
|
|
37
|
+
const radius = chord * chord / (8 * sagitta) + sagitta / 2;
|
|
38
|
+
const mid = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2];
|
|
39
|
+
const inward = [(b[1] - a[1]) / chord, -(b[0] - a[0]) / chord];
|
|
40
|
+
const center = [mid[0] + inward[0] * (radius - sagitta), mid[1] + inward[1] * (radius - sagitta)];
|
|
41
|
+
const angleOf = (p) => Math.atan2(p[0] - center[0], p[1] - center[1]);
|
|
42
|
+
let from = angleOf(a), to = angleOf(b);
|
|
43
|
+
if (to < from)
|
|
44
|
+
to += Math.PI * 2;
|
|
45
|
+
return arc(center, radius, from, to, false, step);
|
|
46
|
+
}
|
|
47
|
+
// A rectangular plan from its bounds, counterclockwise from above: the south, east, north,
|
|
48
|
+
// then west faces. North is the smaller z.
|
|
49
|
+
export function rectangle(west, east, north, south) {
|
|
50
|
+
const [sw, se, ne, nw] = [[west, south], [east, south], [east, north], [west, north]];
|
|
51
|
+
return [line(sw, se), line(se, ne), line(ne, nw), line(nw, sw)];
|
|
52
|
+
}
|
|
53
|
+
export const evenly = (length, count) => Array.from({ length: count + 1 }, (_, i) => length * i / count);
|
|
54
|
+
export const cornerNormal = (a, b) => { const l = Math.hypot(a[0] + b[0], a[1] + b[1]); return [(a[0] + b[0]) / l, (a[1] + b[1]) / l]; };
|
|
55
|
+
export const station = (run, s) => ({ at: run.at(s), normal: run.normal(s) });
|
|
56
|
+
// A plan point offset outward along a station's normal and sideways along its run.
|
|
57
|
+
export const along = ({ at, normal }, offset, side = 0) => [at[0] + normal[0] * offset + normal[1] * side, at[1] + normal[1] * offset - normal[0] * side];
|
|
58
|
+
export const point = ([x, z], y) => [x, y, z];
|
|
59
|
+
// The plan outline with the same facets the walls use, so roofs meet them edge to edge.
|
|
60
|
+
export function polygonOf(runs) {
|
|
61
|
+
return runs.flatMap((run) => { const n = run.pieces(0, run.length); return Array.from({ length: n }, (_, i) => run.at(run.length * i / n)); });
|
|
62
|
+
}
|
|
63
|
+
// Twice the signed area with +z south, so a counterclockwise plan from above is negative.
|
|
64
|
+
const shoelace = (polygon) => polygon.reduce((sum, [x, z], i) => { const [x2, z2] = polygon[(i + 1) % polygon.length]; return sum + x * z2 - x2 * z; }, 0);
|
|
65
|
+
export function inside(polygon, [x, z]) {
|
|
66
|
+
let result = false;
|
|
67
|
+
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i, i += 1) {
|
|
68
|
+
const [xi, zi] = polygon[i], [xj, zj] = polygon[j];
|
|
69
|
+
if ((zi > z) !== (zj > z) && x < (xj - xi) * (z - zi) / (zj - zi) + xi)
|
|
70
|
+
result = !result;
|
|
71
|
+
}
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
// A model under construction: toon materials, batched geometry, and the standard model
|
|
75
|
+
// API once finished. Solids are closed by default. A caller may omit only faces another
|
|
76
|
+
// surface covers, by name, and each omission is kept in building.userData.omitted so a
|
|
77
|
+
// test can prove the cover.
|
|
78
|
+
export function createBuilder(name, buildingId, { gradient = [70, 135, 200, 255] } = {}) {
|
|
79
|
+
const building = new THREE.Group();
|
|
80
|
+
building.name = name;
|
|
81
|
+
building.userData["buildingId"] = buildingId;
|
|
82
|
+
const omitted = building.userData["omitted"] = [];
|
|
83
|
+
const ramp = new THREE.DataTexture(new Uint8Array(gradient), gradient.length, 1, THREE.RedFormat);
|
|
84
|
+
ramp.minFilter = ramp.magFilter = THREE.NearestFilter;
|
|
85
|
+
ramp.needsUpdate = true;
|
|
86
|
+
const materials = [], batches = [];
|
|
87
|
+
const material = (color, extras = {}) => {
|
|
88
|
+
const surface = new THREE.MeshToonMaterial({ color, gradientMap: ramp, ...extras });
|
|
89
|
+
materials.push(surface);
|
|
90
|
+
return surface;
|
|
91
|
+
};
|
|
92
|
+
// Each batch is one draw call. Normals are analytic, so arc facets shade as a curve.
|
|
93
|
+
const batch = (batchName, surface) => {
|
|
94
|
+
const data = { name: batchName, surface, positions: [], normals: [], colors: [] };
|
|
95
|
+
batches.push(data);
|
|
96
|
+
return data;
|
|
97
|
+
};
|
|
98
|
+
const always = () => true;
|
|
99
|
+
// Runs must hand over end to start, and a loop must come back to where it began;
|
|
100
|
+
// otherwise the walls leave a gap no omission records.
|
|
101
|
+
const joins = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1]) <= 1e-6;
|
|
102
|
+
function checkJoins(runs, label, loop) {
|
|
103
|
+
runs.forEach((run, index) => {
|
|
104
|
+
const next = runs[index + 1];
|
|
105
|
+
if (next && !joins(run.at(run.length), next.at(0)))
|
|
106
|
+
throw new Error(`A ${label}'s runs must join end to start.`);
|
|
107
|
+
});
|
|
108
|
+
const last = runs.at(-1);
|
|
109
|
+
if (loop && !joins(last.at(last.length), runs[0].at(0)))
|
|
110
|
+
throw new Error(`A ${label}'s runs must return to their start.`);
|
|
111
|
+
}
|
|
112
|
+
const white = new THREE.Color(1, 1, 1);
|
|
113
|
+
// Winding follows the first normal, so every helper stays outward-facing.
|
|
114
|
+
// Quads pass their vertices directly, avoiding temporary triangle and normal arrays
|
|
115
|
+
// for every face while retaining the same winding, vertex order, and attributes.
|
|
116
|
+
function appendTriangle(target, a, b, c, na, nb, nc, color = white) {
|
|
117
|
+
const ux = b[0] - a[0], uy = b[1] - a[1], uz = b[2] - a[2];
|
|
118
|
+
const vx = c[0] - a[0], vy = c[1] - a[1], vz = c[2] - a[2];
|
|
119
|
+
const facing = (uy * vz - uz * vy) * na[0] + (uz * vx - ux * vz) * na[1] + (ux * vy - uy * vx) * na[2];
|
|
120
|
+
if (facing < 0) {
|
|
121
|
+
const point = b;
|
|
122
|
+
b = c;
|
|
123
|
+
c = point;
|
|
124
|
+
const normal = nb;
|
|
125
|
+
nb = nc;
|
|
126
|
+
nc = normal;
|
|
127
|
+
}
|
|
128
|
+
target.positions.push(a[0], a[1], a[2], b[0], b[1], b[2], c[0], c[1], c[2]);
|
|
129
|
+
target.normals.push(na[0], na[1], na[2], nb[0], nb[1], nb[2], nc[0], nc[1], nc[2]);
|
|
130
|
+
target.colors.push(color.r, color.g, color.b, color.r, color.g, color.b, color.r, color.g, color.b);
|
|
131
|
+
}
|
|
132
|
+
function triangle(target, points, normals, color) {
|
|
133
|
+
appendTriangle(target, points[0], points[1], points[2], normals[0], normals[1], normals[2], color);
|
|
134
|
+
}
|
|
135
|
+
function quad(target, [a, b, c, d], normals, color) {
|
|
136
|
+
const na = normals[0], flat = normals.length === 1;
|
|
137
|
+
const nb = flat ? na : normals[1], nc = flat ? na : normals[2], nd = flat ? na : normals[3];
|
|
138
|
+
appendTriangle(target, a, b, c, na, nb, nc, color);
|
|
139
|
+
appendTriangle(target, a, c, d, na, nc, nd, color);
|
|
140
|
+
}
|
|
141
|
+
// Names a face that does not exist, so a typo cannot silently keep it.
|
|
142
|
+
const checkOmit = (omit, allowed, what) => {
|
|
143
|
+
const unknown = (omit ?? []).find((faceName) => !allowed.includes(faceName));
|
|
144
|
+
if (unknown)
|
|
145
|
+
throw new Error(`A ${what} has no "${unknown}" face; it has ${allowed.join(", ")}.`);
|
|
146
|
+
};
|
|
147
|
+
// A flat face of a solid, unless the caller omitted it as covered.
|
|
148
|
+
function face(target, corners, normal, skip) {
|
|
149
|
+
if (skip)
|
|
150
|
+
omitted.push({ batch: target.name, corners, normal });
|
|
151
|
+
else
|
|
152
|
+
quad(target, corners, [normal]);
|
|
153
|
+
}
|
|
154
|
+
// A rectangle mapped onto a plan run, split into facets that follow an arc. Panels
|
|
155
|
+
// are single surfaces for panes and trim, not solids.
|
|
156
|
+
function panel(target, run, s0, s1, y0, y1, offset, color) {
|
|
157
|
+
const n = run.pieces(s0, s1);
|
|
158
|
+
for (let i = 0; i < n; i += 1) {
|
|
159
|
+
const sa = s0 + (s1 - s0) * i / n, sb = s0 + (s1 - s0) * (i + 1) / n;
|
|
160
|
+
const [pa, pb] = [run.at(sa, offset), run.at(sb, offset)];
|
|
161
|
+
const [na, nb] = [run.normal(sa), run.normal(sb)].map(([x, z]) => [x, 0, z]);
|
|
162
|
+
quad(target, [[pa[0], y0, pa[1]], [pb[0], y0, pb[1]], [pb[0], y1, pb[1]], [pa[0], y1, pa[1]]], [na, nb, nb, na], color);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// A horizontal strip between two offsets from a run: a soffit or a ledge top.
|
|
166
|
+
function ledge(target, run, s0, s1, y, inner, outer, up) {
|
|
167
|
+
const n = run.pieces(s0, s1), normal = [[0, up ? 1 : -1, 0]];
|
|
168
|
+
for (let i = 0; i < n; i += 1) {
|
|
169
|
+
const sa = s0 + (s1 - s0) * i / n, sb = s0 + (s1 - s0) * (i + 1) / n;
|
|
170
|
+
quad(target, [point(run.at(sa, inner), y), point(run.at(sb, inner), y), point(run.at(sb, outer), y), point(run.at(sa, outer), y)], normal);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// A projecting band along consecutive runs, chamfered at convex corners. It is closed
|
|
174
|
+
// by its back, soffit, top, and start and end returns unless `omit` names them; a
|
|
175
|
+
// `closed` band, such as a parapet around a whole roof, turns its last corner onto its
|
|
176
|
+
// first run instead of ending. `visible` skips pieces buried inside another volume, and
|
|
177
|
+
// the band closes itself wherever it stops and reopens.
|
|
178
|
+
function band(target, runs, y0, y1, proud, { omit = [], from = 0, to = Infinity, closed = false, visible = always } = {}) {
|
|
179
|
+
if (closed && (from !== 0 || to !== Infinity))
|
|
180
|
+
throw new Error("A closed band wraps whole runs, so it takes no from or to.");
|
|
181
|
+
if (closed && visible !== always)
|
|
182
|
+
throw new Error("A closed band cannot also be trimmed by visible; split it into open bands.");
|
|
183
|
+
checkJoins(runs, closed ? "closed band" : "band", closed);
|
|
184
|
+
const faceNames = ["back", "soffit", "top", "start", "end"];
|
|
185
|
+
checkOmit(omit, faceNames, "band");
|
|
186
|
+
const [back, soffit, top, start, end] = faceNames.map((faceName) => !omit.includes(faceName));
|
|
187
|
+
const endFace = (run, s, sign, keep) => {
|
|
188
|
+
const [inner, outer] = [run.at(s), run.at(s, proud)], n = run.normal(s);
|
|
189
|
+
face(target, [point(inner, y0), point(outer, y0), point(outer, y1), point(inner, y1)], [n[1] * sign, 0, -n[0] * sign], !keep);
|
|
190
|
+
};
|
|
191
|
+
// Outward normals turn counterclockwise from above at a convex corner, opening a wedge to fill.
|
|
192
|
+
const cornerWedge = (run, next) => {
|
|
193
|
+
const at = run.at(run.length), n1 = run.normal(run.length), n2 = next.normal(0);
|
|
194
|
+
const turn = n1[0] * n2[1] - n1[1] * n2[0];
|
|
195
|
+
// Runs that double back share a point but face opposite ways, so no wedge can close
|
|
196
|
+
// the joint and both returns are missing: a hole no omission would record. Runs that
|
|
197
|
+
// merely continue straight face the same way and need no wedge at all.
|
|
198
|
+
if (Math.abs(turn) <= 1e-6 && n1[0] * n2[0] + n1[1] * n2[1] < 0)
|
|
199
|
+
throw new Error("A band cannot double back on itself; end the band and start another.");
|
|
200
|
+
if (turn > 1e-6)
|
|
201
|
+
throw new Error("A band cannot turn a concave corner; split it into two bands there.");
|
|
202
|
+
if (turn >= -1e-6)
|
|
203
|
+
return;
|
|
204
|
+
const a = run.at(run.length, proud), b = next.at(0, proud), mitre = cornerNormal(n1, n2);
|
|
205
|
+
quad(target, [point(a, y0), point(b, y0), point(b, y1), point(a, y1)], [[mitre[0], 0, mitre[1]]]);
|
|
206
|
+
if (soffit)
|
|
207
|
+
triangle(target, [point(at, y0), point(b, y0), point(a, y0)], [[0, -1, 0], [0, -1, 0], [0, -1, 0]]);
|
|
208
|
+
else
|
|
209
|
+
omitted.push({ batch: target.name, corners: [point(at, y0), point(b, y0), point(a, y0)], normal: [0, -1, 0] });
|
|
210
|
+
if (top)
|
|
211
|
+
triangle(target, [point(at, y1), point(a, y1), point(b, y1)], [[0, 1, 0], [0, 1, 0], [0, 1, 0]]);
|
|
212
|
+
else
|
|
213
|
+
omitted.push({ batch: target.name, corners: [point(at, y1), point(a, y1), point(b, y1)], normal: [0, 1, 0] });
|
|
214
|
+
};
|
|
215
|
+
const pieces = [];
|
|
216
|
+
runs.forEach((run, index) => {
|
|
217
|
+
const first = index === 0 ? from : 0, last = Math.min(run.length, index === runs.length - 1 ? to : Infinity);
|
|
218
|
+
if (last - first <= 1e-9)
|
|
219
|
+
return;
|
|
220
|
+
const count = run.pieces(first, last);
|
|
221
|
+
for (let i = 0; i < count; i += 1) {
|
|
222
|
+
const sa = first + (last - first) * i / count, sb = first + (last - first) * (i + 1) / count;
|
|
223
|
+
pieces.push({ run, index, sa, sb, shown: visible(run.at((sa + sb) / 2, proud + 0.2), y0) });
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
let drawn = false, opened = false, previous = null;
|
|
227
|
+
for (const piece of pieces) {
|
|
228
|
+
const { run, sa, sb, shown } = piece;
|
|
229
|
+
// A closed band opens with no return: its wrap-around corner closes it instead.
|
|
230
|
+
if (shown && !drawn && !(closed && !opened))
|
|
231
|
+
endFace(run, sa, -1, opened ? true : start);
|
|
232
|
+
if (!shown && drawn && previous)
|
|
233
|
+
endFace(previous.run, previous.sb, 1, true);
|
|
234
|
+
if (shown && drawn && previous && previous.index !== piece.index)
|
|
235
|
+
cornerWedge(previous.run, run);
|
|
236
|
+
drawn = shown;
|
|
237
|
+
previous = piece;
|
|
238
|
+
if (!shown)
|
|
239
|
+
continue;
|
|
240
|
+
opened = true;
|
|
241
|
+
panel(target, run, sa, sb, y0, y1, proud);
|
|
242
|
+
const [ia, ib, oa, ob] = [run.at(sa), run.at(sb), run.at(sa, proud), run.at(sb, proud)];
|
|
243
|
+
if (soffit)
|
|
244
|
+
ledge(target, run, sa, sb, y0, 0, proud, false);
|
|
245
|
+
else
|
|
246
|
+
omitted.push({ batch: target.name, corners: [point(ia, y0), point(ib, y0), point(ob, y0), point(oa, y0)], normal: [0, -1, 0] });
|
|
247
|
+
if (top)
|
|
248
|
+
ledge(target, run, sa, sb, y1, 0, proud, true);
|
|
249
|
+
else
|
|
250
|
+
omitted.push({ batch: target.name, corners: [point(ia, y1), point(ib, y1), point(ob, y1), point(oa, y1)], normal: [0, 1, 0] });
|
|
251
|
+
const [na, nb] = [run.normal(sa), run.normal(sb)].map(([x, z]) => [-x, 0, -z]);
|
|
252
|
+
const backCorners = [point(ib, y0), point(ia, y0), point(ia, y1), point(ib, y1)];
|
|
253
|
+
if (back)
|
|
254
|
+
quad(target, backCorners, [nb, na, na, nb]);
|
|
255
|
+
else
|
|
256
|
+
omitted.push({ batch: target.name, corners: backCorners, normal: [(na[0] + nb[0]) / 2, 0, (na[2] + nb[2]) / 2] });
|
|
257
|
+
}
|
|
258
|
+
if (drawn && closed && previous)
|
|
259
|
+
cornerWedge(previous.run, runs[0]);
|
|
260
|
+
else if (drawn && previous)
|
|
261
|
+
endFace(previous.run, previous.sb, 1, end);
|
|
262
|
+
}
|
|
263
|
+
// A box on a local frame: fins, mullions, posts, and rooftop masses. Depths run along
|
|
264
|
+
// the frame's normal from `back` to `front`.
|
|
265
|
+
function box(target, [x, z], normal, halfWidth, back, front, y0, y1, { omit = [] } = {}) {
|
|
266
|
+
const t = [normal[1], -normal[0]];
|
|
267
|
+
const p = (side, depth, y) => [x + t[0] * side + normal[0] * depth, y, z + t[1] * side + normal[1] * depth];
|
|
268
|
+
const faces = {
|
|
269
|
+
front: [[p(-halfWidth, front, y0), p(halfWidth, front, y0), p(halfWidth, front, y1), p(-halfWidth, front, y1)], [normal[0], 0, normal[1]]],
|
|
270
|
+
back: [[p(halfWidth, back, y0), p(-halfWidth, back, y0), p(-halfWidth, back, y1), p(halfWidth, back, y1)], [-normal[0], 0, -normal[1]]],
|
|
271
|
+
left: [[p(-halfWidth, back, y0), p(-halfWidth, front, y0), p(-halfWidth, front, y1), p(-halfWidth, back, y1)], [-t[0], 0, -t[1]]],
|
|
272
|
+
right: [[p(halfWidth, front, y0), p(halfWidth, back, y0), p(halfWidth, back, y1), p(halfWidth, front, y1)], [t[0], 0, t[1]]],
|
|
273
|
+
bottom: [[p(-halfWidth, back, y0), p(halfWidth, back, y0), p(halfWidth, front, y0), p(-halfWidth, front, y0)], [0, -1, 0]],
|
|
274
|
+
top: [[p(-halfWidth, back, y1), p(-halfWidth, front, y1), p(halfWidth, front, y1), p(halfWidth, back, y1)], [0, 1, 0]],
|
|
275
|
+
};
|
|
276
|
+
checkOmit(omit, Object.keys(faces), "box");
|
|
277
|
+
for (const [faceName, [corners, n]] of Object.entries(faces))
|
|
278
|
+
face(target, corners, n, omit.includes(faceName));
|
|
279
|
+
}
|
|
280
|
+
// Ear clipping for non-convex roofs and floors; the bundle omits ShapeUtils.
|
|
281
|
+
function slab(target, polygon, y, up) {
|
|
282
|
+
const area = shoelace(polygon);
|
|
283
|
+
const ring = polygon.map((_, i) => i), normal = [0, up ? 1 : -1, 0];
|
|
284
|
+
const convex = (a, b, c) => {
|
|
285
|
+
const pa = polygon[a], pb = polygon[b], pc = polygon[c];
|
|
286
|
+
return ((pb[0] - pa[0]) * (pc[1] - pa[1]) - (pb[1] - pa[1]) * (pc[0] - pa[0])) * area > 0;
|
|
287
|
+
};
|
|
288
|
+
const touches = (a, b, q) => {
|
|
289
|
+
const pa = polygon[a], pb = polygon[b];
|
|
290
|
+
return ((pb[0] - pa[0]) * (q[1] - pa[1]) - (pb[1] - pa[1]) * (q[0] - pa[0])) * area >= -1e-9;
|
|
291
|
+
};
|
|
292
|
+
const contains = (a, b, c, q) => touches(a, b, q) && touches(b, c, q) && touches(c, a, q);
|
|
293
|
+
for (let guard = 0; ring.length > 3 && guard < polygon.length * polygon.length; guard += 1) {
|
|
294
|
+
for (let i = 0; i < ring.length; i += 1) {
|
|
295
|
+
const [a, b, c] = [ring[(i + ring.length - 1) % ring.length], ring[i], ring[(i + 1) % ring.length]];
|
|
296
|
+
if (!convex(a, b, c) || ring.some((k) => k !== a && k !== b && k !== c && contains(a, b, c, polygon[k])))
|
|
297
|
+
continue;
|
|
298
|
+
triangle(target, [point(polygon[a], y), point(polygon[b], y), point(polygon[c], y)], [normal, normal, normal]);
|
|
299
|
+
ring.splice(i, 1);
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (ring.length > 3)
|
|
304
|
+
throw new Error(`A slab's polygon could not be triangulated; ${ring.length} vertices remain, so check it for repeated or crossing edges.`);
|
|
305
|
+
triangle(target, ring.map((k) => point(polygon[k], y)), [normal, normal, normal]);
|
|
306
|
+
}
|
|
307
|
+
// Walls rise through the given heights, so a seam can land on a drawn corner; a roof
|
|
308
|
+
// and a floor close the volume. Returns the plan polygon.
|
|
309
|
+
function prism(target, runs, heights, { omit = [] } = {}) {
|
|
310
|
+
checkJoins(runs, "prism", true);
|
|
311
|
+
const polygon = polygonOf(runs);
|
|
312
|
+
if (shoelace(polygon) >= 0)
|
|
313
|
+
throw new Error("A prism's plan must run counterclockwise from above, or its walls face inward.");
|
|
314
|
+
runs.forEach((run) => heights.slice(1).forEach((top, i) => panel(target, run, 0, run.length, heights[i], top, 0)));
|
|
315
|
+
checkOmit(omit, ["top", "bottom"], "prism");
|
|
316
|
+
for (const [faceName, y, up] of [["top", heights.at(-1), true], ["bottom", heights[0], false]]) {
|
|
317
|
+
if (!omit.includes(faceName))
|
|
318
|
+
slab(target, polygon, y, up);
|
|
319
|
+
else
|
|
320
|
+
omitted.push({ batch: target.name, corners: polygon.map((corner) => point(corner, y)), normal: [0, up ? 1 : -1, 0] });
|
|
321
|
+
}
|
|
322
|
+
return polygon;
|
|
323
|
+
}
|
|
324
|
+
// Batches become meshes in creation order, with silhouette lines for `outlines` only.
|
|
325
|
+
function finish({ height, outlines = [], threshold = 20, opacity = 0.3 }) {
|
|
326
|
+
const meshes = new Map(batches.map((data) => {
|
|
327
|
+
const geometry = new THREE.BufferGeometry();
|
|
328
|
+
geometry.setAttribute("position", new THREE.Float32BufferAttribute(data.positions, 3));
|
|
329
|
+
geometry.setAttribute("normal", new THREE.Float32BufferAttribute(data.normals, 3));
|
|
330
|
+
if (data.surface.vertexColors)
|
|
331
|
+
geometry.setAttribute("color", new THREE.Float32BufferAttribute(data.colors, 3));
|
|
332
|
+
const mesh = new THREE.Mesh(geometry, data.surface);
|
|
333
|
+
mesh.name = data.name;
|
|
334
|
+
mesh.castShadow = mesh.receiveShadow = true;
|
|
335
|
+
building.add(mesh);
|
|
336
|
+
return [data, mesh];
|
|
337
|
+
}));
|
|
338
|
+
const lines = outlines.flatMap((data) => [...new THREE.EdgesGeometry(meshes.get(data).geometry, threshold).getAttribute("position").array]);
|
|
339
|
+
const edgeGeometry = new THREE.BufferGeometry();
|
|
340
|
+
edgeGeometry.setAttribute("position", new THREE.Float32BufferAttribute(lines, 3));
|
|
341
|
+
const edges = new THREE.LineSegments(edgeGeometry, new THREE.LineBasicMaterial({ color: 0xcccccc, transparent: true, opacity }));
|
|
342
|
+
edges.name = "silhouette edges";
|
|
343
|
+
building.add(edges);
|
|
344
|
+
return {
|
|
345
|
+
building,
|
|
346
|
+
height,
|
|
347
|
+
triangleCount: batches.reduce((count, data) => count + data.positions.length / 9, 0),
|
|
348
|
+
setHighlighted(highlighted) {
|
|
349
|
+
materials.forEach((surface) => surface.emissive.setHex(highlighted ? 0x222222 : 0x000000));
|
|
350
|
+
},
|
|
351
|
+
setWireframe(enabled) {
|
|
352
|
+
materials.forEach((surface) => { surface.wireframe = enabled; });
|
|
353
|
+
edges.visible = !enabled;
|
|
354
|
+
},
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
return { building, material, batch, triangle, quad, panel, ledge, band, box, slab, prism, finish };
|
|
358
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export declare const celebrations: readonly [{
|
|
2
|
+
readonly id: "bulls";
|
|
3
|
+
readonly label: "Bulls";
|
|
4
|
+
readonly lines: readonly ["GO", "BULLS"];
|
|
5
|
+
readonly adapted: true;
|
|
6
|
+
}, {
|
|
7
|
+
readonly id: "cubs";
|
|
8
|
+
readonly label: "Cubs";
|
|
9
|
+
readonly lines: readonly ["GO", "CUBS", "GO"];
|
|
10
|
+
readonly adapted: false;
|
|
11
|
+
}, {
|
|
12
|
+
readonly id: "sox";
|
|
13
|
+
readonly label: "White Sox";
|
|
14
|
+
readonly lines: readonly ["SOX", "PRIDE"];
|
|
15
|
+
readonly adapted: false;
|
|
16
|
+
}, {
|
|
17
|
+
readonly id: "bears";
|
|
18
|
+
readonly label: "Bears";
|
|
19
|
+
readonly lines: readonly ["BEAR", "DOWN"];
|
|
20
|
+
readonly adapted: false;
|
|
21
|
+
}, {
|
|
22
|
+
readonly id: "hawks";
|
|
23
|
+
readonly label: "Blackhawks";
|
|
24
|
+
readonly lines: readonly ["HAWKS", "WIN"];
|
|
25
|
+
readonly adapted: false;
|
|
26
|
+
}, {
|
|
27
|
+
readonly id: "thanks";
|
|
28
|
+
readonly label: "Thanksgiving";
|
|
29
|
+
readonly lines: readonly ["GIVE", "THANKS"];
|
|
30
|
+
readonly adapted: false;
|
|
31
|
+
}];
|
|
32
|
+
export type CelebrationId = typeof celebrations[number]["id"];
|
|
33
|
+
export interface Celebration {
|
|
34
|
+
readonly id: CelebrationId;
|
|
35
|
+
readonly label: string;
|
|
36
|
+
readonly lines: readonly string[];
|
|
37
|
+
readonly adapted: boolean;
|
|
38
|
+
}
|
|
39
|
+
export declare function windowWord(word: string): string[];
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Documented Chicago window displays, adapted to the modeled south facade.
|
|
2
|
+
// Sources and the limits of each reconstruction: docs/celebration-lighting.md.
|
|
3
|
+
// `adapted` identifies unverified wording; all window placements are reconstructed.
|
|
4
|
+
export const celebrations = [
|
|
5
|
+
{ id: "bulls", label: "Bulls", lines: ["GO", "BULLS"], adapted: true },
|
|
6
|
+
{ id: "cubs", label: "Cubs", lines: ["GO", "CUBS", "GO"], adapted: false },
|
|
7
|
+
{ id: "sox", label: "White Sox", lines: ["SOX", "PRIDE"], adapted: false },
|
|
8
|
+
{ id: "bears", label: "Bears", lines: ["BEAR", "DOWN"], adapted: false },
|
|
9
|
+
{ id: "hawks", label: "Blackhawks", lines: ["HAWKS", "WIN"], adapted: false },
|
|
10
|
+
{ id: "thanks", label: "Thanksgiving", lines: ["GIVE", "THANKS"], adapted: false },
|
|
11
|
+
];
|
|
12
|
+
// Five floors per letter, with a dark window between letters. These are compact
|
|
13
|
+
// reconstructions, not claims to reproduce an electrician's original shade plan.
|
|
14
|
+
const letters = {
|
|
15
|
+
A: ["01110", "10001", "11111", "10001", "10001"],
|
|
16
|
+
B: ["11110", "10001", "11110", "10001", "11110"],
|
|
17
|
+
C: ["01111", "10000", "10000", "10000", "01111"],
|
|
18
|
+
D: ["11110", "10001", "10001", "10001", "11110"],
|
|
19
|
+
E: ["11111", "10000", "11110", "10000", "11111"],
|
|
20
|
+
G: ["01111", "10000", "10111", "10001", "01111"],
|
|
21
|
+
H: ["10001", "10001", "11111", "10001", "10001"],
|
|
22
|
+
I: ["11111", "00100", "00100", "00100", "11111"],
|
|
23
|
+
K: ["10001", "10010", "11100", "10010", "10001"],
|
|
24
|
+
L: ["10000", "10000", "10000", "10000", "11111"],
|
|
25
|
+
N: ["10001", "11001", "10101", "10011", "10001"],
|
|
26
|
+
O: ["01110", "10001", "10001", "10001", "01110"],
|
|
27
|
+
P: ["11110", "10001", "11110", "10000", "10000"],
|
|
28
|
+
R: ["11110", "10001", "11110", "10010", "10001"],
|
|
29
|
+
S: ["01111", "10000", "01110", "00001", "11110"],
|
|
30
|
+
T: ["11111", "00100", "00100", "00100", "00100"],
|
|
31
|
+
U: ["10001", "10001", "10001", "10001", "01110"],
|
|
32
|
+
V: ["10001", "10001", "10001", "01010", "00100"],
|
|
33
|
+
W: ["10001", "10001", "10101", "10101", "01010"],
|
|
34
|
+
X: ["10001", "01010", "00100", "01010", "10001"],
|
|
35
|
+
};
|
|
36
|
+
export function windowWord(word) {
|
|
37
|
+
const glyphs = [...word].map((letter) => {
|
|
38
|
+
const glyph = letters[letter];
|
|
39
|
+
if (!glyph)
|
|
40
|
+
throw new Error(`Unsupported window letter: ${letter}`);
|
|
41
|
+
return glyph;
|
|
42
|
+
});
|
|
43
|
+
return Array.from({ length: 5 }, (_, row) => glyphs.map((glyph) => glyph[row]).join("0"));
|
|
44
|
+
}
|