@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.
Files changed (53) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/LICENSE +201 -0
  3. package/dist/browser/index.js +4 -0
  4. package/dist/browser/index.js.map +7 -0
  5. package/dist/browser/manifest.json +11 -0
  6. package/dist/index.d.ts +3 -0
  7. package/dist/index.d.ts.map +1 -0
  8. package/dist/index.js +3 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/nodes.d.ts +18 -0
  11. package/dist/nodes.d.ts.map +1 -0
  12. package/dist/nodes.js +18 -0
  13. package/dist/nodes.js.map +1 -0
  14. package/dist/protein/chemistry.d.ts +52 -0
  15. package/dist/protein/chemistry.d.ts.map +1 -0
  16. package/dist/protein/chemistry.js +208 -0
  17. package/dist/protein/chemistry.js.map +1 -0
  18. package/dist/protein/index.d.ts +35 -0
  19. package/dist/protein/index.d.ts.map +1 -0
  20. package/dist/protein/index.js +35 -0
  21. package/dist/protein/index.js.map +1 -0
  22. package/dist/protein/parse.d.ts +32 -0
  23. package/dist/protein/parse.d.ts.map +1 -0
  24. package/dist/protein/parse.js +387 -0
  25. package/dist/protein/parse.js.map +1 -0
  26. package/dist/protein/protein.d.ts +265 -0
  27. package/dist/protein/protein.d.ts.map +1 -0
  28. package/dist/protein/protein.js +645 -0
  29. package/dist/protein/protein.js.map +1 -0
  30. package/dist/protein/ribbon.d.ts +83 -0
  31. package/dist/protein/ribbon.d.ts.map +1 -0
  32. package/dist/protein/ribbon.js +468 -0
  33. package/dist/protein/ribbon.js.map +1 -0
  34. package/dist/protein/shared.d.ts +221 -0
  35. package/dist/protein/shared.d.ts.map +1 -0
  36. package/dist/protein/shared.js +478 -0
  37. package/dist/protein/shared.js.map +1 -0
  38. package/dist/protein/structure.d.ts +184 -0
  39. package/dist/protein/structure.d.ts.map +1 -0
  40. package/dist/protein/structure.js +324 -0
  41. package/dist/protein/structure.js.map +1 -0
  42. package/package.json +64 -3
  43. package/registry.json +22 -0
  44. package/src/index.ts +2 -0
  45. package/src/nodes.ts +18 -0
  46. package/src/protein/chemistry.ts +223 -0
  47. package/src/protein/index.ts +34 -0
  48. package/src/protein/parse.ts +427 -0
  49. package/src/protein/protein.ts +897 -0
  50. package/src/protein/ribbon.ts +658 -0
  51. package/src/protein/shared.ts +622 -0
  52. package/src/protein/structure.ts +491 -0
  53. 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