@czi-sds/data-viz 2.4.3 → 2.5.1

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.
@@ -0,0 +1,2157 @@
1
+ "use client";
2
+ import { n as _objectWithoutProperties, t as _objectSpread2 } from "./objectSpread2.esm.js";
3
+ import { n as sampleColorScale, r as sampleSteppedScale, t as PLASMA_COLOR_SCALE } from "./colorScales.esm.js";
4
+ import { Vec3 } from "molstar/lib/mol-math/linear-algebra";
5
+ import { Loci } from "molstar/lib/mol-model/loci";
6
+ import { OrderedSet } from "molstar/lib/mol-data/int";
7
+ import { QueryContext, StructureElement, StructureProperties, StructureSelection } from "molstar/lib/mol-model/structure";
8
+ import { BondType } from "molstar/lib/mol-model/structure/model/types";
9
+ import { elementLabel } from "molstar/lib/mol-theme/label";
10
+ import { MolScriptBuilder } from "molstar/lib/mol-script/language/builder";
11
+ import { Color } from "molstar/lib/mol-util/color";
12
+ import { setSubtreeVisibility } from "molstar/lib/mol-plugin/behavior/static/state";
13
+ import { compile } from "molstar/lib/mol-script/runtime/query/compiler";
14
+ import { PluginContext } from "molstar/lib/mol-plugin/context";
15
+ import { DefaultPluginSpec } from "molstar/lib/mol-plugin/spec";
16
+ import { download } from "molstar/lib/mol-util/download";
17
+ //#region src/core/ProteinStructureViewer/utils/selection.ts
18
+ /**
19
+ * A stable string for a selection, for comparing one against another.
20
+ *
21
+ * A consumer holding the selection in state hands back a new object on every
22
+ * render, so identity says nothing about whether the selection changed. Order
23
+ * is normalized too: `{ residues: [2, 1] }` and `{ residues: [1, 2] }` select
24
+ * the same residues and must not read as a change.
25
+ */
26
+ function selectionKey(selection) {
27
+ var _selection$residues, _selection$chains;
28
+ const residues = [...new Set((_selection$residues = selection.residues) !== null && _selection$residues !== void 0 ? _selection$residues : [])].sort((a, b) => a - b);
29
+ return `${[...new Set((_selection$chains = selection.chains) !== null && _selection$chains !== void 0 ? _selection$chains : [])].sort().join(",")}|${residues.join(",")}`;
30
+ }
31
+ /** True when the selection names nothing at all. */
32
+ function isEmptySelection(selection) {
33
+ var _selection$residues2, _selection$chains2;
34
+ if (!selection) return true;
35
+ return !((_selection$residues2 = selection.residues) === null || _selection$residues2 === void 0 ? void 0 : _selection$residues2.length) && !((_selection$chains2 = selection.chains) === null || _selection$chains2 === void 0 ? void 0 : _selection$chains2.length);
36
+ }
37
+ /**
38
+ * The residues a selection covers, ascending and without repeats.
39
+ *
40
+ * Chains are expanded through `residuesByChain`, which was read off the loaded
41
+ * structure, so a chain contributes exactly the residues it holds rather than
42
+ * the span between its ends. A residue named directly and also carried by a
43
+ * selected chain is counted once.
44
+ */
45
+ function selectionResidues(selection, residuesByChain) {
46
+ var _selection$residues3, _selection$chains3;
47
+ const residues = new Set((_selection$residues3 = selection.residues) !== null && _selection$residues3 !== void 0 ? _selection$residues3 : []);
48
+ for (const chainId of (_selection$chains3 = selection.chains) !== null && _selection$chains3 !== void 0 ? _selection$chains3 : []) {
49
+ var _residuesByChain$get;
50
+ for (const index of (_residuesByChain$get = residuesByChain.get(chainId)) !== null && _residuesByChain$get !== void 0 ? _residuesByChain$get : []) residues.add(index);
51
+ }
52
+ return [...residues].sort((a, b) => a - b);
53
+ }
54
+ /**
55
+ * Names a selection for the legend's readout.
56
+ *
57
+ * A single chain is named as one, since "Chain A" says more than the number of
58
+ * residues it happens to hold. Anything else is reported as a count, which is
59
+ * the only thing true of every shape a selection can take. A lone residue is
60
+ * named by the caller instead, which reads it back off the structure to get its
61
+ * three-letter code and the number the file gives it.
62
+ */
63
+ function selectionLabel(selection, residueCount) {
64
+ var _selection$chains4, _selection$residues4;
65
+ const chains = (_selection$chains4 = selection.chains) !== null && _selection$chains4 !== void 0 ? _selection$chains4 : [];
66
+ if (chains.length === 1 && !((_selection$residues4 = selection.residues) === null || _selection$residues4 === void 0 ? void 0 : _selection$residues4.length)) return `Chain ${chains[0]}`;
67
+ return `${residueCount} residues`;
68
+ }
69
+ /** The mean of the values `read` returns, ignoring residues without one. */
70
+ function meanOverResidues(residues, read) {
71
+ let sum = 0;
72
+ let count = 0;
73
+ for (const index of residues) {
74
+ const value = read(index);
75
+ if (value === null || value === void 0) continue;
76
+ sum += value;
77
+ count++;
78
+ }
79
+ return count === 0 ? null : sum / count;
80
+ }
81
+ //#endregion
82
+ //#region src/core/ProteinStructureViewer/utils/residueLoci.ts
83
+ /**
84
+ * Every atom the predicate accepts, as geometry Mol* can focus, frame and mark,
85
+ * or undefined when it accepts none.
86
+ *
87
+ * One pass over the structure rather than a pass per residue: a whole-chain
88
+ * selection names hundreds of residues, and resolving them one at a time would
89
+ * walk the structure once for each.
90
+ *
91
+ * Atoms are bucketed by the unit they belong to because element indices are
92
+ * unit-local. That bucketing is also what lets a result span a chain break: it
93
+ * carries an entry per unit, which is what a range across two chains needs and
94
+ * what a single unit-local entry cannot express.
95
+ *
96
+ * Whole residues are collected rather than one atom each, since the caller
97
+ * frames the camera on the loci's bounding sphere and a lone atom would zoom
98
+ * far closer than the residue warrants.
99
+ */
100
+ function lociMatching(structure, matches) {
101
+ const location = StructureElement.Location.create(structure);
102
+ const elements = [];
103
+ for (const unit of structure.units) {
104
+ location.unit = unit;
105
+ const indices = [];
106
+ for (let i = 0; i < unit.elements.length; i++) {
107
+ location.element = unit.elements[i];
108
+ if (matches(location)) indices.push(i);
109
+ }
110
+ if (indices.length > 0) elements.push({
111
+ indices,
112
+ unit
113
+ });
114
+ }
115
+ if (elements.length === 0) return void 0;
116
+ return StructureElement.Loci(structure, elements.map(({ indices, unit }) => ({
117
+ indices: OrderedSet.ofSortedArray(indices),
118
+ unit
119
+ })));
120
+ }
121
+ /**
122
+ * Every atom of the residues a selection covers, within one structure.
123
+ *
124
+ * `hiddenChains` is subtracted from the result. A hidden chain is not drawn, so
125
+ * geometry built from it would have nothing to sit on: Mol*'s focus
126
+ * representation draws its own ball-and-stick from whatever is focused, in a
127
+ * part of the state tree the chain's own visibility does not reach, and a
128
+ * hidden chain left in the loci would keep showing up there after its cartoon
129
+ * had gone.
130
+ */
131
+ function lociForSelectionInStructure(structure, selection, hiddenChains) {
132
+ var _selection$residues, _selection$chains;
133
+ if (isEmptySelection(selection)) return void 0;
134
+ const residues = new Set((_selection$residues = selection.residues) !== null && _selection$residues !== void 0 ? _selection$residues : []);
135
+ const chains = new Set((_selection$chains = selection.chains) !== null && _selection$chains !== void 0 ? _selection$chains : []);
136
+ return lociMatching(structure, (location) => {
137
+ const chainId = StructureProperties.chain.auth_asym_id(location);
138
+ if (hiddenChains === null || hiddenChains === void 0 ? void 0 : hiddenChains.has(chainId)) return false;
139
+ return residues.has(StructureProperties.residue.key(location)) || chains.has(chainId);
140
+ });
141
+ }
142
+ /**
143
+ * Every atom of the residues between two 0-based indices, inclusive.
144
+ *
145
+ * A contiguous span is tested by comparison rather than by expanding it into a
146
+ * set of indices: this runs on every residue a drag crosses, and a drag can
147
+ * span the whole structure.
148
+ */
149
+ function lociForResidueRange(structure, min, max) {
150
+ return lociMatching(structure, (location) => {
151
+ const index = StructureProperties.residue.key(location);
152
+ return index >= min && index <= max;
153
+ });
154
+ }
155
+ /**
156
+ * The same, across every structure loaded into a plugin. Results are merged
157
+ * into the first structure that matched: a loci belongs to one structure, and
158
+ * the residue index space already spans them as though they were one.
159
+ */
160
+ function lociForSelection(plugin, selection, hiddenChains) {
161
+ for (const entry of plugin.managers.structure.hierarchy.current.structures) {
162
+ var _entry$cell$obj;
163
+ const structure = (_entry$cell$obj = entry.cell.obj) === null || _entry$cell$obj === void 0 ? void 0 : _entry$cell$obj.data;
164
+ if (!structure) continue;
165
+ const loci = lociForSelectionInStructure(structure, selection, hiddenChains);
166
+ if (loci) return loci;
167
+ }
168
+ }
169
+ /**
170
+ * Every atom on chains other than the named ones, within one structure - what
171
+ * dimming a component that spans chains dims, leaving the lit chains as they
172
+ * are.
173
+ */
174
+ function lociOutsideChains(structure, chains) {
175
+ return lociMatching(structure, (location) => !chains.has(StructureProperties.chain.auth_asym_id(location)));
176
+ }
177
+ /** Every atom of a set of residues, within one structure. */
178
+ function lociForResidueIndices(structure, residueIndices) {
179
+ return lociForSelectionInStructure(structure, { residues: [...residueIndices] });
180
+ }
181
+ //#endregion
182
+ //#region src/core/ProteinStructureViewer/scene/camera.ts
183
+ /** How far two camera readings may differ and still be the same camera. */
184
+ const CAMERA_EPSILON = 1e-4;
185
+ const toTuple = (v) => [
186
+ v[0],
187
+ v[1],
188
+ v[2]
189
+ ];
190
+ /** Where the canvas camera is now, in the form `onCameraChange` reports. */
191
+ function readCameraState(canvas3d) {
192
+ const { camera } = canvas3d;
193
+ const snapshot = camera.getSnapshot();
194
+ return {
195
+ fov: snapshot.fov,
196
+ position: toTuple(snapshot.position),
197
+ projection: snapshot.mode,
198
+ radius: snapshot.radius,
199
+ radiusMax: snapshot.radiusMax,
200
+ target: toTuple(snapshot.target),
201
+ up: toTuple(snapshot.up),
202
+ viewport: {
203
+ height: camera.viewport.height,
204
+ width: camera.viewport.width
205
+ }
206
+ };
207
+ }
208
+ /** A camera state as the Mol* snapshot that puts the camera there. */
209
+ function cameraSnapshot(state) {
210
+ return {
211
+ fov: state.fov,
212
+ mode: state.projection,
213
+ position: Vec3.create(...state.position),
214
+ radius: state.radius,
215
+ radiusMax: state.radiusMax,
216
+ target: Vec3.create(...state.target),
217
+ up: Vec3.create(...state.up)
218
+ };
219
+ }
220
+ const near = (a, b) => Math.abs(a - b) <= CAMERA_EPSILON;
221
+ const nearAll = (a, b) => a.every((value, i) => near(value, b[i]));
222
+ /** True when two readings place the camera in the same spot. */
223
+ function camerasEqual(a, b) {
224
+ return a.projection === b.projection && near(a.fov, b.fov) && near(a.radius, b.radius) && nearAll(a.position, b.position) && nearAll(a.target, b.target) && nearAll(a.up, b.up);
225
+ }
226
+ /**
227
+ * The center of the named residues, or undefined when the structure holds
228
+ * none of them - which is what an orientation relative to highlights falls
229
+ * back to the overview from.
230
+ */
231
+ function residuesCenter(structure, residueIndices) {
232
+ if (residueIndices.length === 0) return void 0;
233
+ const loci = lociForResidueIndices(structure, residueIndices);
234
+ const sphere = loci && Loci.getBoundingSphere(loci);
235
+ return sphere ? Vec3.clone(sphere.center) : void 0;
236
+ }
237
+ /**
238
+ * Looks at the scene from one side or another of the named residues.
239
+ *
240
+ * Resolved against the scene as Mol* draws it, when the reset it is handed to
241
+ * comes round, rather than when it is asked for: the visible bounding sphere
242
+ * is only known once what is visible has been drawn, and a structure just
243
+ * loaded has not been yet.
244
+ *
245
+ * `facing` looks at the residues from outside the structure, down the line
246
+ * from its center through theirs; `opposite` looks from the far side; `side`
247
+ * looks across that line. Without residues, `overview` looks down the view's
248
+ * default axis, with up as up.
249
+ */
250
+ function orientationSnapshot(orientation, residues) {
251
+ return (scene, camera) => {
252
+ const { center, radius } = scene.boundingSphereVisible;
253
+ const outward = Vec3.create(0, 0, 1);
254
+ if (orientation !== "overview" && residues) {
255
+ Vec3.sub(outward, residues, center);
256
+ if (Vec3.magnitude(outward) < 1e-6) Vec3.set(outward, 0, 0, 1);
257
+ Vec3.normalize(outward, outward);
258
+ }
259
+ const reference = Math.abs(outward[1]) < .9 ? Vec3.create(0, 1, 0) : Vec3.create(1, 0, 0);
260
+ const up = Vec3.normalize(Vec3(), Vec3.sub(Vec3(), reference, Vec3.scale(Vec3(), outward, Vec3.dot(reference, outward))));
261
+ let direction = outward;
262
+ if (orientation === "opposite") direction = Vec3.negate(Vec3(), outward);
263
+ if (orientation === "side") direction = Vec3.normalize(Vec3(), Vec3.cross(Vec3(), outward, up));
264
+ return camera.getInvariantFocus(center, radius, up, Vec3.negate(Vec3(), direction));
265
+ };
266
+ }
267
+ /**
268
+ * Places the camera on a structure just loaded: at `initialCamera` when there
269
+ * is one, turned to `orientation` when there is one, and otherwise fitted to
270
+ * the structure when `fit` asks for it.
271
+ *
272
+ * Handed to Mol* as a camera reset, which waits for the scene to be drawn: set
273
+ * directly, the camera would be fitted again by Mol* the first time something
274
+ * is drawn into the empty scene.
275
+ *
276
+ * Returns the orientation now in force, so a change to it can be told from the
277
+ * one the load already applied. `fitDurationMs` is how long a plain fit
278
+ * animates, Mol*'s own default when left out.
279
+ */
280
+ function frameStructure(canvas3d, framing, residues, fit, fitDurationMs) {
281
+ const { initialCamera, orientation, projection } = framing;
282
+ if (projection) canvas3d.setProps({ camera: { mode: projection } });
283
+ if (initialCamera) {
284
+ canvas3d.setProps({ camera: { mode: initialCamera.projection } });
285
+ canvas3d.requestCameraReset({
286
+ durationMs: 0,
287
+ snapshot: cameraSnapshot(initialCamera)
288
+ });
289
+ } else if (orientation) canvas3d.requestCameraReset({
290
+ durationMs: 0,
291
+ snapshot: orientationSnapshot(orientation, residues)
292
+ });
293
+ else if (fit && fitDurationMs === void 0) canvas3d.requestCameraReset();
294
+ else if (fit) canvas3d.requestCameraReset({ durationMs: fitDurationMs });
295
+ return orientation;
296
+ }
297
+ //#endregion
298
+ //#region src/core/ProteinStructureViewer/utils/chains.ts
299
+ /**
300
+ * The polymer of one chain, as named in the file. What each chain's cartoon is
301
+ * built from, and so what makes a chain a thing Mol* can hide on its own.
302
+ *
303
+ * Paired with `chainLigandExpression`: between them they cover everything on a
304
+ * chain the viewer draws, split by what it draws it as. The polymer
305
+ * restriction is the one Mol*'s `polymer-cartoon` preset applied, and is what
306
+ * keeps a heme from being handed to a representation that can only trace a
307
+ * backbone.
308
+ */
309
+ function chainPolymerExpression(chainId) {
310
+ return MolScriptBuilder.struct.generator.atomGroups({
311
+ "chain-test": MolScriptBuilder.core.rel.eq([MolScriptBuilder.ammp("auth_asym_id"), chainId]),
312
+ "entity-test": MolScriptBuilder.core.rel.eq([MolScriptBuilder.ammp("entityType"), "polymer"])
313
+ });
314
+ }
315
+ /**
316
+ * Bonds worth following out of a ligand: the ones holding it to the protein.
317
+ *
318
+ * Mol*'s own pair, and metallic coordination is the half that matters most -
319
+ * a heme's iron is held to its histidine by a coordination bond and not a
320
+ * covalent one, so covalent alone would leave every metalloprotein's cofactor
321
+ * looking dropped in rather than bound.
322
+ */
323
+ const CONNECTING_BOND = MolScriptBuilder.core.flags.hasAny([MolScriptBuilder.struct.bondProperty.flags(), MolScriptBuilder.core.type.bitflags([BondType.fromNames(["covalent", "metal-coordination"])])]);
324
+ /**
325
+ * The heteroatoms of one chain - ligands, ions, glycans, lipids - along with
326
+ * the residues holding them, which is what its ball-and-stick is built from.
327
+ *
328
+ * `non-polymer` is the one entity type a PDB's HETATM records land under,
329
+ * ligands and ions alike, so there is nothing finer to test here: the
330
+ * distinction Mol*'s presets draw between their `ligand`, `ion` and `lipid`
331
+ * components is a residue-name lookup made to give each a slightly different
332
+ * style, and all three are sticks. `branched` is the separate type glycans
333
+ * arrive as.
334
+ *
335
+ * Water is the third type and is left out of the seed. Every solvent molecule
336
+ * drawn as sticks buries the structure they surround, which is why Mol* draws
337
+ * them at 60% alpha rather than plain, and none of them is what "show the
338
+ * ligands" asks for.
339
+ *
340
+ * One layer of bonds out from there, taken as whole residues, is what brings
341
+ * the binding residue along - myoglobin's His93, say. Without it the heme
342
+ * floats unattached in the middle of a cartoon it is visibly bonded to, and
343
+ * the bond drawn to its iron ends in mid-air. One layer and no more: the
344
+ * residue that holds the ligand, not that residue's own neighbours.
345
+ *
346
+ * Intersected back with the chain, so everything a chain is drawn with belongs
347
+ * to that chain. A ligand bonded across an interface would otherwise leave the
348
+ * partner chain's residue in this chain's component, somewhere hiding the
349
+ * partner chain could not reach.
350
+ */
351
+ function chainLigandExpression(chainId) {
352
+ const onThisChain = MolScriptBuilder.core.rel.eq([MolScriptBuilder.ammp("auth_asym_id"), chainId]);
353
+ return MolScriptBuilder.struct.modifier.union([MolScriptBuilder.struct.modifier.intersectBy({
354
+ 0: MolScriptBuilder.struct.modifier.includeConnected({
355
+ 0: MolScriptBuilder.struct.generator.atomGroups({
356
+ "chain-test": onThisChain,
357
+ "entity-test": MolScriptBuilder.core.set.has([MolScriptBuilder.set("non-polymer", "branched"), MolScriptBuilder.ammp("entityType")])
358
+ }),
359
+ "as-whole-residues": true,
360
+ "bond-test": CONNECTING_BOND,
361
+ "layer-count": 1
362
+ }),
363
+ by: MolScriptBuilder.struct.generator.atomGroups({ "chain-test": onThisChain })
364
+ })]);
365
+ }
366
+ /**
367
+ * Matches how the sequence panel captions its chains, which reaches the same
368
+ * label through Mol*'s own `getChainOptions`. Deriving it the same way is what
369
+ * keeps a chain from going by one name in the legend and another in the panel.
370
+ */
371
+ const CHAIN_LABEL_OPTIONS = {
372
+ granularity: "chain",
373
+ hidePrefix: true,
374
+ htmlStyling: false
375
+ };
376
+ /**
377
+ * Reads the chains of a structure and the polymer residues on each.
378
+ *
379
+ * Residue indices are Mol*'s residue keys, counting residues in file order
380
+ * across the whole structure, so what is reported here addresses the same
381
+ * residues as `plddt`, `residueOverlay` and `selection`.
382
+ *
383
+ * Heteroatoms are drawn (see `chainLigandExpression`) but are not counted
384
+ * here. A heme is a residue to the file and not to a reader: it is no part of
385
+ * the sequence the panel lists, and averaging a confidence score over it would
386
+ * divide by a residue that has none.
387
+ *
388
+ * Residues are gathered into a set rather than inferred from the ends: a chain
389
+ * carrying several symmetry operators repeats each of its residues once per
390
+ * operator, and a structure whose chains interleave leaves a chain's indices
391
+ * non-contiguous. Neither is what a predicted complex looks like, but a range
392
+ * would misread both.
393
+ */
394
+ function scanChains(structure) {
395
+ const accumulators = /* @__PURE__ */ new Map();
396
+ const location = StructureElement.Location.create(structure);
397
+ for (const unit of structure.units) {
398
+ location.unit = unit;
399
+ for (let i = 0; i < unit.elements.length; i++) {
400
+ location.element = unit.elements[i];
401
+ const chainId = StructureProperties.chain.auth_asym_id(location);
402
+ let chain = accumulators.get(chainId);
403
+ if (!chain) {
404
+ chain = {
405
+ chainId,
406
+ label: elementLabel(location, CHAIN_LABEL_OPTIONS),
407
+ residues: /* @__PURE__ */ new Set()
408
+ };
409
+ accumulators.set(chainId, chain);
410
+ }
411
+ if (StructureProperties.entity.type(location) !== "polymer") continue;
412
+ chain.residues.add(StructureProperties.residue.key(location));
413
+ }
414
+ }
415
+ const ordered = [...accumulators.values()].filter((chain) => chain.residues.size > 0).map((chain) => _objectSpread2(_objectSpread2({}, chain), {}, { residues: [...chain.residues].sort((a, b) => a - b) })).sort((a, b) => {
416
+ var _a$residues$, _b$residues$;
417
+ return ((_a$residues$ = a.residues[0]) !== null && _a$residues$ !== void 0 ? _a$residues$ : 0) - ((_b$residues$ = b.residues[0]) !== null && _b$residues$ !== void 0 ? _b$residues$ : 0);
418
+ });
419
+ return {
420
+ chainLabels: new Map([...accumulators.values()].map(({ chainId, label }) => [chainId, label])),
421
+ chains: ordered.map(({ chainId, label, residues }) => {
422
+ var _residues, _residues$;
423
+ return {
424
+ chainId,
425
+ endIndex: (_residues = residues[residues.length - 1]) !== null && _residues !== void 0 ? _residues : 0,
426
+ label,
427
+ residueCount: residues.length,
428
+ startIndex: (_residues$ = residues[0]) !== null && _residues$ !== void 0 ? _residues$ : 0
429
+ };
430
+ }),
431
+ residuesByChain: new Map(ordered.map(({ chainId, residues }) => [chainId, residues]))
432
+ };
433
+ }
434
+ /** True when the two lists name the same chains, in the same order. */
435
+ function chainsEqual(a, b) {
436
+ return a.length === b.length && a.every((chain, i) => {
437
+ const other = b[i];
438
+ return other !== void 0 && chain.chainId === other.chainId && chain.startIndex === other.startIndex && chain.endIndex === other.endIndex && chain.residueCount === other.residueCount;
439
+ });
440
+ }
441
+ //#endregion
442
+ //#region src/core/ProteinStructureViewer/utils/residueAddress.ts
443
+ const _excluded = ["addresses"];
444
+ /**
445
+ * A residue's address as one comparable string.
446
+ *
447
+ * Insertion codes are trimmed, so a blank one and an absent one are the same
448
+ * code: a file writes "no insertion" as a space, and a caller as nothing.
449
+ */
450
+ function residueAddressKey(address) {
451
+ var _address$insCode;
452
+ const insCode = ((_address$insCode = address.insCode) !== null && _address$insCode !== void 0 ? _address$insCode : "").trim();
453
+ return `${address.chainId}\u0000${address.seqId}\u0000${insCode}`;
454
+ }
455
+ /**
456
+ * Every residue's 0-based index by its address, from one pass over the
457
+ * structure: what turns the address a caller knows a residue by into the index
458
+ * the viewer keys everything else on.
459
+ *
460
+ * A residue carrying several symmetry operators is indexed once, under the
461
+ * first, as `ChainRef` counts its chain once.
462
+ */
463
+ function indexResidueAddresses(structure) {
464
+ const index = /* @__PURE__ */ new Map();
465
+ const location = StructureElement.Location.create(structure);
466
+ for (const unit of structure.units) {
467
+ location.unit = unit;
468
+ let previous = -1;
469
+ for (let i = 0; i < unit.elements.length; i++) {
470
+ location.element = unit.elements[i];
471
+ const residue = StructureProperties.residue.key(location);
472
+ if (residue === previous) continue;
473
+ previous = residue;
474
+ const key = residueAddressKey({
475
+ chainId: StructureProperties.chain.auth_asym_id(location),
476
+ insCode: StructureProperties.residue.pdbx_PDB_ins_code(location),
477
+ seqId: StructureProperties.residue.auth_seq_id(location)
478
+ });
479
+ if (!index.has(key)) index.set(key, residue);
480
+ }
481
+ }
482
+ return index;
483
+ }
484
+ /** The indices of the addresses the structure has, in the order given. */
485
+ function resolveResidueAddresses(index, addresses) {
486
+ const resolved = [];
487
+ for (const address of addresses) {
488
+ const residue = index.get(residueAddressKey(address));
489
+ if (residue !== void 0) resolved.push(residue);
490
+ }
491
+ return resolved;
492
+ }
493
+ /**
494
+ * A selection with its addresses resolved into `residues`, which is the only
495
+ * form the rest of the viewer reads. Handed back unchanged when it names none.
496
+ */
497
+ function resolveSelectionAddresses(selection, index) {
498
+ var _selection$addresses, _rest$residues;
499
+ if (!(selection === null || selection === void 0 || (_selection$addresses = selection.addresses) === null || _selection$addresses === void 0 ? void 0 : _selection$addresses.length)) return selection;
500
+ const { addresses } = selection, rest = _objectWithoutProperties(selection, _excluded);
501
+ const residues = [...new Set([...(_rest$residues = rest.residues) !== null && _rest$residues !== void 0 ? _rest$residues : [], ...resolveResidueAddresses(index, addresses)])];
502
+ return _objectSpread2(_objectSpread2({}, rest), {}, { residues });
503
+ }
504
+ /** A stable string for a list of addresses, for comparing one with another. */
505
+ function residueAddressesKey(addresses) {
506
+ return (addresses !== null && addresses !== void 0 ? addresses : []).map(residueAddressKey).join("");
507
+ }
508
+ //#endregion
509
+ //#region src/core/ProteinStructureViewer/utils/structureFormat.ts
510
+ /** How much of the file to inspect when guessing the format. */
511
+ const DETECT_SAMPLE = 16384;
512
+ /**
513
+ * Infers whether `text` is PDB or mmCIF (PDBx).
514
+ *
515
+ * mmCIF documents start with a `data_` block and name their coordinates
516
+ * `_atom_site.`. PDB never does either, so either marker is enough. A leading
517
+ * comment (`# ...`) before `data_` is still mmCIF.
518
+ */
519
+ function detectStructureFormat(text) {
520
+ const sample = text.slice(0, DETECT_SAMPLE);
521
+ if (/(?:^|\n)\s*data_/.test(sample) || /_atom_site\./.test(sample)) return "mmcif";
522
+ return "pdb";
523
+ }
524
+ //#endregion
525
+ //#region \0@oxc-project+runtime@0.134.0/helpers/esm/asyncToGenerator.js
526
+ function asyncGeneratorStep(n, t, e, r, o, a, c) {
527
+ try {
528
+ var i = n[a](c), u = i.value;
529
+ } catch (n) {
530
+ e(n);
531
+ return;
532
+ }
533
+ i.done ? t(u) : Promise.resolve(u).then(r, o);
534
+ }
535
+ function _asyncToGenerator(n) {
536
+ return function() {
537
+ var t = this, e = arguments;
538
+ return new Promise(function(r, o) {
539
+ var a = n.apply(t, e);
540
+ function _next(n) {
541
+ asyncGeneratorStep(a, r, o, _next, _throw, "next", n);
542
+ }
543
+ function _throw(n) {
544
+ asyncGeneratorStep(a, r, o, _next, _throw, "throw", n);
545
+ }
546
+ _next(void 0);
547
+ });
548
+ };
549
+ }
550
+ //#endregion
551
+ //#region src/core/ProteinStructureViewer/scene/load.ts
552
+ const NOTHING_LOADED = {
553
+ addressIndex: /* @__PURE__ */ new Map(),
554
+ atomCount: 0,
555
+ chainLabels: /* @__PURE__ */ new Map(),
556
+ chains: [],
557
+ residuesByChain: /* @__PURE__ */ new Map()
558
+ };
559
+ /**
560
+ * Clears the plugin and parses the structure into it, reading off what the
561
+ * rest of the viewer addresses it by. Nothing is drawn here.
562
+ *
563
+ * The format is detected from the text - PDB or mmCIF - and handed to Mol*.
564
+ */
565
+ function parseStructure(_x, _x2) {
566
+ return _parseStructure.apply(this, arguments);
567
+ }
568
+ function _parseStructure() {
569
+ _parseStructure = _asyncToGenerator(function* (plugin, structureText) {
570
+ try {
571
+ yield plugin.clear();
572
+ const data = yield plugin.builders.data.rawData({
573
+ data: structureText,
574
+ label: "Structure"
575
+ });
576
+ const trajectory = yield plugin.builders.structure.parseTrajectory(data, detectStructureFormat(structureText));
577
+ const model = yield plugin.builders.structure.createModel(trajectory);
578
+ const structure = yield plugin.builders.structure.createStructure(model, {
579
+ name: "model",
580
+ params: {}
581
+ });
582
+ const data3d = structure.data;
583
+ if (!data3d) return {
584
+ loaded: NOTHING_LOADED,
585
+ ok: true
586
+ };
587
+ const { chainLabels, chains, residuesByChain } = scanChains(data3d);
588
+ return {
589
+ loaded: {
590
+ addressIndex: indexResidueAddresses(data3d),
591
+ atomCount: data3d.elementCount,
592
+ chainLabels,
593
+ chains,
594
+ residuesByChain,
595
+ structure,
596
+ structureData: data3d
597
+ },
598
+ ok: true
599
+ };
600
+ } catch (error) {
601
+ return {
602
+ error,
603
+ ok: false
604
+ };
605
+ }
606
+ });
607
+ return _parseStructure.apply(this, arguments);
608
+ }
609
+ /** What a load reports to a consumer. */
610
+ function loadInfo(loaded) {
611
+ return {
612
+ atomCount: loaded.atomCount,
613
+ chains: loaded.chains,
614
+ residueCount: loaded.chains.reduce((count, chain) => count + chain.residueCount, 0)
615
+ };
616
+ }
617
+ //#endregion
618
+ //#region src/common/chainColors.ts
619
+ /**
620
+ * Default colors for telling chains apart, in assignment order.
621
+ *
622
+ * The Okabe-Ito qualitative palette, which is built to stay distinguishable
623
+ * under every common form of color blindness - the reason to prefer it over a
624
+ * hand-picked set, since a complex is read by which chain is which and a
625
+ * viewer that collapses two of them into one color stops answering that.
626
+ *
627
+ * Its black is left out: the canvas inverts with the theme, so a chain painted
628
+ * black would vanish against the dark background. The rest carry enough
629
+ * lightness to hold up against both.
630
+ *
631
+ * Chains past the end of the palette wrap around to the start.
632
+ */
633
+ const CHAIN_COLOR_PALETTE = [
634
+ "#009E73",
635
+ "#0072B2",
636
+ "#E69F00",
637
+ "#CC79A7",
638
+ "#56B4E9",
639
+ "#D55E00",
640
+ "#F0E442"
641
+ ];
642
+ /**
643
+ * Assigns each chain a color, in the order given, with `overrides` taking
644
+ * precedence by `chainId`.
645
+ *
646
+ * Palette positions are spent on every chain whether or not it is overridden,
647
+ * so overriding one chain does not shift the colors of the others.
648
+ */
649
+ function chainColorMap(chainIds, overrides) {
650
+ const colors = /* @__PURE__ */ new Map();
651
+ chainIds.forEach((chainId, i) => {
652
+ var _overrides$chainId;
653
+ const fallback = CHAIN_COLOR_PALETTE[i % CHAIN_COLOR_PALETTE.length];
654
+ colors.set(chainId, (_overrides$chainId = overrides === null || overrides === void 0 ? void 0 : overrides[chainId]) !== null && _overrides$chainId !== void 0 ? _overrides$chainId : fallback);
655
+ });
656
+ return colors;
657
+ }
658
+ //#endregion
659
+ //#region src/core/ProteinStructureViewer/utils/color.ts
660
+ /** Parses `#RRGGBB` (or `RRGGBB`) into a Mol* `Color`. */
661
+ function parseHexColor(hex) {
662
+ const h = hex.replace("#", "");
663
+ return Color.fromRgb(parseInt(h.substring(0, 2), 16), parseInt(h.substring(2, 4), 16), parseInt(h.substring(4, 6), 16));
664
+ }
665
+ /**
666
+ * Parses a theme color into a Mol* `Color`, falling back when the token is
667
+ * missing or is a format Mol* cannot consume (theme values are hex in practice,
668
+ * but the palette types allow any CSS color string).
669
+ */
670
+ function themeColor(value, fallback) {
671
+ const candidate = value === null || value === void 0 ? void 0 : value.trim();
672
+ return parseHexColor(candidate !== void 0 && /^#?[0-9a-f]{6}$/i.test(candidate) ? candidate : fallback);
673
+ }
674
+ /**
675
+ * Neutral fill for residues with no overlay value. Sits between the light and
676
+ * dark backgrounds so unhighlighted geometry stays legible in both modes.
677
+ */
678
+ function neutralResidueColor(mode) {
679
+ const g = mode === "dark" ? 100 : 180;
680
+ return Color.fromRgb(g, g, g);
681
+ }
682
+ //#endregion
683
+ //#region src/core/ProteinStructureViewer/utils/highlights.ts
684
+ /**
685
+ * Colors for highlights that name none, in the order the highlights are given.
686
+ * Saturated enough to stand out over a chain, pLDDT or overlay coloring, and
687
+ * wrapping around once they run out.
688
+ */
689
+ const HIGHLIGHT_COLOR_PALETTE = [
690
+ "#E64545",
691
+ "#F0C229",
692
+ "#12A9A0",
693
+ "#BD57D4",
694
+ "#FF8A32",
695
+ "#559DF3"
696
+ ];
697
+ /**
698
+ * Resolves highlights to the residues they name and the colors they are
699
+ * painted. A highlight naming a residue the structure does not have paints
700
+ * nothing, and still spends its place in the palette, so the others keep
701
+ * their colors whichever structure they land on.
702
+ */
703
+ function resolveHighlights(highlights, addressIndex) {
704
+ const colors = /* @__PURE__ */ new Map();
705
+ const byChain = /* @__PURE__ */ new Map();
706
+ (highlights !== null && highlights !== void 0 ? highlights : []).forEach((highlight, i) => {
707
+ var _byChain$get;
708
+ const residue = addressIndex.get(residueAddressKey(highlight));
709
+ if (residue === void 0) return;
710
+ const fallback = HIGHLIGHT_COLOR_PALETTE[i % HIGHLIGHT_COLOR_PALETTE.length];
711
+ colors.set(residue, themeColor(highlight.color, fallback));
712
+ const onChain = (_byChain$get = byChain.get(highlight.chainId)) !== null && _byChain$get !== void 0 ? _byChain$get : [];
713
+ if (!onChain.includes(residue)) onChain.push(residue);
714
+ byChain.set(highlight.chainId, onChain);
715
+ });
716
+ return {
717
+ byChain,
718
+ colors
719
+ };
720
+ }
721
+ /** A stable string for a set of highlights, colors included. */
722
+ function highlightsKey(highlights) {
723
+ return (highlights !== null && highlights !== void 0 ? highlights : []).map((highlight) => {
724
+ var _highlight$color;
725
+ return `${residueAddressKey(highlight)}\u0000${(_highlight$color = highlight.color) !== null && _highlight$color !== void 0 ? _highlight$color : ""}`;
726
+ }).join("");
727
+ }
728
+ //#endregion
729
+ //#region src/core/ProteinStructureViewer/utils/chainColorTheme.ts
730
+ /** Mol* color theme name under which per-chain coloring is registered. */
731
+ const CHAIN_COLOR_THEME_NAME = "chain-color";
732
+ /** Painted for a chain with no color assigned, or one assigned a bad value. */
733
+ const DEFAULT_CHAIN_COLOR_HEX = "#808080";
734
+ const DEFAULT_CHAIN_COLOR = Color.fromRgb(128, 128, 128);
735
+ /**
736
+ * Builds a Mol* color theme that paints each chain its own color.
737
+ *
738
+ * This stands in for Mol*'s built-in `chain-id` theme, which picks its own
739
+ * colors from a palette we cannot read back. Owning the assignment is what
740
+ * lets the chain legend show a swatch that matches the structure, rather than
741
+ * a key for colors it has to guess at.
742
+ *
743
+ * Created per plugin instance, and its state read live inside `color()`, for
744
+ * the same reasons as the residue value theme beside it: several viewers can
745
+ * color chains differently on one page, and recoloring does not rebuild the
746
+ * structure. `overrides` paints single residues - the highlights - over it.
747
+ */
748
+ function createChainColorTheme(overrides) {
749
+ const state = {
750
+ colors: /* @__PURE__ */ new Map(),
751
+ version: 0
752
+ };
753
+ const provider = {
754
+ category: "chain property",
755
+ defaultValues: {},
756
+ factory() {
757
+ const { colors } = state;
758
+ const overrideColors = overrides === null || overrides === void 0 ? void 0 : overrides.current;
759
+ const parsed = /* @__PURE__ */ new Map();
760
+ for (const [chainId, hex] of colors) parsed.set(chainId, themeColor(hex, DEFAULT_CHAIN_COLOR_HEX));
761
+ return {
762
+ color(location) {
763
+ var _parsed$get;
764
+ if (!StructureElement.Location.is(location)) return DEFAULT_CHAIN_COLOR;
765
+ const override = overrideColors === null || overrideColors === void 0 ? void 0 : overrideColors.get(StructureProperties.residue.key(location));
766
+ if (override !== void 0) return override;
767
+ const chainId = StructureProperties.chain.auth_asym_id(location);
768
+ return (_parsed$get = parsed.get(chainId)) !== null && _parsed$get !== void 0 ? _parsed$get : DEFAULT_CHAIN_COLOR;
769
+ },
770
+ contextHash: state.version,
771
+ description: "Color residues by the chain they sit on",
772
+ factory: provider.factory,
773
+ granularity: "group",
774
+ props: {}
775
+ };
776
+ },
777
+ getParams: () => ({}),
778
+ isApplicable: () => true,
779
+ label: "Chain Color",
780
+ name: CHAIN_COLOR_THEME_NAME
781
+ };
782
+ return {
783
+ provider,
784
+ setState(colors) {
785
+ state.colors = colors;
786
+ state.version++;
787
+ }
788
+ };
789
+ }
790
+ //#endregion
791
+ //#region src/core/ProteinStructureViewer/utils/plddt.ts
792
+ /**
793
+ * AlphaFold's pLDDT confidence bands. Thresholds are on the 0-1 scale that
794
+ * `plddt` values use, ascending from lowest to highest confidence.
795
+ */
796
+ const PLDDT_COLOR_SCALE = {
797
+ kind: "stepped",
798
+ stops: [
799
+ {
800
+ color: "#FF7C45",
801
+ label: "0.5",
802
+ threshold: .5
803
+ },
804
+ {
805
+ color: "#FFDB11",
806
+ label: "0.7",
807
+ threshold: .7
808
+ },
809
+ {
810
+ color: "#64CBF3",
811
+ label: "0.9",
812
+ threshold: .9
813
+ },
814
+ {
815
+ color: "#0053D5",
816
+ label: "1.0",
817
+ threshold: 1
818
+ }
819
+ ]
820
+ };
821
+ /**
822
+ * The band colors alone, ascending from least to most confident.
823
+ *
824
+ * What the chain legend shows in place of a single color while pLDDT is
825
+ * painting the structure: no one color stands for a chain then, so the swatch
826
+ * carries the whole key instead.
827
+ */
828
+ const PLDDT_BAND_COLORS = (PLDDT_COLOR_SCALE.kind === "stepped" ? PLDDT_COLOR_SCALE.stops : []).map((stop) => stop.color);
829
+ /**
830
+ * Rewrites a PDB file's B-factor column with pLDDT scores so Mol* can read them
831
+ * off the parsed model. B-factors occupy columns 60-66 of ATOM/HETATM lines;
832
+ * scores arrive on a 0-1 scale and are stored on the conventional 0-100 one.
833
+ * Residues past the end of `plddtValues` fall back to a mid-confidence 50.
834
+ *
835
+ * A residue is identified by chain, sequence number and insertion code. The
836
+ * code has to be part of that: `10` and `10A` are two residues, Mol* counts
837
+ * them as two, and treating them as one here would hand every residue after
838
+ * them its neighbour's score.
839
+ */
840
+ function injectPlddtIntoPdb(pdbData, plddtValues) {
841
+ const lines = pdbData.split("\n");
842
+ let residueIndex = -1;
843
+ let lastResidue = "";
844
+ return lines.map((line) => {
845
+ var _plddtValues$residueI;
846
+ if (!line.startsWith("ATOM") && !line.startsWith("HETATM")) return line;
847
+ const residue = line.substring(21, 27);
848
+ if (residue !== lastResidue) {
849
+ residueIndex++;
850
+ lastResidue = residue;
851
+ }
852
+ const bfactorStr = (residueIndex < plddtValues.length ? ((_plddtValues$residueI = plddtValues[residueIndex]) !== null && _plddtValues$residueI !== void 0 ? _plddtValues$residueI : 0) * 100 : 50).toFixed(2).padStart(6, " ");
853
+ const paddedLine = line.padEnd(66, " ");
854
+ return paddedLine.substring(0, 60) + bfactorStr + paddedLine.substring(66);
855
+ }).join("\n");
856
+ }
857
+ const ATOM_SITE_PREFIX = "_atom_site.";
858
+ const B_FACTOR_COLUMN = "B_iso_or_equiv";
859
+ function skipCifWhitespace(line, index) {
860
+ let i = index;
861
+ while (i < line.length && /\s/.test(line.charAt(i))) i += 1;
862
+ return i;
863
+ }
864
+ function readCifToken(line, start) {
865
+ const quote = line.charAt(start);
866
+ if (quote === "'" || quote === "\"") {
867
+ const end = line.indexOf(quote, start + 1);
868
+ if (end < 0) return {
869
+ next: line.length,
870
+ token: line.slice(start + 1)
871
+ };
872
+ return {
873
+ next: end + 1,
874
+ token: line.slice(start + 1, end)
875
+ };
876
+ }
877
+ let i = start;
878
+ while (i < line.length && !/\s/.test(line.charAt(i))) i += 1;
879
+ return {
880
+ next: i,
881
+ token: line.slice(start, i)
882
+ };
883
+ }
884
+ /**
885
+ * Splits one CIF line into tokens, honouring `'quoted'` and `"quoted"` values.
886
+ * Atom-site rows are one line each in practice; multiline `;text;` fields are
887
+ * not used there and are not handled.
888
+ */
889
+ function tokenizeCifLine(line) {
890
+ const tokens = [];
891
+ let i = skipCifWhitespace(line, 0);
892
+ while (i < line.length) {
893
+ const { next, token } = readCifToken(line, i);
894
+ tokens.push(token);
895
+ i = skipCifWhitespace(line, next);
896
+ }
897
+ return tokens;
898
+ }
899
+ function isCifNull(value) {
900
+ return value === void 0 || value === "." || value === "?";
901
+ }
902
+ function columnIndex(columns, name) {
903
+ return columns.indexOf(name);
904
+ }
905
+ /**
906
+ * Residue identity matching `injectPlddtIntoPdb`: chain + sequence number +
907
+ * insertion code. Prefers the `auth_*` columns Mol* reports on the parsed
908
+ * model, falling back to `label_*` when a file omitted them. `.` / `?`
909
+ * insertion codes count as empty, the way a blank PDB column 27 does.
910
+ */
911
+ function mmcifResidueKey(tokens, columns) {
912
+ var _ref, _ref2, _ref3, _ref4;
913
+ const authAsym = columnIndex(columns, "auth_asym_id");
914
+ const labelAsym = columnIndex(columns, "label_asym_id");
915
+ const authSeq = columnIndex(columns, "auth_seq_id");
916
+ const labelSeq = columnIndex(columns, "label_seq_id");
917
+ const ins = columnIndex(columns, "pdbx_PDB_ins_code");
918
+ return `${(_ref = (_ref2 = authAsym >= 0 ? tokens[authAsym] : void 0) !== null && _ref2 !== void 0 ? _ref2 : labelAsym >= 0 ? tokens[labelAsym] : void 0) !== null && _ref !== void 0 ? _ref : ""}|${(_ref3 = (_ref4 = authSeq >= 0 ? tokens[authSeq] : void 0) !== null && _ref4 !== void 0 ? _ref4 : labelSeq >= 0 ? tokens[labelSeq] : void 0) !== null && _ref3 !== void 0 ? _ref3 : ""}|${isCifNull(ins >= 0 ? tokens[ins] : void 0) ? "" : tokens[ins]}`;
919
+ }
920
+ function isAtomSiteRow(tokens, columns) {
921
+ const group = columnIndex(columns, "group_PDB");
922
+ if (group < 0) return true;
923
+ const value = tokens[group];
924
+ return value === "ATOM" || value === "HETATM";
925
+ }
926
+ function plddtBFactor(residueIndex, plddtValues) {
927
+ var _plddtValues$residueI2;
928
+ return (residueIndex < plddtValues.length ? ((_plddtValues$residueI2 = plddtValues[residueIndex]) !== null && _plddtValues$residueI2 !== void 0 ? _plddtValues$residueI2 : 0) * 100 : 50).toFixed(2);
929
+ }
930
+ function isLoopStart(line) {
931
+ return line.trim() === "loop_" || line.trim().startsWith("loop_");
932
+ }
933
+ function isAtomSiteColumn(line) {
934
+ return line.trim().startsWith(ATOM_SITE_PREFIX);
935
+ }
936
+ function endsAtomSiteLoop(line) {
937
+ const trimmed = line.trim();
938
+ if (trimmed === "") return false;
939
+ if (trimmed.startsWith("#")) return true;
940
+ if (isLoopStart(trimmed)) return true;
941
+ if (trimmed.startsWith("data_") || trimmed.startsWith("save_")) return true;
942
+ return trimmed.startsWith("_") && !trimmed.startsWith(ATOM_SITE_PREFIX);
943
+ }
944
+ function isAtomSiteLoop(lines, loopIndex) {
945
+ let peek = loopIndex + 1;
946
+ while (peek < lines.length && lines[peek].trim() === "") peek += 1;
947
+ return peek < lines.length && isAtomSiteColumn(lines[peek]);
948
+ }
949
+ function ensureBFactorColumn(columns, headerLines) {
950
+ const existing = columnIndex(columns, B_FACTOR_COLUMN);
951
+ if (existing >= 0) return existing;
952
+ columns.push(B_FACTOR_COLUMN);
953
+ headerLines.push(`${ATOM_SITE_PREFIX}${B_FACTOR_COLUMN}`);
954
+ return columns.length - 1;
955
+ }
956
+ function rewriteAtomSiteRow(dataLine, columns, bIndex, plddtValues, walk) {
957
+ if (dataLine.trim() === "") return dataLine;
958
+ const tokens = tokenizeCifLine(dataLine);
959
+ if (tokens.length === 0) return dataLine;
960
+ while (tokens.length < columns.length) tokens.push(".");
961
+ if (isAtomSiteRow(tokens, columns)) {
962
+ const residue = mmcifResidueKey(tokens, columns);
963
+ if (residue !== walk.lastResidue) {
964
+ walk.residueIndex += 1;
965
+ walk.lastResidue = residue;
966
+ }
967
+ tokens[bIndex] = plddtBFactor(walk.residueIndex, plddtValues);
968
+ }
969
+ return tokens.join(" ");
970
+ }
971
+ function rewriteAtomSiteLoop(lines, loopIndex, plddtValues, walk) {
972
+ const rows = [lines[loopIndex]];
973
+ let i = loopIndex + 1;
974
+ const columns = [];
975
+ while (i < lines.length && isAtomSiteColumn(lines[i])) {
976
+ columns.push(lines[i].trim().slice(11));
977
+ rows.push(lines[i]);
978
+ i += 1;
979
+ }
980
+ const bIndex = ensureBFactorColumn(columns, rows);
981
+ while (i < lines.length && !endsAtomSiteLoop(lines[i])) {
982
+ rows.push(rewriteAtomSiteRow(lines[i], columns, bIndex, plddtValues, walk));
983
+ i += 1;
984
+ }
985
+ return {
986
+ next: i,
987
+ rows
988
+ };
989
+ }
990
+ /**
991
+ * Rewrites an mmCIF (PDBx) file's `_atom_site.B_iso_or_equiv` column with
992
+ * pLDDT scores so Mol* can read them off the parsed model, the same way
993
+ * `injectPlddtIntoPdb` rewrites the PDB B-factor column. Scores arrive on a
994
+ * 0-1 scale and are stored on the conventional 0-100 one. Residues past the
995
+ * end of `plddtValues` fall back to a mid-confidence 50.
996
+ *
997
+ * If the file has no B-factor column, one is appended to the atom-site loop
998
+ * rather than leaving the scores behind.
999
+ */
1000
+ function injectPlddtIntoMmcif(mmcifData, plddtValues) {
1001
+ const lines = mmcifData.split("\n");
1002
+ const result = [];
1003
+ const walk = {
1004
+ lastResidue: "",
1005
+ residueIndex: -1
1006
+ };
1007
+ let i = 0;
1008
+ let rewrittenLoop = false;
1009
+ while (i < lines.length) {
1010
+ const line = lines[i];
1011
+ if (!rewrittenLoop && isLoopStart(line) && isAtomSiteLoop(lines, i)) {
1012
+ const rewritten = rewriteAtomSiteLoop(lines, i, plddtValues, walk);
1013
+ result.push(...rewritten.rows);
1014
+ i = rewritten.next;
1015
+ rewrittenLoop = true;
1016
+ continue;
1017
+ }
1018
+ result.push(line);
1019
+ i += 1;
1020
+ }
1021
+ return result.join("\n");
1022
+ }
1023
+ /**
1024
+ * Rewrites a structure file's B-factors with pLDDT scores. Dispatches to
1025
+ * `injectPlddtIntoPdb` or `injectPlddtIntoMmcif` from `format`, or from
1026
+ * detecting the format of `data` when `format` is omitted.
1027
+ */
1028
+ function injectPlddt(data, plddtValues, format = detectStructureFormat(data)) {
1029
+ return format === "mmcif" ? injectPlddtIntoMmcif(data, plddtValues) : injectPlddtIntoPdb(data, plddtValues);
1030
+ }
1031
+ //#endregion
1032
+ //#region src/core/ProteinStructureViewer/utils/plddtTheme.ts
1033
+ /** Mol* color theme name under which pLDDT coloring is registered. */
1034
+ const PLDDT_THEME_NAME = "plddt";
1035
+ const PLDDT_BANDS = PLDDT_COLOR_SCALE.kind === "stepped" ? PLDDT_COLOR_SCALE.stops : [];
1036
+ /** The band color for a score on the 0-1 scale `plddt` uses. */
1037
+ function plddtColor(score) {
1038
+ const [r, g, b] = sampleSteppedScale(PLDDT_BANDS, score);
1039
+ return Color.fromRgb(r, g, b);
1040
+ }
1041
+ /**
1042
+ * Builds a Mol* color theme that paints each residue its pLDDT band.
1043
+ *
1044
+ * Read from the scores themselves, by residue index, rather than written into
1045
+ * the file's B-factor column first. A column has no way to say "unscored", so a
1046
+ * residue without a score would have to be given one; here it reads neutral,
1047
+ * as it does under an overlay, and the structure text reaching Mol* is exactly
1048
+ * the text the consumer passed. Scores arriving after the structure recolor it
1049
+ * without reloading it, which leaves the camera where it was.
1050
+ *
1051
+ * Created per plugin instance for the same reasons as the other themes, and
1052
+ * `overrides` paints single residues - the highlights - over it.
1053
+ */
1054
+ function createPlddtTheme(mode, overrides) {
1055
+ const state = {
1056
+ mode,
1057
+ scores: [],
1058
+ version: 0
1059
+ };
1060
+ const provider = {
1061
+ category: "residue property",
1062
+ defaultValues: {},
1063
+ factory() {
1064
+ const { mode: currentMode, scores } = state;
1065
+ const neutral = neutralResidueColor(currentMode);
1066
+ const overrideColors = overrides === null || overrides === void 0 ? void 0 : overrides.current;
1067
+ return {
1068
+ color(location) {
1069
+ if (!StructureElement.Location.is(location)) return neutral;
1070
+ const residue = StructureProperties.residue.key(location);
1071
+ const override = overrideColors === null || overrideColors === void 0 ? void 0 : overrideColors.get(residue);
1072
+ if (override !== void 0) return override;
1073
+ const score = scores[residue];
1074
+ if (score === null || score === void 0 || !Number.isFinite(score)) return neutral;
1075
+ return plddtColor(score);
1076
+ },
1077
+ contextHash: state.version,
1078
+ description: "Color residues by pLDDT confidence",
1079
+ factory: provider.factory,
1080
+ granularity: "group",
1081
+ props: {}
1082
+ };
1083
+ },
1084
+ getParams: () => ({}),
1085
+ isApplicable: () => true,
1086
+ label: "pLDDT",
1087
+ name: PLDDT_THEME_NAME
1088
+ };
1089
+ return {
1090
+ provider,
1091
+ setState(next) {
1092
+ Object.assign(state, next);
1093
+ state.version++;
1094
+ }
1095
+ };
1096
+ }
1097
+ //#endregion
1098
+ //#region src/core/ProteinStructureViewer/utils/residueColorOverrides.ts
1099
+ function createResidueColorOverrides() {
1100
+ return { current: /* @__PURE__ */ new Map() };
1101
+ }
1102
+ //#endregion
1103
+ //#region src/core/ProteinStructureViewer/utils/residueValueTheme.ts
1104
+ /** Mol* color theme name under which residue value overlays are registered. */
1105
+ const RESIDUE_VALUE_THEME_NAME = "residue-value";
1106
+ /**
1107
+ * Builds a Mol* color theme that paints residues by an arbitrary per-residue
1108
+ * value.
1109
+ *
1110
+ * The theme is created per plugin instance rather than shared at module scope,
1111
+ * so several viewers can render different overlays on the same page. Its state
1112
+ * lives in the closure below and is read live inside `color()`, which is what
1113
+ * lets an overlay update recolor the structure without rebuilding it.
1114
+ * `overrides` paints single residues - the highlights - over it.
1115
+ */
1116
+ function createResidueValueTheme(mode, overrides) {
1117
+ const state = {
1118
+ colorScale: PLASMA_COLOR_SCALE,
1119
+ max: 1,
1120
+ min: 0,
1121
+ mode,
1122
+ values: /* @__PURE__ */ new Map(),
1123
+ version: 0
1124
+ };
1125
+ const provider = {
1126
+ category: "residue property",
1127
+ defaultValues: {},
1128
+ factory() {
1129
+ const { colorScale, max, min, mode: currentMode, values } = state;
1130
+ const neutral = neutralResidueColor(currentMode);
1131
+ const overrideColors = overrides === null || overrides === void 0 ? void 0 : overrides.current;
1132
+ return {
1133
+ color(location) {
1134
+ if (!StructureElement.Location.is(location)) return neutral;
1135
+ const residue = StructureProperties.residue.key(location);
1136
+ const override = overrideColors === null || overrideColors === void 0 ? void 0 : overrideColors.get(residue);
1137
+ if (override !== void 0) return override;
1138
+ const value = values.get(residue);
1139
+ if (value === void 0) return neutral;
1140
+ const rgb = sampleColorScale(colorScale, value, max, min);
1141
+ return rgb ? Color.fromRgb(rgb[0], rgb[1], rgb[2]) : neutral;
1142
+ },
1143
+ contextHash: state.version,
1144
+ description: "Color residues by a per-residue value",
1145
+ factory: provider.factory,
1146
+ granularity: "group",
1147
+ props: {}
1148
+ };
1149
+ },
1150
+ getParams: () => ({}),
1151
+ isApplicable: () => true,
1152
+ label: "Residue Value",
1153
+ name: RESIDUE_VALUE_THEME_NAME
1154
+ };
1155
+ return {
1156
+ provider,
1157
+ setState(next) {
1158
+ Object.assign(state, next);
1159
+ state.version++;
1160
+ }
1161
+ };
1162
+ }
1163
+ //#endregion
1164
+ //#region src/core/ProteinStructureViewer/scene/coloring.ts
1165
+ function createSceneThemes(mode) {
1166
+ const overrides = createResidueColorOverrides();
1167
+ return {
1168
+ chain: createChainColorTheme(overrides),
1169
+ overrides,
1170
+ plddt: createPlddtTheme(mode, overrides),
1171
+ value: createResidueValueTheme(mode, overrides)
1172
+ };
1173
+ }
1174
+ /** Registers the themes on a plugin, so representations can name them. */
1175
+ function registerSceneThemes(plugin, themes) {
1176
+ const registry = plugin.representation.structure.themes.colorThemeRegistry;
1177
+ for (const { provider } of [
1178
+ themes.chain,
1179
+ themes.plddt,
1180
+ themes.value
1181
+ ]) registry.add(provider);
1182
+ }
1183
+ const themesByPlugin = /* @__PURE__ */ new WeakMap();
1184
+ /**
1185
+ * The themes a plugin paints with, created and registered the first time it
1186
+ * is asked for them.
1187
+ *
1188
+ * One set per plugin rather than one per scene: Mol*'s registry refuses a
1189
+ * name it already holds, so a plugin a scene is applied to a second time -
1190
+ * the same offscreen plugin rendering one structure after another - reuses
1191
+ * the themes it has instead of failing to register new ones.
1192
+ */
1193
+ function sceneThemesFor(plugin, mode) {
1194
+ let themes = themesByPlugin.get(plugin);
1195
+ if (!themes) {
1196
+ themes = createSceneThemes(mode);
1197
+ registerSceneThemes(plugin, themes);
1198
+ themesByPlugin.set(plugin, themes);
1199
+ }
1200
+ return themes;
1201
+ }
1202
+ /** The Mol* theme name each way of coloring is registered under. */
1203
+ const COLOR_THEME_NAMES = {
1204
+ chain: CHAIN_COLOR_THEME_NAME,
1205
+ overlay: RESIDUE_VALUE_THEME_NAME,
1206
+ plddt: PLDDT_THEME_NAME
1207
+ };
1208
+ /**
1209
+ * What paints the structure: `colorBy` when it names one, and otherwise the
1210
+ * first of an overlay, pLDDT scores and chain colors that is supplied.
1211
+ *
1212
+ * Set is what makes an overlay active, not populated. An overlay whose values
1213
+ * have not arrived yet still owns the coloring: its residues read as missing
1214
+ * and come out neutral, which is the honest answer and the one the legend
1215
+ * beside it is describing.
1216
+ */
1217
+ function resolveColorBy(colorBy, overlay, plddt) {
1218
+ if (colorBy) return colorBy;
1219
+ if (overlay) return "overlay";
1220
+ return plddt && plddt.length > 0 ? "plddt" : "chain";
1221
+ }
1222
+ /**
1223
+ * Hands every theme the data it paints from and names the one to paint with.
1224
+ *
1225
+ * All three are kept current whichever is showing, so switching between them
1226
+ * reveals the right colors at once rather than the ones a theme had when it
1227
+ * was last on screen.
1228
+ */
1229
+ function syncSceneThemes(themes, coloring) {
1230
+ var _overlay$colorScale, _overlay$min;
1231
+ const { chainColors, highlightColors, mode, overlay, plddt } = coloring;
1232
+ themes.overrides.current = highlightColors;
1233
+ themes.chain.setState(chainColors);
1234
+ themes.plddt.setState({
1235
+ mode,
1236
+ scores: plddt !== null && plddt !== void 0 ? plddt : []
1237
+ });
1238
+ themes.value.setState(overlay ? {
1239
+ colorScale: (_overlay$colorScale = overlay.colorScale) !== null && _overlay$colorScale !== void 0 ? _overlay$colorScale : PLASMA_COLOR_SCALE,
1240
+ max: overlay.max,
1241
+ min: (_overlay$min = overlay.min) !== null && _overlay$min !== void 0 ? _overlay$min : 0,
1242
+ mode,
1243
+ values: overlay.values
1244
+ } : {
1245
+ mode,
1246
+ values: /* @__PURE__ */ new Map()
1247
+ });
1248
+ return COLOR_THEME_NAMES[coloring.colorBy];
1249
+ }
1250
+ //#endregion
1251
+ //#region src/core/ProteinStructureViewer/scene/representation.ts
1252
+ /**
1253
+ * The parts the viewer draws a structure in. Each names the components built
1254
+ * for it and tags the representations drawn over them, and it is those names
1255
+ * and tags the rest of the viewer reads back: to tell its own components from
1256
+ * a consumer's, to keep ligands on element colors, and to hide or dim a chain.
1257
+ */
1258
+ const POLYMER_PART = "polymer";
1259
+ const LIGAND_PART = "ligand";
1260
+ const HIGHLIGHT_PART = "highlight";
1261
+ const SURFACE_PART = "surface";
1262
+ /**
1263
+ * How heteroatoms are colored: the theme Mol* puts on a ball-and-stick when
1264
+ * left to itself, with the parameters it chooses too.
1265
+ *
1266
+ * Named rather than left blank only so that it survives `applyColorTheme`,
1267
+ * which would otherwise repaint the ligands along with everything else. The
1268
+ * structure-wide theme is the wrong answer for them twice over: a heme reads
1269
+ * as a heme because its iron is orange and its nitrogens are blue, which one
1270
+ * flat color throws away, and pLDDT and residue overlays have no value for a
1271
+ * HETATM to be colored by at all.
1272
+ */
1273
+ const ELEMENT_THEME = "element-symbol";
1274
+ /** Grid spacing and probe of the molecular surface, in angstroms. */
1275
+ const SURFACE_RESOLUTION = .6;
1276
+ const SURFACE_PROBE_RADIUS = 1.4;
1277
+ /**
1278
+ * The representation parameters Mol*'s own presets compute, read from the same
1279
+ * manager state they read. Ported rather than hardcoded so a chain drawn here
1280
+ * looks exactly like the one the preset used to draw. Mol* hands the same set
1281
+ * to its cartoons and its ball-and-sticks, and so does this.
1282
+ */
1283
+ function representationTypeParams(plugin) {
1284
+ const { hydrogens, ignoreLight, visualQuality } = plugin.managers.structure.component.state.options;
1285
+ return {
1286
+ ignoreHydrogens: hydrogens !== "all",
1287
+ ignoreHydrogensVariant: hydrogens === "only-polar" ? "non-polar" : "all",
1288
+ ignoreLight,
1289
+ quality: visualQuality
1290
+ };
1291
+ }
1292
+ /**
1293
+ * True for the representation drawing a chain's heteroatoms.
1294
+ *
1295
+ * Read off the tag put on the representation when it was built, rather than
1296
+ * off the component's key. Mol* does not store the key as it was given: it
1297
+ * files components under `structure-component-<key>`, so a test against the
1298
+ * key as written here silently matches nothing, and every ligand quietly takes
1299
+ * the structure-wide theme instead. The tag survives untouched.
1300
+ */
1301
+ function isLigandRepresentation(representation) {
1302
+ var _representation$cell$;
1303
+ return ((_representation$cell$ = representation.cell.transform.tags) === null || _representation$cell$ === void 0 ? void 0 : _representation$cell$.includes(LIGAND_PART)) === true;
1304
+ }
1305
+ const VIEWER_COMPONENT = /structure-component-(?:(?:polymer|ligand|highlight)-[^,]+|surface(?:,|$))/;
1306
+ /**
1307
+ * True for a component the viewer built, as opposed to one a consumer added
1308
+ * through `onReady`. Only the viewer's own are recolored, hidden or replaced.
1309
+ */
1310
+ function isViewerComponent(component) {
1311
+ var _component$key;
1312
+ return VIEWER_COMPONENT.test((_component$key = component.key) !== null && _component$key !== void 0 ? _component$key : component.cell.transform.ref);
1313
+ }
1314
+ /**
1315
+ * Builds one component and draws it, handing back its ref - or nothing, when
1316
+ * the expression selects no atoms, which is what a chain with no ligands or
1317
+ * no polymer looks like.
1318
+ *
1319
+ * Mol* files components by key, and updates the one it finds under a key
1320
+ * rather than adding a second. So a component built again under its old key
1321
+ * comes back under its old ref, with the representation drawn over it still
1322
+ * in place and recomputing against the new selection by itself; drawing it
1323
+ * again would put a second one on top. `previous` is that old ref, which is
1324
+ * how the two cases are told apart.
1325
+ */
1326
+ function drawComponent(_x, _x2, _x3, _x4, _x5, _x6, _x7, _x8) {
1327
+ return _drawComponent.apply(this, arguments);
1328
+ }
1329
+ function _drawComponent() {
1330
+ _drawComponent = _asyncToGenerator(function* (plugin, structure, key, label, expression, props, tag, previous) {
1331
+ const builders = plugin.builders.structure;
1332
+ const component = yield builders.tryCreateComponentFromExpression(structure, expression, key, { label });
1333
+ if (!component) return void 0;
1334
+ if (component.ref !== previous) yield builders.representation.addRepresentation(component, props, { tag });
1335
+ return component.ref;
1336
+ });
1337
+ return _drawComponent.apply(this, arguments);
1338
+ }
1339
+ /**
1340
+ * Each chain's polymer as a cartoon, by `chainId`.
1341
+ *
1342
+ * One component per chain rather than Mol*'s `default` preset, which groups
1343
+ * every polymer chain into one: a component is the unit Mol* can hide, so a
1344
+ * component per chain is what makes a chain individually hideable.
1345
+ *
1346
+ * Every chain the file names, not just the ones the legend lists; a chain
1347
+ * with no polymer simply gets no cartoon.
1348
+ */
1349
+ function buildCartoonLayer(_x9, _x10, _x11, _x12, _x13) {
1350
+ return _buildCartoonLayer.apply(this, arguments);
1351
+ }
1352
+ function _buildCartoonLayer() {
1353
+ _buildCartoonLayer = _asyncToGenerator(function* (plugin, structure, chainLabels, typeParams, colorTheme, previous = /* @__PURE__ */ new Map()) {
1354
+ const refs = /* @__PURE__ */ new Map();
1355
+ for (const [chainId, label] of chainLabels) {
1356
+ const ref = yield drawComponent(plugin, structure, `${POLYMER_PART}-${chainId}`, label, chainPolymerExpression(chainId), {
1357
+ color: colorTheme,
1358
+ type: "cartoon",
1359
+ typeParams
1360
+ }, POLYMER_PART, previous.get(chainId));
1361
+ if (ref) refs.set(chainId, ref);
1362
+ }
1363
+ return refs;
1364
+ });
1365
+ return _buildCartoonLayer.apply(this, arguments);
1366
+ }
1367
+ /**
1368
+ * Each chain's ligands and ions as ball-and-stick in element colors, by
1369
+ * `chainId`. A cartoon has no backbone to trace through a heme, so these are
1370
+ * drawn beside it; grouping them under their chain is what makes hiding a
1371
+ * chain take its ligands with it.
1372
+ */
1373
+ function buildLigandLayer(_x14, _x15, _x16, _x17) {
1374
+ return _buildLigandLayer.apply(this, arguments);
1375
+ }
1376
+ function _buildLigandLayer() {
1377
+ _buildLigandLayer = _asyncToGenerator(function* (plugin, structure, chainLabels, typeParams) {
1378
+ const refs = /* @__PURE__ */ new Map();
1379
+ for (const [chainId, label] of chainLabels) {
1380
+ const ref = yield drawComponent(plugin, structure, `${LIGAND_PART}-${chainId}`, label, chainLigandExpression(chainId), {
1381
+ color: ELEMENT_THEME,
1382
+ type: "ball-and-stick",
1383
+ typeParams
1384
+ }, LIGAND_PART);
1385
+ if (ref) refs.set(chainId, ref);
1386
+ }
1387
+ return refs;
1388
+ });
1389
+ return _buildLigandLayer.apply(this, arguments);
1390
+ }
1391
+ /**
1392
+ * The highlighted residues on each chain as ball-and-stick, by `chainId`,
1393
+ * painted by the structure-wide theme - which gives them their highlight
1394
+ * colors.
1395
+ *
1396
+ * Matched by residue index rather than by number and insertion code: the index
1397
+ * is what the highlights were resolved to, and it names one residue however
1398
+ * the file spells its numbering.
1399
+ */
1400
+ function buildHighlightLayer(_x18, _x19, _x20, _x21, _x22) {
1401
+ return _buildHighlightLayer.apply(this, arguments);
1402
+ }
1403
+ function _buildHighlightLayer() {
1404
+ _buildHighlightLayer = _asyncToGenerator(function* (plugin, structure, byChain, typeParams, colorTheme, previous = /* @__PURE__ */ new Map()) {
1405
+ const refs = /* @__PURE__ */ new Map();
1406
+ for (const [chainId, residues] of byChain) {
1407
+ if (residues.length === 0) continue;
1408
+ const ref = yield drawComponent(plugin, structure, `${HIGHLIGHT_PART}-${chainId}`, `Highlights on ${chainId}`, MolScriptBuilder.struct.generator.atomGroups({ "residue-test": MolScriptBuilder.core.set.has([MolScriptBuilder.set(...residues), MolScriptBuilder.ammp("residueKey")]) }), {
1409
+ color: colorTheme,
1410
+ type: "ball-and-stick",
1411
+ typeParams
1412
+ }, HIGHLIGHT_PART, previous.get(chainId));
1413
+ if (ref) refs.set(chainId, ref);
1414
+ }
1415
+ return refs;
1416
+ });
1417
+ return _buildHighlightLayer.apply(this, arguments);
1418
+ }
1419
+ /** The polymer of the named chains, together. */
1420
+ function polymerOfChainsExpression(chainIds) {
1421
+ return MolScriptBuilder.struct.generator.atomGroups({
1422
+ "chain-test": MolScriptBuilder.core.set.has([MolScriptBuilder.set(...chainIds), MolScriptBuilder.ammp("auth_asym_id")]),
1423
+ "entity-test": MolScriptBuilder.core.rel.eq([MolScriptBuilder.ammp("entityType"), "polymer"])
1424
+ });
1425
+ }
1426
+ /**
1427
+ * Throws when a molecular surface over the structure would take more grid
1428
+ * cells than `MAX_SURFACE_GRID_CELLS`.
1429
+ *
1430
+ * Estimated the way the surface is computed: a grid at the surface's spacing
1431
+ * over the bounding box, padded on every side by the largest atom and the
1432
+ * probe rolled over it.
1433
+ */
1434
+ function assertSurfaceBudget(structure) {
1435
+ let maxRadius = 0;
1436
+ const location = StructureElement.Location.create(structure);
1437
+ for (const unit of structure.units) {
1438
+ location.unit = unit;
1439
+ for (let i = 0; i < unit.elements.length; i++) {
1440
+ location.element = unit.elements[i];
1441
+ maxRadius = Math.max(maxRadius, StructureProperties.atom.vdw_radius(location));
1442
+ }
1443
+ }
1444
+ const { max, min } = structure.boundary.box;
1445
+ const padding = maxRadius + SURFACE_PROBE_RADIUS;
1446
+ let cells = 1;
1447
+ for (let axis = 0; axis < 3; axis++) {
1448
+ const extent = max[axis] - min[axis];
1449
+ cells *= Math.ceil((extent + 2 * padding) / SURFACE_RESOLUTION);
1450
+ }
1451
+ if (!Number.isFinite(cells) || cells > 4e6) throw new Error("The molecular surface of the visible chains is too large to compute.");
1452
+ }
1453
+ /** What an expression selects in a structure, as a structure of its own. */
1454
+ function selectStructure(structure, expression) {
1455
+ const selection = compile(expression)(new QueryContext(structure));
1456
+ return StructureSelection.unionStructure(selection);
1457
+ }
1458
+ /**
1459
+ * One molecular surface over the polymers of the visible chains, or nothing
1460
+ * when no chain is visible.
1461
+ *
1462
+ * One surface rather than one per chain: where two chains meet, a surface of
1463
+ * each would wrap the interface twice over, and a surface of the pair buries
1464
+ * it - so hiding the partner is what exposes the face it was bound to.
1465
+ *
1466
+ * Built again under the same key when the visible chains change, which
1467
+ * updates the surface in place rather than replacing it. Throws, before
1468
+ * anything is built or updated, when the surface would outgrow its grid.
1469
+ */
1470
+ function buildSurfaceLayer(_x23, _x24, _x25, _x26, _x27, _x28) {
1471
+ return _buildSurfaceLayer.apply(this, arguments);
1472
+ }
1473
+ function _buildSurfaceLayer() {
1474
+ _buildSurfaceLayer = _asyncToGenerator(function* (plugin, structure, visibleChainIds, typeParams, colorTheme, previous) {
1475
+ if (visibleChainIds.length === 0) return void 0;
1476
+ const expression = polymerOfChainsExpression(visibleChainIds);
1477
+ if (structure.data) assertSurfaceBudget(selectStructure(structure.data, expression));
1478
+ return drawComponent(plugin, structure, SURFACE_PART, "Surface", expression, {
1479
+ color: colorTheme,
1480
+ type: "molecular-surface",
1481
+ typeParams: _objectSpread2(_objectSpread2({}, typeParams), {}, {
1482
+ probeRadius: SURFACE_PROBE_RADIUS,
1483
+ quality: "custom",
1484
+ resolution: SURFACE_RESOLUTION,
1485
+ visuals: ["structure-molecular-surface-mesh"]
1486
+ })
1487
+ }, SURFACE_PART, previous);
1488
+ });
1489
+ return _buildSurfaceLayer.apply(this, arguments);
1490
+ }
1491
+ /** Deletes components, and everything drawn over them, in one commit. */
1492
+ function removeComponents(_x29, _x30) {
1493
+ return _removeComponents.apply(this, arguments);
1494
+ }
1495
+ function _removeComponents() {
1496
+ _removeComponents = _asyncToGenerator(function* (plugin, refs) {
1497
+ if (refs.length === 0) return;
1498
+ const update = plugin.state.data.build();
1499
+ for (const ref of refs) update.delete(ref);
1500
+ yield update.commit();
1501
+ });
1502
+ return _removeComponents.apply(this, arguments);
1503
+ }
1504
+ /**
1505
+ * Shows or hides everything each chain is drawn with. The whole subtree of
1506
+ * each component, since visibility is per-cell and it is the representation
1507
+ * underneath that actually draws.
1508
+ *
1509
+ * Set outright rather than through the component manager's `toggleVisibility`,
1510
+ * which flips whatever it finds: the hidden set is derived from props on every
1511
+ * pass, so it has to be applied as a statement of what should be true and not
1512
+ * as a change to what already is.
1513
+ */
1514
+ function applyChainVisibility(plugin, refsByChain, hidden) {
1515
+ for (const [chainId, refs] of refsByChain) for (const ref of refs) setSubtreeVisibility(plugin.state.data, ref, hidden.has(chainId));
1516
+ }
1517
+ /**
1518
+ * Recolors the viewer's own representations with the named theme, leaving the
1519
+ * heteroatoms on `ELEMENT_THEME` and a consumer's representations as they
1520
+ * were made.
1521
+ *
1522
+ * The per-representation form of `updateRepresentationsTheme`, so the two can
1523
+ * be told apart in one pass; handing it a filtered list instead would leave
1524
+ * the ligands on whichever theme happened to be current when they were built.
1525
+ */
1526
+ function applyColorTheme(_x31, _x32) {
1527
+ return _applyColorTheme.apply(this, arguments);
1528
+ }
1529
+ function _applyColorTheme() {
1530
+ _applyColorTheme = _asyncToGenerator(function* (plugin, colorTheme) {
1531
+ yield plugin.dataTransaction(_asyncToGenerator(function* () {
1532
+ for (const s of plugin.managers.structure.hierarchy.current.structures) {
1533
+ const own = s.components.filter(isViewerComponent);
1534
+ if (own.length === 0) continue;
1535
+ yield plugin.managers.structure.component.updateRepresentationsTheme(own, (_component, representation) => ({ color: isLigandRepresentation(representation) ? ELEMENT_THEME : colorTheme }));
1536
+ }
1537
+ }));
1538
+ });
1539
+ return _applyColorTheme.apply(this, arguments);
1540
+ }
1541
+ /** A set of chain ids as one comparable string. */
1542
+ function chainSetKey(chains) {
1543
+ return [...chains].sort().join("\0");
1544
+ }
1545
+ //#endregion
1546
+ //#region src/core/ProteinStructureViewer/scene/StructureScene.ts
1547
+ const emptyLayers = () => ({
1548
+ highlight: /* @__PURE__ */ new Map(),
1549
+ ligand: /* @__PURE__ */ new Map(),
1550
+ polymer: {
1551
+ cartoon: /* @__PURE__ */ new Map(),
1552
+ drawn: "cartoon"
1553
+ }
1554
+ });
1555
+ const polymerRefs = (layer) => [...layer.cartoon.values(), ...layer.surface ? [layer.surface] : []];
1556
+ /**
1557
+ * Objects compared by identity, as numbers a string key can carry. A new
1558
+ * `plddt` array or overlay is new data even when it holds the same values,
1559
+ * which is how the rest of the viewer treats them too.
1560
+ */
1561
+ const identities = /* @__PURE__ */ new WeakMap();
1562
+ let nextIdentity = 1;
1563
+ function identityKey(value) {
1564
+ if (!value) return 0;
1565
+ let id = identities.get(value);
1566
+ if (id === void 0) {
1567
+ id = nextIdentity++;
1568
+ identities.set(value, id);
1569
+ }
1570
+ return id;
1571
+ }
1572
+ /**
1573
+ * A structure scene on a Mol* plugin: loads a structure into it, draws it from
1574
+ * props, keeps the drawing in line with them, and places the camera.
1575
+ *
1576
+ * It takes any plugin - the viewer's own, a consumer's, or one rendering
1577
+ * offscreen - and draws the same scene on each, which is what makes an image
1578
+ * rendered without the viewer match what the viewer shows.
1579
+ *
1580
+ * Every change to the plugin's state tree - a load clearing it, an update
1581
+ * rebuilding part of it - runs through one queue, so no two ever interleave:
1582
+ * a surface half built when the next structure arrives would otherwise be
1583
+ * built on a cell that no longer exists.
1584
+ *
1585
+ * An update rebuilds only the parts whose inputs changed. Recoloring is a
1586
+ * theme update and rebuilds nothing, hiding a chain toggles its components,
1587
+ * and only the surface - which is computed over whatever is visible - is
1588
+ * rebuilt when visibility changes.
1589
+ */
1590
+ var StructureScene = class {
1591
+ constructor(plugin, options) {
1592
+ this.plugin = plugin;
1593
+ this.options = options;
1594
+ this.tail = Promise.resolve();
1595
+ this.pending = 0;
1596
+ this.generation = 0;
1597
+ this.disposed = false;
1598
+ this.scene = null;
1599
+ this.parsed = null;
1600
+ this.layers = emptyLayers();
1601
+ this.applied = null;
1602
+ this.themes = sceneThemesFor(plugin, options.mode);
1603
+ }
1604
+ /** Runs `task` once everything queued before it has finished. */
1605
+ enqueue(task) {
1606
+ this.changePending(1);
1607
+ const run = this.tail.then(task);
1608
+ const settle = () => this.changePending(-1);
1609
+ this.tail = run.then(settle, settle);
1610
+ return run;
1611
+ }
1612
+ /**
1613
+ * Clears the plugin, parses the structure, and draws it from `props` when
1614
+ * the scene draws. Call it from inside the queue.
1615
+ *
1616
+ * A scene disposed of while the structure was parsing draws nothing: the
1617
+ * plugin it would draw on is going.
1618
+ */
1619
+ load(text, props) {
1620
+ var _this = this;
1621
+ return _asyncToGenerator(function* () {
1622
+ const outcome = yield parseStructure(_this.plugin, text);
1623
+ if (!outcome.ok || _this.disposed) return outcome;
1624
+ _this.parsed = outcome.loaded;
1625
+ const { addressIndex, chainLabels, chains, structure } = outcome.loaded;
1626
+ if (!_this.options.draw || !structure) return outcome;
1627
+ try {
1628
+ _this.generation++;
1629
+ _this.scene = {
1630
+ addressIndex,
1631
+ chainLabels,
1632
+ chains,
1633
+ structure
1634
+ };
1635
+ _this.layers = emptyLayers();
1636
+ _this.applied = null;
1637
+ yield _this.apply(props, true);
1638
+ } catch (error) {
1639
+ return {
1640
+ error,
1641
+ ok: false
1642
+ };
1643
+ }
1644
+ return outcome;
1645
+ })();
1646
+ }
1647
+ /** Brings the scene in line with new props. */
1648
+ update(props) {
1649
+ var _this2 = this;
1650
+ const { generation } = this;
1651
+ return this.enqueue(_asyncToGenerator(function* () {
1652
+ if (_this2.disposed || !_this2.scene || generation !== _this2.generation) return;
1653
+ try {
1654
+ yield _this2.apply(props, false);
1655
+ } catch (error) {
1656
+ if (!_this2.disposed) _this2.options.reportError(error, "representation");
1657
+ }
1658
+ }));
1659
+ }
1660
+ /**
1661
+ * Places the camera on the structure last loaded, as `framing` says, and
1662
+ * returns the orientation now in force. Call it once something is drawn:
1663
+ * Mol* settles the reset on its next frame, and one settled against an empty
1664
+ * scene is lost.
1665
+ *
1666
+ * `fitDurationMs` is how long a plain fit animates; an offscreen render asks
1667
+ * for none, so the image it takes is of the camera at rest.
1668
+ */
1669
+ frame(framing, fit, fitDurationMs) {
1670
+ const { canvas3d } = this.plugin;
1671
+ if (!canvas3d) return void 0;
1672
+ return frameStructure(canvas3d, framing, Boolean(framing.orientation && !framing.initialCamera) ? this.highlightsCenter(framing.highlights) : void 0, fit, fitDurationMs);
1673
+ }
1674
+ /** Turns the camera to an orientation, over `durationMs`. */
1675
+ orient(orientation, highlights, durationMs) {
1676
+ var _this$plugin$canvas3d;
1677
+ (_this$plugin$canvas3d = this.plugin.canvas3d) === null || _this$plugin$canvas3d === void 0 || _this$plugin$canvas3d.requestCameraReset({
1678
+ durationMs,
1679
+ snapshot: orientationSnapshot(orientation, this.highlightsCenter(highlights))
1680
+ });
1681
+ }
1682
+ /** Where the camera is now, or undefined before there is a canvas. */
1683
+ camera() {
1684
+ const { canvas3d } = this.plugin;
1685
+ return canvas3d ? readCameraState(canvas3d) : void 0;
1686
+ }
1687
+ /** Stops the scene touching a plugin that is being disposed of. */
1688
+ dispose() {
1689
+ this.disposed = true;
1690
+ }
1691
+ /** The center of the highlighted residues on the structure last loaded. */
1692
+ highlightsCenter(highlights) {
1693
+ const parsed = this.parsed;
1694
+ if (!(parsed === null || parsed === void 0 ? void 0 : parsed.structureData)) return void 0;
1695
+ const residues = [...resolveHighlights(highlights, parsed.addressIndex).colors.keys()];
1696
+ return residuesCenter(parsed.structureData, residues);
1697
+ }
1698
+ changePending(delta) {
1699
+ var _this$options$onBusyC, _this$options;
1700
+ const wasBusy = this.pending > 0;
1701
+ this.pending += delta;
1702
+ const busy = this.pending > 0;
1703
+ if (busy !== wasBusy) (_this$options$onBusyC = (_this$options = this.options).onBusyChange) === null || _this$options$onBusyC === void 0 || _this$options$onBusyC.call(_this$options, busy);
1704
+ }
1705
+ apply(props, fresh) {
1706
+ var _this3 = this;
1707
+ return _asyncToGenerator(function* () {
1708
+ var _this$options$onRebui, _this$options2;
1709
+ const { plugin } = _this3;
1710
+ const scene = _this3.scene;
1711
+ const previous = _this3.applied;
1712
+ const highlights = resolveHighlights(props.highlights, scene.addressIndex);
1713
+ const coloring = {
1714
+ chainColors: chainColorMap(scene.chains.map((chain) => chain.chainId), props.chainColors),
1715
+ colorBy: resolveColorBy(props.colorBy, props.overlay, props.plddt),
1716
+ highlightColors: highlights.colors,
1717
+ mode: props.mode,
1718
+ overlay: props.overlay,
1719
+ plddt: props.plddt
1720
+ };
1721
+ const colorTheme = COLOR_THEME_NAMES[coloring.colorBy];
1722
+ const coloringKey = [
1723
+ coloring.colorBy,
1724
+ [...coloring.chainColors].join(";"),
1725
+ identityKey(props.plddt),
1726
+ identityKey(props.overlay),
1727
+ props.mode,
1728
+ highlightsKey(props.highlights)
1729
+ ].join("|");
1730
+ const hiddenKey = chainSetKey(props.hiddenChains);
1731
+ const recolor = !previous || previous.coloringKey !== coloringKey;
1732
+ if (recolor) syncSceneThemes(_this3.themes, coloring);
1733
+ const typeParams = representationTypeParams(plugin);
1734
+ const replaced = [];
1735
+ let rebuilt = false;
1736
+ if (!previous || previous.representation !== props.representation || props.representation === "surface" && previous.hiddenKey !== hiddenKey) {
1737
+ replaced.push(...polymerRefs(_this3.layers.polymer));
1738
+ _this3.layers.polymer = yield _this3.buildPolymer(scene, props, typeParams, colorTheme);
1739
+ rebuilt = true;
1740
+ }
1741
+ if (fresh) _this3.layers.ligand = yield buildLigandLayer(plugin, scene.structure, scene.chainLabels, typeParams);
1742
+ const { drawn } = _this3.layers.polymer;
1743
+ const highlightLayerKey = `${drawn}|${residueAddressesKey(props.highlights)}`;
1744
+ if (!previous || previous.highlightLayerKey !== highlightLayerKey) {
1745
+ replaced.push(..._this3.layers.highlight.values());
1746
+ _this3.layers.highlight = yield buildHighlightLayer(plugin, scene.structure, drawn === "cartoon" ? highlights.byChain : /* @__PURE__ */ new Map(), typeParams, colorTheme, _this3.layers.highlight);
1747
+ rebuilt = true;
1748
+ }
1749
+ const kept = new Set([...polymerRefs(_this3.layers.polymer), ..._this3.layers.highlight.values()]);
1750
+ yield removeComponents(plugin, replaced.filter((ref) => !kept.has(ref)));
1751
+ if (recolor) yield applyColorTheme(plugin, colorTheme);
1752
+ applyChainVisibility(plugin, _this3.refsByChain(), props.hiddenChains);
1753
+ _this3.applied = {
1754
+ coloringKey,
1755
+ hiddenKey,
1756
+ highlightLayerKey,
1757
+ representation: props.representation
1758
+ };
1759
+ if (rebuilt && !fresh) (_this$options$onRebui = (_this$options2 = _this3.options).onRebuild) === null || _this$options$onRebui === void 0 || _this$options$onRebui.call(_this$options2);
1760
+ })();
1761
+ }
1762
+ /**
1763
+ * The polymer as the props ask: a cartoon per chain, or one surface over
1764
+ * the visible chains - falling back to the cartoon, reported, when the
1765
+ * surface cannot be drawn. What is already drawn is updated in place where
1766
+ * it can be.
1767
+ */
1768
+ buildPolymer(scene, props, typeParams, colorTheme) {
1769
+ var _this4 = this;
1770
+ return _asyncToGenerator(function* () {
1771
+ const current = _this4.layers.polymer;
1772
+ if (props.representation === "surface") {
1773
+ const visible = scene.chains.map((chain) => chain.chainId).filter((chainId) => !props.hiddenChains.has(chainId));
1774
+ try {
1775
+ const surface = yield buildSurfaceLayer(_this4.plugin, scene.structure, visible, typeParams, colorTheme, current.surface);
1776
+ return {
1777
+ cartoon: /* @__PURE__ */ new Map(),
1778
+ drawn: "surface",
1779
+ surface
1780
+ };
1781
+ } catch (error) {
1782
+ _this4.options.reportError(error, "representation");
1783
+ }
1784
+ }
1785
+ return {
1786
+ cartoon: yield buildCartoonLayer(_this4.plugin, scene.structure, scene.chainLabels, typeParams, colorTheme, current.cartoon),
1787
+ drawn: "cartoon"
1788
+ };
1789
+ })();
1790
+ }
1791
+ /**
1792
+ * Each chain's components, for hiding it: its cartoon, its ligands and its
1793
+ * highlights. A surface is not among them - it is rebuilt over what is
1794
+ * visible instead.
1795
+ */
1796
+ refsByChain() {
1797
+ const refs = /* @__PURE__ */ new Map();
1798
+ const add = (layer) => {
1799
+ for (const [chainId, ref] of layer) {
1800
+ var _refs$get;
1801
+ refs.set(chainId, [...(_refs$get = refs.get(chainId)) !== null && _refs$get !== void 0 ? _refs$get : [], ref]);
1802
+ }
1803
+ };
1804
+ add(this.layers.polymer.cartoon);
1805
+ add(this.layers.ligand);
1806
+ add(this.layers.highlight);
1807
+ return refs;
1808
+ }
1809
+ };
1810
+ //#endregion
1811
+ //#region src/core/ProteinStructureViewer/scene/applyStructureScene.ts
1812
+ /** The options as the props the scene draws from. */
1813
+ function sceneProps(options) {
1814
+ var _options$hiddenChains, _options$mode, _options$representati;
1815
+ return {
1816
+ chainColors: options.chainColors,
1817
+ colorBy: options.colorBy,
1818
+ hiddenChains: new Set((_options$hiddenChains = options.hiddenChains) !== null && _options$hiddenChains !== void 0 ? _options$hiddenChains : []),
1819
+ highlights: options.highlights,
1820
+ mode: (_options$mode = options.mode) !== null && _options$mode !== void 0 ? _options$mode : "light",
1821
+ overlay: options.residueOverlay,
1822
+ plddt: options.plddt,
1823
+ representation: (_options$representati = options.representation) !== null && _options$representati !== void 0 ? _options$representati : "cartoon"
1824
+ };
1825
+ }
1826
+ /**
1827
+ * Draws a scene on a plugin and hands back its handle. `fitDurationMs` is how
1828
+ * long fitting the camera to each structure animates - none, for an image
1829
+ * taken straight after.
1830
+ */
1831
+ function applyScene(_x2, _x3, _x4) {
1832
+ return _applyScene.apply(this, arguments);
1833
+ }
1834
+ function _applyScene() {
1835
+ _applyScene = _asyncToGenerator(function* (plugin, options, fitDurationMs) {
1836
+ var _options$mode2;
1837
+ let current = options;
1838
+ const reportError = (error, phase) => {
1839
+ if (current.onError) current.onError(error, phase);
1840
+ else console.error("Failed to draw the structure:", error);
1841
+ };
1842
+ const scene = new StructureScene(plugin, {
1843
+ draw: true,
1844
+ mode: (_options$mode2 = options.mode) !== null && _options$mode2 !== void 0 ? _options$mode2 : "light",
1845
+ reportError
1846
+ });
1847
+ const load = function() {
1848
+ var _ref = _asyncToGenerator(function* (next) {
1849
+ const outcome = yield scene.enqueue(() => scene.load(next.structure, sceneProps(next)));
1850
+ if (!outcome.ok) throw outcome.error;
1851
+ scene.frame(next, true, fitDurationMs);
1852
+ return loadInfo(outcome.loaded);
1853
+ });
1854
+ return function load(_x) {
1855
+ return _ref.apply(this, arguments);
1856
+ };
1857
+ }();
1858
+ let info = yield load(options);
1859
+ return {
1860
+ dispose: () => scene.dispose(),
1861
+ getCamera: () => scene.camera(),
1862
+ get info() {
1863
+ return info;
1864
+ },
1865
+ update(changes) {
1866
+ return _asyncToGenerator(function* () {
1867
+ const previous = current;
1868
+ current = _objectSpread2(_objectSpread2({}, current), changes);
1869
+ if (current.structure !== previous.structure) {
1870
+ info = yield load(current);
1871
+ return;
1872
+ }
1873
+ yield scene.update(sceneProps(current));
1874
+ const { canvas3d } = plugin;
1875
+ if (current.projection && current.projection !== previous.projection) canvas3d === null || canvas3d === void 0 || canvas3d.setProps({ camera: { mode: current.projection } });
1876
+ if (current.orientation && current.orientation !== previous.orientation) scene.orient(current.orientation, current.highlights, 250);
1877
+ })();
1878
+ }
1879
+ };
1880
+ });
1881
+ return _applyScene.apply(this, arguments);
1882
+ }
1883
+ /**
1884
+ * Draws a structure on a Mol* plugin of your own, the way the viewer draws it:
1885
+ * the same representations, coloring, highlights and camera, from the same
1886
+ * options its props take. Works on any plugin - one rendering offscreen, one
1887
+ * inside a viewer of your own - and resolves once the structure is drawn and
1888
+ * the camera placed on it.
1889
+ *
1890
+ * The scene clears the plugin's state to load the structure, and keeps what it
1891
+ * draws in line with `update` from then on. It adds nothing to the plugin
1892
+ * beyond the structure and its representations, and three color themes
1893
+ * registered once per plugin.
1894
+ */
1895
+ function applyStructureScene(plugin, options) {
1896
+ return applyScene(plugin, options);
1897
+ }
1898
+ //#endregion
1899
+ //#region src/core/ProteinStructureViewer/utils/axes.ts
1900
+ /**
1901
+ * The orientation axes widget, as Mol* parameterizes it.
1902
+ *
1903
+ * Shared rather than declared where it is used, because two different parts of
1904
+ * Mol* take the same shape: the live canvas takes it as
1905
+ * `camera.helper.axes`, and the screenshot helper takes it as its own `axes`,
1906
+ * so an image can carry the widget or leave it out independently of the view
1907
+ * it was captured from.
1908
+ */
1909
+ const AXES_ON = {
1910
+ name: "on",
1911
+ params: {
1912
+ alpha: .51,
1913
+ colorX: 16711680,
1914
+ colorY: 32768,
1915
+ colorZ: 255,
1916
+ labelColorX: 8421504,
1917
+ labelColorY: 8421504,
1918
+ labelColorZ: 8421504,
1919
+ labelOpacity: 1,
1920
+ labelScale: .25,
1921
+ labelX: "X",
1922
+ labelY: "Y",
1923
+ labelZ: "Z",
1924
+ location: "bottom-left",
1925
+ locationOffsetX: 0,
1926
+ locationOffsetY: 0,
1927
+ originColor: 8421504,
1928
+ planeColorXY: 8421504,
1929
+ planeColorXZ: 8421504,
1930
+ planeColorYZ: 8421504,
1931
+ radiusScale: .075,
1932
+ scale: .15,
1933
+ showLabels: false,
1934
+ showPlanes: true
1935
+ }
1936
+ };
1937
+ const AXES_OFF = {
1938
+ name: "off",
1939
+ params: {}
1940
+ };
1941
+ //#endregion
1942
+ //#region src/core/ProteinStructureViewer/utils/screenshot.ts
1943
+ /**
1944
+ * Each resolution as the Mol* preset that produces it: 1280x720, 1920x1080,
1945
+ * 3840x2160, 7680x4320. Named here rather than given as dimensions so the
1946
+ * aspect stays Mol*'s to choose.
1947
+ *
1948
+ * Mol*'s own `viewport` preset - the canvas at its current size - is left out
1949
+ * deliberately: it makes the image depend on how big the viewer happens to be
1950
+ * on screen, which is not something a caller asking for "high" is asking for.
1951
+ */
1952
+ const RESOLUTION_PRESETS = {
1953
+ high: "ultra-hd",
1954
+ low: "hd",
1955
+ maximum: "8k-ultra-hd",
1956
+ medium: "full-hd"
1957
+ };
1958
+ /** Painted behind the structure when a background is asked for but unreadable. */
1959
+ const FALLBACK_BACKGROUND$1 = "#ffffff";
1960
+ /** Named for a file when neither the caller nor Mol* has a better name. */
1961
+ const FALLBACK_FILENAME = "structure.png";
1962
+ /**
1963
+ * Runs `capture` with Mol*'s screenshot helper set up the way `options` asks,
1964
+ * and resolves undefined when the plugin has no helper to capture with.
1965
+ *
1966
+ * Mol*'s screenshot helper renders its own pass rather than reading the canvas
1967
+ * back, so the image is drawn fresh at the requested size and carries its own
1968
+ * transparency and axes settings - none of which disturb the view on screen.
1969
+ *
1970
+ * A named background is the one exception. The helper only chooses between
1971
+ * transparent and whatever the renderer is already painting, so a color has to
1972
+ * be put on the canvas for the length of the capture and taken off afterwards.
1973
+ * `finally` is what guarantees the second half: leaving a figure's background
1974
+ * behind on the viewer would be a worse bug than a failed download.
1975
+ */
1976
+ function withScreenshotHelper(_x, _x2, _x3) {
1977
+ return _withScreenshotHelper.apply(this, arguments);
1978
+ }
1979
+ function _withScreenshotHelper() {
1980
+ _withScreenshotHelper = _asyncToGenerator(function* (plugin, options, capture) {
1981
+ const helper = plugin.helpers.viewportScreenshot;
1982
+ if (!helper) return void 0;
1983
+ const { backgroundColor, resolution = "medium", showAxes } = options;
1984
+ const transparent = backgroundColor === void 0;
1985
+ helper.behaviors.values.next(_objectSpread2(_objectSpread2({}, helper.behaviors.values.value), {}, {
1986
+ axes: showAxes ? AXES_ON : AXES_OFF,
1987
+ format: {
1988
+ name: "png",
1989
+ params: {}
1990
+ },
1991
+ resolution: {
1992
+ name: RESOLUTION_PRESETS[resolution],
1993
+ params: {}
1994
+ },
1995
+ transparent
1996
+ }));
1997
+ const canvas3d = plugin.canvas3d;
1998
+ const painted = canvas3d === null || canvas3d === void 0 ? void 0 : canvas3d.props.renderer.backgroundColor;
1999
+ const overrideBackground = !transparent && canvas3d !== void 0;
2000
+ if (overrideBackground) canvas3d === null || canvas3d === void 0 || canvas3d.setProps({ renderer: { backgroundColor: themeColor(backgroundColor, FALLBACK_BACKGROUND$1) } });
2001
+ try {
2002
+ return yield capture(helper);
2003
+ } finally {
2004
+ if (overrideBackground && painted !== void 0) canvas3d === null || canvas3d === void 0 || canvas3d.setProps({ renderer: { backgroundColor: painted } });
2005
+ }
2006
+ });
2007
+ return _withScreenshotHelper.apply(this, arguments);
2008
+ }
2009
+ /**
2010
+ * Decodes a `data:` URI into a Blob.
2011
+ *
2012
+ * By hand rather than through `fetch`, which a page's `connect-src` can refuse
2013
+ * for `data:` URIs - and the sandboxed hosts that need `deliver` in the first
2014
+ * place are exactly the pages that set one.
2015
+ */
2016
+ function blobFromDataUri(uri) {
2017
+ const comma = uri.indexOf(",");
2018
+ const header = uri.slice(0, comma);
2019
+ const body = uri.slice(comma + 1);
2020
+ const type = header.slice(5).split(";")[0] || "image/png";
2021
+ if (!header.endsWith(";base64")) return new Blob([decodeURIComponent(body)], { type });
2022
+ const binary = atob(body);
2023
+ const bytes = new Uint8Array(binary.length);
2024
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
2025
+ return new Blob([bytes], { type });
2026
+ }
2027
+ /** Downloads the structure as a PNG, the capture button's default. */
2028
+ function downloadStructureImage(_x4) {
2029
+ return _downloadStructureImage.apply(this, arguments);
2030
+ }
2031
+ function _downloadStructureImage() {
2032
+ _downloadStructureImage = _asyncToGenerator(function* (plugin, options = {}) {
2033
+ yield withScreenshotHelper(plugin, options, (helper) => helper.download(options.filename));
2034
+ });
2035
+ return _downloadStructureImage.apply(this, arguments);
2036
+ }
2037
+ /**
2038
+ * Renders the structure as a PNG and hands it back rather than downloading it,
2039
+ * the same image `downloadStructureImage` would have saved.
2040
+ */
2041
+ function captureStructureImage(_x5) {
2042
+ return _captureStructureImage.apply(this, arguments);
2043
+ }
2044
+ function _captureStructureImage() {
2045
+ _captureStructureImage = _asyncToGenerator(function* (plugin, options = {}) {
2046
+ const uri = yield withScreenshotHelper(plugin, options, (helper) => helper.getImageDataUri());
2047
+ if (uri === void 0) throw new Error("This Mol* plugin has no screenshot helper to capture with.");
2048
+ return blobFromDataUri(uri);
2049
+ });
2050
+ return _captureStructureImage.apply(this, arguments);
2051
+ }
2052
+ /**
2053
+ * The name an image is saved or delivered under: the caller's, or else the
2054
+ * one Mol* derives from the loaded structure, `.png` included either way.
2055
+ */
2056
+ function imageFilename(plugin, filename) {
2057
+ var _plugin$helpers$viewp, _plugin$helpers$viewp2, _plugin$helpers$viewp3;
2058
+ if (filename) return `${filename}.png`;
2059
+ return (_plugin$helpers$viewp = (_plugin$helpers$viewp2 = plugin.helpers.viewportScreenshot) === null || _plugin$helpers$viewp2 === void 0 || (_plugin$helpers$viewp3 = _plugin$helpers$viewp2.getFilename) === null || _plugin$helpers$viewp3 === void 0 ? void 0 : _plugin$helpers$viewp3.call(_plugin$helpers$viewp2, ".png")) !== null && _plugin$helpers$viewp !== void 0 ? _plugin$helpers$viewp : FALLBACK_FILENAME;
2060
+ }
2061
+ /**
2062
+ * What the capture button does: produces the image and gets it to the user.
2063
+ *
2064
+ * Without `render` or `deliver` this is the stock download, unchanged. With
2065
+ * them, the image comes from `render` when one is given and from the viewer's
2066
+ * own capture otherwise, and goes to `deliver` when one is given and to a
2067
+ * browser download otherwise.
2068
+ */
2069
+ function exportStructureImage(_x6) {
2070
+ return _exportStructureImage.apply(this, arguments);
2071
+ }
2072
+ function _exportStructureImage() {
2073
+ _exportStructureImage = _asyncToGenerator(function* (plugin, download$1 = {}) {
2074
+ const { deliver, filename, render } = download$1;
2075
+ if (!render && !deliver) {
2076
+ yield downloadStructureImage(plugin, download$1);
2077
+ return;
2078
+ }
2079
+ const image = render ? yield render(plugin) : yield captureStructureImage(plugin, download$1);
2080
+ const name = imageFilename(plugin, filename);
2081
+ if (deliver) yield deliver(image, name);
2082
+ else download(image, name);
2083
+ });
2084
+ return _exportStructureImage.apply(this, arguments);
2085
+ }
2086
+ //#endregion
2087
+ //#region src/core/ProteinStructureViewer/scene/renderStructureImage.ts
2088
+ /** Painted behind the structure when a background is asked for but unreadable. */
2089
+ const FALLBACK_BACKGROUND = "#ffffff";
2090
+ /**
2091
+ * Renders a structure to a PNG without a viewer on the page, drawn exactly as
2092
+ * the viewer would draw it from the same options - the image a caller can show
2093
+ * beside the interactive view, or hand to someone who cannot open one.
2094
+ *
2095
+ * A plugin is created for the one image, offscreen, and disposed of once it is
2096
+ * taken, whether or not taking it succeeded. The image is `width` by `height`
2097
+ * exactly: rendered at that size rather than scaled, and not cropped to the
2098
+ * structure.
2099
+ *
2100
+ * It needs a DOM and WebGL - a browser, or headless Chromium - and rejects
2101
+ * when there is no WebGL to render with.
2102
+ */
2103
+ function renderStructureImage(_x) {
2104
+ return _renderStructureImage.apply(this, arguments);
2105
+ }
2106
+ function _renderStructureImage() {
2107
+ _renderStructureImage = _asyncToGenerator(function* (options) {
2108
+ const { backgroundColor, height, width } = options;
2109
+ if (!Number.isFinite(width) || !Number.isFinite(height) || width < 1 || height < 1) throw new Error("An image needs a width and a height of at least 1.");
2110
+ const container = document.createElement("div");
2111
+ container.setAttribute("aria-hidden", "true");
2112
+ container.style.cssText = `position:fixed;left:-10000px;top:0;width:${width}px;height:${height}px;pointer-events:none`;
2113
+ const canvas = document.createElement("canvas");
2114
+ canvas.style.cssText = "display:block;width:100%;height:100%";
2115
+ container.append(canvas);
2116
+ document.body.append(container);
2117
+ const plugin = new PluginContext(DefaultPluginSpec());
2118
+ plugin.canvas3dInitialized.catch(() => void 0);
2119
+ try {
2120
+ yield plugin.init();
2121
+ const initialized = yield plugin.initViewerAsync(canvas, container);
2122
+ const { canvas3d } = plugin;
2123
+ const helper = plugin.helpers.viewportScreenshot;
2124
+ if (!initialized || !canvas3d || !helper) throw new Error("WebGL is not available to render the structure.");
2125
+ if (backgroundColor !== void 0) canvas3d.setProps({ renderer: { backgroundColor: themeColor(backgroundColor, FALLBACK_BACKGROUND) } });
2126
+ yield applyScene(plugin, options, 0);
2127
+ canvas3d.commit(true);
2128
+ helper.behaviors.cropParams.next({
2129
+ auto: false,
2130
+ relativePadding: 0
2131
+ });
2132
+ helper.resetCrop();
2133
+ helper.behaviors.values.next(_objectSpread2(_objectSpread2({}, helper.behaviors.values.value), {}, {
2134
+ axes: AXES_OFF,
2135
+ format: {
2136
+ name: "png",
2137
+ params: {}
2138
+ },
2139
+ resolution: {
2140
+ name: "custom",
2141
+ params: {
2142
+ height,
2143
+ width
2144
+ }
2145
+ },
2146
+ transparent: backgroundColor === void 0
2147
+ }));
2148
+ return blobFromDataUri(yield helper.getImageDataUri());
2149
+ } finally {
2150
+ plugin.dispose();
2151
+ container.remove();
2152
+ }
2153
+ });
2154
+ return _renderStructureImage.apply(this, arguments);
2155
+ }
2156
+ //#endregion
2157
+ export { isEmptySelection as A, resolveSelectionAddresses as C, lociForResidueRange as D, readCameraState as E, selectionKey as M, selectionLabel as N, lociForSelection as O, selectionResidues as P, residueAddressesKey as S, camerasEqual as T, chainColorMap as _, applyStructureScene as a, _asyncToGenerator as b, resolveColorBy as c, injectPlddt as d, injectPlddtIntoMmcif as f, themeColor as g, highlightsKey as h, AXES_ON as i, meanOverResidues as j, lociOutsideChains as k, PLDDT_BAND_COLORS as l, HIGHLIGHT_COLOR_PALETTE as m, exportStructureImage as n, StructureScene as o, injectPlddtIntoPdb as p, AXES_OFF as r, chainSetKey as s, renderStructureImage as t, PLDDT_COLOR_SCALE as u, NOTHING_LOADED as v, chainsEqual as w, detectStructureFormat as x, loadInfo as y };