@a2f0/skyline 0.0.0-stage → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE.md +20 -0
- package/README.md +52 -2
- package/docs/package.md +224 -0
- package/docs/skyline-geography.md +503 -0
- package/lib/models/aon-center.d.ts +4 -0
- package/lib/models/aon-center.js +60 -0
- package/lib/models/aon-geographic.d.ts +9 -0
- package/lib/models/aon-geographic.js +35 -0
- package/lib/models/aon-tower.d.ts +23 -0
- package/lib/models/aon-tower.js +130 -0
- package/lib/models/blue-cross-geographic.d.ts +14 -0
- package/lib/models/blue-cross-geographic.js +248 -0
- package/lib/models/borg-warner-geographic.d.ts +20 -0
- package/lib/models/borg-warner-geographic.js +143 -0
- package/lib/models/buckingham-geographic.d.ts +10 -0
- package/lib/models/buckingham-geographic.js +121 -0
- package/lib/models/building-kit.d.ts +84 -0
- package/lib/models/building-kit.js +358 -0
- package/lib/models/celebrations.d.ts +39 -0
- package/lib/models/celebrations.js +44 -0
- package/lib/models/chicago-athletic-association-geographic.d.ts +32 -0
- package/lib/models/chicago-athletic-association-geographic.js +173 -0
- package/lib/models/crain-communications.d.ts +5 -0
- package/lib/models/crain-communications.js +75 -0
- package/lib/models/crain-geographic.d.ts +10 -0
- package/lib/models/crain-geographic.js +69 -0
- package/lib/models/crain-tower.d.ts +32 -0
- package/lib/models/crain-tower.js +378 -0
- package/lib/models/facade-grid.d.ts +18 -0
- package/lib/models/facade-grid.js +176 -0
- package/lib/models/gage-geographic.d.ts +19 -0
- package/lib/models/gage-geographic.js +154 -0
- package/lib/models/heritage-at-millennium-park.d.ts +3 -0
- package/lib/models/heritage-at-millennium-park.js +284 -0
- package/lib/models/heritage-geographic.d.ts +11 -0
- package/lib/models/heritage-geographic.js +208 -0
- package/lib/models/hyatt-west-tower-geographic.d.ts +26 -0
- package/lib/models/hyatt-west-tower-geographic.js +86 -0
- package/lib/models/keith-ascher-geographic.d.ts +29 -0
- package/lib/models/keith-ascher-geographic.js +110 -0
- package/lib/models/kemper-geographic.d.ts +8 -0
- package/lib/models/kemper-geographic.js +153 -0
- package/lib/models/kemper.d.ts +2 -0
- package/lib/models/kemper.js +78 -0
- package/lib/models/lake-view-geographic.d.ts +15 -0
- package/lib/models/lake-view-geographic.js +134 -0
- package/lib/models/maclean-center-geographic.d.ts +20 -0
- package/lib/models/maclean-center-geographic.js +162 -0
- package/lib/models/michigan-boulevard-geographic.d.ts +12 -0
- package/lib/models/michigan-boulevard-geographic.js +157 -0
- package/lib/models/michigan-plaza-south-geographic.d.ts +7 -0
- package/lib/models/michigan-plaza-south-geographic.js +84 -0
- package/lib/models/michigan-plaza-south.d.ts +2 -0
- package/lib/models/michigan-plaza-south.js +68 -0
- package/lib/models/millennium-park-plaza-geographic.d.ts +9 -0
- package/lib/models/millennium-park-plaza-geographic.js +103 -0
- package/lib/models/monroe-geographic.d.ts +19 -0
- package/lib/models/monroe-geographic.js +177 -0
- package/lib/models/north-michigan-180-geographic.d.ts +14 -0
- package/lib/models/north-michigan-180-geographic.js +117 -0
- package/lib/models/north-wabash-geographic.d.ts +14 -0
- package/lib/models/north-wabash-geographic.js +122 -0
- package/lib/models/on-the-park-geographic.d.ts +11 -0
- package/lib/models/on-the-park-geographic.js +245 -0
- package/lib/models/one-prudential-geographic.d.ts +12 -0
- package/lib/models/one-prudential-geographic.js +55 -0
- package/lib/models/one-prudential-plaza.d.ts +5 -0
- package/lib/models/one-prudential-plaza.js +72 -0
- package/lib/models/one-prudential-tower.d.ts +45 -0
- package/lib/models/one-prudential-tower.js +246 -0
- package/lib/models/peoples-gas-geographic.d.ts +23 -0
- package/lib/models/peoples-gas-geographic.js +250 -0
- package/lib/models/railway-exchange-geographic.d.ts +18 -0
- package/lib/models/railway-exchange-geographic.js +176 -0
- package/lib/models/river-plaza-geographic.d.ts +16 -0
- package/lib/models/river-plaza-geographic.js +104 -0
- package/lib/models/sheraton-grand-geographic.d.ts +24 -0
- package/lib/models/sheraton-grand-geographic.js +112 -0
- package/lib/models/six-north-michigan-geographic.d.ts +16 -0
- package/lib/models/six-north-michigan-geographic.js +180 -0
- package/lib/models/skyline-geography-data.d.ts +44 -0
- package/lib/models/skyline-geography-data.js +607 -0
- package/lib/models/skyline-geography.d.ts +23 -0
- package/lib/models/skyline-geography.js +322 -0
- package/lib/models/swissotel-geographic.d.ts +24 -0
- package/lib/models/swissotel-geographic.js +79 -0
- package/lib/models/three-illinois-center-geographic.d.ts +26 -0
- package/lib/models/three-illinois-center-geographic.js +86 -0
- package/lib/models/trump-geographic.d.ts +14 -0
- package/lib/models/trump-geographic.js +51 -0
- package/lib/models/trump-international-tower.d.ts +4 -0
- package/lib/models/trump-international-tower.js +84 -0
- package/lib/models/trump-tower.d.ts +43 -0
- package/lib/models/trump-tower.js +229 -0
- package/lib/models/two-illinois-center-geographic.d.ts +26 -0
- package/lib/models/two-illinois-center-geographic.js +85 -0
- package/lib/models/two-prudential-geographic.d.ts +25 -0
- package/lib/models/two-prudential-geographic.js +47 -0
- package/lib/models/two-prudential-plaza.d.ts +8 -0
- package/lib/models/two-prudential-plaza.js +55 -0
- package/lib/models/two-prudential-tower.d.ts +53 -0
- package/lib/models/two-prudential-tower.js +441 -0
- package/lib/models/university-club-geographic.d.ts +24 -0
- package/lib/models/university-club-geographic.js +217 -0
- package/lib/models/willoughby-tower-geographic.d.ts +17 -0
- package/lib/models/willoughby-tower-geographic.js +195 -0
- package/lib/models/window-illumination.d.ts +14 -0
- package/lib/models/window-illumination.js +64 -0
- package/lib/package-assets.d.ts +5 -0
- package/lib/package-assets.js +9 -0
- package/lib/skyline-package.d.ts +23 -0
- package/lib/skyline-package.js +25 -0
- package/lib/skyline-scene.d.ts +25 -0
- package/lib/skyline-scene.js +86 -0
- package/lib/study-types.d.ts +49 -0
- package/lib/study-types.js +1 -0
- package/lib/vendor/three-r186.d.ts +1 -0
- package/lib/vendor/three-r186.js +1 -0
- package/package.json +64 -4
- package/site/building-detail.html +54 -0
- package/site/building-detail.js +84 -0
- package/site/building-study.html +58 -0
- package/site/building-study.js +3 -0
- package/site/index.html +407 -0
- package/site/models/aon-center.js +60 -0
- package/site/models/aon-geographic.js +35 -0
- package/site/models/aon-tower.js +130 -0
- package/site/models/blue-cross-geographic.js +248 -0
- package/site/models/borg-warner-geographic.js +143 -0
- package/site/models/buckingham-geographic.js +121 -0
- package/site/models/building-kit.js +358 -0
- package/site/models/celebrations.js +44 -0
- package/site/models/chicago-athletic-association-geographic.js +173 -0
- package/site/models/crain-communications.js +75 -0
- package/site/models/crain-geographic.js +69 -0
- package/site/models/crain-reference.svg +10 -0
- package/site/models/crain-tower.js +378 -0
- package/site/models/facade-grid.js +176 -0
- package/site/models/gage-geographic.js +154 -0
- package/site/models/heritage-at-millennium-park.js +284 -0
- package/site/models/heritage-geographic.js +208 -0
- package/site/models/hyatt-west-tower-geographic.js +86 -0
- package/site/models/keith-ascher-geographic.js +110 -0
- package/site/models/kemper-geographic.js +153 -0
- package/site/models/kemper.js +78 -0
- package/site/models/lake-view-geographic.js +134 -0
- package/site/models/maclean-center-geographic.js +162 -0
- package/site/models/michigan-boulevard-geographic.js +157 -0
- package/site/models/michigan-plaza-south-geographic.js +84 -0
- package/site/models/michigan-plaza-south.js +68 -0
- package/site/models/millennium-park-plaza-geographic.js +103 -0
- package/site/models/monroe-geographic.js +177 -0
- package/site/models/north-michigan-180-geographic.js +117 -0
- package/site/models/north-wabash-geographic.js +122 -0
- package/site/models/on-the-park-geographic.js +245 -0
- package/site/models/one-prudential-geographic.js +55 -0
- package/site/models/one-prudential-plaza.js +72 -0
- package/site/models/one-prudential-tower.js +246 -0
- package/site/models/peoples-gas-geographic.js +250 -0
- package/site/models/railway-exchange-geographic.js +176 -0
- package/site/models/river-plaza-geographic.js +104 -0
- package/site/models/sheraton-grand-geographic.js +112 -0
- package/site/models/six-north-michigan-geographic.js +180 -0
- package/site/models/skyline-geography-data.js +607 -0
- package/site/models/skyline-geography.js +322 -0
- package/site/models/skyline-panorama.svg +3491 -0
- package/site/models/skyline-reference.svg +3090 -0
- package/site/models/swissotel-geographic.js +79 -0
- package/site/models/three-illinois-center-geographic.js +86 -0
- package/site/models/trump-geographic.js +51 -0
- package/site/models/trump-international-tower.js +84 -0
- package/site/models/trump-tower.js +229 -0
- package/site/models/two-illinois-center-geographic.js +85 -0
- package/site/models/two-prudential-geographic.js +47 -0
- package/site/models/two-prudential-plaza.js +55 -0
- package/site/models/two-prudential-tower.js +441 -0
- package/site/models/university-club-geographic.js +217 -0
- package/site/models/willoughby-tower-geographic.js +195 -0
- package/site/models/window-illumination.js +64 -0
- package/site/skyline-3d.html +92 -0
- package/site/skyline-3d.js +333 -0
- package/site/skyline-animated.svg +18795 -0
- package/site/skyline-comparison.js +97 -0
- package/site/skyline-original-fit.svg +18312 -0
- package/site/skyline-scene.js +86 -0
- package/site/skyline-study.html +83 -0
- package/site/skyline-study.js +109 -0
- package/site/skyline-webgl.html +667 -0
- package/site/stars.svg +176 -0
- package/site/study-loader.js +20 -0
- package/site/study-types.js +1 -0
- package/site/study-viewer.js +605 -0
- package/site/study.css +204 -0
- package/site/vendor/THREE-LICENSE.txt +21 -0
- package/site/vendor/three-r186.js +4228 -0
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import * as THREE from "../vendor/three-r186.js";
|
|
2
|
+
import { createBuilder, inside, line } from "./building-kit.js";
|
|
3
|
+
// The curtain wall's rhythm. The drawing and the photograph both give a 3.5 m band pitch,
|
|
4
|
+
// so the 41 published floors, above a taller lobby, fit under the 152.5 m main roof. Each
|
|
5
|
+
// floor is a ribbon of glass between sill and head, with white spandrels covering the slab.
|
|
6
|
+
export const crainFloors = Object.freeze({
|
|
7
|
+
lobbyTop: 7.0,
|
|
8
|
+
pitch: 3.5,
|
|
9
|
+
sill: 0.9,
|
|
10
|
+
head: 2.55,
|
|
11
|
+
module: 1.524,
|
|
12
|
+
lobbyGlass: [0.45, 5.9],
|
|
13
|
+
});
|
|
14
|
+
export const crainMainRoof = 152.5;
|
|
15
|
+
const orient = (corners) => {
|
|
16
|
+
const area = corners.reduce((sum, [x, z], i) => { const [x2, z2] = corners[(i + 1) % corners.length]; return sum + x * z2 - x2 * z; }, 0);
|
|
17
|
+
return area < 0 ? corners : [...corners].reverse();
|
|
18
|
+
};
|
|
19
|
+
const planOf = (corners) => corners.map((corner, i) => line(corner, corners[(i + 1) % corners.length]));
|
|
20
|
+
// The ribbon glazing's tones: mostly dark reflective glass, with a few lit and dim panes.
|
|
21
|
+
const paneTones = [0x232323, 0x2a2a2a, 0x303030, 0x1f1f1f].map((hex) => new THREE.Color(hex));
|
|
22
|
+
const litPane = new THREE.Color(0x8a8a8a), dimPane = new THREE.Color(0x565656), mullionTone = new THREE.Color(0x585858);
|
|
23
|
+
function paneColor(row, module, wall) {
|
|
24
|
+
const hash = (Math.imul(row + 29, 0x9e3779b1) ^ Math.imul(module + 41, 0x85ebca77) ^ Math.imul(wall + 13, 0xc2b2ae3d)) >>> 0;
|
|
25
|
+
const value = hash % 97;
|
|
26
|
+
if (value < 3)
|
|
27
|
+
return litPane;
|
|
28
|
+
if (value < 6)
|
|
29
|
+
return dimPane;
|
|
30
|
+
return paneTones[value % paneTones.length];
|
|
31
|
+
}
|
|
32
|
+
// A plane's upward normal from a height function sampled around a point.
|
|
33
|
+
function slopeNormal(roof, [x, z]) {
|
|
34
|
+
const dx = roof([x + 1, z]) - roof([x, z]), dz = roof([x, z + 1]) - roof([x, z]);
|
|
35
|
+
const length = Math.hypot(dx, 1, dz);
|
|
36
|
+
return [-dx / length, 1 / length, -dz / length];
|
|
37
|
+
}
|
|
38
|
+
function faceNormal(a, b, c) {
|
|
39
|
+
const u = [b[0] - a[0], b[1] - a[1], b[2] - a[2]], v = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
|
|
40
|
+
const n = [u[1] * v[2] - u[2] * v[1], u[2] * v[0] - u[0] * v[2], u[0] * v[1] - u[1] * v[0]];
|
|
41
|
+
const length = Math.hypot(...n);
|
|
42
|
+
return [n[0] / length, n[1] / length, n[2] / length];
|
|
43
|
+
}
|
|
44
|
+
// Where a line crosses a polygon, as the parameter intervals that lie inside it.
|
|
45
|
+
function clipLine(polygon, origin, direction) {
|
|
46
|
+
const hits = [];
|
|
47
|
+
for (let i = 0; i < polygon.length; i += 1) {
|
|
48
|
+
const a = polygon[i], b = polygon[(i + 1) % polygon.length];
|
|
49
|
+
const e = [b[0] - a[0], b[1] - a[1]];
|
|
50
|
+
const denominator = direction[0] * e[1] - direction[1] * e[0];
|
|
51
|
+
if (Math.abs(denominator) < 1e-12)
|
|
52
|
+
continue;
|
|
53
|
+
const w = [a[0] - origin[0], a[1] - origin[1]];
|
|
54
|
+
const t = (w[0] * e[1] - w[1] * e[0]) / denominator, u = (w[0] * direction[1] - w[1] * direction[0]) / denominator;
|
|
55
|
+
if (u >= 0 && u < 1)
|
|
56
|
+
hits.push(t);
|
|
57
|
+
}
|
|
58
|
+
hits.sort((p, q) => p - q);
|
|
59
|
+
const spans = [];
|
|
60
|
+
for (let i = 0; i + 1 < hits.length; i += 2)
|
|
61
|
+
spans.push([hits[i], hits[i + 1]]);
|
|
62
|
+
return spans;
|
|
63
|
+
}
|
|
64
|
+
export function buildCrainTower(form) {
|
|
65
|
+
const kit = createBuilder(form.name, form.id);
|
|
66
|
+
const shell = kit.batch("Crain · aluminum spandrels", kit.material(0xc4c4c4));
|
|
67
|
+
const ribbons = kit.batch("Crain · ribbon glazing", kit.material(0xffffff, { vertexColors: true }));
|
|
68
|
+
const glazing = kit.batch("Crain · sloped glazing", kit.material(0x333333));
|
|
69
|
+
const grid = kit.batch("Crain · glazing grid", kit.material(0x727272));
|
|
70
|
+
const lights = kit.batch("Crain · diamond outline lights", kit.material(0xf0f0f0));
|
|
71
|
+
const coping = kit.batch("Crain · coping", kit.material(0x5c5c5c));
|
|
72
|
+
const deck = kit.batch("Crain · slot floor", kit.material(0x4a4a4a));
|
|
73
|
+
const vents = form.crown ? kit.batch("Crain · mechanical louvers", kit.material(0xffffff, { vertexColors: true })) : ribbons;
|
|
74
|
+
const blades = form.crown ? kit.batch("Crain · louver blades", kit.material(0xffffff, { vertexColors: true })) : ribbons;
|
|
75
|
+
const shadowStrips = form.crown ? kit.batch("Crain · crown shadow strips", kit.material(0xffffff, { vertexColors: true })) : ribbons;
|
|
76
|
+
const ventTone = new THREE.Color(0x303030), bladeTone = new THREE.Color(0x696969), shadowTone = new THREE.Color(0x252525);
|
|
77
|
+
const base = form.base;
|
|
78
|
+
const volumes = form.volumes.map((volume) => {
|
|
79
|
+
const corners = orient(volume.corners);
|
|
80
|
+
return { ...volume, corners, plan: planOf(corners) };
|
|
81
|
+
});
|
|
82
|
+
// Model height from a real height.
|
|
83
|
+
const y = (real) => real - base;
|
|
84
|
+
// A slab of the plan lifted onto a plane `drop` below the roof. kit.slab lays a flat
|
|
85
|
+
// polygon; lifting its vertices keeps each triangle planar, and its winding still faces
|
|
86
|
+
// the way it was laid, so the normal follows the plane, flipped for a downward face.
|
|
87
|
+
const liftedSlab = (target, corners, roof, drop, up) => {
|
|
88
|
+
const start = target.positions.length;
|
|
89
|
+
kit.slab(target, corners, 0, up);
|
|
90
|
+
const n = slopeNormal(roof, corners[0]), normal = up ? n : [-n[0], -n[1], -n[2]];
|
|
91
|
+
for (let i = start; i < target.positions.length; i += 3) {
|
|
92
|
+
target.positions[i + 1] = y(roof([target.positions[i], target.positions[i + 2]]) - drop);
|
|
93
|
+
target.normals.splice(i, 3, ...normal);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
// The masses: walls, a floor, and a lid a skin's depth under the roof, closed within the
|
|
97
|
+
// spandrels' mesh. The roof itself is a thin closed slab on the lid, glass on the halves
|
|
98
|
+
// and a deck on the slot's floor, so it stays the building's top. Neighbouring volumes
|
|
99
|
+
// keep their own walls on shared edges, facing opposite ways.
|
|
100
|
+
const skin = 0.05;
|
|
101
|
+
for (const volume of volumes) {
|
|
102
|
+
const { corners, plan, roof } = volume;
|
|
103
|
+
const top = volume.glazed ? glazing : deck;
|
|
104
|
+
for (const run of plan) {
|
|
105
|
+
const a = run.at(0), b = run.at(run.length), n = run.normal(0), normal = [n[0], 0, n[1]];
|
|
106
|
+
kit.quad(shell, [[a[0], 0, a[1]], [b[0], 0, b[1]], [b[0], y(roof(b) - skin), b[1]], [a[0], y(roof(a) - skin), a[1]]], [normal]);
|
|
107
|
+
kit.quad(top, [[a[0], y(roof(a) - skin), a[1]], [b[0], y(roof(b) - skin), b[1]], [b[0], y(roof(b)), b[1]], [a[0], y(roof(a)), a[1]]], [normal]);
|
|
108
|
+
}
|
|
109
|
+
kit.slab(shell, corners, 0, false);
|
|
110
|
+
liftedSlab(shell, corners, roof, skin, true);
|
|
111
|
+
liftedSlab(top, corners, roof, skin, false);
|
|
112
|
+
liftedSlab(top, corners, roof, 0, true);
|
|
113
|
+
}
|
|
114
|
+
// A wall is exposed where no neighbour stands against it, and a neighbour's roof covers
|
|
115
|
+
// it up to that roof's height. Probing just outside the wall splits a run into stretches,
|
|
116
|
+
// each facing open air or one neighbour, so a plan needs no corner where that changes.
|
|
117
|
+
const neighbourAt = (self, run, s) => volumes.findIndex((volume, index) => index !== self && inside(volume.corners, run.at(s, 0.3)));
|
|
118
|
+
const stretches = (self, run) => {
|
|
119
|
+
const out = [];
|
|
120
|
+
let start = 0, current = neighbourAt(self, run, Math.min(0.05, run.length / 2));
|
|
121
|
+
for (let s = 0.25; s < run.length - 0.05; s += 0.25) {
|
|
122
|
+
if (neighbourAt(self, run, s) === current)
|
|
123
|
+
continue;
|
|
124
|
+
let low = s - 0.25, high = s;
|
|
125
|
+
for (let i = 0; i < 30; i += 1) {
|
|
126
|
+
const middle = (low + high) / 2;
|
|
127
|
+
if (neighbourAt(self, run, middle) === current)
|
|
128
|
+
low = middle;
|
|
129
|
+
else
|
|
130
|
+
high = middle;
|
|
131
|
+
}
|
|
132
|
+
out.push({ from: start, to: high, neighbour: current });
|
|
133
|
+
start = high;
|
|
134
|
+
current = neighbourAt(self, run, s);
|
|
135
|
+
}
|
|
136
|
+
out.push({ from: start, to: run.length, neighbour: current });
|
|
137
|
+
// Near an acute corner the probe can land outside the neighbour for a few centimetres;
|
|
138
|
+
// no real change of neighbour comes that close to a corner, so a short stretch joins
|
|
139
|
+
// the longer one beside it.
|
|
140
|
+
for (let i = out.findIndex((piece) => piece.to - piece.from < 0.6); out.length > 1 && i >= 0; i = out.findIndex((piece) => piece.to - piece.from < 0.6)) {
|
|
141
|
+
const piece = out[i], before = out[i - 1], after = out[i + 1];
|
|
142
|
+
const into = !after || (before && before.to - before.from >= after.to - after.from) ? before : after;
|
|
143
|
+
into.from = Math.min(into.from, piece.from);
|
|
144
|
+
into.to = Math.max(into.to, piece.to);
|
|
145
|
+
out.splice(i, 1);
|
|
146
|
+
}
|
|
147
|
+
return out;
|
|
148
|
+
};
|
|
149
|
+
// A closed box on a run between two heights that each vary linearly along it. Its front is
|
|
150
|
+
// cut into panes and mullions on the curtain wall's module, counted from the run's start so
|
|
151
|
+
// mullions line up from floor to floor.
|
|
152
|
+
const strip = (run, from, to, b0, b1, t0, t1, row, wall, surface) => {
|
|
153
|
+
const target = surface === "glass" ? ribbons : surface === "vent" ? vents : surface === "blade" ? blades : shadowStrips;
|
|
154
|
+
const back = surface === "blade" ? 0.045 : 0.02, front = surface === "blade" ? 0.13 : 0.08;
|
|
155
|
+
const n = run.normal(0), tangent = [n[1], -n[0]];
|
|
156
|
+
const at = (s, depth, h) => { const p = run.at(s, depth); return [p[0], y(h), p[1]]; };
|
|
157
|
+
const bottom = (s) => b0 + (b1 - b0) * (s - from) / (to - from), top = (s) => t0 + (t1 - t0) * (s - from) / (to - from);
|
|
158
|
+
const outward = [n[0], 0, n[1]];
|
|
159
|
+
const cuts = [from];
|
|
160
|
+
const module = crainFloors.module, half = 0.035;
|
|
161
|
+
if (surface !== "blade")
|
|
162
|
+
for (let k = Math.ceil((from + half) / module); k * module < to - half; k += 1)
|
|
163
|
+
cuts.push(k * module - half, k * module + half);
|
|
164
|
+
cuts.push(to);
|
|
165
|
+
for (let i = 0; i + 1 < cuts.length; i += 1) {
|
|
166
|
+
const sa = cuts[i], sb = cuts[i + 1];
|
|
167
|
+
if (sb - sa < 1e-6)
|
|
168
|
+
continue;
|
|
169
|
+
const mullion = i % 2 === 1;
|
|
170
|
+
const color = mullion ? mullionTone : surface === "glass" ? paneColor(row, Math.floor((sa + sb) / 2 / module), wall)
|
|
171
|
+
: surface === "vent" ? ventTone : surface === "blade" ? bladeTone : shadowTone;
|
|
172
|
+
kit.quad(target, [at(sa, front, bottom(sa)), at(sb, front, bottom(sb)), at(sb, front, top(sb)), at(sa, front, top(sa))], [outward], color);
|
|
173
|
+
}
|
|
174
|
+
const dark = paneTones[0];
|
|
175
|
+
kit.quad(target, [at(to, back, b1), at(from, back, b0), at(from, back, t0), at(to, back, t1)], [[-n[0], 0, -n[1]]], dark);
|
|
176
|
+
// The top and bottom fan out from a back corner through every cut on the front, so each
|
|
177
|
+
// pane's edge meets its partner and the strip closes without T-junctions.
|
|
178
|
+
const ends = cuts.filter((s, i) => i === 0 || s - cuts[i - 1] >= 1e-6);
|
|
179
|
+
const fan = (height, h0, h1, up) => {
|
|
180
|
+
const hinge = at(from, back, h0), front0 = at(from, front, h0);
|
|
181
|
+
const normal = faceNormal(hinge, at(to, back, h1), front0);
|
|
182
|
+
const facing = (normal[1] > 0) === up ? normal : [-normal[0], -normal[1], -normal[2]];
|
|
183
|
+
for (let i = 0; i + 1 < ends.length; i += 1)
|
|
184
|
+
kit.triangle(target, [hinge, at(ends[i], front, height(ends[i])), at(ends[i + 1], front, height(ends[i + 1]))], [facing, facing, facing], dark);
|
|
185
|
+
kit.triangle(target, [hinge, at(to, front, h1), at(to, back, h1)], [facing, facing, facing], dark);
|
|
186
|
+
};
|
|
187
|
+
fan(top, t0, t1, true);
|
|
188
|
+
fan(bottom, b0, b1, false);
|
|
189
|
+
kit.quad(target, [at(from, front, b0), at(from, back, b0), at(from, back, t0), at(from, front, t0)], [[-tangent[0], 0, -tangent[1]]], dark);
|
|
190
|
+
kit.quad(target, [at(to, back, b1), at(to, front, b1), at(to, front, t1), at(to, back, t1)], [[tangent[0], 0, tangent[1]]], dark);
|
|
191
|
+
};
|
|
192
|
+
// Ribbons stop this far under the roof edge, below the deepest fascia hung there, so the
|
|
193
|
+
// two never meet in one plane where a traced wall splits into collinear pieces.
|
|
194
|
+
const underRoof = 0.8;
|
|
195
|
+
// One ribbon on one wall: between the band's sill and head, under the roof, and above any
|
|
196
|
+
// neighbour. The bounds are linear along the run, so splitting it where one bound overtakes
|
|
197
|
+
// another leaves pieces whose bottom and top are each a single linear function.
|
|
198
|
+
const ribbon = (run, clear, roof, cover, g0, g1, row, wall, surface = "glass") => {
|
|
199
|
+
const least = 0.05;
|
|
200
|
+
const from = clear[0], to = run.length - clear[1];
|
|
201
|
+
if (to - from < 0.3)
|
|
202
|
+
return;
|
|
203
|
+
const low = (s) => Math.max(g0, cover ? cover(s) + 0.2 : -Infinity, base);
|
|
204
|
+
const high = (s) => Math.min(g1, roof(s) - underRoof);
|
|
205
|
+
// Each bound is the larger or smaller of linear pieces; split where they trade places.
|
|
206
|
+
const trades = [(s) => roof(s) - underRoof - g1];
|
|
207
|
+
if (cover)
|
|
208
|
+
trades.push((s) => cover(s) + 0.2 - g0, (s) => cover(s) + 0.2 - base);
|
|
209
|
+
const breaks = [from, to];
|
|
210
|
+
for (const f of trades) {
|
|
211
|
+
const fa = f(from), fb = f(to);
|
|
212
|
+
if ((fa > 0) !== (fb > 0))
|
|
213
|
+
breaks.push(from + (to - from) * fa / (fa - fb));
|
|
214
|
+
}
|
|
215
|
+
breaks.sort((p, q) => p - q);
|
|
216
|
+
for (let i = 0; i + 1 < breaks.length; i += 1) {
|
|
217
|
+
let sa = breaks[i], sb = breaks[i + 1];
|
|
218
|
+
// Within a piece the ribbon's height is linear. Where the roof or a neighbour pinches
|
|
219
|
+
// it off, end the piece where it is still `least` tall rather than at a knife edge.
|
|
220
|
+
const ha = high(sa) - low(sa), hb = high(sb) - low(sb);
|
|
221
|
+
if (ha < least && hb < least)
|
|
222
|
+
continue;
|
|
223
|
+
if (ha < least)
|
|
224
|
+
sa += (sb - sa) * (least - ha) / (hb - ha);
|
|
225
|
+
else if (hb < least)
|
|
226
|
+
sb -= (sb - sa) * (least - hb) / (ha - hb);
|
|
227
|
+
if (sb - sa < 0.12)
|
|
228
|
+
continue;
|
|
229
|
+
strip(run, sa, sb, low(sa), low(sb), high(sa), high(sb), row, wall, surface);
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
// A fascia hung on a wall just under its roof edge: its top follows the edge, flush with
|
|
233
|
+
// the roof, and it stands out from the wall rather than over the roof, so it never shares
|
|
234
|
+
// the roof's plane and the roof stays the building's top. At a convex corner it runs on by
|
|
235
|
+
// its own width to close the corner with its neighbour.
|
|
236
|
+
const fascia = (target, run, roof, width, drop, extend) => {
|
|
237
|
+
const s0 = -extend[0], s1 = run.length + extend[1], n = run.normal(0), tangent = [n[1], -n[0]];
|
|
238
|
+
const at = (s, depth, lower) => { const p = run.at(s, depth); return [p[0], y(roof(run.at(s)) - lower), p[1]]; };
|
|
239
|
+
const T = [at(s0, 0, 0), at(s0, width, 0), at(s1, width, 0), at(s1, 0, 0)];
|
|
240
|
+
const b = [at(s0, 0, drop), at(s1, 0, drop), at(s1, width, drop), at(s0, width, drop)];
|
|
241
|
+
kit.quad(target, T, [faceNormal(T[0], T[1], T[2])]);
|
|
242
|
+
kit.quad(target, b, [faceNormal(b[0], b[1], b[2])]);
|
|
243
|
+
kit.quad(target, [at(s0, width, drop), at(s1, width, drop), at(s1, width, 0), at(s0, width, 0)], [[n[0], 0, n[1]]]);
|
|
244
|
+
kit.quad(target, [at(s1, 0, drop), at(s0, 0, drop), at(s0, 0, 0), at(s1, 0, 0)], [[-n[0], 0, -n[1]]]);
|
|
245
|
+
kit.quad(target, [at(s0, 0, drop), at(s0, width, drop), at(s0, width, 0), at(s0, 0, 0)], [[-tangent[0], 0, -tangent[1]]]);
|
|
246
|
+
kit.quad(target, [at(s1, width, drop), at(s1, 0, drop), at(s1, 0, 0), at(s1, width, 0)], [[tangent[0], 0, tangent[1]]]);
|
|
247
|
+
};
|
|
248
|
+
// A slim bar on a roof between two plan points, for the glazing grid. Every corner sits a
|
|
249
|
+
// fixed height off the roof plane, so its top and bottom lie parallel to the glass across
|
|
250
|
+
// its width as well as along it.
|
|
251
|
+
const roofBox = (target, p0, p1, roof, halfWidth, lift, drop) => {
|
|
252
|
+
const chord = Math.hypot(p1[0] - p0[0], p1[1] - p0[1]);
|
|
253
|
+
const t = [(p1[0] - p0[0]) / chord, (p1[1] - p0[1]) / chord], n = [-t[1], t[0]];
|
|
254
|
+
const corner = (end, across, offset) => {
|
|
255
|
+
const at = [end[0] + n[0] * across, end[1] + n[1] * across];
|
|
256
|
+
return [at[0], y(roof(at) + offset), at[1]];
|
|
257
|
+
};
|
|
258
|
+
const b = [corner(p0, -halfWidth, -drop), corner(p0, halfWidth, -drop), corner(p1, halfWidth, -drop), corner(p1, -halfWidth, -drop)];
|
|
259
|
+
const T = [corner(p0, -halfWidth, lift), corner(p0, halfWidth, lift), corner(p1, halfWidth, lift), corner(p1, -halfWidth, lift)];
|
|
260
|
+
kit.quad(target, [T[0], T[1], T[2], T[3]], [faceNormal(T[0], T[1], T[2])]);
|
|
261
|
+
kit.quad(target, [b[0], b[3], b[2], b[1]], [faceNormal(b[0], b[3], b[2])]);
|
|
262
|
+
for (const i of [0, 1, 2, 3]) {
|
|
263
|
+
const j = (i + 1) % 4;
|
|
264
|
+
kit.quad(target, [T[j], T[i], b[i], b[j]], [faceNormal(T[j], T[i], b[i])]);
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
// How a run meets its neighbour at each end: the signed angle between their outward
|
|
268
|
+
// normals, negative at a convex corner, positive at a concave one, and near zero where a
|
|
269
|
+
// traced outline splits one wall into nearly collinear pieces.
|
|
270
|
+
const straight = 5 * Math.PI / 180;
|
|
271
|
+
const turns = (plan, index) => {
|
|
272
|
+
const turn = (a, b) => { const n1 = a.normal(a.length), n2 = b.normal(0); return Math.atan2(n1[0] * n2[1] - n1[1] * n2[0], n1[0] * n2[0] + n1[1] * n2[1]); };
|
|
273
|
+
const count = plan.length, run = plan[index];
|
|
274
|
+
return [turn(plan[(index + count - 1) % count], run), turn(run, plan[(index + 1) % count])];
|
|
275
|
+
};
|
|
276
|
+
// Two walls' ribbons stand out 0.08 m and cross within this distance of their corner; the
|
|
277
|
+
// sharper the turn, the further, so a ribbon stops that far short of it. Across a nearly
|
|
278
|
+
// straight joint a 2 mm gap is enough, and keeps their level sills and heads from
|
|
279
|
+
// overlapping in the same plane.
|
|
280
|
+
const cornerClearance = (turn) => 0.08 * Math.tan(Math.abs(turn) / 2) + (Math.abs(turn) < straight ? 0.002 : 0.025);
|
|
281
|
+
let wall = 0;
|
|
282
|
+
volumes.forEach((volume, self) => {
|
|
283
|
+
volume.plan.forEach((whole, index) => {
|
|
284
|
+
const [startTurn, endTurn] = turns(volume.plan, index);
|
|
285
|
+
stretches(self, whole).forEach(({ from, to, neighbour }, piece, all) => {
|
|
286
|
+
wall += 1;
|
|
287
|
+
// A stretch is its own run, so everything below measures along it; a whole run keeps
|
|
288
|
+
// its mullions counted from its own start.
|
|
289
|
+
const run = from === 0 && to === whole.length ? whole : line(whole.at(from), whole.at(to));
|
|
290
|
+
const covering = neighbour < 0 ? null : volumes[neighbour];
|
|
291
|
+
const cover = covering ? (s) => covering.roof(run.at(s)) : null;
|
|
292
|
+
const roof = (s) => volume.roof(run.at(s));
|
|
293
|
+
// Ribbons run on across a straight joint and stop short of a corner, so the two walls'
|
|
294
|
+
// ribbons never overlap there.
|
|
295
|
+
const first = piece === 0, last = piece === all.length - 1;
|
|
296
|
+
const clear = [cornerClearance(first ? startTurn : 0), cornerClearance(last ? endTurn : 0)];
|
|
297
|
+
const n = run.normal(0), outer = Math.max(Math.abs(n[0]), Math.abs(n[1])) > 0.95;
|
|
298
|
+
const crown = volume.glazed && outer ? form.crown : undefined;
|
|
299
|
+
// Ribbons: the lobby's tall storefront, then one per floor up to the roof.
|
|
300
|
+
ribbon(run, clear, roof, cover, crainFloors.lobbyGlass[0], crainFloors.lobbyGlass[1], 0, wall);
|
|
301
|
+
const peak = Math.max(roof(0), roof(run.length));
|
|
302
|
+
for (let row = 1; crainFloors.lobbyTop + (row - 1) * crainFloors.pitch + crainFloors.sill < peak; row += 1) {
|
|
303
|
+
const level = crainFloors.lobbyTop + (row - 1) * crainFloors.pitch;
|
|
304
|
+
if (crown && level + crainFloors.head > crown.officeTop)
|
|
305
|
+
continue;
|
|
306
|
+
ribbon(run, clear, roof, cover, level + crainFloors.sill, level + crainFloors.head, row, wall);
|
|
307
|
+
}
|
|
308
|
+
if (crown) {
|
|
309
|
+
for (const [lo, hi] of crown.louvers) {
|
|
310
|
+
ribbon(run, clear, roof, cover, lo, hi, 0, wall, "vent");
|
|
311
|
+
// Slim closed blades seated in the dark backing. Extra end clearance
|
|
312
|
+
// accommodates their 5 cm relief at corners and collinear map joints.
|
|
313
|
+
const bladeClear = [clear[0] + 0.1, clear[1] + 0.1];
|
|
314
|
+
for (let bottom = lo + 0.12; bottom + 0.07 < hi; bottom += 0.22)
|
|
315
|
+
ribbon(run, bladeClear, (s) => roof(s) - 0.04, cover, bottom, bottom + 0.07, 0, wall, "blade");
|
|
316
|
+
}
|
|
317
|
+
for (const [lo, hi] of crown.shadowStrips)
|
|
318
|
+
ribbon(run, clear, roof, cover, lo, hi, 0, wall, "shadow");
|
|
319
|
+
}
|
|
320
|
+
// Roof edges: only where this volume stands above its neighbour. The lit outline runs
|
|
321
|
+
// along the four faces; the diagonal edges of the split, the notches, and the slot
|
|
322
|
+
// take a dark coping. Each run's fascia is a few millimetres wider and deeper than the
|
|
323
|
+
// last, so where two overlap at a corner no faces share a plane.
|
|
324
|
+
if (cover && Math.max(cover(0) - roof(0), cover(run.length) - roof(run.length)) > -0.5)
|
|
325
|
+
return;
|
|
326
|
+
const lit = volume.glazed && outer;
|
|
327
|
+
const step = 0.004 * index, width = (lit ? 0.42 : 0.3) + step;
|
|
328
|
+
// A sloped roof's fascia carries on past a convex corner, downhill only so none rises
|
|
329
|
+
// above the peaks. Otherwise it stops short: across a straight joint or into a concave
|
|
330
|
+
// corner its neighbour's top would share its plane where the two overlapped, and at a
|
|
331
|
+
// convex corner its end would lie in the plane of the next wall, which a taller
|
|
332
|
+
// neighbour may carry on up.
|
|
333
|
+
const adjust = (turn, at, past) => {
|
|
334
|
+
if (turn < -straight)
|
|
335
|
+
return volume.glazed && volume.roof(run.at(past)) < volume.roof(run.at(at)) ? width : -0.005;
|
|
336
|
+
return -(width * Math.tan(Math.abs(turn) / 2) + 0.002);
|
|
337
|
+
};
|
|
338
|
+
const extend = [adjust(first ? startTurn : 0, 0, -width), adjust(last ? endTurn : 0, run.length, run.length + width)];
|
|
339
|
+
fascia(lit ? lights : coping, run, volume.roof, width, (lit ? 0.7 : 0.5) + step, extend);
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
// The sloped glazing's grid, raised a little above the glass along the building's axes and
|
|
344
|
+
// seated in it, between the glass's top and its underside. The two directions stand and
|
|
345
|
+
// sit at different depths, so their crossings never share a plane.
|
|
346
|
+
// Bars keep their full width at least 0.3 m inside the roof's edge.
|
|
347
|
+
const spacing = 2 * crainFloors.module, inset = 0.9, barHalf = 0.08, barReach = barHalf + 0.3;
|
|
348
|
+
const { origin, axis } = form.grid;
|
|
349
|
+
const across = [-axis[1], axis[0]];
|
|
350
|
+
for (const volume of volumes.filter((v) => v.glazed)) {
|
|
351
|
+
for (const [direction, normal, lift, seat] of [[axis, across, 0.06, 0.02], [across, axis, 0.09, 0.03]]) {
|
|
352
|
+
const offsets = volume.corners.map((p) => (p[0] - origin[0]) * normal[0] + (p[1] - origin[1]) * normal[1]);
|
|
353
|
+
for (let k = Math.ceil(Math.min(...offsets) / spacing + 1e-9); k * spacing < Math.max(...offsets) - 1e-9; k += 1) {
|
|
354
|
+
const start = [origin[0] + normal[0] * k * spacing, origin[1] + normal[1] * k * spacing];
|
|
355
|
+
// Clip both long sides of the bar's footprint, not just its centre line, so a bar
|
|
356
|
+
// running nearly parallel to an edge cannot lean out over it.
|
|
357
|
+
const side = (offset) => [start[0] + normal[0] * offset, start[1] + normal[1] * offset];
|
|
358
|
+
const [left, right] = [clipLine(volume.corners, side(-barReach), direction), clipLine(volume.corners, side(barReach), direction)];
|
|
359
|
+
// The geographic crown follows the observed grid to the edge; the
|
|
360
|
+
// drawing-fit copies retain their original half-module end snapping.
|
|
361
|
+
// Geographic ends rely on the building-kit coplanar-overlap check to
|
|
362
|
+
// catch any end face meeting a crossing bar after grid/inset changes.
|
|
363
|
+
for (const [a0, a1] of left)
|
|
364
|
+
for (const [b0, b1] of right) {
|
|
365
|
+
const t0 = form.grid.edgeInset === undefined ? (Math.ceil((Math.max(a0, b0) + inset) / spacing - 0.5) + 0.5) * spacing : Math.max(a0, b0) + form.grid.edgeInset;
|
|
366
|
+
const t1 = form.grid.edgeInset === undefined ? (Math.floor((Math.min(a1, b1) - inset) / spacing - 0.5) + 0.5) * spacing : Math.min(a1, b1) - form.grid.edgeInset;
|
|
367
|
+
if (t1 - t0 < spacing - 1e-9)
|
|
368
|
+
continue;
|
|
369
|
+
const p0 = [start[0] + direction[0] * t0, start[1] + direction[1] * t0];
|
|
370
|
+
const p1 = [start[0] + direction[0] * t1, start[1] + direction[1] * t1];
|
|
371
|
+
roofBox(grid, p0, p1, volume.roof, barHalf, lift, seat);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
const height = Math.max(...volumes.flatMap((volume) => volume.corners.map((p) => y(volume.roof(p)))));
|
|
377
|
+
return kit.finish({ height, outlines: [shell], opacity: 0.2 });
|
|
378
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as THREE from "../vendor/three-r186.js";
|
|
2
|
+
import type { BatchData, Builder, Plan, Run, Vec2 } from "./building-kit.js";
|
|
3
|
+
export declare const orient: (corners: Vec2[]) => Vec2[];
|
|
4
|
+
export declare const planOf: (corners: Vec2[]) => Plan;
|
|
5
|
+
export declare const turnAt: (a: Run, b: Run) => number;
|
|
6
|
+
export interface Chain {
|
|
7
|
+
runs: number[];
|
|
8
|
+
starts: number[];
|
|
9
|
+
length: number;
|
|
10
|
+
}
|
|
11
|
+
export declare function chainsOf(plan: Plan, straight?: number): Chain[];
|
|
12
|
+
export declare function mitredBox(kit: Builder, target: BatchData, run: Run, s0: number, s1: number, before: number | undefined, after: number | undefined, width: number, lo: number, hi: number, y?: (real: number) => number): void;
|
|
13
|
+
export declare function gridBox(kit: Builder, target: BatchData, run: Run, columns: number[], rows: number[], back: number, front: number, paint: (column: number, row: number) => THREE.Color, sides: THREE.Color, y?: (real: number) => number): void;
|
|
14
|
+
export declare function paintedSlab(kit: Builder, target: BatchData, polygon: [number, number][], height: number, up: boolean, paint: THREE.Color): void;
|
|
15
|
+
export declare const turnsAt: (plan: Plan, index: number) => readonly [number, number];
|
|
16
|
+
export declare function jointTrim(plan: Plan, chain: Chain, froms: number[], k: number, other: number, clearance?: number): number;
|
|
17
|
+
export declare function chainPier(kit: Builder, target: BatchData, plan: Plan, chain: Chain, froms: number[], a: number, b: number, depth: number, top: number | number[], y?: (real: number) => number): void;
|
|
18
|
+
export declare function chainSkin(kit: Builder, target: BatchData, plan: Plan, chain: Chain, k0: number, k1: number, s0: number, s1: number, columns: number[], rows: number[], back: number, front: number, paint: (bay: number, row: number) => THREE.Color, sides: THREE.Color, y?: (real: number) => number): void;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { line } from "./building-kit.js";
|
|
2
|
+
// An outline's corners in the winding that gives every run of its plan an outward normal.
|
|
3
|
+
export const orient = (corners) => {
|
|
4
|
+
const area = corners.reduce((sum, [x, z], i) => { const [x2, z2] = corners[(i + 1) % corners.length]; return sum + x * z2 - x2 * z; }, 0);
|
|
5
|
+
return area < 0 ? corners : [...corners].reverse();
|
|
6
|
+
};
|
|
7
|
+
export const planOf = (corners) => corners.map((corner, i) => line(corner, corners[(i + 1) % corners.length]));
|
|
8
|
+
// The turn from run a into run b at their shared corner; negative where the outline turns
|
|
9
|
+
// out (a convex corner) and positive where it turns in.
|
|
10
|
+
export const turnAt = (a, b) => { const n1 = a.normal(a.length), n2 = b.normal(0); return Math.atan2(n1[0] * n2[1] - n1[1] * n2[0], n1[0] * n2[0] + n1[1] * n2[1]); };
|
|
11
|
+
export function chainsOf(plan, straight = 5 * Math.PI / 180) {
|
|
12
|
+
const breaks = plan.map((run, i) => Math.abs(turnAt(plan[(i + plan.length - 1) % plan.length], run)) >= straight);
|
|
13
|
+
const first = Math.max(0, breaks.indexOf(true));
|
|
14
|
+
const chains = [];
|
|
15
|
+
for (let k = 0; k < plan.length; k += 1) {
|
|
16
|
+
const i = (first + k) % plan.length, chain = chains.at(-1);
|
|
17
|
+
if (breaks[i] || !chain)
|
|
18
|
+
chains.push({ runs: [i], starts: [0], length: plan[i].length });
|
|
19
|
+
else {
|
|
20
|
+
chain.runs.push(i);
|
|
21
|
+
chain.starts.push(chain.length);
|
|
22
|
+
chain.length += plan[i].length;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return chains;
|
|
26
|
+
}
|
|
27
|
+
// A closed box standing `width` proud of a run from s0 to s1 and lo to hi: a cap, a band
|
|
28
|
+
// course, a corner pier. An end given the turn at its corner is mitred on that corner's
|
|
29
|
+
// bisector, so the neighbouring run's box meets it in one plane, facing the other way, and
|
|
30
|
+
// the two never overlap; an end without one is square. At a convex corner (a negative turn)
|
|
31
|
+
// the outer edge runs past the wall's end; at a concave one it stops short of it.
|
|
32
|
+
export function mitredBox(kit, target, run, s0, s1, before, after, width, lo, hi, y = (real) => real) {
|
|
33
|
+
const outer0 = s0 + (before === undefined ? 0 : Math.tan(before / 2) * width), outer1 = s1 - (after === undefined ? 0 : Math.tan(after / 2) * width);
|
|
34
|
+
const n = run.normal(0);
|
|
35
|
+
const at = (s, depth, v) => { const q = run.at(s, depth); return [q[0], y(v), q[1]]; };
|
|
36
|
+
const inner = [at(s0, 0, lo), at(s1, 0, lo), at(s1, 0, hi), at(s0, 0, hi)];
|
|
37
|
+
const outer = [at(outer0, width, lo), at(outer1, width, lo), at(outer1, width, hi), at(outer0, width, hi)];
|
|
38
|
+
const normal = (a, b, c) => {
|
|
39
|
+
const u = [b[0] - a[0], b[1] - a[1], b[2] - a[2]], v = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
|
|
40
|
+
const g = [u[1] * v[2] - u[2] * v[1], u[2] * v[0] - u[0] * v[2], u[0] * v[1] - u[1] * v[0]], l = Math.hypot(...g);
|
|
41
|
+
return [g[0] / l, g[1] / l, g[2] / l];
|
|
42
|
+
};
|
|
43
|
+
kit.quad(target, [outer[0], outer[1], outer[2], outer[3]], [[n[0], 0, n[1]]]);
|
|
44
|
+
kit.quad(target, [inner[1], inner[0], inner[3], inner[2]], [[-n[0], 0, -n[1]]]);
|
|
45
|
+
kit.quad(target, [inner[3], outer[3], outer[2], inner[2]], [[0, 1, 0]]);
|
|
46
|
+
kit.quad(target, [inner[0], inner[1], outer[1], outer[0]], [[0, -1, 0]]);
|
|
47
|
+
kit.quad(target, [inner[0], outer[0], outer[3], inner[3]], [normal(inner[0], outer[0], outer[3])]);
|
|
48
|
+
kit.quad(target, [outer[1], inner[1], inner[2], outer[2]], [normal(outer[1], inner[1], inner[2])]);
|
|
49
|
+
}
|
|
50
|
+
// A closed box on a run, standing `back` to `front` off the wall between the first and last
|
|
51
|
+
// of `columns` along it and the first and last of `rows` up it, whose front is a grid of
|
|
52
|
+
// coloured cells: panes and mullions, window ribbons, louver blades. Its top and bottom fan
|
|
53
|
+
// out from a back corner through every column cut, and its ends through every row cut, so
|
|
54
|
+
// each cell's edge meets its partner and the box closes edge for edge, without the
|
|
55
|
+
// T-junctions a single top face would leave against a cut front. Heights are real heights;
|
|
56
|
+
// `y` maps them to the model's.
|
|
57
|
+
export function gridBox(kit, target, run, columns, rows, back, front, paint, sides, y = (real) => real) {
|
|
58
|
+
const n = run.normal(0), tangent = [n[1], -n[0]];
|
|
59
|
+
const at = (s, depth, height) => { const p = run.at(s, depth); return [p[0], y(height), p[1]]; };
|
|
60
|
+
const [s0, s1] = [columns[0], columns.at(-1)], [y0, y1] = [rows[0], rows.at(-1)];
|
|
61
|
+
for (let c = 0; c + 1 < columns.length; c += 1) {
|
|
62
|
+
for (let r = 0; r + 1 < rows.length; r += 1) {
|
|
63
|
+
const [a, b, lo, hi] = [columns[c], columns[c + 1], rows[r], rows[r + 1]];
|
|
64
|
+
kit.quad(target, [at(a, front, lo), at(b, front, lo), at(b, front, hi), at(a, front, hi)], [[n[0], 0, n[1]]], paint(c, r));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
kit.quad(target, [at(s1, back, y0), at(s0, back, y0), at(s0, back, y1), at(s1, back, y1)], [[-n[0], 0, -n[1]]], sides);
|
|
68
|
+
const fan = (hinge, rim, normal) => {
|
|
69
|
+
for (let i = 0; i + 1 < rim.length; i += 1)
|
|
70
|
+
kit.triangle(target, [hinge, rim[i], rim[i + 1]], [normal, normal, normal], sides);
|
|
71
|
+
};
|
|
72
|
+
fan(at(s0, back, y1), [...columns.map((s) => at(s, front, y1)), at(s1, back, y1)], [0, 1, 0]);
|
|
73
|
+
fan(at(s0, back, y0), [...columns.map((s) => at(s, front, y0)), at(s1, back, y0)], [0, -1, 0]);
|
|
74
|
+
fan(at(s0, back, y0), [...rows.map((v) => at(s0, front, v)), at(s0, back, y1)], [-tangent[0], 0, -tangent[1]]);
|
|
75
|
+
fan(at(s1, back, y0), [...rows.map((v) => at(s1, front, v)), at(s1, back, y1)], [tangent[0], 0, tangent[1]]);
|
|
76
|
+
}
|
|
77
|
+
// A slab in a vertex-coloured batch, painted, since kit.slab lays its vertices white.
|
|
78
|
+
export function paintedSlab(kit, target, polygon, height, up, paint) {
|
|
79
|
+
const start = target.colors.length;
|
|
80
|
+
kit.slab(target, polygon, height, up);
|
|
81
|
+
for (let i = start; i < target.colors.length; i += 3)
|
|
82
|
+
target.colors.splice(i, 3, paint.r, paint.g, paint.b);
|
|
83
|
+
}
|
|
84
|
+
// The turns at a run's two corners, into it and out of it.
|
|
85
|
+
export const turnsAt = (plan, index) => [turnAt(plan[(index + plan.length - 1) % plan.length], plan[index]), turnAt(plan[index], plan[(index + 1) % plan.length])];
|
|
86
|
+
// Piers and mullions cross the joints of a chain in pieces, one per run, mitred on each
|
|
87
|
+
// joint's bisector as mitred courses are, each standing from its own run's `froms` to its
|
|
88
|
+
// own run's top. Where the cover changes at a joint, the less covered run's last 2 cm stand
|
|
89
|
+
// from the higher cover, so the piece ends clear of the neighbour's wall. A run's cells stop
|
|
90
|
+
// a little short of every joint, of a more covered neighbour, and of the wedge a concave
|
|
91
|
+
// joint would push into the next run's cells: `jointTrim` gives how far, toward `other`.
|
|
92
|
+
// `clearance` is how far cells stop short of a more covered neighbour.
|
|
93
|
+
export function jointTrim(plan, chain, froms, k, other, clearance = 0.01) {
|
|
94
|
+
if (other < 0 || other >= chain.runs.length)
|
|
95
|
+
return 0;
|
|
96
|
+
const [a, b] = other > k ? [chain.runs[k], chain.runs[other]] : [chain.runs[other], chain.runs[k]];
|
|
97
|
+
const turn = turnAt(plan[a], plan[b]);
|
|
98
|
+
return 0.005 + (turn > 0 ? 0.08 * Math.tan(turn / 2) : 0) + (froms[k] < froms[other] ? clearance : 0);
|
|
99
|
+
}
|
|
100
|
+
export function chainPier(kit, target, plan, chain, froms, a, b, depth, top, y = (real) => real) {
|
|
101
|
+
const last = chain.runs.length - 1;
|
|
102
|
+
chain.runs.forEach((index, k) => {
|
|
103
|
+
const run = plan[index], start = chain.starts[k], end = start + run.length, ceiling = typeof top === "number" ? top : top[k];
|
|
104
|
+
const lo = Math.max(a, start), hi = Math.min(b, end);
|
|
105
|
+
if (hi - lo < 0.005)
|
|
106
|
+
return;
|
|
107
|
+
const [before, after] = turnsAt(plan, index);
|
|
108
|
+
const mitreLo = lo === start && (a < start || (k === 0 && a === 0)) ? before : undefined;
|
|
109
|
+
const mitreHi = hi === end && (b > end || (k === last && b === chain.length)) ? after : undefined;
|
|
110
|
+
const pieces = [[lo, hi, froms[k], mitreLo, mitreHi]];
|
|
111
|
+
if (mitreHi !== undefined && k < last && froms[k] < froms[k + 1] && hi - 0.02 > lo)
|
|
112
|
+
pieces.splice(0, 1, [lo, hi - 0.02, froms[k], mitreLo, undefined], [hi - 0.02, hi, froms[k + 1], undefined, mitreHi]);
|
|
113
|
+
const first = pieces[0];
|
|
114
|
+
if (mitreLo !== undefined && k > 0 && froms[k] < froms[k - 1] && first[1] > lo + 0.02)
|
|
115
|
+
pieces.splice(0, 1, [lo, lo + 0.02, froms[k - 1], mitreLo, undefined], [lo + 0.02, first[1], first[2], undefined, first[4]]);
|
|
116
|
+
for (const [p0, p1, from, m0, m1] of pieces) {
|
|
117
|
+
if (from < ceiling)
|
|
118
|
+
mitredBox(kit, target, run, p0 - start, p1 - start, m0, m1, depth, Math.max(from, 0.3), ceiling, y);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
// A closed box along runs k0 to k1 of a chain, from s0 to s1 along it, standing `back` to
|
|
123
|
+
// `front` off the walls, whose front is a grid of coloured cells cut at `columns` along the
|
|
124
|
+
// chain and at `rows` up it: a curtain wall's glass and spandrels round a traced curve. Where
|
|
125
|
+
// two of its runs meet, its faces meet on the joint's mitre, so a curve carries one box with
|
|
126
|
+
// ends only at s0 and s1. Its tops and bottoms fan out from a back corner through each run's
|
|
127
|
+
// column cuts, and its ends through every row cut, so it closes edge for edge. A cell is
|
|
128
|
+
// painted by its bay, the columns at or before its start, so a cell a joint splits keeps
|
|
129
|
+
// one colour.
|
|
130
|
+
// `paint` runs immediately before its front quad appends six vertices to target.
|
|
131
|
+
// Blue Cross records that vertex offset to address individual window lights.
|
|
132
|
+
export function chainSkin(kit, target, plan, chain, k0, k1, s0, s1, columns, rows, back, front, paint, sides, y = (real) => real) {
|
|
133
|
+
const lift = (q, v) => [q[0], y(v), q[1]];
|
|
134
|
+
const mitre = (k, depth) => {
|
|
135
|
+
const a = plan[chain.runs[k]], b = plan[chain.runs[k + 1]], v = a.at(a.length), n1 = a.normal(a.length), n2 = b.normal(0);
|
|
136
|
+
const scale = depth / (1 + n1[0] * n2[0] + n1[1] * n2[1]);
|
|
137
|
+
return [v[0] + (n1[0] + n2[0]) * scale, v[1] + (n1[1] + n2[1]) * scale];
|
|
138
|
+
};
|
|
139
|
+
const [y0, y1] = [rows[0], rows.at(-1)];
|
|
140
|
+
const up = [0, 1, 0], down = [0, -1, 0];
|
|
141
|
+
const fan = (hinge, rim, normal, paintWith = sides) => {
|
|
142
|
+
for (let i = 0; i + 1 < rim.length; i += 1)
|
|
143
|
+
kit.triangle(target, [hinge, rim[i], rim[i + 1]], [normal, normal, normal], paintWith);
|
|
144
|
+
};
|
|
145
|
+
for (let k = k0; k <= k1; k += 1) {
|
|
146
|
+
const run = plan[chain.runs[k]], start = chain.starts[k], n = run.normal(0), out = [n[0], 0, n[1]];
|
|
147
|
+
const from = k === k0 ? s0 : start, to = k === k1 ? s1 : start + run.length;
|
|
148
|
+
const cuts = columns.filter((s) => s > from + 1e-6 && s < to - 1e-6);
|
|
149
|
+
const arcs = [from, ...cuts, to];
|
|
150
|
+
const at = (s, depth, index) => {
|
|
151
|
+
if (index === 0 && k > k0)
|
|
152
|
+
return mitre(k - 1, depth);
|
|
153
|
+
if (index === arcs.length - 1 && k < k1)
|
|
154
|
+
return mitre(k, depth);
|
|
155
|
+
return run.at(s - start, depth);
|
|
156
|
+
};
|
|
157
|
+
const fronts = arcs.map((s, i) => at(s, front, i));
|
|
158
|
+
const [backStart, backEnd] = [at(from, back, 0), at(to, back, arcs.length - 1)];
|
|
159
|
+
for (let i = 0; i + 1 < fronts.length; i += 1) {
|
|
160
|
+
const bay = columns.filter((s) => s <= arcs[i] + 1e-6).length;
|
|
161
|
+
for (let r = 0; r + 1 < rows.length; r += 1) {
|
|
162
|
+
kit.quad(target, [lift(fronts[i], rows[r]), lift(fronts[i + 1], rows[r]), lift(fronts[i + 1], rows[r + 1]), lift(fronts[i], rows[r + 1])], [out], paint(bay, r));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
kit.quad(target, [lift(backEnd, y0), lift(backStart, y0), lift(backStart, y1), lift(backEnd, y1)], [[-n[0], 0, -n[1]]], sides);
|
|
166
|
+
fan(lift(backStart, y1), [...fronts.map((q) => lift(q, y1)), lift(backEnd, y1)], up);
|
|
167
|
+
fan(lift(backStart, y0), [...fronts.map((q) => lift(q, y0)), lift(backEnd, y0)], down);
|
|
168
|
+
}
|
|
169
|
+
// The ends, square to their runs.
|
|
170
|
+
const ends = [[k0, s0, -1], [k1, s1, 1]];
|
|
171
|
+
for (const [k, s, sign] of ends) {
|
|
172
|
+
// Outward along the run at s1, back along it at s0.
|
|
173
|
+
const run = plan[chain.runs[k]], start = chain.starts[k], n = run.normal(0), t = [n[1] * sign, 0, -n[0] * sign];
|
|
174
|
+
fan(lift(run.at(s - start, back), y0), [...rows.map((v) => lift(run.at(s - start, front), v)), lift(run.at(s - start, back), y1)], t);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { BuildingModel, Plan } from "./building-kit.js";
|
|
2
|
+
import type { GeoBuilding } from "./skyline-geography-data.js";
|
|
3
|
+
export declare const gageLevels: Readonly<{
|
|
4
|
+
shopfront: 4;
|
|
5
|
+
second: 5.2;
|
|
6
|
+
eighth: number;
|
|
7
|
+
pitch: number;
|
|
8
|
+
top: 46.94;
|
|
9
|
+
cartouches: 47.74;
|
|
10
|
+
}>;
|
|
11
|
+
export declare const gagePalette: Readonly<{
|
|
12
|
+
glass: number[];
|
|
13
|
+
lit: 7237230;
|
|
14
|
+
dim: 3815994;
|
|
15
|
+
terracotta: 6184542;
|
|
16
|
+
band: 4737096;
|
|
17
|
+
brick: 4210752;
|
|
18
|
+
}>;
|
|
19
|
+
export declare function createGageGeographicBuilding(record: GeoBuilding, projectPlan: (coordinates: [number, number][]) => Plan, offset: [number, number]): BuildingModel;
|