@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,645 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { command, driveCommand, easeInOut, node, Canvas3D, Geo, Graphics3D, Mat, Scene3D, property, } from "@motionscript/core";
|
|
8
|
+
import { lerpColor3D, cameraOrbit, resolveColor3D, snapValue, orbitProps, } from "@motionscript/core/component";
|
|
9
|
+
import { atomColors, atomRadius, backboneRuns, DEFAULT_PROTEIN_PALETTE, hexOf, lerpHexColor, stickPlacement, traceColors, } from "./shared.js";
|
|
10
|
+
import { cartoonGeometry, cartoonSpans } from "./ribbon.js";
|
|
11
|
+
import { EMPTY_STRUCTURE, } from "./structure.js";
|
|
12
|
+
/** {@link ProteinProps.zoom}, held inside what can actually be framed. */
|
|
13
|
+
const MIN_ZOOM = 0.2;
|
|
14
|
+
const MAX_ZOOM = 12;
|
|
15
|
+
const DEFAULT_ZOOM = 2.6;
|
|
16
|
+
/**
|
|
17
|
+
* How much of its true size an atom is drawn at in ball-and-stick, on top of
|
|
18
|
+
* whatever {@link ProteinProps.atomScale} says.
|
|
19
|
+
*
|
|
20
|
+
* Ball-and-stick is not a smaller space-filling model — it is a different claim
|
|
21
|
+
* about what matters. Atoms at a quarter of their radius stop touching, which is
|
|
22
|
+
* what makes room for the bonds that are the whole point of the representation.
|
|
23
|
+
*/
|
|
24
|
+
const BALL_SCALE = 0.28;
|
|
25
|
+
/**
|
|
26
|
+
* A molecular structure viewer.
|
|
27
|
+
*
|
|
28
|
+
* Extends {@link Canvas3D} — as `Graph3D` does — so this class only has to say
|
|
29
|
+
* *what* to draw, and the bridge from the 2D scene graph into the 3D renderer is
|
|
30
|
+
* the base class's business. It overrides `buildScene3D`, the single seam both
|
|
31
|
+
* the real render and the asset declaration pass go through.
|
|
32
|
+
*
|
|
33
|
+
* <Protein width="fill" height="fill"
|
|
34
|
+
* structure={parseStructure(text, "6LU7")}
|
|
35
|
+
* representation="ribbon" colorScheme="secondary" />
|
|
36
|
+
*
|
|
37
|
+
* **There is no pointer here.** Every other molecular viewer is driven by
|
|
38
|
+
* dragging the molecule around, and this one cannot be: the studio renders a
|
|
39
|
+
* timeline, so the camera has to be something a `to` command can tween and a
|
|
40
|
+
* scrub can reproduce exactly — frame 300 must be identical whether it was
|
|
41
|
+
* reached by playing forward or by dragging the playhead backwards. So the
|
|
42
|
+
* camera is three tweenable numbers (`orbit`, `elevation`, `zoom`), and spinning
|
|
43
|
+
* the molecule is a command rather than a gesture.
|
|
44
|
+
*
|
|
45
|
+
* Atoms and bonds are drawn **instanced**: one sphere geometry and one cylinder
|
|
46
|
+
* geometry, placed thousands of times in a single draw call each. A mesh per
|
|
47
|
+
* atom would be tens of thousands of draw calls a frame, which no renderer
|
|
48
|
+
* survives — and the instance lists themselves are cached (see {@link built}),
|
|
49
|
+
* because they don't change when the camera moves and the camera is what
|
|
50
|
+
* normally moves.
|
|
51
|
+
*/
|
|
52
|
+
let Protein = class Protein extends Canvas3D {
|
|
53
|
+
constructor(props) {
|
|
54
|
+
super(props);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Frame the scene at a spherical camera placement. Every axis is optional,
|
|
58
|
+
* so "pull back" and "spin round" stay separate intentions — see
|
|
59
|
+
* {@link OrbitTarget}.
|
|
60
|
+
*/
|
|
61
|
+
orbitTo(args) {
|
|
62
|
+
return this.to({
|
|
63
|
+
data: orbitProps(args.data?.target ?? {}),
|
|
64
|
+
duration: args.duration,
|
|
65
|
+
easing: args.easing ?? easeInOut(),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Drift the camera around the molecule for the command's duration — a
|
|
70
|
+
* continuous orbit at `speed` with a lateral `sway` and a `zoom` pulse
|
|
71
|
+
* (`breathe`), both on a shared `cycle` so they don't beat against each
|
|
72
|
+
* other. `settle` eases in from wherever the camera already is, so `float`
|
|
73
|
+
* after an `orbitTo` does not snap.
|
|
74
|
+
*/
|
|
75
|
+
float(args) {
|
|
76
|
+
const { speed = 36, elevation = 18, zoom = 2.6, sway = 6, breathe = 0.08, cycle = 8, settle = 1, } = args.data ?? {};
|
|
77
|
+
const duration = args.duration ?? 8;
|
|
78
|
+
const easing = args.easing ?? easeInOut();
|
|
79
|
+
const fromOrbit = this.orbit;
|
|
80
|
+
const fromElevation = this.elevation;
|
|
81
|
+
const fromZoom = this.zoom;
|
|
82
|
+
const period = Math.max(cycle, 0.0001);
|
|
83
|
+
return driveCommand(duration, (t) => {
|
|
84
|
+
const seconds = t * duration;
|
|
85
|
+
// Ease from the camera's current placement into the drift over `settle`
|
|
86
|
+
// seconds. A zero `settle` starts the drift immediately.
|
|
87
|
+
const blend = settle <= 0 ? 1 : easing(Math.min(1, seconds / settle));
|
|
88
|
+
const phase = (seconds / period) * Math.PI * 2;
|
|
89
|
+
const driftOrbit = fromOrbit + speed * seconds + Math.sin(phase) * sway;
|
|
90
|
+
const driftZoom = zoom * (1 + Math.sin(phase) * breathe);
|
|
91
|
+
this.set({
|
|
92
|
+
orbit: fromOrbit + (driftOrbit - fromOrbit) * blend,
|
|
93
|
+
elevation: fromElevation + (elevation - fromElevation) * blend,
|
|
94
|
+
zoom: fromZoom + (driftZoom - fromZoom) * blend,
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* The thirteen colour props, as the one value the colourers take.
|
|
100
|
+
*
|
|
101
|
+
* Rebuilt per call rather than cached: it is thirteen property reads behind
|
|
102
|
+
* `built()`'s own cache, which is what stops it happening per frame.
|
|
103
|
+
*/
|
|
104
|
+
paletteOf() {
|
|
105
|
+
return {
|
|
106
|
+
helix: this.helixColor,
|
|
107
|
+
sheet: this.sheetColor,
|
|
108
|
+
coil: this.coilColor,
|
|
109
|
+
residueStart: this.residueStartColor,
|
|
110
|
+
residueEnd: this.residueEndColor,
|
|
111
|
+
chains: [
|
|
112
|
+
this.chainColor1,
|
|
113
|
+
this.chainColor2,
|
|
114
|
+
this.chainColor3,
|
|
115
|
+
this.chainColor4,
|
|
116
|
+
this.chainColor5,
|
|
117
|
+
this.chainColor6,
|
|
118
|
+
this.chainColor7,
|
|
119
|
+
this.chainColor8,
|
|
120
|
+
],
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
// ---- Drawing ------------------------------------------------------------
|
|
124
|
+
buildScene3D() {
|
|
125
|
+
const scene = new Scene3D();
|
|
126
|
+
const structure = this.structure;
|
|
127
|
+
// Framed on what is actually **drawn**, not on the whole file. A crystal
|
|
128
|
+
// structure carries hundreds of ordered waters scattered well beyond the
|
|
129
|
+
// molecule, and they are hidden by default — framing the sphere that holds
|
|
130
|
+
// them would park the camera 40% further back than the picture needs, which
|
|
131
|
+
// reads as a molecule adrift in an empty box.
|
|
132
|
+
const radius = this.built().radius;
|
|
133
|
+
const distance = radius * clampZoom(this.zoom);
|
|
134
|
+
// The clipping planes are cut to the molecule rather than fixed, because
|
|
135
|
+
// "far enough away to see all of it" differs a hundredfold between a peptide
|
|
136
|
+
// and a virus capsid — and a depth buffer stretched over a range it doesn't
|
|
137
|
+
// need spends its precision on empty space, which shows up as z-fighting
|
|
138
|
+
// between atoms that are genuinely an Ångström apart.
|
|
139
|
+
// Kept explicit rather than left to the camera's own derivation: that sizes
|
|
140
|
+
// the planes to the scene's bounding box, and a molecule's *atoms* are what
|
|
141
|
+
// need the precision here, not its extent.
|
|
142
|
+
//
|
|
143
|
+
// Polar placement is the camera's vocabulary now, so the three props pass
|
|
144
|
+
// straight through; `cameraOrbit` is the quarter turn between the stored
|
|
145
|
+
// zero and the camera's.
|
|
146
|
+
scene.perspective({
|
|
147
|
+
fov: 45,
|
|
148
|
+
near: Math.max(0.1, radius * 0.02),
|
|
149
|
+
far: distance + radius * 4,
|
|
150
|
+
orbit: cameraOrbit(this.orbit),
|
|
151
|
+
elevation: this.elevation,
|
|
152
|
+
distance,
|
|
153
|
+
});
|
|
154
|
+
// The backdrop is the node's own **Fills** section — a `Canvas3D`
|
|
155
|
+
// composites its 3D pass over its fill layers, so a paint stack behind a
|
|
156
|
+
// molecule is not merely possible but strictly better than one colour, and
|
|
157
|
+
// an empty stack is what transparency already means everywhere else in the
|
|
158
|
+
// app. There is no 3D background pass to reach for and no clear colour to
|
|
159
|
+
// set; see `resolveViewport3DFills`, which carries the colour this node
|
|
160
|
+
// used to hold in its appearance bag across to that stack.
|
|
161
|
+
// A key light, a weaker rim from the opposite side, and enough ambient that
|
|
162
|
+
// an atom facing away is still an atom rather than a silhouette. Placed
|
|
163
|
+
// relative to the molecule so the lighting holds at any size — a light's
|
|
164
|
+
// parameters and its placement are separate arguments, because placement is
|
|
165
|
+
// a transform rather than a property of the lamp.
|
|
166
|
+
//
|
|
167
|
+
// The rim takes the key's colour as well as a fraction of its strength: it
|
|
168
|
+
// is the key seen from the other side rather than a light of its own, which
|
|
169
|
+
// is why there are four controls and not six. The same rig, and the same
|
|
170
|
+
// argument, as the 3D viewport's — see `Canvas3DView.buildScene3D`.
|
|
171
|
+
scene
|
|
172
|
+
.light({
|
|
173
|
+
type: "ambient",
|
|
174
|
+
intensity: this.ambientIntensity,
|
|
175
|
+
color: this.ambientColor,
|
|
176
|
+
})
|
|
177
|
+
.light({
|
|
178
|
+
type: "directional",
|
|
179
|
+
intensity: this.keyIntensity,
|
|
180
|
+
color: this.keyColor,
|
|
181
|
+
}, { position: [radius, radius * 2, radius * 2] })
|
|
182
|
+
.light({
|
|
183
|
+
type: "directional",
|
|
184
|
+
intensity: this.keyIntensity * 0.4,
|
|
185
|
+
color: this.keyColor,
|
|
186
|
+
}, { position: [-radius * 2, -radius, -radius * 1.5] });
|
|
187
|
+
// Everything is drawn inside one group that puts the molecule's centroid on
|
|
188
|
+
// the origin, so the camera — which always looks at the origin — frames it
|
|
189
|
+
// wherever the crystallographer's coordinate system happened to put it. PDB
|
|
190
|
+
// coordinates are in the crystal's frame and routinely sit hundreds of
|
|
191
|
+
// Ångströms from zero.
|
|
192
|
+
//
|
|
193
|
+
// `begin`/`end` is what a `Graphics3D.group` was: a transform pushed over
|
|
194
|
+
// everything drawn between them. It lives on the scene now rather than on
|
|
195
|
+
// the graphics, because hierarchy is a property of the scene.
|
|
196
|
+
const g3 = new Graphics3D();
|
|
197
|
+
this.addMolecule(g3);
|
|
198
|
+
const { center } = structure;
|
|
199
|
+
scene.begin({
|
|
200
|
+
id: "molecule",
|
|
201
|
+
transform: { position: [-center.x, -center.y, -center.z] },
|
|
202
|
+
});
|
|
203
|
+
scene.draw(g3);
|
|
204
|
+
scene.end();
|
|
205
|
+
return scene;
|
|
206
|
+
}
|
|
207
|
+
/** The molecule itself, in whichever representation is showing. */
|
|
208
|
+
addMolecule(g3) {
|
|
209
|
+
const built = this.built();
|
|
210
|
+
this.addSpheres(g3, built.spheres, built.sphereTints, "atoms");
|
|
211
|
+
this.addSticks(g3, built.sticks, built.stickTints, "bonds");
|
|
212
|
+
for (const run of built.runs) {
|
|
213
|
+
g3.mesh(Geo.tube({
|
|
214
|
+
points: run.points,
|
|
215
|
+
radius: run.radius,
|
|
216
|
+
// `[along the curve, around it]`. Segments along the curve are
|
|
217
|
+
// proportional to the run's own length, so the sweep follows it rather
|
|
218
|
+
// than cutting corners across a tight turn — and a short run doesn't
|
|
219
|
+
// pay for a long one's tessellation.
|
|
220
|
+
segments: [Math.max(6, run.points.length * 4), this.stickQuality()],
|
|
221
|
+
}), Mat.standard({ color: run.color, roughness: 0.45, metalness: 0.05 }));
|
|
222
|
+
}
|
|
223
|
+
for (const ribbon of built.ribbons) {
|
|
224
|
+
g3.mesh(ribbon,
|
|
225
|
+
// `vertexColors`, where every other material here takes a `color`. A
|
|
226
|
+
// cartoon span is one mesh covering many residues and the colouring is
|
|
227
|
+
// per residue, so the colour varies *within* the primitive — which is
|
|
228
|
+
// the one thing a material's uniform colour cannot express. Safe in a
|
|
229
|
+
// way it would not be on the instanced spheres (see {@link addSpheres}):
|
|
230
|
+
// this geometry genuinely carries a `color` attribute.
|
|
231
|
+
Mat.standard({
|
|
232
|
+
vertexColors: true,
|
|
233
|
+
roughness: 0.45,
|
|
234
|
+
metalness: 0.05,
|
|
235
|
+
}));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
/** One instanced sphere op, skipped entirely when there is nothing in it. */
|
|
239
|
+
addSpheres(g3, placements, tints, key) {
|
|
240
|
+
if (placements.length === 0)
|
|
241
|
+
return;
|
|
242
|
+
const quality = this.sphereQuality();
|
|
243
|
+
g3.instances(
|
|
244
|
+
// A unit sphere, sized per instance. One geometry for the whole molecule:
|
|
245
|
+
// the renderer dedupes by identity and uploads it once, where a radius
|
|
246
|
+
// baked into the geometry would mean one upload per element.
|
|
247
|
+
Geo.sphere({
|
|
248
|
+
radius: 1,
|
|
249
|
+
// `[longitude, latitude]` — half as many bands as meridians, which is
|
|
250
|
+
// what a sphere wants and what three's own defaults do.
|
|
251
|
+
segments: [quality, Math.max(4, Math.round(quality / 2))],
|
|
252
|
+
}),
|
|
253
|
+
// No `vertexColors` here, and that is the whole trick rather than an
|
|
254
|
+
// omission. An instance's tint arrives as its own attribute and the
|
|
255
|
+
// renderer samples it whenever one is present; asking for *vertex* colours
|
|
256
|
+
// as well makes the shader read a per-vertex `color` the geometry doesn't
|
|
257
|
+
// have, which supplies zeroes — and the molecule renders solid black.
|
|
258
|
+
Mat.standard({ roughness: 0.35, metalness: 0.05 }), placements, { colors: tints, transform: { key } });
|
|
259
|
+
}
|
|
260
|
+
/** One instanced cylinder op. */
|
|
261
|
+
addSticks(g3, placements, tints, key) {
|
|
262
|
+
if (placements.length === 0)
|
|
263
|
+
return;
|
|
264
|
+
g3.instances(
|
|
265
|
+
// A unit cylinder — one tall, centred on the origin, running along +Y —
|
|
266
|
+
// which `stickPlacement` moves, turns and stretches onto each bond.
|
|
267
|
+
Geo.cylinder({
|
|
268
|
+
radius: 1,
|
|
269
|
+
height: 1,
|
|
270
|
+
segments: this.stickQuality(),
|
|
271
|
+
// The ends are inside the atoms they join, so nothing can see them.
|
|
272
|
+
capped: false,
|
|
273
|
+
}),
|
|
274
|
+
// Bare of `vertexColors` for the reason the spheres are — see above.
|
|
275
|
+
Mat.standard({ roughness: 0.4, metalness: 0.05 }), placements, { colors: tints, transform: { key } });
|
|
276
|
+
}
|
|
277
|
+
// ---- What gets drawn ----------------------------------------------------
|
|
278
|
+
/**
|
|
279
|
+
* Everything the builder needs, cached against the inputs that decide it.
|
|
280
|
+
*
|
|
281
|
+
* This is the node's one piece of memoisation and it earns its keep. The
|
|
282
|
+
* builder runs every frame; between two frames the thing that has normally
|
|
283
|
+
* changed is the camera; and none of what is cached here depends on the
|
|
284
|
+
* camera. Without it, orbiting a 20,000-atom structure would rebuild a
|
|
285
|
+
* 20,000-entry placement list and a 40,000-entry stick list sixty times a
|
|
286
|
+
* second — for a picture whose atoms have not moved.
|
|
287
|
+
*
|
|
288
|
+
* The structure is compared by **identity** rather than by contents: it is a
|
|
289
|
+
* parsed value produced once, upstream, so a different object genuinely means
|
|
290
|
+
* a different molecule.
|
|
291
|
+
*/
|
|
292
|
+
cache = null;
|
|
293
|
+
built() {
|
|
294
|
+
const structure = this.structure;
|
|
295
|
+
const key = [
|
|
296
|
+
this.representation,
|
|
297
|
+
this.colorScheme,
|
|
298
|
+
hexOf(this.color),
|
|
299
|
+
// The palette decides every colour the scheme paints, so a change to it
|
|
300
|
+
// changes the tint arrays this cache is holding — exactly as `color` does.
|
|
301
|
+
// Missing it meant editing a chain colour repainted nothing until
|
|
302
|
+
// something else in this key happened to move.
|
|
303
|
+
paletteKey(this.paletteOf()),
|
|
304
|
+
this.ligands,
|
|
305
|
+
this.waters,
|
|
306
|
+
this.atomScale,
|
|
307
|
+
this.bondRadius,
|
|
308
|
+
this.ribbonRadius,
|
|
309
|
+
// In the key now, where it never used to be: the cartoon's curve is
|
|
310
|
+
// subdivided against the mesh detail (see `subdivisionsFor`), so a change
|
|
311
|
+
// to it changes the geometry this cache is holding. The atoms never
|
|
312
|
+
// needed it — `quality` reaches their spheres as a `segments` count the
|
|
313
|
+
// renderer reads off the descriptor, not as anything built here.
|
|
314
|
+
this.quality,
|
|
315
|
+
].join("|");
|
|
316
|
+
const cached = this.cache;
|
|
317
|
+
if (cached && cached.structure === structure && cached.key === key) {
|
|
318
|
+
return cached.built;
|
|
319
|
+
}
|
|
320
|
+
const built = this.build(structure);
|
|
321
|
+
this.cache = { key, structure, built };
|
|
322
|
+
return built;
|
|
323
|
+
}
|
|
324
|
+
/** Builds the placement lists for the current representation. */
|
|
325
|
+
build(structure) {
|
|
326
|
+
const palette = this.paletteOf();
|
|
327
|
+
const colors = atomColors(structure, this.colorScheme, hexOf(this.color), palette);
|
|
328
|
+
const showing = structure.atoms.map((atom) => this.isShowing(atom));
|
|
329
|
+
const representation = this.representation;
|
|
330
|
+
const spaceFilling = representation === "spacefill";
|
|
331
|
+
const atomic = spaceFilling || representation === "ballAndStick";
|
|
332
|
+
// On a backbone or a ribbon the polymer is the curve, so only what *isn't*
|
|
333
|
+
// polymer keeps its atoms — an inhibitor sitting in an active site is
|
|
334
|
+
// usually the entire subject of the picture, and a bare backbone doesn't
|
|
335
|
+
// show it. On the atomic representations everything showing is drawn.
|
|
336
|
+
const drawAtom = (index) => {
|
|
337
|
+
if (!showing[index])
|
|
338
|
+
return false;
|
|
339
|
+
if (atomic)
|
|
340
|
+
return true;
|
|
341
|
+
const kind = structure.atoms[index].kind;
|
|
342
|
+
return kind !== "amino" && kind !== "nucleic";
|
|
343
|
+
};
|
|
344
|
+
const built = {
|
|
345
|
+
spheres: [],
|
|
346
|
+
sphereTints: [],
|
|
347
|
+
sticks: [],
|
|
348
|
+
stickTints: [],
|
|
349
|
+
runs: [],
|
|
350
|
+
ribbons: [],
|
|
351
|
+
radius: structure.radius,
|
|
352
|
+
};
|
|
353
|
+
const scale = this.atomScale * (spaceFilling ? 1 : BALL_SCALE);
|
|
354
|
+
for (let i = 0; i < structure.atoms.length; i++) {
|
|
355
|
+
if (!drawAtom(i))
|
|
356
|
+
continue;
|
|
357
|
+
const atom = structure.atoms[i];
|
|
358
|
+
built.spheres.push({
|
|
359
|
+
position: [atom.x, atom.y, atom.z],
|
|
360
|
+
scale: atomRadius(atom, scale),
|
|
361
|
+
});
|
|
362
|
+
built.sphereTints.push(colors[i]);
|
|
363
|
+
}
|
|
364
|
+
// A space-filling model has no visible bonds — the atoms are at their true
|
|
365
|
+
// radii and already overlapping — so the sticks are simply not built.
|
|
366
|
+
if (!spaceFilling) {
|
|
367
|
+
for (const bond of structure.bonds) {
|
|
368
|
+
if (!drawAtom(bond.a) || !drawAtom(bond.b))
|
|
369
|
+
continue;
|
|
370
|
+
const a = structure.atoms[bond.a];
|
|
371
|
+
const b = structure.atoms[bond.b];
|
|
372
|
+
// Two half-sticks rather than one, each in its own atom's colour: a
|
|
373
|
+
// single stick has to be *some* colour, and whichever atom's it takes,
|
|
374
|
+
// the far end reads as belonging to the wrong one. Splitting at the
|
|
375
|
+
// midpoint is what ball-and-stick models have done since they were brass.
|
|
376
|
+
const mid = {
|
|
377
|
+
x: (a.x + b.x) / 2,
|
|
378
|
+
y: (a.y + b.y) / 2,
|
|
379
|
+
z: (a.z + b.z) / 2,
|
|
380
|
+
};
|
|
381
|
+
built.sticks.push(stickPlacement(a, mid, this.bondRadius));
|
|
382
|
+
built.stickTints.push(colors[bond.a]);
|
|
383
|
+
built.sticks.push(stickPlacement(mid, b, this.bondRadius));
|
|
384
|
+
built.stickTints.push(colors[bond.b]);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
if (!atomic)
|
|
388
|
+
this.addRuns(built, structure);
|
|
389
|
+
built.radius = framingRadius(built, structure);
|
|
390
|
+
return built;
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* The polymer, as whichever sweep its representation calls for.
|
|
394
|
+
*
|
|
395
|
+
* Two genuinely different builds rather than one with a radius switch, and
|
|
396
|
+
* the split is the same one the representations themselves are: `backbone`
|
|
397
|
+
* asks *where does the chain go*, which an even round tube answers exactly
|
|
398
|
+
* and completely; `ribbon` asks *what is it folded into*, which is the
|
|
399
|
+
* cartoon convention and needs a cross-section with a width, a thickness and
|
|
400
|
+
* a side (see `ribbon.ts`).
|
|
401
|
+
*/
|
|
402
|
+
addRuns(built, structure) {
|
|
403
|
+
const cartoon = this.representation === "ribbon";
|
|
404
|
+
const uniform = hexOf(this.color);
|
|
405
|
+
const palette = this.paletteOf();
|
|
406
|
+
structure.chains.forEach((chain, index) => {
|
|
407
|
+
const colors = traceColors(structure, chain, this.colorScheme, uniform, palette);
|
|
408
|
+
if (cartoon) {
|
|
409
|
+
for (const span of cartoonSpans(chain, colors)) {
|
|
410
|
+
const geometry = cartoonGeometry(span, {
|
|
411
|
+
radius: this.ribbonRadius,
|
|
412
|
+
quality: this.quality,
|
|
413
|
+
});
|
|
414
|
+
if (geometry)
|
|
415
|
+
built.ribbons.push(geometry);
|
|
416
|
+
}
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
backboneRuns(chain, colors, false).forEach((run, runIndex) => {
|
|
420
|
+
built.runs.push({
|
|
421
|
+
points: run.points,
|
|
422
|
+
color: run.color,
|
|
423
|
+
radius: this.ribbonRadius,
|
|
424
|
+
// Keyed by its place in the chain rather than by its slot in the op
|
|
425
|
+
// list, so adding a run doesn't renumber every later one and force the
|
|
426
|
+
// tail of the renderer's cache to rebuild.
|
|
427
|
+
key: `chain:${index}:${runIndex}`,
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
/** Whether an atom passes the ligand and water filters. */
|
|
433
|
+
isShowing(atom) {
|
|
434
|
+
if (atom.kind === "water")
|
|
435
|
+
return this.waters;
|
|
436
|
+
if (atom.kind === "amino" || atom.kind === "nucleic")
|
|
437
|
+
return true;
|
|
438
|
+
return this.ligands;
|
|
439
|
+
}
|
|
440
|
+
/** Segments around a sphere's equator. */
|
|
441
|
+
sphereQuality() {
|
|
442
|
+
return Math.max(4, Math.round(this.quality));
|
|
443
|
+
}
|
|
444
|
+
/** Segments around a stick or a tube — half a sphere's, since it has no poles. */
|
|
445
|
+
stickQuality() {
|
|
446
|
+
return Math.max(3, Math.round(this.quality / 2));
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
__decorate([
|
|
450
|
+
property({ default: EMPTY_STRUCTURE, tween: snapValue })
|
|
451
|
+
], Protein.prototype, "structure", void 0);
|
|
452
|
+
__decorate([
|
|
453
|
+
property({ default: "ribbon", tween: snapValue })
|
|
454
|
+
], Protein.prototype, "representation", void 0);
|
|
455
|
+
__decorate([
|
|
456
|
+
property({ default: "secondary", tween: snapValue })
|
|
457
|
+
], Protein.prototype, "colorScheme", void 0);
|
|
458
|
+
__decorate([
|
|
459
|
+
property({ default: "#4f9dff", mapper: resolveColor3D, tween: lerpColor3D })
|
|
460
|
+
], Protein.prototype, "color", void 0);
|
|
461
|
+
__decorate([
|
|
462
|
+
property({ default: "#f0605d", tween: lerpHexColor })
|
|
463
|
+
], Protein.prototype, "helixColor", void 0);
|
|
464
|
+
__decorate([
|
|
465
|
+
property({ default: "#f5c451", tween: lerpHexColor })
|
|
466
|
+
], Protein.prototype, "sheetColor", void 0);
|
|
467
|
+
__decorate([
|
|
468
|
+
property({ default: "#c8ccd4", tween: lerpHexColor })
|
|
469
|
+
], Protein.prototype, "coilColor", void 0);
|
|
470
|
+
__decorate([
|
|
471
|
+
property({
|
|
472
|
+
default: DEFAULT_PROTEIN_PALETTE.residueStart,
|
|
473
|
+
tween: lerpHexColor,
|
|
474
|
+
})
|
|
475
|
+
], Protein.prototype, "residueStartColor", void 0);
|
|
476
|
+
__decorate([
|
|
477
|
+
property({
|
|
478
|
+
default: DEFAULT_PROTEIN_PALETTE.residueEnd,
|
|
479
|
+
tween: lerpHexColor,
|
|
480
|
+
})
|
|
481
|
+
], Protein.prototype, "residueEndColor", void 0);
|
|
482
|
+
__decorate([
|
|
483
|
+
property({ default: "#4f9dff", tween: lerpHexColor })
|
|
484
|
+
], Protein.prototype, "chainColor1", void 0);
|
|
485
|
+
__decorate([
|
|
486
|
+
property({ default: "#ff8a3d", tween: lerpHexColor })
|
|
487
|
+
], Protein.prototype, "chainColor2", void 0);
|
|
488
|
+
__decorate([
|
|
489
|
+
property({ default: "#4ddb9a", tween: lerpHexColor })
|
|
490
|
+
], Protein.prototype, "chainColor3", void 0);
|
|
491
|
+
__decorate([
|
|
492
|
+
property({ default: "#e05fd0", tween: lerpHexColor })
|
|
493
|
+
], Protein.prototype, "chainColor4", void 0);
|
|
494
|
+
__decorate([
|
|
495
|
+
property({ default: "#ffd166", tween: lerpHexColor })
|
|
496
|
+
], Protein.prototype, "chainColor5", void 0);
|
|
497
|
+
__decorate([
|
|
498
|
+
property({ default: "#8b7bff", tween: lerpHexColor })
|
|
499
|
+
], Protein.prototype, "chainColor6", void 0);
|
|
500
|
+
__decorate([
|
|
501
|
+
property({ default: "#ff6b6b", tween: lerpHexColor })
|
|
502
|
+
], Protein.prototype, "chainColor7", void 0);
|
|
503
|
+
__decorate([
|
|
504
|
+
property({ default: "#3fd0d6", tween: lerpHexColor })
|
|
505
|
+
], Protein.prototype, "chainColor8", void 0);
|
|
506
|
+
__decorate([
|
|
507
|
+
property({ default: 1 })
|
|
508
|
+
], Protein.prototype, "atomScale", void 0);
|
|
509
|
+
__decorate([
|
|
510
|
+
property({ default: 0.18 })
|
|
511
|
+
], Protein.prototype, "bondRadius", void 0);
|
|
512
|
+
__decorate([
|
|
513
|
+
property({ default: 0.45 })
|
|
514
|
+
], Protein.prototype, "ribbonRadius", void 0);
|
|
515
|
+
__decorate([
|
|
516
|
+
property({ default: true, tween: snapValue })
|
|
517
|
+
], Protein.prototype, "ligands", void 0);
|
|
518
|
+
__decorate([
|
|
519
|
+
property({ default: false, tween: snapValue })
|
|
520
|
+
], Protein.prototype, "waters", void 0);
|
|
521
|
+
__decorate([
|
|
522
|
+
property({ default: 12 })
|
|
523
|
+
], Protein.prototype, "quality", void 0);
|
|
524
|
+
__decorate([
|
|
525
|
+
property({ default: 35 })
|
|
526
|
+
], Protein.prototype, "orbit", void 0);
|
|
527
|
+
__decorate([
|
|
528
|
+
property({ default: 18 })
|
|
529
|
+
], Protein.prototype, "elevation", void 0);
|
|
530
|
+
__decorate([
|
|
531
|
+
property({ default: DEFAULT_ZOOM })
|
|
532
|
+
], Protein.prototype, "zoom", void 0);
|
|
533
|
+
__decorate([
|
|
534
|
+
property({ default: 0.55 })
|
|
535
|
+
], Protein.prototype, "ambientIntensity", void 0);
|
|
536
|
+
__decorate([
|
|
537
|
+
property({ default: "#ffffff", mapper: resolveColor3D, tween: lerpColor3D })
|
|
538
|
+
], Protein.prototype, "ambientColor", void 0);
|
|
539
|
+
__decorate([
|
|
540
|
+
property({ default: 1.6 })
|
|
541
|
+
], Protein.prototype, "keyIntensity", void 0);
|
|
542
|
+
__decorate([
|
|
543
|
+
property({ default: "#ffffff", mapper: resolveColor3D, tween: lerpColor3D })
|
|
544
|
+
], Protein.prototype, "keyColor", void 0);
|
|
545
|
+
__decorate([
|
|
546
|
+
command({
|
|
547
|
+
args: [
|
|
548
|
+
{
|
|
549
|
+
key: "target", kind: "orbit", properties: {
|
|
550
|
+
orbit: { kind: "number", step: 1, unit: "°" },
|
|
551
|
+
elevation: { kind: "number", min: -89, max: 89, step: 1, unit: "°" },
|
|
552
|
+
zoom: { kind: "number", min: 0.2, max: 24, step: 0.1, unit: "×" },
|
|
553
|
+
},
|
|
554
|
+
},
|
|
555
|
+
],
|
|
556
|
+
})
|
|
557
|
+
], Protein.prototype, "orbitTo", null);
|
|
558
|
+
__decorate([
|
|
559
|
+
command({
|
|
560
|
+
args: [
|
|
561
|
+
{ key: "speed", kind: "number", default: 36, min: -360, max: 360, step: 1, unit: "°/s" },
|
|
562
|
+
{ key: "elevation", kind: "number", default: 18, min: -89, max: 89, step: 1, unit: "°" },
|
|
563
|
+
{ key: "zoom", kind: "number", default: 2.6, min: 0.2, max: 12, step: 0.1, unit: "×" },
|
|
564
|
+
{ key: "sway", kind: "number", default: 6, min: 0, max: 45, step: 1, unit: "°" },
|
|
565
|
+
{ key: "breathe", kind: "number", default: 0.08, min: 0, max: 0.6, step: 0.01, scale: 100, unit: "%" },
|
|
566
|
+
{ key: "cycle", kind: "number", default: 8, min: 0.5, max: 60, step: 0.5, unit: "s" },
|
|
567
|
+
{ key: "settle", kind: "number", default: 1, min: 0, max: 20, step: 0.1, unit: "s" },
|
|
568
|
+
],
|
|
569
|
+
})
|
|
570
|
+
], Protein.prototype, "float", null);
|
|
571
|
+
Protein = __decorate([
|
|
572
|
+
node({
|
|
573
|
+
key: "protein",
|
|
574
|
+
parentKey: "node",
|
|
575
|
+
forkable: true,
|
|
576
|
+
layout: {
|
|
577
|
+
children: "freeform",
|
|
578
|
+
acceptsChildren: false,
|
|
579
|
+
defaultWidthMode: "fixed",
|
|
580
|
+
defaultHeightMode: "fixed",
|
|
581
|
+
},
|
|
582
|
+
seed: {
|
|
583
|
+
width: 720,
|
|
584
|
+
height: 560,
|
|
585
|
+
},
|
|
586
|
+
})
|
|
587
|
+
], Protein);
|
|
588
|
+
export { Protein };
|
|
589
|
+
/** A palette flattened for {@link Protein.built}'s cache key. */
|
|
590
|
+
function paletteKey(palette) {
|
|
591
|
+
return [
|
|
592
|
+
palette.helix,
|
|
593
|
+
palette.sheet,
|
|
594
|
+
palette.coil,
|
|
595
|
+
palette.residueStart,
|
|
596
|
+
palette.residueEnd,
|
|
597
|
+
...palette.chains,
|
|
598
|
+
].join(",");
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* The radius the camera frames: the furthest *drawn* thing from the centroid.
|
|
602
|
+
*
|
|
603
|
+
* Measured over the placements rather than over the atom list, so it covers
|
|
604
|
+
* exactly what will appear — the backbone runs when a ribbon is showing, the
|
|
605
|
+
* atoms when it isn't, and neither the hidden waters nor the ligands somebody
|
|
606
|
+
* switched off.
|
|
607
|
+
*
|
|
608
|
+
* Falls back to the whole structure's radius when nothing is drawn at all, so a
|
|
609
|
+
* node whose filters have hidden everything still has a camera distance rather
|
|
610
|
+
* than a zero.
|
|
611
|
+
*/
|
|
612
|
+
function framingRadius(built, structure) {
|
|
613
|
+
const { center } = structure;
|
|
614
|
+
let furthest = 0;
|
|
615
|
+
const reach = (x, y, z) => {
|
|
616
|
+
const d = Math.hypot(x - center.x, y - center.y, z - center.z);
|
|
617
|
+
if (d > furthest)
|
|
618
|
+
furthest = d;
|
|
619
|
+
};
|
|
620
|
+
for (const sphere of built.spheres) {
|
|
621
|
+
const [x, y, z] = sphere.position;
|
|
622
|
+
reach(x, y, z);
|
|
623
|
+
}
|
|
624
|
+
for (const run of built.runs) {
|
|
625
|
+
for (const point of run.points)
|
|
626
|
+
reach(point.x, point.y, point.z);
|
|
627
|
+
}
|
|
628
|
+
// The cartoon's vertices rather than its residues: it is already a finished
|
|
629
|
+
// buffer by the time this runs, and reading it is both exact — a strand's
|
|
630
|
+
// arrowhead reaches wider than the α-carbons it was built from — and cheaper
|
|
631
|
+
// than keeping a parallel copy of the curve alive to measure instead.
|
|
632
|
+
for (const ribbon of built.ribbons) {
|
|
633
|
+
const position = ribbon.position;
|
|
634
|
+
for (let i = 0; i + 2 < position.length; i += 3) {
|
|
635
|
+
reach(position[i], position[i + 1], position[i + 2]);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
return furthest > 0 ? furthest : structure.radius;
|
|
639
|
+
}
|
|
640
|
+
function clampZoom(zoom) {
|
|
641
|
+
if (!Number.isFinite(zoom))
|
|
642
|
+
return DEFAULT_ZOOM;
|
|
643
|
+
return Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, zoom));
|
|
644
|
+
}
|
|
645
|
+
//# sourceMappingURL=protein.js.map
|