@motionscript/molecule 0.0.0-stage → 0.1.0-alpha.0
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/CHANGELOG.md +5 -0
- package/LICENSE +201 -0
- package/dist/browser/index.js +4 -0
- package/dist/browser/index.js.map +7 -0
- package/dist/browser/manifest.json +11 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/nodes.d.ts +18 -0
- package/dist/nodes.d.ts.map +1 -0
- package/dist/nodes.js +18 -0
- package/dist/nodes.js.map +1 -0
- package/dist/protein/chemistry.d.ts +52 -0
- package/dist/protein/chemistry.d.ts.map +1 -0
- package/dist/protein/chemistry.js +208 -0
- package/dist/protein/chemistry.js.map +1 -0
- package/dist/protein/index.d.ts +35 -0
- package/dist/protein/index.d.ts.map +1 -0
- package/dist/protein/index.js +35 -0
- package/dist/protein/index.js.map +1 -0
- package/dist/protein/parse.d.ts +32 -0
- package/dist/protein/parse.d.ts.map +1 -0
- package/dist/protein/parse.js +387 -0
- package/dist/protein/parse.js.map +1 -0
- package/dist/protein/protein.d.ts +265 -0
- package/dist/protein/protein.d.ts.map +1 -0
- package/dist/protein/protein.js +645 -0
- package/dist/protein/protein.js.map +1 -0
- package/dist/protein/ribbon.d.ts +83 -0
- package/dist/protein/ribbon.d.ts.map +1 -0
- package/dist/protein/ribbon.js +468 -0
- package/dist/protein/ribbon.js.map +1 -0
- package/dist/protein/shared.d.ts +221 -0
- package/dist/protein/shared.d.ts.map +1 -0
- package/dist/protein/shared.js +478 -0
- package/dist/protein/shared.js.map +1 -0
- package/dist/protein/structure.d.ts +184 -0
- package/dist/protein/structure.d.ts.map +1 -0
- package/dist/protein/structure.js +324 -0
- package/dist/protein/structure.js.map +1 -0
- package/package.json +64 -3
- package/registry.json +22 -0
- package/src/index.ts +2 -0
- package/src/nodes.ts +18 -0
- package/src/protein/chemistry.ts +223 -0
- package/src/protein/index.ts +34 -0
- package/src/protein/parse.ts +427 -0
- package/src/protein/protein.ts +897 -0
- package/src/protein/ribbon.ts +658 -0
- package/src/protein/shared.ts +622 -0
- package/src/protein/structure.ts +491 -0
- package/README.md +0 -4
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The cartoon: a chain's backbone swept as a **ribbon** rather than as a tube.
|
|
3
|
+
*
|
|
4
|
+
* This is the one representation whose whole job is to be recognised. A figure
|
|
5
|
+
* in a paper draws a helix as a twisted band and a strand as a flat arrow, and
|
|
6
|
+
* has since Jane Richardson drew the first one by hand in 1980 — the convention
|
|
7
|
+
* is old enough that a reader decodes it without a legend, which is exactly why
|
|
8
|
+
* a picture that departs from it reads as *wrong* rather than as different.
|
|
9
|
+
*
|
|
10
|
+
* The node drew that with `Geo.tube` before this module existed, and the two
|
|
11
|
+
* things it could not express are the two the convention is made of:
|
|
12
|
+
*
|
|
13
|
+
* **A cross-section has a width and a thickness, not a radius.** A tube's is a
|
|
14
|
+
* circle, so a helix could only be told from a coil by being *fatter* — and a
|
|
15
|
+
* fat circle swept along the α-carbons of a helix is close to self-intersecting,
|
|
16
|
+
* because those carbons corkscrew about the helix axis at ~2.3 Å with a 5.4 Å
|
|
17
|
+
* pitch. What comes out is a lumpy sausage: the tube's own diameter is most of
|
|
18
|
+
* the coil's, so the spiral fills in and the shape stops being legible. Swept
|
|
19
|
+
* flat instead — ~2.2 Å across and ~0.5 Å thick — the same spiral reads as the
|
|
20
|
+
* band every textbook draws, because the eye is now reading the *face* turning
|
|
21
|
+
* rather than a diameter changing.
|
|
22
|
+
*
|
|
23
|
+
* **A band has a side, and a curve through α-carbons does not say which.** So
|
|
24
|
+
* the frame is built from the residue's own carbonyl (see
|
|
25
|
+
* {@link TracePoint.normal}), which is what turns with the fold. A ribbon swept
|
|
26
|
+
* on an arbitrary frame twists at random and looks like a misprint.
|
|
27
|
+
*
|
|
28
|
+
* Everything here is derived **per structure**, cached by the node against the
|
|
29
|
+
* inputs that produced it, and never rebuilt for a camera move — the same
|
|
30
|
+
* bargain the instance lists make in `shared.ts`.
|
|
31
|
+
*/
|
|
32
|
+
import { resolveColor3D } from "@motionscript/core/component";
|
|
33
|
+
/**
|
|
34
|
+
* How wide and how thick each secondary structure's ribbon is, as multiples of
|
|
35
|
+
* the node's `ribbonRadius`.
|
|
36
|
+
*
|
|
37
|
+
* Stated as a ratio to one control rather than as six numbers in the inspector:
|
|
38
|
+
* "how heavy is the cartoon" is one question an author actually asks, and the
|
|
39
|
+
* *proportions* between a helix, a strand and a loop are the convention rather
|
|
40
|
+
* than a preference. The absolute values are the ones the established viewers
|
|
41
|
+
* settled on — a helix band about 2.2 Å across and half an Ångström thick, a
|
|
42
|
+
* strand slightly wider still, and a loop a thin round cord.
|
|
43
|
+
*
|
|
44
|
+
* A coil stays round (width equals thickness), which is the other half of the
|
|
45
|
+
* statement: a loop has no face to turn, so giving it one would claim a
|
|
46
|
+
* structure the file did not annotate.
|
|
47
|
+
*/
|
|
48
|
+
const SECTION = {
|
|
49
|
+
helix: { width: 2.4, thickness: 0.5 },
|
|
50
|
+
sheet: { width: 2.6, thickness: 0.45 },
|
|
51
|
+
coil: { width: 1, thickness: 1 },
|
|
52
|
+
};
|
|
53
|
+
/** How wide a strand's arrowhead is at its base, in the same multiples. */
|
|
54
|
+
const ARROW_WIDTH = 4.4;
|
|
55
|
+
/**
|
|
56
|
+
* How many residues at the C-terminal end of a strand the arrowhead occupies.
|
|
57
|
+
*
|
|
58
|
+
* Residues rather than a fraction of the span, because an arrowhead is a fixed
|
|
59
|
+
* *shape* — the same barb whether the strand is five residues or fifteen. A
|
|
60
|
+
* fraction would draw a stubby wedge on a short strand and a long spike on a
|
|
61
|
+
* β-hairpin's arm. Capped below at a third of the span so a three-residue
|
|
62
|
+
* strand is still a strand with a point on it rather than one long taper.
|
|
63
|
+
*/
|
|
64
|
+
const ARROW_RESIDUES = 2.4;
|
|
65
|
+
/** What a strand's arrow narrows to at its tip, so the point is a point. */
|
|
66
|
+
const ARROW_TIP = 0.08;
|
|
67
|
+
/**
|
|
68
|
+
* How many samples each residue-to-residue step is drawn with.
|
|
69
|
+
*
|
|
70
|
+
* The curve is the picture here. Consecutive α-carbons are 3.8 Å apart and a
|
|
71
|
+
* helix turns fully in 3.6 of them, so a ribbon drawn straight between residues
|
|
72
|
+
* is a polygon with about five sides per turn — which is exactly the faceted,
|
|
73
|
+
* angular look that separates a rendering nobody trusts from one that reads as
|
|
74
|
+
* a molecule. Tied to the node's mesh detail so the same control that coarsens
|
|
75
|
+
* the spheres coarsens this.
|
|
76
|
+
*/
|
|
77
|
+
function subdivisionsFor(quality) {
|
|
78
|
+
return Math.min(10, Math.max(3, Math.round(quality / 2)));
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Splits a chain's trace into the spans a cartoon is built from — one per
|
|
82
|
+
* unbroken stretch of the same secondary structure.
|
|
83
|
+
*
|
|
84
|
+
* Consecutive spans overlap by one point, for the reason `backboneRuns`'
|
|
85
|
+
* consecutive runs do: the shared residue is the last sample of one sweep and
|
|
86
|
+
* the first of the next, so the two meshes butt up rather than leaving a hole
|
|
87
|
+
* at every helix-to-loop transition.
|
|
88
|
+
*/
|
|
89
|
+
export function cartoonSpans(chain, colors) {
|
|
90
|
+
const spans = [];
|
|
91
|
+
chain.trace.forEach((point, index) => {
|
|
92
|
+
const color = colors[index] ?? colors[0] ?? "#ffffff";
|
|
93
|
+
let current = spans[spans.length - 1];
|
|
94
|
+
if (!current || current.secondary !== point.secondary) {
|
|
95
|
+
const previous = current;
|
|
96
|
+
current = { secondary: point.secondary, points: [], colors: [] };
|
|
97
|
+
// Carry the previous span's last residue in as this one's first.
|
|
98
|
+
if (previous) {
|
|
99
|
+
current.points.push(previous.points[previous.points.length - 1]);
|
|
100
|
+
current.colors.push(previous.colors[previous.colors.length - 1]);
|
|
101
|
+
}
|
|
102
|
+
spans.push(current);
|
|
103
|
+
}
|
|
104
|
+
current.points.push(point);
|
|
105
|
+
current.colors.push(color);
|
|
106
|
+
});
|
|
107
|
+
// A lone point is not a curve: there is no direction to sweep along and no
|
|
108
|
+
// tangent to build a frame from. Only ever the first span, since every later
|
|
109
|
+
// one is seeded with its predecessor's last point.
|
|
110
|
+
return spans.filter((span) => span.points.length > 1);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* One span, as a mesh.
|
|
114
|
+
*
|
|
115
|
+
* `null` when there is nothing to sweep, so the caller can skip the op rather
|
|
116
|
+
* than hand the renderer an empty buffer.
|
|
117
|
+
*
|
|
118
|
+
* The arrays are freshly allocated and never mutated afterwards, which is what
|
|
119
|
+
* makes `staticData` honest: the renderer then compares them by identity and
|
|
120
|
+
* uploads each exactly once, where re-deriving them per frame would re-upload
|
|
121
|
+
* the whole fold sixty times a second to spin the camera.
|
|
122
|
+
*/
|
|
123
|
+
export function cartoonGeometry(span, options) {
|
|
124
|
+
const samples = sampleSpan(span, options);
|
|
125
|
+
if (samples.length < 2)
|
|
126
|
+
return null;
|
|
127
|
+
return sweep(samples, profileFor(span.secondary, options.quality));
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* The span, resampled onto a smooth curve with a frame and a size at every
|
|
131
|
+
* sample.
|
|
132
|
+
*
|
|
133
|
+
* Three things happen here that each fix something visible:
|
|
134
|
+
*
|
|
135
|
+
* **Catmull-Rom through the residues**, so the ribbon is a curve rather than a
|
|
136
|
+
* five-sided polygon per helical turn — see {@link subdivisionsFor}.
|
|
137
|
+
*
|
|
138
|
+
* **The frames are made continuous before anything is interpolated.** A
|
|
139
|
+
* β-strand's carbonyls point alternately to one side and the other, which is
|
|
140
|
+
* chemistry rather than noise: hydrogen bonds go to the neighbouring strand on
|
|
141
|
+
* both sides. Swept as given, the ribbon would flip 180° at every residue and
|
|
142
|
+
* come out as a chain of triangles. Flipping each frame to agree with its
|
|
143
|
+
* predecessor is what every viewer does and what makes a strand flat.
|
|
144
|
+
*
|
|
145
|
+
* **The arrow is measured in residues from the C-terminal end**, before
|
|
146
|
+
* subdivision, so it is the same barb on a long strand and a short one.
|
|
147
|
+
*/
|
|
148
|
+
function sampleSpan(span, options) {
|
|
149
|
+
const points = span.points;
|
|
150
|
+
const count = points.length;
|
|
151
|
+
const section = SECTION[span.secondary];
|
|
152
|
+
const radius = Math.max(0.01, options.radius);
|
|
153
|
+
const frames = continuousFrames(points);
|
|
154
|
+
const tints = span.colors.map((color) => linearRgb(color));
|
|
155
|
+
// Where the arrowhead starts, as a position along the span in *residues*.
|
|
156
|
+
// A strand shorter than the barb gives up a third of itself to it rather than
|
|
157
|
+
// becoming one long taper.
|
|
158
|
+
const arrow = span.secondary === "sheet"
|
|
159
|
+
? Math.max(count - 1 - ARROW_RESIDUES, (count - 1) * (2 / 3))
|
|
160
|
+
: Infinity;
|
|
161
|
+
const steps = subdivisionsFor(options.quality);
|
|
162
|
+
const samples = [];
|
|
163
|
+
// Positions first, frames second: a tangent is a finite difference over the
|
|
164
|
+
// *sampled* curve, so every position has to exist before any of them can be
|
|
165
|
+
// taken. Doing it in one pass would take each tangent from the residues
|
|
166
|
+
// instead, which is the polyline this subdivision exists to get away from.
|
|
167
|
+
const curve = [];
|
|
168
|
+
const carried = [];
|
|
169
|
+
const colors = [];
|
|
170
|
+
const positions = [];
|
|
171
|
+
for (let i = 0; i < count - 1; i++) {
|
|
172
|
+
// The last segment emits its endpoint too, so the sweep reaches the final
|
|
173
|
+
// residue rather than stopping one sample short of it.
|
|
174
|
+
const last = i === count - 2;
|
|
175
|
+
for (let s = 0; s < steps + (last ? 1 : 0); s++) {
|
|
176
|
+
const t = s / steps;
|
|
177
|
+
curve.push(catmullRom(points[i - 1] ?? points[i], points[i], points[i + 1], points[i + 2] ?? points[i + 1], t));
|
|
178
|
+
carried.push(lerpVector(frames[i], frames[i + 1], t));
|
|
179
|
+
colors.push(lerpRgb(tints[i], tints[i + 1], t));
|
|
180
|
+
positions.push(i + t);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
for (let i = 0; i < curve.length; i++) {
|
|
184
|
+
const position = curve[i];
|
|
185
|
+
const tangent = normalize(subtract(curve[Math.min(curve.length - 1, i + 1)], curve[Math.max(0, i - 1)]));
|
|
186
|
+
// Squared against the tangent, so the frame is orthonormal even though the
|
|
187
|
+
// carbonyl it came from is not perpendicular to the chain.
|
|
188
|
+
const normal = perpendicular(carried[i], tangent);
|
|
189
|
+
const binormal = normalize(cross(tangent, normal));
|
|
190
|
+
const along = positions[i];
|
|
191
|
+
const width = along <= arrow
|
|
192
|
+
? section.width
|
|
193
|
+
: // Linear from the barb's base to its point. The base is *wider* than
|
|
194
|
+
// the strand, which is what makes an arrowhead read as one rather
|
|
195
|
+
// than as the strand simply stopping.
|
|
196
|
+
ARROW_WIDTH +
|
|
197
|
+
(ARROW_TIP - ARROW_WIDTH) *
|
|
198
|
+
clamp01((along - arrow) / Math.max(1e-6, count - 1 - arrow));
|
|
199
|
+
samples.push({
|
|
200
|
+
position,
|
|
201
|
+
tangent,
|
|
202
|
+
normal,
|
|
203
|
+
binormal,
|
|
204
|
+
halfWidth: radius * width,
|
|
205
|
+
halfThickness: radius * section.thickness,
|
|
206
|
+
color: colors[i],
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
return samples;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Each residue's ribbon direction, made to agree with its predecessor.
|
|
213
|
+
*
|
|
214
|
+
* Falls back to the curve's own binormal — the axis its turn is about — where
|
|
215
|
+
* the file gives no carbonyl. That is the right stand-in rather than an
|
|
216
|
+
* arbitrary perpendicular: in a helix the carbonyl points very nearly along the
|
|
217
|
+
* helix axis, and so does the binormal of the curve the α-carbons trace, so a
|
|
218
|
+
* Cα-only model comes out with its bands lying the same way a complete file's
|
|
219
|
+
* do.
|
|
220
|
+
*/
|
|
221
|
+
function continuousFrames(points) {
|
|
222
|
+
const frames = [];
|
|
223
|
+
for (let i = 0; i < points.length; i++) {
|
|
224
|
+
const point = points[i];
|
|
225
|
+
let direction = point.normal ?? binormalAt(points, i);
|
|
226
|
+
// Degenerate — three collinear residues with no carbonyl. Any perpendicular
|
|
227
|
+
// will do, and the frame is squared against the tangent anyway.
|
|
228
|
+
if (lengthOf(direction) < 1e-6)
|
|
229
|
+
direction = { x: 0, y: 0, z: 1 };
|
|
230
|
+
const previous = frames[i - 1];
|
|
231
|
+
// The 180° flip a β-strand's alternating carbonyls would otherwise put into
|
|
232
|
+
// the sweep. See {@link sampleSpan}.
|
|
233
|
+
if (previous && dot(previous, direction) < 0)
|
|
234
|
+
direction = negate(direction);
|
|
235
|
+
frames.push(normalize(direction));
|
|
236
|
+
}
|
|
237
|
+
return frames;
|
|
238
|
+
}
|
|
239
|
+
/** The curve's binormal at a residue — the axis its local turn is about. */
|
|
240
|
+
function binormalAt(points, index) {
|
|
241
|
+
const before = points[Math.max(0, index - 1)];
|
|
242
|
+
const here = points[index];
|
|
243
|
+
const after = points[Math.min(points.length - 1, index + 1)];
|
|
244
|
+
return cross(subtract(here, before), subtract(after, here));
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* The cross-section a secondary structure is swept with.
|
|
248
|
+
*
|
|
249
|
+
* A loop is a round cord and a helix or a strand is a flat band — the whole
|
|
250
|
+
* distinction the convention rests on, and the one a radius could not express.
|
|
251
|
+
* The band is a rectangle rather than an ellipse because its *edge* is what the
|
|
252
|
+
* eye reads a twist by: an ellipse turning through 90° goes smoothly from wide
|
|
253
|
+
* to narrow and reads as a tube pinching, where a rectangle's corner catches the
|
|
254
|
+
* light and reads as a face turning over.
|
|
255
|
+
*/
|
|
256
|
+
function profileFor(secondary, quality) {
|
|
257
|
+
if (secondary === "coil") {
|
|
258
|
+
const sides = Math.min(16, Math.max(5, Math.round(quality / 2)));
|
|
259
|
+
const profile = [];
|
|
260
|
+
for (let i = 0; i < sides; i++) {
|
|
261
|
+
const angle = (i / sides) * Math.PI * 2;
|
|
262
|
+
const u = Math.cos(angle);
|
|
263
|
+
const v = Math.sin(angle);
|
|
264
|
+
// Smooth: one vertex per side, its normal radial, so a cord reads as
|
|
265
|
+
// round rather than as a faceted prism.
|
|
266
|
+
profile.push({ u, v, nu: u, nv: v });
|
|
267
|
+
}
|
|
268
|
+
return profile;
|
|
269
|
+
}
|
|
270
|
+
// Four faces, each with its own pair of corners, walked so consecutive
|
|
271
|
+
// entries are consecutive around the section.
|
|
272
|
+
return [
|
|
273
|
+
{ u: 1, v: 1, nu: 0, nv: 1 },
|
|
274
|
+
{ u: -1, v: 1, nu: 0, nv: 1 },
|
|
275
|
+
{ u: -1, v: 1, nu: -1, nv: 0 },
|
|
276
|
+
{ u: -1, v: -1, nu: -1, nv: 0 },
|
|
277
|
+
{ u: -1, v: -1, nu: 0, nv: -1 },
|
|
278
|
+
{ u: 1, v: -1, nu: 0, nv: -1 },
|
|
279
|
+
{ u: 1, v: -1, nu: 1, nv: 0 },
|
|
280
|
+
{ u: 1, v: 1, nu: 1, nv: 0 },
|
|
281
|
+
];
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* The samples and the profile, as one indexed mesh.
|
|
285
|
+
*
|
|
286
|
+
* Ends are **capped**: a ribbon is a solid band and an open end shows the
|
|
287
|
+
* inside of the far face, which at a helix-to-loop transition would be a
|
|
288
|
+
* visible hole rather than a join. Cheap — one fan per end — and it is what
|
|
289
|
+
* lets a span be drawn as its own mesh at all.
|
|
290
|
+
*/
|
|
291
|
+
function sweep(samples, profile) {
|
|
292
|
+
const ring = profile.length;
|
|
293
|
+
const position = [];
|
|
294
|
+
const normal = [];
|
|
295
|
+
const color = [];
|
|
296
|
+
const index = [];
|
|
297
|
+
for (const sample of samples) {
|
|
298
|
+
for (const vertex of profile) {
|
|
299
|
+
const u = vertex.u * sample.halfWidth;
|
|
300
|
+
const v = vertex.v * sample.halfThickness;
|
|
301
|
+
position.push(sample.position.x + sample.normal.x * u + sample.binormal.x * v, sample.position.y + sample.normal.y * u + sample.binormal.y * v, sample.position.z + sample.normal.z * u + sample.binormal.z * v);
|
|
302
|
+
// A normal does not survive a non-uniform scale the way a position does:
|
|
303
|
+
// squashing a circle to a 5:1 ellipse tips every one of its normals
|
|
304
|
+
// toward the wide axis. Dividing by the half-extents is the inverse
|
|
305
|
+
// transpose of that scale, written out for two dimensions.
|
|
306
|
+
const n = normalize2(vertex.nu / sample.halfWidth, vertex.nv / sample.halfThickness);
|
|
307
|
+
normal.push(sample.normal.x * n[0] + sample.binormal.x * n[1], sample.normal.y * n[0] + sample.binormal.y * n[1], sample.normal.z * n[0] + sample.binormal.z * n[1]);
|
|
308
|
+
color.push(sample.color[0], sample.color[1], sample.color[2]);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
for (let i = 0; i < samples.length - 1; i++) {
|
|
312
|
+
for (let j = 0; j < ring; j++) {
|
|
313
|
+
const next = (j + 1) % ring;
|
|
314
|
+
const a = profile[j];
|
|
315
|
+
const b = profile[next];
|
|
316
|
+
// The two halves of a hard corner sit in the same place, so the quad
|
|
317
|
+
// between them has no area. Skipping it keeps the index buffer honest
|
|
318
|
+
// rather than filling it with degenerate triangles.
|
|
319
|
+
if (a.u === b.u && a.v === b.v)
|
|
320
|
+
continue;
|
|
321
|
+
const i0 = i * ring + j;
|
|
322
|
+
const i1 = i * ring + next;
|
|
323
|
+
const i2 = (i + 1) * ring + next;
|
|
324
|
+
const i3 = (i + 1) * ring + j;
|
|
325
|
+
index.push(i0, i1, i2, i0, i2, i3);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
capEnd(samples[0], profile, position, normal, color, index, false);
|
|
329
|
+
capEnd(samples[samples.length - 1], profile, position, normal, color, index, true);
|
|
330
|
+
return {
|
|
331
|
+
type: "buffer",
|
|
332
|
+
position: new Float32Array(position),
|
|
333
|
+
normal: new Float32Array(normal),
|
|
334
|
+
color: new Float32Array(color),
|
|
335
|
+
index: new Uint32Array(index),
|
|
336
|
+
// Built once per structure and cached by the node; nothing mutates these
|
|
337
|
+
// arrays afterwards, so the renderer may compare them by identity alone.
|
|
338
|
+
staticData: true,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
/** A triangle fan closing one end of the sweep, facing along the tangent. */
|
|
342
|
+
function capEnd(sample, profile, position, normal, color, index, forward) {
|
|
343
|
+
const sign = forward ? 1 : -1;
|
|
344
|
+
const nx = sample.tangent.x * sign;
|
|
345
|
+
const ny = sample.tangent.y * sign;
|
|
346
|
+
const nz = sample.tangent.z * sign;
|
|
347
|
+
const center = position.length / 3;
|
|
348
|
+
position.push(sample.position.x, sample.position.y, sample.position.z);
|
|
349
|
+
normal.push(nx, ny, nz);
|
|
350
|
+
color.push(sample.color[0], sample.color[1], sample.color[2]);
|
|
351
|
+
const first = position.length / 3;
|
|
352
|
+
for (const vertex of profile) {
|
|
353
|
+
const u = vertex.u * sample.halfWidth;
|
|
354
|
+
const v = vertex.v * sample.halfThickness;
|
|
355
|
+
position.push(sample.position.x + sample.normal.x * u + sample.binormal.x * v, sample.position.y + sample.normal.y * u + sample.binormal.y * v, sample.position.z + sample.normal.z * u + sample.binormal.z * v);
|
|
356
|
+
normal.push(nx, ny, nz);
|
|
357
|
+
color.push(sample.color[0], sample.color[1], sample.color[2]);
|
|
358
|
+
}
|
|
359
|
+
for (let j = 0; j < profile.length; j++) {
|
|
360
|
+
const next = (j + 1) % profile.length;
|
|
361
|
+
// Wound the opposite way at the two ends, so both caps face outward.
|
|
362
|
+
if (forward)
|
|
363
|
+
index.push(center, first + j, first + next);
|
|
364
|
+
else
|
|
365
|
+
index.push(center, first + next, first + j);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
// --- Small vector maths ----------------------------------------------------
|
|
369
|
+
//
|
|
370
|
+
// Written out rather than reached for, on the same terms as `chemistry.ts`:
|
|
371
|
+
// this package has to stay loadable in a bare Node process, and none of it is
|
|
372
|
+
// more than a few lines.
|
|
373
|
+
function catmullRom(p0, p1, p2, p3, t) {
|
|
374
|
+
const t2 = t * t;
|
|
375
|
+
const t3 = t2 * t;
|
|
376
|
+
const a = -0.5 * t3 + t2 - 0.5 * t;
|
|
377
|
+
const b = 1.5 * t3 - 2.5 * t2 + 1;
|
|
378
|
+
const c = -1.5 * t3 + 2 * t2 + 0.5 * t;
|
|
379
|
+
const d = 0.5 * t3 - 0.5 * t2;
|
|
380
|
+
return {
|
|
381
|
+
x: p0.x * a + p1.x * b + p2.x * c + p3.x * d,
|
|
382
|
+
y: p0.y * a + p1.y * b + p2.y * c + p3.y * d,
|
|
383
|
+
z: p0.z * a + p1.z * b + p2.z * c + p3.z * d,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
function subtract(a, b) {
|
|
387
|
+
return { x: a.x - b.x, y: a.y - b.y, z: a.z - b.z };
|
|
388
|
+
}
|
|
389
|
+
function cross(a, b) {
|
|
390
|
+
return {
|
|
391
|
+
x: a.y * b.z - a.z * b.y,
|
|
392
|
+
y: a.z * b.x - a.x * b.z,
|
|
393
|
+
z: a.x * b.y - a.y * b.x,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
function dot(a, b) {
|
|
397
|
+
return a.x * b.x + a.y * b.y + a.z * b.z;
|
|
398
|
+
}
|
|
399
|
+
function negate(v) {
|
|
400
|
+
return { x: -v.x, y: -v.y, z: -v.z };
|
|
401
|
+
}
|
|
402
|
+
function lengthOf(v) {
|
|
403
|
+
return Math.hypot(v.x, v.y, v.z);
|
|
404
|
+
}
|
|
405
|
+
function normalize(v) {
|
|
406
|
+
const length = lengthOf(v);
|
|
407
|
+
if (length < 1e-9)
|
|
408
|
+
return { x: 0, y: 0, z: 1 };
|
|
409
|
+
return { x: v.x / length, y: v.y / length, z: v.z / length };
|
|
410
|
+
}
|
|
411
|
+
function normalize2(u, v) {
|
|
412
|
+
const length = Math.hypot(u, v);
|
|
413
|
+
if (length < 1e-9)
|
|
414
|
+
return [1, 0];
|
|
415
|
+
return [u / length, v / length];
|
|
416
|
+
}
|
|
417
|
+
/** `v` with everything along `axis` taken out of it, normalized. */
|
|
418
|
+
function perpendicular(v, axis) {
|
|
419
|
+
const along = dot(v, axis);
|
|
420
|
+
const out = {
|
|
421
|
+
x: v.x - axis.x * along,
|
|
422
|
+
y: v.y - axis.y * along,
|
|
423
|
+
z: v.z - axis.z * along,
|
|
424
|
+
};
|
|
425
|
+
// The carbonyl parallel to the chain — vanishingly rare, but it would leave
|
|
426
|
+
// no frame at all. Any perpendicular is correct; take one from the axis.
|
|
427
|
+
if (lengthOf(out) < 1e-6) {
|
|
428
|
+
const fallback = Math.abs(axis.x) < 0.9 ? { x: 1, y: 0, z: 0 } : { x: 0, y: 1, z: 0 };
|
|
429
|
+
return normalize(cross(axis, fallback));
|
|
430
|
+
}
|
|
431
|
+
return normalize(out);
|
|
432
|
+
}
|
|
433
|
+
function lerpVector(a, b, t) {
|
|
434
|
+
return {
|
|
435
|
+
x: a.x + (b.x - a.x) * t,
|
|
436
|
+
y: a.y + (b.y - a.y) * t,
|
|
437
|
+
z: a.z + (b.z - a.z) * t,
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
function lerpRgb(a, b, t) {
|
|
441
|
+
return [
|
|
442
|
+
a[0] + (b[0] - a[0]) * t,
|
|
443
|
+
a[1] + (b[1] - a[1]) * t,
|
|
444
|
+
a[2] + (b[2] - a[2]) * t,
|
|
445
|
+
];
|
|
446
|
+
}
|
|
447
|
+
function clamp01(value) {
|
|
448
|
+
return Math.min(1, Math.max(0, value));
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* A colour as the **linear** RGB triple a vertex attribute holds.
|
|
452
|
+
*
|
|
453
|
+
* The same conversion `grid3d.ts` documents and for the same reason: three
|
|
454
|
+
* reads vertex colours as linear, so an sRGB channel uploaded raw paints far
|
|
455
|
+
* too bright. Skipping it here would wash every ribbon out against the atoms
|
|
456
|
+
* beside it, which take their colour through the material instead.
|
|
457
|
+
*/
|
|
458
|
+
function linearRgb(value) {
|
|
459
|
+
const [r, g, b] = resolveColor3D(value);
|
|
460
|
+
return [srgbToLinear(r), srgbToLinear(g), srgbToLinear(b)];
|
|
461
|
+
}
|
|
462
|
+
/** The sRGB transfer function, inverted. three's own `SRGBToLinear`. */
|
|
463
|
+
function srgbToLinear(channel) {
|
|
464
|
+
return channel < 0.04045
|
|
465
|
+
? channel * 0.0773993808
|
|
466
|
+
: Math.pow(channel * 0.9478672986 + 0.0521327014, 2.4);
|
|
467
|
+
}
|
|
468
|
+
//# sourceMappingURL=ribbon.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ribbon.js","sourceRoot":"","sources":["../../src/protein/ribbon.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The cartoon: a chain's backbone swept as a **ribbon** rather than as a tube.\n *\n * This is the one representation whose whole job is to be recognised. A figure\n * in a paper draws a helix as a twisted band and a strand as a flat arrow, and\n * has since Jane Richardson drew the first one by hand in 1980 — the convention\n * is old enough that a reader decodes it without a legend, which is exactly why\n * a picture that departs from it reads as *wrong* rather than as different.\n *\n * The node drew that with `Geo.tube` before this module existed, and the two\n * things it could not express are the two the convention is made of:\n *\n * **A cross-section has a width and a thickness, not a radius.** A tube's is a\n * circle, so a helix could only be told from a coil by being *fatter* — and a\n * fat circle swept along the α-carbons of a helix is close to self-intersecting,\n * because those carbons corkscrew about the helix axis at ~2.3 Å with a 5.4 Å\n * pitch. What comes out is a lumpy sausage: the tube's own diameter is most of\n * the coil's, so the spiral fills in and the shape stops being legible. Swept\n * flat instead — ~2.2 Å across and ~0.5 Å thick — the same spiral reads as the\n * band every textbook draws, because the eye is now reading the *face* turning\n * rather than a diameter changing.\n *\n * **A band has a side, and a curve through α-carbons does not say which.** So\n * the frame is built from the residue's own carbonyl (see\n * {@link TracePoint.normal}), which is what turns with the fold. A ribbon swept\n * on an arbitrary frame twists at random and looks like a misprint.\n *\n * Everything here is derived **per structure**, cached by the node against the\n * inputs that produced it, and never rebuilt for a camera move — the same\n * bargain the instance lists make in `shared.ts`.\n */\n\nimport type { BufferGeometry3D, Color, Vector3 } from \"@motionscript/core\"\n\nimport { resolveColor3D } from \"@motionscript/core/component\"\nimport type { ProteinChain, SecondaryStructure, TracePoint } from \"./structure\"\n\n/**\n * A run of backbone drawn as one mesh: a whole stretch of one secondary\n * structure, with its per-residue colours carried as a vertex attribute.\n *\n * The unit is the **span**, not the run of constant colour that a tube is swept\n * as (see `backboneRuns`), and both halves of that matter. An arrowhead is a\n * statement about where a strand *ends*, so it can only be placed by something\n * that can see the whole strand — split the strand into four differently\n * coloured pieces first and each piece grows an arrow of its own. And a colour\n * that varies along the span is what a vertex attribute is for, which is also\n * what retires the faceted-polyline compromise the tube path has to make under\n * the residue spectrum.\n */\nexport interface CartoonSpan {\n /** What this stretch is doing, which decides its cross-section. */\n secondary: SecondaryStructure\n /**\n * The residues of the span, N→C, **overlapping its neighbours by one point**\n * at each end so consecutive spans meet rather than leaving a gap.\n */\n points: TracePoint[]\n /** One sRGB colour per entry of {@link points}. */\n colors: string[]\n}\n\n/**\n * How wide and how thick each secondary structure's ribbon is, as multiples of\n * the node's `ribbonRadius`.\n *\n * Stated as a ratio to one control rather than as six numbers in the inspector:\n * \"how heavy is the cartoon\" is one question an author actually asks, and the\n * *proportions* between a helix, a strand and a loop are the convention rather\n * than a preference. The absolute values are the ones the established viewers\n * settled on — a helix band about 2.2 Å across and half an Ångström thick, a\n * strand slightly wider still, and a loop a thin round cord.\n *\n * A coil stays round (width equals thickness), which is the other half of the\n * statement: a loop has no face to turn, so giving it one would claim a\n * structure the file did not annotate.\n */\nconst SECTION: Readonly<\n Record<SecondaryStructure, { width: number; thickness: number }>\n> = {\n helix: { width: 2.4, thickness: 0.5 },\n sheet: { width: 2.6, thickness: 0.45 },\n coil: { width: 1, thickness: 1 },\n}\n\n/** How wide a strand's arrowhead is at its base, in the same multiples. */\nconst ARROW_WIDTH = 4.4\n\n/**\n * How many residues at the C-terminal end of a strand the arrowhead occupies.\n *\n * Residues rather than a fraction of the span, because an arrowhead is a fixed\n * *shape* — the same barb whether the strand is five residues or fifteen. A\n * fraction would draw a stubby wedge on a short strand and a long spike on a\n * β-hairpin's arm. Capped below at a third of the span so a three-residue\n * strand is still a strand with a point on it rather than one long taper.\n */\nconst ARROW_RESIDUES = 2.4\n\n/** What a strand's arrow narrows to at its tip, so the point is a point. */\nconst ARROW_TIP = 0.08\n\n/**\n * How many samples each residue-to-residue step is drawn with.\n *\n * The curve is the picture here. Consecutive α-carbons are 3.8 Å apart and a\n * helix turns fully in 3.6 of them, so a ribbon drawn straight between residues\n * is a polygon with about five sides per turn — which is exactly the faceted,\n * angular look that separates a rendering nobody trusts from one that reads as\n * a molecule. Tied to the node's mesh detail so the same control that coarsens\n * the spheres coarsens this.\n */\nfunction subdivisionsFor(quality: number): number {\n return Math.min(10, Math.max(3, Math.round(quality / 2)))\n}\n\n/**\n * Splits a chain's trace into the spans a cartoon is built from — one per\n * unbroken stretch of the same secondary structure.\n *\n * Consecutive spans overlap by one point, for the reason `backboneRuns`'\n * consecutive runs do: the shared residue is the last sample of one sweep and\n * the first of the next, so the two meshes butt up rather than leaving a hole\n * at every helix-to-loop transition.\n */\nexport function cartoonSpans(\n chain: ProteinChain,\n colors: string[]\n): CartoonSpan[] {\n const spans: CartoonSpan[] = []\n\n chain.trace.forEach((point, index) => {\n const color = colors[index] ?? colors[0] ?? \"#ffffff\"\n let current = spans[spans.length - 1]\n\n if (!current || current.secondary !== point.secondary) {\n const previous = current\n current = { secondary: point.secondary, points: [], colors: [] }\n // Carry the previous span's last residue in as this one's first.\n if (previous) {\n current.points.push(previous.points[previous.points.length - 1]!)\n current.colors.push(previous.colors[previous.colors.length - 1]!)\n }\n spans.push(current)\n }\n\n current.points.push(point)\n current.colors.push(color)\n })\n\n // A lone point is not a curve: there is no direction to sweep along and no\n // tangent to build a frame from. Only ever the first span, since every later\n // one is seeded with its predecessor's last point.\n return spans.filter((span) => span.points.length > 1)\n}\n\n/**\n * One span, as a mesh.\n *\n * `null` when there is nothing to sweep, so the caller can skip the op rather\n * than hand the renderer an empty buffer.\n *\n * The arrays are freshly allocated and never mutated afterwards, which is what\n * makes `staticData` honest: the renderer then compares them by identity and\n * uploads each exactly once, where re-deriving them per frame would re-upload\n * the whole fold sixty times a second to spin the camera.\n */\nexport function cartoonGeometry(\n span: CartoonSpan,\n options: { radius: number; quality: number }\n): BufferGeometry3D | null {\n const samples = sampleSpan(span, options)\n if (samples.length < 2) return null\n return sweep(samples, profileFor(span.secondary, options.quality))\n}\n\n// --- Sampling --------------------------------------------------------------\n\n/** One cross-section of the sweep: where it is, how it is turned, how big. */\ninterface Sample {\n position: Vector3\n /** Unit tangent — the direction the sweep is travelling. */\n tangent: Vector3\n /** Unit **wide** direction: the ribbon's face turns with this. */\n normal: Vector3\n /** Unit binormal, completing the frame. */\n binormal: Vector3\n halfWidth: number\n halfThickness: number\n /** Linear RGB, as a vertex colour attribute holds it. */\n color: [number, number, number]\n}\n\n/**\n * The span, resampled onto a smooth curve with a frame and a size at every\n * sample.\n *\n * Three things happen here that each fix something visible:\n *\n * **Catmull-Rom through the residues**, so the ribbon is a curve rather than a\n * five-sided polygon per helical turn — see {@link subdivisionsFor}.\n *\n * **The frames are made continuous before anything is interpolated.** A\n * β-strand's carbonyls point alternately to one side and the other, which is\n * chemistry rather than noise: hydrogen bonds go to the neighbouring strand on\n * both sides. Swept as given, the ribbon would flip 180° at every residue and\n * come out as a chain of triangles. Flipping each frame to agree with its\n * predecessor is what every viewer does and what makes a strand flat.\n *\n * **The arrow is measured in residues from the C-terminal end**, before\n * subdivision, so it is the same barb on a long strand and a short one.\n */\nfunction sampleSpan(\n span: CartoonSpan,\n options: { radius: number; quality: number }\n): Sample[] {\n const points = span.points\n const count = points.length\n const section = SECTION[span.secondary]\n const radius = Math.max(0.01, options.radius)\n\n const frames = continuousFrames(points)\n const tints = span.colors.map((color) => linearRgb(color))\n\n // Where the arrowhead starts, as a position along the span in *residues*.\n // A strand shorter than the barb gives up a third of itself to it rather than\n // becoming one long taper.\n const arrow =\n span.secondary === \"sheet\"\n ? Math.max(count - 1 - ARROW_RESIDUES, (count - 1) * (2 / 3))\n : Infinity\n\n const steps = subdivisionsFor(options.quality)\n const samples: Sample[] = []\n\n // Positions first, frames second: a tangent is a finite difference over the\n // *sampled* curve, so every position has to exist before any of them can be\n // taken. Doing it in one pass would take each tangent from the residues\n // instead, which is the polyline this subdivision exists to get away from.\n const curve: Vector3[] = []\n const carried: Vector3[] = []\n const colors: [number, number, number][] = []\n const positions: number[] = []\n\n for (let i = 0; i < count - 1; i++) {\n // The last segment emits its endpoint too, so the sweep reaches the final\n // residue rather than stopping one sample short of it.\n const last = i === count - 2\n for (let s = 0; s < steps + (last ? 1 : 0); s++) {\n const t = s / steps\n curve.push(\n catmullRom(\n points[i - 1] ?? points[i]!,\n points[i]!,\n points[i + 1]!,\n points[i + 2] ?? points[i + 1]!,\n t\n )\n )\n carried.push(lerpVector(frames[i]!, frames[i + 1]!, t))\n colors.push(lerpRgb(tints[i]!, tints[i + 1]!, t))\n positions.push(i + t)\n }\n }\n\n for (let i = 0; i < curve.length; i++) {\n const position = curve[i]!\n const tangent = normalize(\n subtract(\n curve[Math.min(curve.length - 1, i + 1)]!,\n curve[Math.max(0, i - 1)]!\n )\n )\n // Squared against the tangent, so the frame is orthonormal even though the\n // carbonyl it came from is not perpendicular to the chain.\n const normal = perpendicular(carried[i]!, tangent)\n const binormal = normalize(cross(tangent, normal))\n\n const along = positions[i]!\n const width =\n along <= arrow\n ? section.width\n : // Linear from the barb's base to its point. The base is *wider* than\n // the strand, which is what makes an arrowhead read as one rather\n // than as the strand simply stopping.\n ARROW_WIDTH +\n (ARROW_TIP - ARROW_WIDTH) *\n clamp01((along - arrow) / Math.max(1e-6, count - 1 - arrow))\n\n samples.push({\n position,\n tangent,\n normal,\n binormal,\n halfWidth: radius * width,\n halfThickness: radius * section.thickness,\n color: colors[i]!,\n })\n }\n\n return samples\n}\n\n/**\n * Each residue's ribbon direction, made to agree with its predecessor.\n *\n * Falls back to the curve's own binormal — the axis its turn is about — where\n * the file gives no carbonyl. That is the right stand-in rather than an\n * arbitrary perpendicular: in a helix the carbonyl points very nearly along the\n * helix axis, and so does the binormal of the curve the α-carbons trace, so a\n * Cα-only model comes out with its bands lying the same way a complete file's\n * do.\n */\nfunction continuousFrames(points: TracePoint[]): Vector3[] {\n const frames: Vector3[] = []\n\n for (let i = 0; i < points.length; i++) {\n const point = points[i]!\n let direction = point.normal ?? binormalAt(points, i)\n // Degenerate — three collinear residues with no carbonyl. Any perpendicular\n // will do, and the frame is squared against the tangent anyway.\n if (lengthOf(direction) < 1e-6) direction = { x: 0, y: 0, z: 1 }\n\n const previous = frames[i - 1]\n // The 180° flip a β-strand's alternating carbonyls would otherwise put into\n // the sweep. See {@link sampleSpan}.\n if (previous && dot(previous, direction) < 0) direction = negate(direction)\n frames.push(normalize(direction))\n }\n\n return frames\n}\n\n/** The curve's binormal at a residue — the axis its local turn is about. */\nfunction binormalAt(points: TracePoint[], index: number): Vector3 {\n const before = points[Math.max(0, index - 1)]!\n const here = points[index]!\n const after = points[Math.min(points.length - 1, index + 1)]!\n return cross(subtract(here, before), subtract(after, here))\n}\n\n// --- Sweeping --------------------------------------------------------------\n\n/**\n * One vertex of a cross-section: where it sits in the frame's `(wide, thick)`\n * plane, and which way the surface faces there.\n *\n * A hard edge is **two entries at the same place with different normals**,\n * which is what gives a ribbon the crisp corner a cartoon has rather than the\n * chamfered one a shared-vertex mesh with averaged normals would. It is also\n * why the normals are stated rather than derived: `computeNormals` averages\n * across whatever shares a vertex, and averaging is exactly what a corner must\n * not do.\n */\ninterface ProfileVertex {\n /** Position along the ribbon's width, in `[-1, 1]`. */\n u: number\n /** Position along its thickness, in `[-1, 1]`. */\n v: number\n /** Surface direction at that vertex, in the same plane. */\n nu: number\n nv: number\n}\n\n/**\n * The cross-section a secondary structure is swept with.\n *\n * A loop is a round cord and a helix or a strand is a flat band — the whole\n * distinction the convention rests on, and the one a radius could not express.\n * The band is a rectangle rather than an ellipse because its *edge* is what the\n * eye reads a twist by: an ellipse turning through 90° goes smoothly from wide\n * to narrow and reads as a tube pinching, where a rectangle's corner catches the\n * light and reads as a face turning over.\n */\nfunction profileFor(\n secondary: SecondaryStructure,\n quality: number\n): ProfileVertex[] {\n if (secondary === \"coil\") {\n const sides = Math.min(16, Math.max(5, Math.round(quality / 2)))\n const profile: ProfileVertex[] = []\n for (let i = 0; i < sides; i++) {\n const angle = (i / sides) * Math.PI * 2\n const u = Math.cos(angle)\n const v = Math.sin(angle)\n // Smooth: one vertex per side, its normal radial, so a cord reads as\n // round rather than as a faceted prism.\n profile.push({ u, v, nu: u, nv: v })\n }\n return profile\n }\n\n // Four faces, each with its own pair of corners, walked so consecutive\n // entries are consecutive around the section.\n return [\n { u: 1, v: 1, nu: 0, nv: 1 },\n { u: -1, v: 1, nu: 0, nv: 1 },\n { u: -1, v: 1, nu: -1, nv: 0 },\n { u: -1, v: -1, nu: -1, nv: 0 },\n { u: -1, v: -1, nu: 0, nv: -1 },\n { u: 1, v: -1, nu: 0, nv: -1 },\n { u: 1, v: -1, nu: 1, nv: 0 },\n { u: 1, v: 1, nu: 1, nv: 0 },\n ]\n}\n\n/**\n * The samples and the profile, as one indexed mesh.\n *\n * Ends are **capped**: a ribbon is a solid band and an open end shows the\n * inside of the far face, which at a helix-to-loop transition would be a\n * visible hole rather than a join. Cheap — one fan per end — and it is what\n * lets a span be drawn as its own mesh at all.\n */\nfunction sweep(samples: Sample[], profile: ProfileVertex[]): BufferGeometry3D {\n const ring = profile.length\n const position: number[] = []\n const normal: number[] = []\n const color: number[] = []\n const index: number[] = []\n\n for (const sample of samples) {\n for (const vertex of profile) {\n const u = vertex.u * sample.halfWidth\n const v = vertex.v * sample.halfThickness\n position.push(\n sample.position.x + sample.normal.x * u + sample.binormal.x * v,\n sample.position.y + sample.normal.y * u + sample.binormal.y * v,\n sample.position.z + sample.normal.z * u + sample.binormal.z * v\n )\n // A normal does not survive a non-uniform scale the way a position does:\n // squashing a circle to a 5:1 ellipse tips every one of its normals\n // toward the wide axis. Dividing by the half-extents is the inverse\n // transpose of that scale, written out for two dimensions.\n const n = normalize2(\n vertex.nu / sample.halfWidth,\n vertex.nv / sample.halfThickness\n )\n normal.push(\n sample.normal.x * n[0] + sample.binormal.x * n[1],\n sample.normal.y * n[0] + sample.binormal.y * n[1],\n sample.normal.z * n[0] + sample.binormal.z * n[1]\n )\n color.push(sample.color[0], sample.color[1], sample.color[2])\n }\n }\n\n for (let i = 0; i < samples.length - 1; i++) {\n for (let j = 0; j < ring; j++) {\n const next = (j + 1) % ring\n const a = profile[j]!\n const b = profile[next]!\n // The two halves of a hard corner sit in the same place, so the quad\n // between them has no area. Skipping it keeps the index buffer honest\n // rather than filling it with degenerate triangles.\n if (a.u === b.u && a.v === b.v) continue\n const i0 = i * ring + j\n const i1 = i * ring + next\n const i2 = (i + 1) * ring + next\n const i3 = (i + 1) * ring + j\n index.push(i0, i1, i2, i0, i2, i3)\n }\n }\n\n capEnd(samples[0]!, profile, position, normal, color, index, false)\n capEnd(\n samples[samples.length - 1]!,\n profile,\n position,\n normal,\n color,\n index,\n true\n )\n\n return {\n type: \"buffer\",\n position: new Float32Array(position),\n normal: new Float32Array(normal),\n color: new Float32Array(color),\n index: new Uint32Array(index),\n // Built once per structure and cached by the node; nothing mutates these\n // arrays afterwards, so the renderer may compare them by identity alone.\n staticData: true,\n }\n}\n\n/** A triangle fan closing one end of the sweep, facing along the tangent. */\nfunction capEnd(\n sample: Sample,\n profile: ProfileVertex[],\n position: number[],\n normal: number[],\n color: number[],\n index: number[],\n forward: boolean\n): void {\n const sign = forward ? 1 : -1\n const nx = sample.tangent.x * sign\n const ny = sample.tangent.y * sign\n const nz = sample.tangent.z * sign\n\n const center = position.length / 3\n position.push(sample.position.x, sample.position.y, sample.position.z)\n normal.push(nx, ny, nz)\n color.push(sample.color[0], sample.color[1], sample.color[2])\n\n const first = position.length / 3\n for (const vertex of profile) {\n const u = vertex.u * sample.halfWidth\n const v = vertex.v * sample.halfThickness\n position.push(\n sample.position.x + sample.normal.x * u + sample.binormal.x * v,\n sample.position.y + sample.normal.y * u + sample.binormal.y * v,\n sample.position.z + sample.normal.z * u + sample.binormal.z * v\n )\n normal.push(nx, ny, nz)\n color.push(sample.color[0], sample.color[1], sample.color[2])\n }\n\n for (let j = 0; j < profile.length; j++) {\n const next = (j + 1) % profile.length\n // Wound the opposite way at the two ends, so both caps face outward.\n if (forward) index.push(center, first + j, first + next)\n else index.push(center, first + next, first + j)\n }\n}\n\n// --- Small vector maths ----------------------------------------------------\n//\n// Written out rather than reached for, on the same terms as `chemistry.ts`:\n// this package has to stay loadable in a bare Node process, and none of it is\n// more than a few lines.\n\nfunction catmullRom(\n p0: { x: number; y: number; z: number },\n p1: { x: number; y: number; z: number },\n p2: { x: number; y: number; z: number },\n p3: { x: number; y: number; z: number },\n t: number\n): Vector3 {\n const t2 = t * t\n const t3 = t2 * t\n const a = -0.5 * t3 + t2 - 0.5 * t\n const b = 1.5 * t3 - 2.5 * t2 + 1\n const c = -1.5 * t3 + 2 * t2 + 0.5 * t\n const d = 0.5 * t3 - 0.5 * t2\n return {\n x: p0.x * a + p1.x * b + p2.x * c + p3.x * d,\n y: p0.y * a + p1.y * b + p2.y * c + p3.y * d,\n z: p0.z * a + p1.z * b + p2.z * c + p3.z * d,\n }\n}\n\nfunction subtract(\n a: { x: number; y: number; z: number },\n b: { x: number; y: number; z: number }\n): Vector3 {\n return { x: a.x - b.x, y: a.y - b.y, z: a.z - b.z }\n}\n\nfunction cross(a: Vector3, b: Vector3): Vector3 {\n return {\n x: a.y * b.z - a.z * b.y,\n y: a.z * b.x - a.x * b.z,\n z: a.x * b.y - a.y * b.x,\n }\n}\n\nfunction dot(a: Vector3, b: Vector3): number {\n return a.x * b.x + a.y * b.y + a.z * b.z\n}\n\nfunction negate(v: Vector3): Vector3 {\n return { x: -v.x, y: -v.y, z: -v.z }\n}\n\nfunction lengthOf(v: Vector3): number {\n return Math.hypot(v.x, v.y, v.z)\n}\n\nfunction normalize(v: Vector3): Vector3 {\n const length = lengthOf(v)\n if (length < 1e-9) return { x: 0, y: 0, z: 1 }\n return { x: v.x / length, y: v.y / length, z: v.z / length }\n}\n\nfunction normalize2(u: number, v: number): [number, number] {\n const length = Math.hypot(u, v)\n if (length < 1e-9) return [1, 0]\n return [u / length, v / length]\n}\n\n/** `v` with everything along `axis` taken out of it, normalized. */\nfunction perpendicular(v: Vector3, axis: Vector3): Vector3 {\n const along = dot(v, axis)\n const out = {\n x: v.x - axis.x * along,\n y: v.y - axis.y * along,\n z: v.z - axis.z * along,\n }\n // The carbonyl parallel to the chain — vanishingly rare, but it would leave\n // no frame at all. Any perpendicular is correct; take one from the axis.\n if (lengthOf(out) < 1e-6) {\n const fallback =\n Math.abs(axis.x) < 0.9 ? { x: 1, y: 0, z: 0 } : { x: 0, y: 1, z: 0 }\n return normalize(cross(axis, fallback))\n }\n return normalize(out)\n}\n\nfunction lerpVector(\n a: { x: number; y: number; z: number },\n b: { x: number; y: number; z: number },\n t: number\n): Vector3 {\n return {\n x: a.x + (b.x - a.x) * t,\n y: a.y + (b.y - a.y) * t,\n z: a.z + (b.z - a.z) * t,\n }\n}\n\nfunction lerpRgb(\n a: [number, number, number],\n b: [number, number, number],\n t: number\n): [number, number, number] {\n return [\n a[0] + (b[0] - a[0]) * t,\n a[1] + (b[1] - a[1]) * t,\n a[2] + (b[2] - a[2]) * t,\n ]\n}\n\nfunction clamp01(value: number): number {\n return Math.min(1, Math.max(0, value))\n}\n\n/**\n * A colour as the **linear** RGB triple a vertex attribute holds.\n *\n * The same conversion `grid3d.ts` documents and for the same reason: three\n * reads vertex colours as linear, so an sRGB channel uploaded raw paints far\n * too bright. Skipping it here would wash every ribbon out against the atoms\n * beside it, which take their colour through the material instead.\n */\nfunction linearRgb(value: Color): [number, number, number] {\n const [r, g, b] = resolveColor3D(value)\n return [srgbToLinear(r), srgbToLinear(g), srgbToLinear(b)]\n}\n\n/** The sRGB transfer function, inverted. three's own `SRGBToLinear`. */\nfunction srgbToLinear(channel: number): number {\n return channel < 0.04045\n ? channel * 0.0773993808\n : Math.pow(channel * 0.9478672986 + 0.0521327014, 2.4)\n}\n"]}
|