@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,478 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the {@link Protein} node turns a parsed molecule into: which atoms are
|
|
3
|
+
* showing, what colour each one takes, and how a bond becomes a cylinder.
|
|
4
|
+
*
|
|
5
|
+
* Everything here is **derived per structure**, not per frame. The node caches
|
|
6
|
+
* what these return against the inputs that produced them (see
|
|
7
|
+
* `Protein.instances`), because the camera moves every frame and none of this
|
|
8
|
+
* changes when it does — and a mid-sized entry is tens of thousands of atoms, so
|
|
9
|
+
* rebuilding the arrays sixty times a second to spin the view would be the
|
|
10
|
+
* difference between a smooth orbit and a slideshow.
|
|
11
|
+
*/
|
|
12
|
+
import { elementColor, vanDerWaalsRadius } from "./chemistry.js";
|
|
13
|
+
/**
|
|
14
|
+
* How the molecule is drawn.
|
|
15
|
+
*
|
|
16
|
+
* The four that answer genuinely different questions, which is why there are
|
|
17
|
+
* four rather than a dozen:
|
|
18
|
+
*
|
|
19
|
+
* - `spacefill` — every atom at its van der Waals radius. What the molecule's
|
|
20
|
+
* *surface* is: the shape it presents to everything around it.
|
|
21
|
+
* - `ballAndStick` — atoms shrunk, bonds drawn. What the molecule's *chemistry*
|
|
22
|
+
* is, and the only one where an individual atom can be picked out.
|
|
23
|
+
* - `backbone` — a thin even tube through the α-carbons. Where the chain *goes*,
|
|
24
|
+
* with nothing else in the way.
|
|
25
|
+
* - `ribbon` — the same curve, swelling along helices and strands and coloured
|
|
26
|
+
* by them. What the protein is *folded into*, which is what a figure in a
|
|
27
|
+
* paper is nearly always about.
|
|
28
|
+
*/
|
|
29
|
+
export const PROTEIN_REPRESENTATIONS = [
|
|
30
|
+
"ribbon",
|
|
31
|
+
"backbone",
|
|
32
|
+
"ballAndStick",
|
|
33
|
+
"spacefill",
|
|
34
|
+
];
|
|
35
|
+
/**
|
|
36
|
+
* What decides an atom's colour.
|
|
37
|
+
*
|
|
38
|
+
* Each one answers a question the picture might be asking, and the default
|
|
39
|
+
* differs by representation for exactly that reason: a space-filling model with
|
|
40
|
+
* no element colouring is a featureless blob, while a ribbon coloured by element
|
|
41
|
+
* is a single shade of grey.
|
|
42
|
+
*/
|
|
43
|
+
export const PROTEIN_COLOR_SCHEMES = [
|
|
44
|
+
/** Helix / sheet / coil. Fold. */
|
|
45
|
+
"secondary",
|
|
46
|
+
/** CPK — oxygen red, nitrogen blue, carbon grey. Chemistry. */
|
|
47
|
+
"element",
|
|
48
|
+
/** One hue per chain. Which subunit is which, in a complex. */
|
|
49
|
+
"chain",
|
|
50
|
+
/** A spectrum from each chain's N terminus to its C terminus. Direction. */
|
|
51
|
+
"residue",
|
|
52
|
+
/** One colour throughout, whatever the node's own `color` is. */
|
|
53
|
+
"uniform",
|
|
54
|
+
];
|
|
55
|
+
/**
|
|
56
|
+
* The per-chain palette.
|
|
57
|
+
*
|
|
58
|
+
* Eight, spread around the wheel rather than sampled from a gradient: chains are
|
|
59
|
+
* a *nominal* scale — chain B is not between A and C in any sense — so the
|
|
60
|
+
* colours have to be distinguishable rather than ordered. A ninth chain wraps,
|
|
61
|
+
* which is the honest failure for a complex that large; the alternative is
|
|
62
|
+
* colours nobody can tell apart.
|
|
63
|
+
*
|
|
64
|
+
* Eight is therefore the *palette's* length rather than a constant anything
|
|
65
|
+
* downstream may assume: an author who edits these edits eight entries, and the
|
|
66
|
+
* wrap is modulo whatever the list holds.
|
|
67
|
+
*/
|
|
68
|
+
export const CHAIN_COLORS = [
|
|
69
|
+
"#4f9dff",
|
|
70
|
+
"#ff8a3d",
|
|
71
|
+
"#4ddb9a",
|
|
72
|
+
"#e05fd0",
|
|
73
|
+
"#ffd166",
|
|
74
|
+
"#8b7bff",
|
|
75
|
+
"#ff6b6b",
|
|
76
|
+
"#3fd0d6",
|
|
77
|
+
];
|
|
78
|
+
/**
|
|
79
|
+
* Helix, sheet and coil.
|
|
80
|
+
*
|
|
81
|
+
* The conventional assignment — warm for helices, cool for strands, pale for
|
|
82
|
+
* everything else — which is old enough to be read without a legend.
|
|
83
|
+
*/
|
|
84
|
+
export const SECONDARY_COLORS = {
|
|
85
|
+
helix: "#f0605d",
|
|
86
|
+
sheet: "#f5c451",
|
|
87
|
+
coil: "#c8ccd4",
|
|
88
|
+
};
|
|
89
|
+
/** Where the residue spectrum starts and ends, as hues in degrees. */
|
|
90
|
+
const SPECTRUM_START = 250;
|
|
91
|
+
const SPECTRUM_END = 0;
|
|
92
|
+
/** The saturation and lightness the residue spectrum is swept at. */
|
|
93
|
+
const SPECTRUM_SATURATION = 0.72;
|
|
94
|
+
const SPECTRUM_LIGHTNESS = 0.58;
|
|
95
|
+
/**
|
|
96
|
+
* The palette every scheme has always drawn — the conventional assignment, now
|
|
97
|
+
* stated as a value the author can take over rather than as a constant.
|
|
98
|
+
*
|
|
99
|
+
* The two spectrum ends are **computed** from the hues the ramp used to be
|
|
100
|
+
* written as, rather than typed out as hex, so the default sweep is the same
|
|
101
|
+
* blue-through-green-to-red it has always been by construction instead of by
|
|
102
|
+
* somebody having done the arithmetic correctly once.
|
|
103
|
+
*/
|
|
104
|
+
export const DEFAULT_PROTEIN_PALETTE = {
|
|
105
|
+
helix: SECONDARY_COLORS.helix,
|
|
106
|
+
sheet: SECONDARY_COLORS.sheet,
|
|
107
|
+
coil: SECONDARY_COLORS.coil,
|
|
108
|
+
chains: CHAIN_COLORS,
|
|
109
|
+
residueStart: hslHex(SPECTRUM_START, SPECTRUM_SATURATION, SPECTRUM_LIGHTNESS),
|
|
110
|
+
residueEnd: hslHex(SPECTRUM_END, SPECTRUM_SATURATION, SPECTRUM_LIGHTNESS),
|
|
111
|
+
};
|
|
112
|
+
/**
|
|
113
|
+
* How many chain colours the palette offers, and therefore what the chain cycle
|
|
114
|
+
* wraps at.
|
|
115
|
+
*
|
|
116
|
+
* Eight, because that is how many hues can be told apart at a glance — the
|
|
117
|
+
* reason {@link CHAIN_COLORS} has eight — and it is named here because three
|
|
118
|
+
* places have to agree on it: the schema declares this many rows, the props
|
|
119
|
+
* mapper reads this many, and the node holds this many.
|
|
120
|
+
*/
|
|
121
|
+
export const CHAIN_COLOR_COUNT = CHAIN_COLORS.length;
|
|
122
|
+
/**
|
|
123
|
+
* The appearance-bag field names the palette is edited as, in panel order.
|
|
124
|
+
*
|
|
125
|
+
* Three places have to agree on this list and none of them can derive it from
|
|
126
|
+
* the others: the schema declares a row per name, `proteinMotionProps` copies a
|
|
127
|
+
* row per name onto the props, and the node declares a `@property` per name.
|
|
128
|
+
* The third is the one that cannot be written as a loop — a decorator is a
|
|
129
|
+
* declaration — so what this buys is the first two staying in step with it, and
|
|
130
|
+
* a test pins the third against it.
|
|
131
|
+
*
|
|
132
|
+
* `color` is deliberately not here. It is the node's own colour rather than part
|
|
133
|
+
* of a scheme's palette: it is what `uniform` paints and what every scheme falls
|
|
134
|
+
* back to, so it lives beside `representation` as a field of the molecule.
|
|
135
|
+
*/
|
|
136
|
+
export const PROTEIN_PALETTE_FIELDS = [
|
|
137
|
+
"helixColor",
|
|
138
|
+
"sheetColor",
|
|
139
|
+
"coilColor",
|
|
140
|
+
"residueStartColor",
|
|
141
|
+
"residueEndColor",
|
|
142
|
+
...CHAIN_COLORS.map((_, index) => `chainColor${index + 1}`),
|
|
143
|
+
];
|
|
144
|
+
/** The default each {@link PROTEIN_PALETTE_FIELDS} entry carries, by name. */
|
|
145
|
+
export const PROTEIN_PALETTE_DEFAULTS = {
|
|
146
|
+
helixColor: DEFAULT_PROTEIN_PALETTE.helix,
|
|
147
|
+
sheetColor: DEFAULT_PROTEIN_PALETTE.sheet,
|
|
148
|
+
coilColor: DEFAULT_PROTEIN_PALETTE.coil,
|
|
149
|
+
residueStartColor: DEFAULT_PROTEIN_PALETTE.residueStart,
|
|
150
|
+
residueEndColor: DEFAULT_PROTEIN_PALETTE.residueEnd,
|
|
151
|
+
...Object.fromEntries(DEFAULT_PROTEIN_PALETTE.chains.map((colour, index) => [
|
|
152
|
+
`chainColor${index + 1}`,
|
|
153
|
+
colour,
|
|
154
|
+
])),
|
|
155
|
+
};
|
|
156
|
+
/**
|
|
157
|
+
* Tween: two `#rrggbb` strings, blended per channel.
|
|
158
|
+
*
|
|
159
|
+
* The palette entries are the one family of colours here that stay **strings**
|
|
160
|
+
* rather than going through `resolveColor3D` — an instance tint is handed a hex
|
|
161
|
+
* string — so they need a tween of their own rather than {@link lerpColor3D}.
|
|
162
|
+
*
|
|
163
|
+
* Blended in RGB, unlike the residue ramp {@link spectrum} sweeps: that one is a
|
|
164
|
+
* *spectrum*, where covering the wheel is the whole point, while this is one
|
|
165
|
+
* colour becoming another, where the short straight path is what "fading to red"
|
|
166
|
+
* means. Anything unparseable snaps, which is the only honest answer for a value
|
|
167
|
+
* with no channels to interpolate.
|
|
168
|
+
*/
|
|
169
|
+
export function lerpHexColor(from, to, t) {
|
|
170
|
+
const a = hexRgb(from);
|
|
171
|
+
const b = hexRgb(to);
|
|
172
|
+
if (!a || !b)
|
|
173
|
+
return t < 1 ? from : to;
|
|
174
|
+
const byte = (x, y) => Math.round(x + (y - x) * t)
|
|
175
|
+
.toString(16)
|
|
176
|
+
.padStart(2, "0");
|
|
177
|
+
return `#${byte(a[0], b[0])}${byte(a[1], b[1])}${byte(a[2], b[2])}`;
|
|
178
|
+
}
|
|
179
|
+
/** A `#rrggbb` string as three 0–255 channels, or `null` if it isn't one. */
|
|
180
|
+
function hexRgb(value) {
|
|
181
|
+
const hex = /^#?([0-9a-f]{6})$/i.exec(value.trim());
|
|
182
|
+
if (!hex)
|
|
183
|
+
return null;
|
|
184
|
+
const int = parseInt(hex[1], 16);
|
|
185
|
+
return [(int >> 16) & 255, (int >> 8) & 255, int & 255];
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* The colour of every atom, in the order the structure holds them.
|
|
189
|
+
*
|
|
190
|
+
* One flat array rather than a function called per atom, because the instance
|
|
191
|
+
* builder walks the whole list and an array is what `Graphics3D.instances` takes
|
|
192
|
+
* anyway.
|
|
193
|
+
*/
|
|
194
|
+
export function atomColors(structure, scheme, uniform, palette = DEFAULT_PROTEIN_PALETTE) {
|
|
195
|
+
if (scheme === "uniform")
|
|
196
|
+
return structure.atoms.map(() => uniform);
|
|
197
|
+
if (scheme === "element") {
|
|
198
|
+
return structure.atoms.map((atom) => elementColor(atom.element));
|
|
199
|
+
}
|
|
200
|
+
const chains = chainOrder(structure);
|
|
201
|
+
if (scheme === "chain") {
|
|
202
|
+
return structure.atoms.map((atom) => chainColor(chains, atom.chain, palette));
|
|
203
|
+
}
|
|
204
|
+
if (scheme === "secondary") {
|
|
205
|
+
const lookup = secondaryByResidue(structure);
|
|
206
|
+
return structure.atoms.map((atom) => palette[lookup.get(residueKey(atom)) ?? "coil"]);
|
|
207
|
+
}
|
|
208
|
+
// `residue` — a spectrum along each chain, so position is read against the
|
|
209
|
+
// chain the residue is in rather than against the whole file. Without that, a
|
|
210
|
+
// short chain beside a long one would be drawn in a single colour.
|
|
211
|
+
const positions = residuePositions(structure);
|
|
212
|
+
return structure.atoms.map((atom) => {
|
|
213
|
+
const position = positions.get(residueKey(atom));
|
|
214
|
+
return position === undefined ? uniform : spectrum(position, palette);
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* The colour of each point along one chain's backbone.
|
|
219
|
+
*
|
|
220
|
+
* Separate from {@link atomColors} because a trace point is a *residue* and an
|
|
221
|
+
* atom is not: the element scheme has nothing to say about a residue (every
|
|
222
|
+
* trace atom is a carbon, so the whole ribbon would be grey), so it falls back
|
|
223
|
+
* to the chain colouring — which is what somebody who picked "element" and then
|
|
224
|
+
* switched to a ribbon actually wants to see.
|
|
225
|
+
*/
|
|
226
|
+
export function traceColors(structure, chain, scheme, uniform, palette = DEFAULT_PROTEIN_PALETTE) {
|
|
227
|
+
if (scheme === "uniform")
|
|
228
|
+
return chain.trace.map(() => uniform);
|
|
229
|
+
if (scheme === "secondary") {
|
|
230
|
+
return chain.trace.map((point) => palette[point.secondary]);
|
|
231
|
+
}
|
|
232
|
+
if (scheme === "residue") {
|
|
233
|
+
const last = Math.max(1, chain.trace.length - 1);
|
|
234
|
+
return chain.trace.map((point) => spectrum(point.index / last, palette));
|
|
235
|
+
}
|
|
236
|
+
const chains = chainOrder(structure);
|
|
237
|
+
const colour = chainColor(chains, chain.id, palette);
|
|
238
|
+
return chain.trace.map(() => colour);
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* A colour on the N→C spectrum, `0` at the start of a chain and `1` at its end.
|
|
242
|
+
*
|
|
243
|
+
* Interpolated in **HSL**, which is what makes the default a spectrum at all: a
|
|
244
|
+
* straight RGB blend between the two ends passes through grey, where a hue sweep
|
|
245
|
+
* runs the ramp everybody recognises. And along the *direct numeric path*
|
|
246
|
+
* between the two hues rather than the shorter way round the wheel — 250° to 0°
|
|
247
|
+
* is the familiar blue-through-green-to-red run, and taking the short arc would
|
|
248
|
+
* hop through magenta instead and cover a quarter of the colours.
|
|
249
|
+
*/
|
|
250
|
+
function spectrum(position, palette) {
|
|
251
|
+
const from = hexHsl(palette.residueStart);
|
|
252
|
+
const to = hexHsl(palette.residueEnd);
|
|
253
|
+
const t = clamp01(position);
|
|
254
|
+
return hslHex(from.h + (to.h - from.h) * t, from.s + (to.s - from.s) * t, from.l + (to.l - from.l) * t);
|
|
255
|
+
}
|
|
256
|
+
function clamp01(value) {
|
|
257
|
+
return Math.min(1, Math.max(0, value));
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* HSL to a `#rrggbb` string.
|
|
261
|
+
*
|
|
262
|
+
* Written out rather than reached for, because the spectrum is the one place
|
|
263
|
+
* this package generates a colour instead of being handed one, and every other
|
|
264
|
+
* colour path here takes hex.
|
|
265
|
+
*/
|
|
266
|
+
function hslHex(hue, saturation, lightness) {
|
|
267
|
+
const h = ((hue % 360) + 360) % 360;
|
|
268
|
+
const c = (1 - Math.abs(2 * lightness - 1)) * saturation;
|
|
269
|
+
const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
|
|
270
|
+
const m = lightness - c / 2;
|
|
271
|
+
const [r, g, b] = h < 60
|
|
272
|
+
? [c, x, 0]
|
|
273
|
+
: h < 120
|
|
274
|
+
? [x, c, 0]
|
|
275
|
+
: h < 180
|
|
276
|
+
? [0, c, x]
|
|
277
|
+
: h < 240
|
|
278
|
+
? [0, x, c]
|
|
279
|
+
: h < 300
|
|
280
|
+
? [x, 0, c]
|
|
281
|
+
: [c, 0, x];
|
|
282
|
+
const byte = (value) => Math.round((value + m) * 255)
|
|
283
|
+
.toString(16)
|
|
284
|
+
.padStart(2, "0");
|
|
285
|
+
return `#${byte(r)}${byte(g)}${byte(b)}`;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* A `#rrggbb` string back to HSL — the inverse of {@link hslHex}, and here for
|
|
289
|
+
* the one caller that needs it: the residue ramp is authored as two colours and
|
|
290
|
+
* swept as a hue.
|
|
291
|
+
*
|
|
292
|
+
* Anything unparseable comes back as the default ramp's own start, so a half
|
|
293
|
+
* typed hex in the inspector fades toward a colour rather than painting `NaN`
|
|
294
|
+
* into every residue of the chain.
|
|
295
|
+
*/
|
|
296
|
+
function hexHsl(value) {
|
|
297
|
+
const hex = /^#?([0-9a-f]{6})$/i.exec(value.trim());
|
|
298
|
+
if (!hex)
|
|
299
|
+
return { h: SPECTRUM_START, s: SPECTRUM_SATURATION, l: SPECTRUM_LIGHTNESS };
|
|
300
|
+
const int = parseInt(hex[1], 16);
|
|
301
|
+
const r = ((int >> 16) & 255) / 255;
|
|
302
|
+
const g = ((int >> 8) & 255) / 255;
|
|
303
|
+
const b = (int & 255) / 255;
|
|
304
|
+
const max = Math.max(r, g, b);
|
|
305
|
+
const min = Math.min(r, g, b);
|
|
306
|
+
const l = (max + min) / 2;
|
|
307
|
+
const d = max - min;
|
|
308
|
+
if (d === 0)
|
|
309
|
+
return { h: 0, s: 0, l };
|
|
310
|
+
const s = d / (1 - Math.abs(2 * l - 1));
|
|
311
|
+
const h = max === r
|
|
312
|
+
? 60 * (((g - b) / d) % 6)
|
|
313
|
+
: max === g
|
|
314
|
+
? 60 * ((b - r) / d + 2)
|
|
315
|
+
: 60 * ((r - g) / d + 4);
|
|
316
|
+
return { h: (h + 360) % 360, s, l };
|
|
317
|
+
}
|
|
318
|
+
/** The chain identifiers in the order they first appear, for palette indexing. */
|
|
319
|
+
function chainOrder(structure) {
|
|
320
|
+
const order = new Map();
|
|
321
|
+
for (const atom of structure.atoms) {
|
|
322
|
+
if (!order.has(atom.chain))
|
|
323
|
+
order.set(atom.chain, order.size);
|
|
324
|
+
}
|
|
325
|
+
return order;
|
|
326
|
+
}
|
|
327
|
+
/** The palette entry for a chain, wrapping past the end. */
|
|
328
|
+
function chainColor(order, chain, palette) {
|
|
329
|
+
const colours = palette.chains.length > 0 ? palette.chains : DEFAULT_PROTEIN_PALETTE.chains;
|
|
330
|
+
const index = order.get(chain) ?? 0;
|
|
331
|
+
return colours[index % colours.length];
|
|
332
|
+
}
|
|
333
|
+
/** A residue's identity across the whole structure — its chain and its number. */
|
|
334
|
+
function residueKey(atom) {
|
|
335
|
+
return `${atom.chain}:${atom.residueSeq}`;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Each residue's position along its own chain, `0`–`1`.
|
|
339
|
+
*
|
|
340
|
+
* Read off the *traces* rather than counted over the atoms, so it means the same
|
|
341
|
+
* thing the ribbon means by it — and so a residue the file resolved no backbone
|
|
342
|
+
* for simply has no position, and takes the fallback colour rather than shifting
|
|
343
|
+
* every residue after it along the spectrum.
|
|
344
|
+
*/
|
|
345
|
+
function residuePositions(structure) {
|
|
346
|
+
const positions = new Map();
|
|
347
|
+
for (const chain of structure.chains) {
|
|
348
|
+
const last = Math.max(1, chain.trace.length - 1);
|
|
349
|
+
for (const point of chain.trace) {
|
|
350
|
+
positions.set(`${chain.id}:${point.residueSeq}`, point.index / last);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return positions;
|
|
354
|
+
}
|
|
355
|
+
/** Each residue's secondary structure, keyed as {@link residueKey} does. */
|
|
356
|
+
function secondaryByResidue(structure) {
|
|
357
|
+
const lookup = new Map();
|
|
358
|
+
for (const chain of structure.chains) {
|
|
359
|
+
for (const point of chain.trace) {
|
|
360
|
+
lookup.set(`${chain.id}:${point.residueSeq}`, point.secondary);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return lookup;
|
|
364
|
+
}
|
|
365
|
+
// --- Geometry --------------------------------------------------------------
|
|
366
|
+
/** An atom's drawn radius in Ångströms, at a given scale of its true size. */
|
|
367
|
+
export function atomRadius(atom, scale) {
|
|
368
|
+
return vanDerWaalsRadius(atom.element) * scale;
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* The placement of a stick running from `from` to `to`.
|
|
372
|
+
*
|
|
373
|
+
* A cylinder geometry is built along **+Y**, centred on the origin and one unit
|
|
374
|
+
* tall, so placing one takes all three parts of a transform: move it to the
|
|
375
|
+
* midpoint, scale it to the bond's length, and turn +Y onto the bond's
|
|
376
|
+
* direction.
|
|
377
|
+
*
|
|
378
|
+
* The turn is a quaternion rather than an Euler triple because there is no
|
|
379
|
+
* ordering of three axis rotations that doesn't gimbal somewhere, and a molecule
|
|
380
|
+
* has bonds pointing every way there is — one of them would land exactly on the
|
|
381
|
+
* degenerate axis and the stick would spin to a wrong orientation.
|
|
382
|
+
*/
|
|
383
|
+
export function stickPlacement(from, to, radius) {
|
|
384
|
+
const dx = to.x - from.x;
|
|
385
|
+
const dy = to.y - from.y;
|
|
386
|
+
const dz = to.z - from.z;
|
|
387
|
+
const length = Math.hypot(dx, dy, dz) || 1;
|
|
388
|
+
return {
|
|
389
|
+
position: [
|
|
390
|
+
(from.x + to.x) / 2,
|
|
391
|
+
(from.y + to.y) / 2,
|
|
392
|
+
(from.z + to.z) / 2,
|
|
393
|
+
],
|
|
394
|
+
scale: [radius, length, radius],
|
|
395
|
+
quaternion: upTo(dx / length, dy / length, dz / length),
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* The shortest rotation taking **+Y** onto a unit direction.
|
|
400
|
+
*
|
|
401
|
+
* The general form is "rotate about the axis perpendicular to both, by the angle
|
|
402
|
+
* between them", which for a fixed source axis collapses to the cross product
|
|
403
|
+
* with `(0, 1, 0)` — hence the two components rather than three.
|
|
404
|
+
*
|
|
405
|
+
* The antiparallel case has to be handled outright: a direction pointing
|
|
406
|
+
* straight down has a *zero* cross product with up, so the axis is undefined and
|
|
407
|
+
* the general form produces a quaternion of all zeros, which is not a rotation
|
|
408
|
+
* at all. Any half-turn about a perpendicular axis is correct there, and X is as
|
|
409
|
+
* good as any.
|
|
410
|
+
*/
|
|
411
|
+
function upTo(x, y, z) {
|
|
412
|
+
if (y > 0.999999)
|
|
413
|
+
return { x: 0, y: 0, z: 0, w: 1 };
|
|
414
|
+
if (y < -0.999999)
|
|
415
|
+
return { x: 1, y: 0, z: 0, w: 0 };
|
|
416
|
+
// axis = up × d, normalized; angle = acos(up · d) = acos(y).
|
|
417
|
+
const axisX = z;
|
|
418
|
+
const axisZ = -x;
|
|
419
|
+
const axisLength = Math.hypot(axisX, axisZ) || 1;
|
|
420
|
+
const angle = Math.acos(Math.min(1, Math.max(-1, y)));
|
|
421
|
+
const sin = Math.sin(angle / 2);
|
|
422
|
+
return {
|
|
423
|
+
x: (axisX / axisLength) * sin,
|
|
424
|
+
y: 0,
|
|
425
|
+
z: (axisZ / axisLength) * sin,
|
|
426
|
+
w: Math.cos(angle / 2),
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Splits a chain's trace into the runs a backbone is swept as.
|
|
431
|
+
*
|
|
432
|
+
* A run is a stretch that can be drawn as **one** `Geo.tube`, and what forces a
|
|
433
|
+
* break is anything the tube carries once for its whole length: its colour
|
|
434
|
+
* always, and — for a cartoon — its radius, since a helix that doesn't swell is
|
|
435
|
+
* not a helix anyone will recognise.
|
|
436
|
+
*
|
|
437
|
+
* Consecutive runs **overlap by one point**, which is what keeps the tubes
|
|
438
|
+
* meeting rather than leaving a gap at every transition: the shared point is the
|
|
439
|
+
* end of one sweep and the start of the next, so their ends sit in the same
|
|
440
|
+
* place.
|
|
441
|
+
*
|
|
442
|
+
* The trade this makes is worth stating. A tube is swept along a Catmull-Rom
|
|
443
|
+
* curve through its own points, so a long run comes out smooth and a two-point
|
|
444
|
+
* run comes out straight. Under the colourings a fold is usually drawn in —
|
|
445
|
+
* secondary structure, chain, one colour — runs are long and the curve is
|
|
446
|
+
* smooth. Under the residue spectrum every point is its own colour, so the
|
|
447
|
+
* chain becomes a faceted polyline with a gradient along it. That is the honest
|
|
448
|
+
* cost of colouring per residue without a mesh built vertex by vertex, and at
|
|
449
|
+
* the scale a whole fold is viewed at it reads as a curve anyway.
|
|
450
|
+
*/
|
|
451
|
+
export function backboneRuns(chain, colors, splitBySecondary) {
|
|
452
|
+
const runs = [];
|
|
453
|
+
for (let index = 0; index < chain.trace.length; index++) {
|
|
454
|
+
const point = chain.trace[index];
|
|
455
|
+
const color = colors[index] ?? colors[0] ?? "#ffffff";
|
|
456
|
+
let current = runs[runs.length - 1];
|
|
457
|
+
const breaks = !current ||
|
|
458
|
+
current.color !== color ||
|
|
459
|
+
(splitBySecondary && current.secondary !== point.secondary);
|
|
460
|
+
if (breaks) {
|
|
461
|
+
// Carry the previous run's last point in as this one's first, so the two
|
|
462
|
+
// sweeps butt up against each other.
|
|
463
|
+
const seed = current ? [current.points[current.points.length - 1]] : [];
|
|
464
|
+
current = { points: seed, color, secondary: point.secondary };
|
|
465
|
+
runs.push(current);
|
|
466
|
+
}
|
|
467
|
+
current.points.push({ x: point.x, y: point.y, z: point.z });
|
|
468
|
+
}
|
|
469
|
+
// A run of one point is not a curve — there is no direction to sweep along.
|
|
470
|
+
// It can only be the very first run when the trace starts with a lone point,
|
|
471
|
+
// since every later run is seeded with its predecessor's last point.
|
|
472
|
+
return runs.filter((run) => run.points.length > 1);
|
|
473
|
+
}
|
|
474
|
+
/** The colour a `Color` prop resolves to when a scheme wants a plain string. */
|
|
475
|
+
export function hexOf(value) {
|
|
476
|
+
return typeof value === "string" ? value : "#ffffff";
|
|
477
|
+
}
|
|
478
|
+
//# sourceMappingURL=shared.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shared.js","sourceRoot":"","sources":["../../src/protein/shared.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What the {@link Protein} node turns a parsed molecule into: which atoms are\n * showing, what colour each one takes, and how a bond becomes a cylinder.\n *\n * Everything here is **derived per structure**, not per frame. The node caches\n * what these return against the inputs that produced them (see\n * `Protein.instances`), because the camera moves every frame and none of this\n * changes when it does — and a mid-sized entry is tens of thousands of atoms, so\n * rebuilding the arrays sixty times a second to spin the view would be the\n * difference between a smooth orbit and a slideshow.\n */\n\nimport type {\n Color,\n Quaternion,\n Transform3D,\n Vector3,\n} from \"@motionscript/core\"\n\nimport { elementColor, vanDerWaalsRadius } from \"./chemistry\"\nimport type {\n ProteinAtom,\n ProteinChain,\n ProteinStructure,\n SecondaryStructure,\n} from \"./structure\"\n\n/**\n * How the molecule is drawn.\n *\n * The four that answer genuinely different questions, which is why there are\n * four rather than a dozen:\n *\n * - `spacefill` — every atom at its van der Waals radius. What the molecule's\n * *surface* is: the shape it presents to everything around it.\n * - `ballAndStick` — atoms shrunk, bonds drawn. What the molecule's *chemistry*\n * is, and the only one where an individual atom can be picked out.\n * - `backbone` — a thin even tube through the α-carbons. Where the chain *goes*,\n * with nothing else in the way.\n * - `ribbon` — the same curve, swelling along helices and strands and coloured\n * by them. What the protein is *folded into*, which is what a figure in a\n * paper is nearly always about.\n */\nexport const PROTEIN_REPRESENTATIONS = [\n \"ribbon\",\n \"backbone\",\n \"ballAndStick\",\n \"spacefill\",\n] as const\n\nexport type ProteinRepresentation = (typeof PROTEIN_REPRESENTATIONS)[number]\n\n/**\n * What decides an atom's colour.\n *\n * Each one answers a question the picture might be asking, and the default\n * differs by representation for exactly that reason: a space-filling model with\n * no element colouring is a featureless blob, while a ribbon coloured by element\n * is a single shade of grey.\n */\nexport const PROTEIN_COLOR_SCHEMES = [\n /** Helix / sheet / coil. Fold. */\n \"secondary\",\n /** CPK — oxygen red, nitrogen blue, carbon grey. Chemistry. */\n \"element\",\n /** One hue per chain. Which subunit is which, in a complex. */\n \"chain\",\n /** A spectrum from each chain's N terminus to its C terminus. Direction. */\n \"residue\",\n /** One colour throughout, whatever the node's own `color` is. */\n \"uniform\",\n] as const\n\nexport type ProteinColorScheme = (typeof PROTEIN_COLOR_SCHEMES)[number]\n\n/**\n * The per-chain palette.\n *\n * Eight, spread around the wheel rather than sampled from a gradient: chains are\n * a *nominal* scale — chain B is not between A and C in any sense — so the\n * colours have to be distinguishable rather than ordered. A ninth chain wraps,\n * which is the honest failure for a complex that large; the alternative is\n * colours nobody can tell apart.\n *\n * Eight is therefore the *palette's* length rather than a constant anything\n * downstream may assume: an author who edits these edits eight entries, and the\n * wrap is modulo whatever the list holds.\n */\nexport const CHAIN_COLORS = [\n \"#4f9dff\",\n \"#ff8a3d\",\n \"#4ddb9a\",\n \"#e05fd0\",\n \"#ffd166\",\n \"#8b7bff\",\n \"#ff6b6b\",\n \"#3fd0d6\",\n] as const\n\n/**\n * Helix, sheet and coil.\n *\n * The conventional assignment — warm for helices, cool for strands, pale for\n * everything else — which is old enough to be read without a legend.\n */\nexport const SECONDARY_COLORS: Readonly<Record<SecondaryStructure, string>> = {\n helix: \"#f0605d\",\n sheet: \"#f5c451\",\n coil: \"#c8ccd4\",\n}\n\n/** Where the residue spectrum starts and ends, as hues in degrees. */\nconst SPECTRUM_START = 250\nconst SPECTRUM_END = 0\n\n/** The saturation and lightness the residue spectrum is swept at. */\nconst SPECTRUM_SATURATION = 0.72\nconst SPECTRUM_LIGHTNESS = 0.58\n\n/**\n * Every colour a scheme reaches for that is **not** the node's own `color`.\n *\n * Three schemes paint from more than one colour — the fold's three states, the\n * eight chain hues, the two ends of the N→C ramp — and until now all three were\n * frozen in this file. They are conventional rather than arbitrary, which is why\n * the defaults below are exactly what they were, but \"conventional\" is a good\n * reason for a *default* and a poor one for a rule: a figure has a palette, and\n * a molecule in it that refuses to join in is the one element on the slide\n * somebody has to work around.\n *\n * A **read** shape rather than a prop. The node holds these as thirteen separate\n * props and gathers them into one of these to hand to the colourers below, and\n * that split is deliberate rather than clumsy: a prop is a whole value, so a\n * single `palette` prop would make every `to` that reddens the helices a\n * statement about all thirteen colours, snapping the twelve the author never\n * touched back to whatever the command happened to carry. Thirteen props carry\n * over one at a time and tween one at a time, which is what every other colour\n * in the app does.\n *\n * Nothing here reaches the `element` scheme, deliberately. CPK is not a palette\n * anybody chose — oxygen is red because oxygen is red — so it stays in\n * `chemistry.ts` with the van der Waals radii, which are facts about the same\n * atoms and equally not a matter of taste.\n */\nexport interface ProteinPalette {\n /** Secondary structure, when the scheme is `secondary`. */\n helix: string\n sheet: string\n coil: string\n /**\n * One hue per chain, when the scheme is `chain`, wrapping past the end.\n *\n * A list rather than a fixed eight so the wrap follows what is actually here:\n * an author who deletes down to three colours gets a three-colour cycle, which\n * is a legible answer, where a wrap modulo a constant would index past it.\n */\n chains: readonly string[]\n /** The two ends of the N→C ramp, when the scheme is `residue`. */\n residueStart: string\n residueEnd: string\n}\n\n/**\n * The palette every scheme has always drawn — the conventional assignment, now\n * stated as a value the author can take over rather than as a constant.\n *\n * The two spectrum ends are **computed** from the hues the ramp used to be\n * written as, rather than typed out as hex, so the default sweep is the same\n * blue-through-green-to-red it has always been by construction instead of by\n * somebody having done the arithmetic correctly once.\n */\nexport const DEFAULT_PROTEIN_PALETTE: ProteinPalette = {\n helix: SECONDARY_COLORS.helix,\n sheet: SECONDARY_COLORS.sheet,\n coil: SECONDARY_COLORS.coil,\n chains: CHAIN_COLORS,\n residueStart: hslHex(SPECTRUM_START, SPECTRUM_SATURATION, SPECTRUM_LIGHTNESS),\n residueEnd: hslHex(SPECTRUM_END, SPECTRUM_SATURATION, SPECTRUM_LIGHTNESS),\n}\n\n/**\n * How many chain colours the palette offers, and therefore what the chain cycle\n * wraps at.\n *\n * Eight, because that is how many hues can be told apart at a glance — the\n * reason {@link CHAIN_COLORS} has eight — and it is named here because three\n * places have to agree on it: the schema declares this many rows, the props\n * mapper reads this many, and the node holds this many.\n */\nexport const CHAIN_COLOR_COUNT = CHAIN_COLORS.length\n\n/**\n * The appearance-bag field names the palette is edited as, in panel order.\n *\n * Three places have to agree on this list and none of them can derive it from\n * the others: the schema declares a row per name, `proteinMotionProps` copies a\n * row per name onto the props, and the node declares a `@property` per name.\n * The third is the one that cannot be written as a loop — a decorator is a\n * declaration — so what this buys is the first two staying in step with it, and\n * a test pins the third against it.\n *\n * `color` is deliberately not here. It is the node's own colour rather than part\n * of a scheme's palette: it is what `uniform` paints and what every scheme falls\n * back to, so it lives beside `representation` as a field of the molecule.\n */\nexport const PROTEIN_PALETTE_FIELDS = [\n \"helixColor\",\n \"sheetColor\",\n \"coilColor\",\n \"residueStartColor\",\n \"residueEndColor\",\n ...CHAIN_COLORS.map((_, index) => `chainColor${index + 1}` as const),\n] as const\n\n/** The default each {@link PROTEIN_PALETTE_FIELDS} entry carries, by name. */\nexport const PROTEIN_PALETTE_DEFAULTS: Readonly<Record<string, string>> = {\n helixColor: DEFAULT_PROTEIN_PALETTE.helix,\n sheetColor: DEFAULT_PROTEIN_PALETTE.sheet,\n coilColor: DEFAULT_PROTEIN_PALETTE.coil,\n residueStartColor: DEFAULT_PROTEIN_PALETTE.residueStart,\n residueEndColor: DEFAULT_PROTEIN_PALETTE.residueEnd,\n ...Object.fromEntries(\n DEFAULT_PROTEIN_PALETTE.chains.map((colour, index) => [\n `chainColor${index + 1}`,\n colour,\n ])\n ),\n}\n\n/**\n * Tween: two `#rrggbb` strings, blended per channel.\n *\n * The palette entries are the one family of colours here that stay **strings**\n * rather than going through `resolveColor3D` — an instance tint is handed a hex\n * string — so they need a tween of their own rather than {@link lerpColor3D}.\n *\n * Blended in RGB, unlike the residue ramp {@link spectrum} sweeps: that one is a\n * *spectrum*, where covering the wheel is the whole point, while this is one\n * colour becoming another, where the short straight path is what \"fading to red\"\n * means. Anything unparseable snaps, which is the only honest answer for a value\n * with no channels to interpolate.\n */\nexport function lerpHexColor(from: string, to: string, t: number): string {\n const a = hexRgb(from)\n const b = hexRgb(to)\n if (!a || !b) return t < 1 ? from : to\n const byte = (x: number, y: number): string =>\n Math.round(x + (y - x) * t)\n .toString(16)\n .padStart(2, \"0\")\n return `#${byte(a[0], b[0])}${byte(a[1], b[1])}${byte(a[2], b[2])}`\n}\n\n/** A `#rrggbb` string as three 0–255 channels, or `null` if it isn't one. */\nfunction hexRgb(value: string): [number, number, number] | null {\n const hex = /^#?([0-9a-f]{6})$/i.exec(value.trim())\n if (!hex) return null\n const int = parseInt(hex[1]!, 16)\n return [(int >> 16) & 255, (int >> 8) & 255, int & 255]\n}\n\n/**\n * The colour of every atom, in the order the structure holds them.\n *\n * One flat array rather than a function called per atom, because the instance\n * builder walks the whole list and an array is what `Graphics3D.instances` takes\n * anyway.\n */\nexport function atomColors(\n structure: ProteinStructure,\n scheme: ProteinColorScheme,\n uniform: string,\n palette: ProteinPalette = DEFAULT_PROTEIN_PALETTE\n): string[] {\n if (scheme === \"uniform\") return structure.atoms.map(() => uniform)\n if (scheme === \"element\") {\n return structure.atoms.map((atom) => elementColor(atom.element))\n }\n\n const chains = chainOrder(structure)\n if (scheme === \"chain\") {\n return structure.atoms.map((atom) =>\n chainColor(chains, atom.chain, palette)\n )\n }\n\n if (scheme === \"secondary\") {\n const lookup = secondaryByResidue(structure)\n return structure.atoms.map(\n (atom) => palette[lookup.get(residueKey(atom)) ?? \"coil\"]\n )\n }\n\n // `residue` — a spectrum along each chain, so position is read against the\n // chain the residue is in rather than against the whole file. Without that, a\n // short chain beside a long one would be drawn in a single colour.\n const positions = residuePositions(structure)\n return structure.atoms.map((atom) => {\n const position = positions.get(residueKey(atom))\n return position === undefined ? uniform : spectrum(position, palette)\n })\n}\n\n/**\n * The colour of each point along one chain's backbone.\n *\n * Separate from {@link atomColors} because a trace point is a *residue* and an\n * atom is not: the element scheme has nothing to say about a residue (every\n * trace atom is a carbon, so the whole ribbon would be grey), so it falls back\n * to the chain colouring — which is what somebody who picked \"element\" and then\n * switched to a ribbon actually wants to see.\n */\nexport function traceColors(\n structure: ProteinStructure,\n chain: ProteinChain,\n scheme: ProteinColorScheme,\n uniform: string,\n palette: ProteinPalette = DEFAULT_PROTEIN_PALETTE\n): string[] {\n if (scheme === \"uniform\") return chain.trace.map(() => uniform)\n if (scheme === \"secondary\") {\n return chain.trace.map((point) => palette[point.secondary])\n }\n if (scheme === \"residue\") {\n const last = Math.max(1, chain.trace.length - 1)\n return chain.trace.map((point) => spectrum(point.index / last, palette))\n }\n\n const chains = chainOrder(structure)\n const colour = chainColor(chains, chain.id, palette)\n return chain.trace.map(() => colour)\n}\n\n/**\n * A colour on the N→C spectrum, `0` at the start of a chain and `1` at its end.\n *\n * Interpolated in **HSL**, which is what makes the default a spectrum at all: a\n * straight RGB blend between the two ends passes through grey, where a hue sweep\n * runs the ramp everybody recognises. And along the *direct numeric path*\n * between the two hues rather than the shorter way round the wheel — 250° to 0°\n * is the familiar blue-through-green-to-red run, and taking the short arc would\n * hop through magenta instead and cover a quarter of the colours.\n */\nfunction spectrum(position: number, palette: ProteinPalette): string {\n const from = hexHsl(palette.residueStart)\n const to = hexHsl(palette.residueEnd)\n const t = clamp01(position)\n return hslHex(\n from.h + (to.h - from.h) * t,\n from.s + (to.s - from.s) * t,\n from.l + (to.l - from.l) * t\n )\n}\n\nfunction clamp01(value: number): number {\n return Math.min(1, Math.max(0, value))\n}\n\n/**\n * HSL to a `#rrggbb` string.\n *\n * Written out rather than reached for, because the spectrum is the one place\n * this package generates a colour instead of being handed one, and every other\n * colour path here takes hex.\n */\nfunction hslHex(hue: number, saturation: number, lightness: number): string {\n const h = ((hue % 360) + 360) % 360\n const c = (1 - Math.abs(2 * lightness - 1)) * saturation\n const x = c * (1 - Math.abs(((h / 60) % 2) - 1))\n const m = lightness - c / 2\n\n const [r, g, b] =\n h < 60\n ? [c, x, 0]\n : h < 120\n ? [x, c, 0]\n : h < 180\n ? [0, c, x]\n : h < 240\n ? [0, x, c]\n : h < 300\n ? [x, 0, c]\n : [c, 0, x]\n\n const byte = (value: number): string =>\n Math.round((value + m) * 255)\n .toString(16)\n .padStart(2, \"0\")\n return `#${byte(r!)}${byte(g!)}${byte(b!)}`\n}\n\n/**\n * A `#rrggbb` string back to HSL — the inverse of {@link hslHex}, and here for\n * the one caller that needs it: the residue ramp is authored as two colours and\n * swept as a hue.\n *\n * Anything unparseable comes back as the default ramp's own start, so a half\n * typed hex in the inspector fades toward a colour rather than painting `NaN`\n * into every residue of the chain.\n */\nfunction hexHsl(value: string): { h: number; s: number; l: number } {\n const hex = /^#?([0-9a-f]{6})$/i.exec(value.trim())\n if (!hex) return { h: SPECTRUM_START, s: SPECTRUM_SATURATION, l: SPECTRUM_LIGHTNESS }\n\n const int = parseInt(hex[1]!, 16)\n const r = ((int >> 16) & 255) / 255\n const g = ((int >> 8) & 255) / 255\n const b = (int & 255) / 255\n\n const max = Math.max(r, g, b)\n const min = Math.min(r, g, b)\n const l = (max + min) / 2\n const d = max - min\n if (d === 0) return { h: 0, s: 0, l }\n\n const s = d / (1 - Math.abs(2 * l - 1))\n const h =\n max === r\n ? 60 * (((g - b) / d) % 6)\n : max === g\n ? 60 * ((b - r) / d + 2)\n : 60 * ((r - g) / d + 4)\n return { h: (h + 360) % 360, s, l }\n}\n\n/** The chain identifiers in the order they first appear, for palette indexing. */\nfunction chainOrder(structure: ProteinStructure): Map<string, number> {\n const order = new Map<string, number>()\n for (const atom of structure.atoms) {\n if (!order.has(atom.chain)) order.set(atom.chain, order.size)\n }\n return order\n}\n\n/** The palette entry for a chain, wrapping past the end. */\nfunction chainColor(\n order: Map<string, number>,\n chain: string,\n palette: ProteinPalette\n): string {\n const colours =\n palette.chains.length > 0 ? palette.chains : DEFAULT_PROTEIN_PALETTE.chains\n const index = order.get(chain) ?? 0\n return colours[index % colours.length]!\n}\n\n/** A residue's identity across the whole structure — its chain and its number. */\nfunction residueKey(atom: ProteinAtom): string {\n return `${atom.chain}:${atom.residueSeq}`\n}\n\n/**\n * Each residue's position along its own chain, `0`–`1`.\n *\n * Read off the *traces* rather than counted over the atoms, so it means the same\n * thing the ribbon means by it — and so a residue the file resolved no backbone\n * for simply has no position, and takes the fallback colour rather than shifting\n * every residue after it along the spectrum.\n */\nfunction residuePositions(structure: ProteinStructure): Map<string, number> {\n const positions = new Map<string, number>()\n for (const chain of structure.chains) {\n const last = Math.max(1, chain.trace.length - 1)\n for (const point of chain.trace) {\n positions.set(`${chain.id}:${point.residueSeq}`, point.index / last)\n }\n }\n return positions\n}\n\n/** Each residue's secondary structure, keyed as {@link residueKey} does. */\nfunction secondaryByResidue(\n structure: ProteinStructure\n): Map<string, SecondaryStructure> {\n const lookup = new Map<string, SecondaryStructure>()\n for (const chain of structure.chains) {\n for (const point of chain.trace) {\n lookup.set(`${chain.id}:${point.residueSeq}`, point.secondary)\n }\n }\n return lookup\n}\n\n// --- Geometry --------------------------------------------------------------\n\n/** An atom's drawn radius in Ångströms, at a given scale of its true size. */\nexport function atomRadius(atom: ProteinAtom, scale: number): number {\n return vanDerWaalsRadius(atom.element) * scale\n}\n\n/**\n * The placement of a stick running from `from` to `to`.\n *\n * A cylinder geometry is built along **+Y**, centred on the origin and one unit\n * tall, so placing one takes all three parts of a transform: move it to the\n * midpoint, scale it to the bond's length, and turn +Y onto the bond's\n * direction.\n *\n * The turn is a quaternion rather than an Euler triple because there is no\n * ordering of three axis rotations that doesn't gimbal somewhere, and a molecule\n * has bonds pointing every way there is — one of them would land exactly on the\n * degenerate axis and the stick would spin to a wrong orientation.\n */\nexport function stickPlacement(\n from: Vector3,\n to: Vector3,\n radius: number\n): Transform3D {\n const dx = to.x - from.x\n const dy = to.y - from.y\n const dz = to.z - from.z\n const length = Math.hypot(dx, dy, dz) || 1\n\n return {\n position: [\n (from.x + to.x) / 2,\n (from.y + to.y) / 2,\n (from.z + to.z) / 2,\n ],\n scale: [radius, length, radius],\n quaternion: upTo(dx / length, dy / length, dz / length),\n }\n}\n\n/**\n * The shortest rotation taking **+Y** onto a unit direction.\n *\n * The general form is \"rotate about the axis perpendicular to both, by the angle\n * between them\", which for a fixed source axis collapses to the cross product\n * with `(0, 1, 0)` — hence the two components rather than three.\n *\n * The antiparallel case has to be handled outright: a direction pointing\n * straight down has a *zero* cross product with up, so the axis is undefined and\n * the general form produces a quaternion of all zeros, which is not a rotation\n * at all. Any half-turn about a perpendicular axis is correct there, and X is as\n * good as any.\n */\nfunction upTo(x: number, y: number, z: number): Quaternion {\n if (y > 0.999999) return { x: 0, y: 0, z: 0, w: 1 }\n if (y < -0.999999) return { x: 1, y: 0, z: 0, w: 0 }\n\n // axis = up × d, normalized; angle = acos(up · d) = acos(y).\n const axisX = z\n const axisZ = -x\n const axisLength = Math.hypot(axisX, axisZ) || 1\n const angle = Math.acos(Math.min(1, Math.max(-1, y)))\n const sin = Math.sin(angle / 2)\n\n return {\n x: (axisX / axisLength) * sin,\n y: 0,\n z: (axisZ / axisLength) * sin,\n w: Math.cos(angle / 2),\n }\n}\n\n/** One stretch of a chain drawn as a single swept tube. */\nexport interface BackboneRun {\n points: Vector3[]\n color: string\n secondary: SecondaryStructure\n}\n\n/**\n * Splits a chain's trace into the runs a backbone is swept as.\n *\n * A run is a stretch that can be drawn as **one** `Geo.tube`, and what forces a\n * break is anything the tube carries once for its whole length: its colour\n * always, and — for a cartoon — its radius, since a helix that doesn't swell is\n * not a helix anyone will recognise.\n *\n * Consecutive runs **overlap by one point**, which is what keeps the tubes\n * meeting rather than leaving a gap at every transition: the shared point is the\n * end of one sweep and the start of the next, so their ends sit in the same\n * place.\n *\n * The trade this makes is worth stating. A tube is swept along a Catmull-Rom\n * curve through its own points, so a long run comes out smooth and a two-point\n * run comes out straight. Under the colourings a fold is usually drawn in —\n * secondary structure, chain, one colour — runs are long and the curve is\n * smooth. Under the residue spectrum every point is its own colour, so the\n * chain becomes a faceted polyline with a gradient along it. That is the honest\n * cost of colouring per residue without a mesh built vertex by vertex, and at\n * the scale a whole fold is viewed at it reads as a curve anyway.\n */\nexport function backboneRuns(\n chain: ProteinChain,\n colors: string[],\n splitBySecondary: boolean\n): BackboneRun[] {\n const runs: BackboneRun[] = []\n\n for (let index = 0; index < chain.trace.length; index++) {\n const point = chain.trace[index]!\n const color = colors[index] ?? colors[0] ?? \"#ffffff\"\n let current = runs[runs.length - 1]\n\n const breaks =\n !current ||\n current.color !== color ||\n (splitBySecondary && current.secondary !== point.secondary)\n\n if (breaks) {\n // Carry the previous run's last point in as this one's first, so the two\n // sweeps butt up against each other.\n const seed = current ? [current.points[current.points.length - 1]!] : []\n current = { points: seed, color, secondary: point.secondary }\n runs.push(current)\n }\n current.points.push({ x: point.x, y: point.y, z: point.z })\n }\n\n // A run of one point is not a curve — there is no direction to sweep along.\n // It can only be the very first run when the trace starts with a lone point,\n // since every later run is seeded with its predecessor's last point.\n return runs.filter((run) => run.points.length > 1)\n}\n\n/** The colour a `Color` prop resolves to when a scheme wants a plain string. */\nexport function hexOf(value: Color): string {\n return typeof value === \"string\" ? value : \"#ffffff\"\n}\n"]}
|