@czi-sds/data-viz 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.esm.js CHANGED
@@ -7,9 +7,8 @@ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
7
7
  import { Button, Legend, Theme, Tooltip, TooltipTable, defaultTheme, fontBodyXs, fontBodyXxxs, fontCodeXs, fontHeaderM, fontHeaderS, fontTabularSemiboldXxxxs, generateDiscreteColors, getCorners, getMode, getSemanticColors, getSpaces } from "@czi-sds/components";
8
8
  import { ThemeProvider, useTheme } from "@mui/material/styles";
9
9
  import { ApertureIcon, ArrowCounterClockwiseIcon, CheckIcon, CopyIcon, EyeIcon, EyeSlashIcon, QuestionIcon } from "@phosphor-icons/react";
10
- import { Color } from "molstar/lib/mol-util/color";
11
- import { OrderedSet } from "molstar/lib/mol-data/int";
12
10
  import { Structure, StructureElement, StructureProperties, Unit } from "molstar/lib/mol-model/structure";
11
+ import { clearStructureTransparency, setStructureTransparency } from "molstar/lib/mol-plugin-state/helpers/structure-transparency";
13
12
  import { createPluginUI } from "molstar/lib/mol-plugin-ui";
14
13
  import { renderReact18 } from "molstar/lib/mol-plugin-ui/react18";
15
14
  import { DefaultPluginUISpec } from "molstar/lib/mol-plugin-ui/spec";
@@ -22,6 +21,7 @@ import { EveryLoci, Loci } from "molstar/lib/mol-model/loci";
22
21
  import { PluginReactContext } from "molstar/lib/mol-plugin-ui/base";
23
22
  import { ButtonsType, getButton, getButtons, getModifiers } from "molstar/lib/mol-util/input/input-observer";
24
23
  import { throttleTime } from "rxjs/operators";
24
+ import { OrderedSet } from "molstar/lib/mol-data/int";
25
25
  import { useTheme as useTheme$1 } from "@mui/material";
26
26
  import { PluginStateObject } from "molstar/lib/mol-plugin-state/objects";
27
27
  import { getChainOptions, getModelEntityOptions, getOperatorOptions, getSequenceWrapper, getStructureOptions } from "molstar/lib/mol-plugin-ui/sequence";
@@ -30,6 +30,7 @@ import { arrayEqual } from "molstar/lib/mol-util";
30
30
  import { ParamDefinition } from "molstar/lib/mol-util/param-definition";
31
31
  import { Toasts } from "molstar/lib/mol-plugin-ui/toast";
32
32
  import { Viewport } from "molstar/lib/mol-plugin-ui/viewport";
33
+ import { Color } from "molstar/lib/mol-util/color";
33
34
  import { PluginCommands } from "molstar/lib/mol-plugin/commands";
34
35
  import { Vec3 } from "molstar/lib/mol-math/linear-algebra";
35
36
  import { BondType } from "molstar/lib/mol-model/structure/model/types";
@@ -1206,249 +1207,183 @@ function StructureLegend({ chainBandColors, chainColors, chains = [], hiddenChai
1206
1207
  });
1207
1208
  }
1208
1209
  //#endregion
1209
- //#region src/core/ProteinStructureViewer/utils/selection.ts
1210
- /**
1211
- * A stable string for a selection, for comparing one against another.
1212
- *
1213
- * A consumer holding the selection in state hands back a new object on every
1214
- * render, so identity says nothing about whether the selection changed. Order
1215
- * is normalized too: `{ residues: [2, 1] }` and `{ residues: [1, 2] }` select
1216
- * the same residues and must not read as a change.
1217
- */
1218
- function selectionKey(selection) {
1219
- var _selection$residues, _selection$chains;
1220
- const residues = [...new Set((_selection$residues = selection.residues) !== null && _selection$residues !== void 0 ? _selection$residues : [])].sort((a, b) => a - b);
1221
- return `${[...new Set((_selection$chains = selection.chains) !== null && _selection$chains !== void 0 ? _selection$chains : [])].sort().join(",")}|${residues.join(",")}`;
1210
+ //#region \0@oxc-project+runtime@0.134.0/helpers/esm/asyncToGenerator.js
1211
+ function asyncGeneratorStep(n, t, e, r, o, a, c) {
1212
+ try {
1213
+ var i = n[a](c), u = i.value;
1214
+ } catch (n) {
1215
+ e(n);
1216
+ return;
1217
+ }
1218
+ i.done ? t(u) : Promise.resolve(u).then(r, o);
1222
1219
  }
1223
- /** True when the selection names nothing at all. */
1224
- function isEmptySelection(selection) {
1225
- var _selection$residues2, _selection$chains2;
1226
- if (!selection) return true;
1227
- 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);
1220
+ function _asyncToGenerator(n) {
1221
+ return function() {
1222
+ var t = this, e = arguments;
1223
+ return new Promise(function(r, o) {
1224
+ var a = n.apply(t, e);
1225
+ function _next(n) {
1226
+ asyncGeneratorStep(a, r, o, _next, _throw, "next", n);
1227
+ }
1228
+ function _throw(n) {
1229
+ asyncGeneratorStep(a, r, o, _next, _throw, "throw", n);
1230
+ }
1231
+ _next(void 0);
1232
+ });
1233
+ };
1228
1234
  }
1235
+ //#endregion
1236
+ //#region src/core/ProteinStructureViewer/hooks/useChainHighlight.ts
1229
1237
  /**
1230
- * The residues a selection covers, ascending and without repeats.
1238
+ * How transparent the other chains go while one is pointed at.
1231
1239
  *
1232
- * Chains are expanded through `residuesByChain`, which was read off the loaded
1233
- * structure, so a chain contributes exactly the residues it holds rather than
1234
- * the span between its ends. A residue named directly and also carried by a
1235
- * selected chain is counted once.
1240
+ * Enough that the chain under the pointer is the one being read, not enough
1241
+ * that the rest of the complex stops being context for it. 0 is opaque, 1 is
1242
+ * invisible.
1236
1243
  */
1237
- function selectionResidues(selection, residuesByChain) {
1238
- var _selection$residues3, _selection$chains3;
1239
- const residues = new Set((_selection$residues3 = selection.residues) !== null && _selection$residues3 !== void 0 ? _selection$residues3 : []);
1240
- for (const chainId of (_selection$chains3 = selection.chains) !== null && _selection$chains3 !== void 0 ? _selection$chains3 : []) {
1241
- var _residuesByChain$get;
1242
- for (const index of (_residuesByChain$get = residuesByChain.get(chainId)) !== null && _residuesByChain$get !== void 0 ? _residuesByChain$get : []) residues.add(index);
1243
- }
1244
- return [...residues].sort((a, b) => a - b);
1245
- }
1244
+ const CHAIN_DIM_TRANSPARENCY = .65;
1246
1245
  /**
1247
- * Names a selection for the legend's readout.
1246
+ * The chain id in a component key.
1248
1247
  *
1249
- * A single chain is named as one, since "Chain A" says more than the number of
1250
- * residues it happens to hold. Anything else is reported as a count, which is
1251
- * the only thing true of every shape a selection can take. A lone residue is
1252
- * named by the caller instead, which reads it back off the structure to get its
1253
- * three-letter code and the number the file gives it.
1248
+ * Chain components are tagged `structure-component-<part>-<chainId>`, and a
1249
+ * key is the component's tags sorted and joined with commas. Anchoring on the
1250
+ * part name and stopping at the comma is what keeps `A` from also matching the
1251
+ * chain named `AA`.
1254
1252
  */
1255
- function selectionLabel(selection, residueCount) {
1256
- var _selection$chains4, _selection$residues4;
1257
- const chains = (_selection$chains4 = selection.chains) !== null && _selection$chains4 !== void 0 ? _selection$chains4 : [];
1258
- if (chains.length === 1 && !((_selection$residues4 = selection.residues) === null || _selection$residues4 === void 0 ? void 0 : _selection$residues4.length)) return `Chain ${chains[0]}`;
1259
- return `${residueCount} residues`;
1253
+ const COMPONENT_CHAIN_ID = /structure-component-(?:polymer|ligand)-([^,]+)/;
1254
+ /** The chain a component was built for, or undefined if it is not a chain's. */
1255
+ function componentChainId(component) {
1256
+ var _component$key, _COMPONENT_CHAIN_ID$e;
1257
+ const key = (_component$key = component.key) !== null && _component$key !== void 0 ? _component$key : component.cell.transform.ref;
1258
+ return (_COMPONENT_CHAIN_ID$e = COMPONENT_CHAIN_ID.exec(key)) === null || _COMPONENT_CHAIN_ID$e === void 0 ? void 0 : _COMPONENT_CHAIN_ID$e[1];
1260
1259
  }
1261
- /** The mean of the values `read` returns, ignoring residues without one. */
1262
- function meanOverResidues(residues, read) {
1263
- let sum = 0;
1264
- let count = 0;
1265
- for (const index of residues) {
1266
- const value = read(index);
1267
- if (value === null || value === void 0) continue;
1268
- sum += value;
1269
- count++;
1270
- }
1271
- return count === 0 ? null : sum / count;
1260
+ /** Every chain component of every loaded structure, polymers and ligands. */
1261
+ function structureComponents(plugin) {
1262
+ return plugin.managers.structure.hierarchy.current.structures.flatMap((entry) => entry.components);
1272
1263
  }
1273
- //#endregion
1274
- //#region src/core/ProteinStructureViewer/utils/residueLoci.ts
1275
- /**
1276
- * Every atom the predicate accepts, as geometry Mol* can focus, frame and mark,
1277
- * or undefined when it accepts none.
1278
- *
1279
- * One pass over the structure rather than a pass per residue: a whole-chain
1280
- * selection names hundreds of residues, and resolving them one at a time would
1281
- * walk the structure once for each.
1282
- *
1283
- * Atoms are bucketed by the unit they belong to because element indices are
1284
- * unit-local. That bucketing is also what lets a result span a chain break: it
1285
- * carries an entry per unit, which is what a range across two chains needs and
1286
- * what a single unit-local entry cannot express.
1287
- *
1288
- * Whole residues are collected rather than one atom each, since the caller
1289
- * frames the camera on the loci's bounding sphere and a lone atom would zoom
1290
- * far closer than the residue warrants.
1291
- */
1292
- function lociMatching(structure, matches) {
1293
- const location = StructureElement.Location.create(structure);
1294
- const elements = [];
1295
- for (const unit of structure.units) {
1296
- location.unit = unit;
1297
- const indices = [];
1298
- for (let i = 0; i < unit.elements.length; i++) {
1299
- location.element = unit.elements[i];
1300
- if (matches(location)) indices.push(i);
1301
- }
1302
- if (indices.length > 0) elements.push({
1303
- indices,
1304
- unit
1305
- });
1306
- }
1307
- if (elements.length === 0) return void 0;
1308
- return StructureElement.Loci(structure, elements.map(({ indices, unit }) => ({
1309
- indices: OrderedSet.ofSortedArray(indices),
1310
- unit
1311
- })));
1264
+ /** A set of chain ids as one comparable string. */
1265
+ function chainsKey(chains) {
1266
+ return [...chains].sort().join("\0");
1312
1267
  }
1313
1268
  /**
1314
- * Every atom of the residues a selection covers, within one structure.
1269
+ * Makes every chain outside `lit` transparent, or all of them opaque again when
1270
+ * nothing is lit.
1315
1271
  *
1316
- * `hiddenChains` is subtracted from the result. A hidden chain is not drawn, so
1317
- * geometry built from it would have nothing to sit on: Mol*'s focus
1318
- * representation draws its own ball-and-stick from whatever is focused, in a
1319
- * part of the state tree the chain's own visibility does not reach, and a
1320
- * hidden chain left in the loci would keep showing up there after its cartoon
1321
- * had gone.
1322
- */
1323
- function lociForSelectionInStructure(structure, selection, hiddenChains) {
1324
- var _selection$residues, _selection$chains;
1325
- if (isEmptySelection(selection)) return void 0;
1326
- const residues = new Set((_selection$residues = selection.residues) !== null && _selection$residues !== void 0 ? _selection$residues : []);
1327
- const chains = new Set((_selection$chains = selection.chains) !== null && _selection$chains !== void 0 ? _selection$chains : []);
1328
- return lociMatching(structure, (location) => {
1329
- const chainId = StructureProperties.chain.auth_asym_id(location);
1330
- if (hiddenChains === null || hiddenChains === void 0 ? void 0 : hiddenChains.has(chainId)) return false;
1331
- return residues.has(StructureProperties.residue.key(location)) || chains.has(chainId);
1332
- });
1333
- }
1334
- /**
1335
- * Every atom of one chain's polymer, in the first structure that has one.
1272
+ * Transparency layers accumulate, so what is already there is cleared before
1273
+ * the next chain is dimmed rather than added to - otherwise moving along the
1274
+ * list would leave every chain it passed dimmed behind it.
1336
1275
  *
1337
- * The polymer alone, so pointing at a chain's name lights up the cartoon the
1338
- * name is captioning. The ligands sitting on the chain belong to it but are
1339
- * not what it is named for, and they are drawn by a representation of their
1340
- * own.
1276
+ * False when there is no structure to write to yet, which is what a selection
1277
+ * arriving before the first load looks like. It is applied again after the load
1278
+ * rather than being taken as done.
1341
1279
  */
1342
- function lociForChainPolymer(plugin, chainId) {
1343
- for (const entry of plugin.managers.structure.hierarchy.current.structures) {
1344
- var _entry$cell$obj;
1345
- const structure = (_entry$cell$obj = entry.cell.obj) === null || _entry$cell$obj === void 0 ? void 0 : _entry$cell$obj.data;
1346
- if (!structure) continue;
1347
- const loci = lociMatching(structure, (location) => StructureProperties.chain.auth_asym_id(location) === chainId && StructureProperties.entity.type(location) === "polymer");
1348
- if (loci) return loci;
1349
- }
1280
+ function dimChainsOutside(_x2, _x3) {
1281
+ return _dimChainsOutside.apply(this, arguments);
1350
1282
  }
1351
- /**
1352
- * Every atom of the residues between two 0-based indices, inclusive.
1353
- *
1354
- * A contiguous span is tested by comparison rather than by expanding it into a
1355
- * set of indices: this runs on every residue a drag crosses, and a drag can
1356
- * span the whole structure.
1357
- */
1358
- function lociForResidueRange(structure, min, max) {
1359
- return lociMatching(structure, (location) => {
1360
- const index = StructureProperties.residue.key(location);
1361
- return index >= min && index <= max;
1283
+ function _dimChainsOutside() {
1284
+ _dimChainsOutside = _asyncToGenerator(function* (plugin, lit) {
1285
+ const components = structureComponents(plugin);
1286
+ if (components.length === 0) return false;
1287
+ yield clearStructureTransparency(plugin, components);
1288
+ if (lit.size === 0) return true;
1289
+ const dimmed = components.filter((component) => {
1290
+ const chainId = componentChainId(component);
1291
+ return chainId !== void 0 && !lit.has(chainId);
1292
+ });
1293
+ if (dimmed.length === 0) return true;
1294
+ yield setStructureTransparency(plugin, dimmed, CHAIN_DIM_TRANSPARENCY, function() {
1295
+ var _ref = _asyncToGenerator(function* (structure) {
1296
+ return Structure.toStructureElementLoci(structure);
1297
+ });
1298
+ return function(_x) {
1299
+ return _ref.apply(this, arguments);
1300
+ };
1301
+ }());
1302
+ return true;
1362
1303
  });
1304
+ return _dimChainsOutside.apply(this, arguments);
1363
1305
  }
1364
1306
  /**
1365
- * The same, across every structure loaded into a plugin. Results are merged
1366
- * into the first structure that matched: a loci belongs to one structure, and
1367
- * the residue index space already spans them as though they were one.
1368
- */
1369
- function lociForSelection(plugin, selection, hiddenChains) {
1370
- for (const entry of plugin.managers.structure.hierarchy.current.structures) {
1371
- var _entry$cell$obj2;
1372
- const structure = (_entry$cell$obj2 = entry.cell.obj) === null || _entry$cell$obj2 === void 0 ? void 0 : _entry$cell$obj2.data;
1373
- if (!structure) continue;
1374
- const loci = lociForSelectionInStructure(structure, selection, hiddenChains);
1375
- if (loci) return loci;
1376
- }
1377
- }
1378
- //#endregion
1379
- //#region src/core/ProteinStructureViewer/hooks/useChainHighlight.ts
1380
- /**
1381
- * What a chain lights up in while its name is under the pointer.
1307
+ * Leaves one chain lit and dims the rest: the chain whose name is pointed at,
1308
+ * in the sequence panel's captions or the chain legend's rows, or the chain the
1309
+ * selection covers once the pointer has gone.
1382
1310
  *
1383
- * Its own color rather than the theme's hover tint, because it answers a
1384
- * different question. Hovering a residue asks "what is this?" and a tint is
1385
- * enough; pointing at a chain's name asks "where is this chain?" of a
1386
- * structure where the answer may be half the screen, and has to carry across
1387
- * a cartoon already painted in the chain palette, in pLDDT bands, or in an
1388
- * overlay. A saturated pink is in none of those, so it cannot be mistaken for
1389
- * the structure's own coloring.
1390
- */
1391
- const CHAIN_HIGHLIGHT_COLOR = Color.fromRgb(200, 45, 149);
1392
- /**
1393
- * Stronger than the residue hover's tint, for the same reason. A whole chain
1394
- * washed in 20% pink reads as a rendering artifact; at this strength it reads
1395
- * as deliberate.
1396
- */
1397
- const CHAIN_HIGHLIGHT_STRENGTH = .7;
1398
- /**
1399
- * Lights up a whole chain in the 3D view while its name is pointed at, in the
1400
- * sequence panel's captions or the chain legend's rows.
1311
+ * The chain left lit is exactly as it was - in the chain palette, in pLDDT
1312
+ * bands, or in an overlay - and the others recede around it. Nothing is painted
1313
+ * over the top, so what the reader is being pointed at is still the color the
1314
+ * legend beside it is describing, and a selected chain reads the same as a
1315
+ * hovered one rather than sprouting a highlight of its own.
1401
1316
  *
1402
- * Mol*'s marking system is what draws it, so the chain is tinted and outlined
1403
- * exactly as a hovered residue is, with no geometry added to the state tree
1404
- * and nothing to tear down. Marking colors are a property of the renderer
1405
- * rather than of the loci, though, so the pink is swapped in for the length of
1406
- * the hover and the theme's own colors handed back on the way out.
1317
+ * A hover outranks the selection while it lasts, since pointing at a chain is a
1318
+ * question about that chain, and hands it back on the way out.
1407
1319
  *
1408
- * Those colors are read off the canvas at that moment rather than passed in.
1409
- * They are already derived from the theme in two places, and a third copy kept
1410
- * in step by hand would be the one to drift.
1411
- */
1412
- function useChainHighlight({ disabled, pluginRef }) {
1413
- const borrowedRef = useRef(null);
1414
- const highlightChain = useCallback((name) => {
1415
- var _borrowedRef$current;
1416
- const plugin = pluginRef.current;
1417
- const canvas3d = plugin === null || plugin === void 0 ? void 0 : plugin.canvas3d;
1418
- if (!plugin || !canvas3d) return;
1419
- const chainId = disabled ? null : name;
1420
- if (chainId === null) {
1421
- plugin.managers.interactivity.lociHighlights.clearHighlights();
1422
- const borrowed = borrowedRef.current;
1423
- if (borrowed) {
1424
- canvas3d.setProps({
1425
- marking: { highlightEdgeColor: borrowed.highlightEdgeColor },
1426
- renderer: {
1427
- highlightColor: borrowed.highlightColor,
1428
- highlightStrength: borrowed.highlightStrength
1429
- }
1430
- });
1431
- borrowedRef.current = null;
1320
+ * Dimming is a state-tree commit rather than a renderer setting, so it is
1321
+ * applied one at a time and coalesced: a pointer crossing the list faster than
1322
+ * a commit finishes skips the chains it passed over and settles on the one it
1323
+ * came to rest on, instead of working through a queue of hovers that have
1324
+ * already been left.
1325
+ */
1326
+ function useChainHighlight({ disabled, hiddenChains, loadCount, pluginRef, selectedChains }) {
1327
+ const hoveredRef = useRef(null);
1328
+ /**
1329
+ * The chains left lit on the canvas as it stands. A freshly loaded structure
1330
+ * carries no dimming at all, which is what the empty key means.
1331
+ */
1332
+ const shownRef = useRef(chainsKey(/* @__PURE__ */ new Set()));
1333
+ const committingRef = useRef(false);
1334
+ /**
1335
+ * Read when a commit comes round rather than closed over, so one already in
1336
+ * flight settles on the current props instead of the ones it started under.
1337
+ */
1338
+ const propsRef = useRef({
1339
+ disabled,
1340
+ hiddenChains,
1341
+ selectedChains
1342
+ });
1343
+ propsRef.current = {
1344
+ disabled,
1345
+ hiddenChains,
1346
+ selectedChains
1347
+ };
1348
+ const litChains = useCallback(() => {
1349
+ const { disabled: hoverOff, hiddenChains: hidden, selectedChains: selected } = propsRef.current;
1350
+ const hovered = hoveredRef.current;
1351
+ if (!hoverOff && hovered !== null && !hidden.has(hovered)) return new Set([hovered]);
1352
+ return new Set([...selected].filter((chainId) => !hidden.has(chainId)));
1353
+ }, []);
1354
+ const settle = useCallback(_asyncToGenerator(function* () {
1355
+ if (committingRef.current) return;
1356
+ committingRef.current = true;
1357
+ try {
1358
+ while (pluginRef.current) {
1359
+ const lit = litChains();
1360
+ const key = chainsKey(lit);
1361
+ if (key === shownRef.current) break;
1362
+ if (!(yield dimChainsOutside(pluginRef.current, lit))) break;
1363
+ shownRef.current = key;
1432
1364
  }
1433
- return;
1365
+ } catch (error) {
1366
+ console.error("Failed to dim chains:", error);
1367
+ } finally {
1368
+ committingRef.current = false;
1434
1369
  }
1435
- const loci = lociForChainPolymer(plugin, chainId);
1436
- if (!loci) return;
1437
- (_borrowedRef$current = borrowedRef.current) !== null && _borrowedRef$current !== void 0 || (borrowedRef.current = {
1438
- highlightColor: canvas3d.props.renderer.highlightColor,
1439
- highlightEdgeColor: canvas3d.props.marking.highlightEdgeColor,
1440
- highlightStrength: canvas3d.props.renderer.highlightStrength
1441
- });
1442
- canvas3d.setProps({
1443
- marking: { highlightEdgeColor: CHAIN_HIGHLIGHT_COLOR },
1444
- renderer: {
1445
- highlightColor: CHAIN_HIGHLIGHT_COLOR,
1446
- highlightStrength: CHAIN_HIGHLIGHT_STRENGTH
1447
- }
1448
- });
1449
- plugin.managers.interactivity.lociHighlights.highlightOnly({ loci });
1450
- }, [disabled, pluginRef]);
1451
- useEffect(() => () => void (borrowedRef.current = null), []);
1370
+ }), [litChains, pluginRef]);
1371
+ const highlightChain = useCallback((chainId) => {
1372
+ hoveredRef.current = chainId;
1373
+ settle();
1374
+ }, [settle]);
1375
+ useEffect(() => {
1376
+ settle();
1377
+ }, [
1378
+ disabled,
1379
+ hiddenChains,
1380
+ selectedChains,
1381
+ settle
1382
+ ]);
1383
+ useEffect(() => {
1384
+ shownRef.current = chainsKey(/* @__PURE__ */ new Set());
1385
+ settle();
1386
+ }, [loadCount, settle]);
1452
1387
  return highlightChain;
1453
1388
  }
1454
1389
  //#endregion
@@ -2149,13 +2084,166 @@ function useResidueMarkers({ containerRef, residueColors, sequenceWrapper }) {
2149
2084
  });
2150
2085
  }
2151
2086
  //#endregion
2087
+ //#region src/core/ProteinStructureViewer/utils/selection.ts
2088
+ /**
2089
+ * A stable string for a selection, for comparing one against another.
2090
+ *
2091
+ * A consumer holding the selection in state hands back a new object on every
2092
+ * render, so identity says nothing about whether the selection changed. Order
2093
+ * is normalized too: `{ residues: [2, 1] }` and `{ residues: [1, 2] }` select
2094
+ * the same residues and must not read as a change.
2095
+ */
2096
+ function selectionKey(selection) {
2097
+ var _selection$residues, _selection$chains;
2098
+ const residues = [...new Set((_selection$residues = selection.residues) !== null && _selection$residues !== void 0 ? _selection$residues : [])].sort((a, b) => a - b);
2099
+ return `${[...new Set((_selection$chains = selection.chains) !== null && _selection$chains !== void 0 ? _selection$chains : [])].sort().join(",")}|${residues.join(",")}`;
2100
+ }
2101
+ /** True when the selection names nothing at all. */
2102
+ function isEmptySelection(selection) {
2103
+ var _selection$residues2, _selection$chains2;
2104
+ if (!selection) return true;
2105
+ 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);
2106
+ }
2107
+ /**
2108
+ * The residues a selection covers, ascending and without repeats.
2109
+ *
2110
+ * Chains are expanded through `residuesByChain`, which was read off the loaded
2111
+ * structure, so a chain contributes exactly the residues it holds rather than
2112
+ * the span between its ends. A residue named directly and also carried by a
2113
+ * selected chain is counted once.
2114
+ */
2115
+ function selectionResidues(selection, residuesByChain) {
2116
+ var _selection$residues3, _selection$chains3;
2117
+ const residues = new Set((_selection$residues3 = selection.residues) !== null && _selection$residues3 !== void 0 ? _selection$residues3 : []);
2118
+ for (const chainId of (_selection$chains3 = selection.chains) !== null && _selection$chains3 !== void 0 ? _selection$chains3 : []) {
2119
+ var _residuesByChain$get;
2120
+ for (const index of (_residuesByChain$get = residuesByChain.get(chainId)) !== null && _residuesByChain$get !== void 0 ? _residuesByChain$get : []) residues.add(index);
2121
+ }
2122
+ return [...residues].sort((a, b) => a - b);
2123
+ }
2124
+ /**
2125
+ * Names a selection for the legend's readout.
2126
+ *
2127
+ * A single chain is named as one, since "Chain A" says more than the number of
2128
+ * residues it happens to hold. Anything else is reported as a count, which is
2129
+ * the only thing true of every shape a selection can take. A lone residue is
2130
+ * named by the caller instead, which reads it back off the structure to get its
2131
+ * three-letter code and the number the file gives it.
2132
+ */
2133
+ function selectionLabel(selection, residueCount) {
2134
+ var _selection$chains4, _selection$residues4;
2135
+ const chains = (_selection$chains4 = selection.chains) !== null && _selection$chains4 !== void 0 ? _selection$chains4 : [];
2136
+ if (chains.length === 1 && !((_selection$residues4 = selection.residues) === null || _selection$residues4 === void 0 ? void 0 : _selection$residues4.length)) return `Chain ${chains[0]}`;
2137
+ return `${residueCount} residues`;
2138
+ }
2139
+ /** The mean of the values `read` returns, ignoring residues without one. */
2140
+ function meanOverResidues(residues, read) {
2141
+ let sum = 0;
2142
+ let count = 0;
2143
+ for (const index of residues) {
2144
+ const value = read(index);
2145
+ if (value === null || value === void 0) continue;
2146
+ sum += value;
2147
+ count++;
2148
+ }
2149
+ return count === 0 ? null : sum / count;
2150
+ }
2151
+ //#endregion
2152
+ //#region src/core/ProteinStructureViewer/utils/residueLoci.ts
2153
+ /**
2154
+ * Every atom the predicate accepts, as geometry Mol* can focus, frame and mark,
2155
+ * or undefined when it accepts none.
2156
+ *
2157
+ * One pass over the structure rather than a pass per residue: a whole-chain
2158
+ * selection names hundreds of residues, and resolving them one at a time would
2159
+ * walk the structure once for each.
2160
+ *
2161
+ * Atoms are bucketed by the unit they belong to because element indices are
2162
+ * unit-local. That bucketing is also what lets a result span a chain break: it
2163
+ * carries an entry per unit, which is what a range across two chains needs and
2164
+ * what a single unit-local entry cannot express.
2165
+ *
2166
+ * Whole residues are collected rather than one atom each, since the caller
2167
+ * frames the camera on the loci's bounding sphere and a lone atom would zoom
2168
+ * far closer than the residue warrants.
2169
+ */
2170
+ function lociMatching(structure, matches) {
2171
+ const location = StructureElement.Location.create(structure);
2172
+ const elements = [];
2173
+ for (const unit of structure.units) {
2174
+ location.unit = unit;
2175
+ const indices = [];
2176
+ for (let i = 0; i < unit.elements.length; i++) {
2177
+ location.element = unit.elements[i];
2178
+ if (matches(location)) indices.push(i);
2179
+ }
2180
+ if (indices.length > 0) elements.push({
2181
+ indices,
2182
+ unit
2183
+ });
2184
+ }
2185
+ if (elements.length === 0) return void 0;
2186
+ return StructureElement.Loci(structure, elements.map(({ indices, unit }) => ({
2187
+ indices: OrderedSet.ofSortedArray(indices),
2188
+ unit
2189
+ })));
2190
+ }
2191
+ /**
2192
+ * Every atom of the residues a selection covers, within one structure.
2193
+ *
2194
+ * `hiddenChains` is subtracted from the result. A hidden chain is not drawn, so
2195
+ * geometry built from it would have nothing to sit on: Mol*'s focus
2196
+ * representation draws its own ball-and-stick from whatever is focused, in a
2197
+ * part of the state tree the chain's own visibility does not reach, and a
2198
+ * hidden chain left in the loci would keep showing up there after its cartoon
2199
+ * had gone.
2200
+ */
2201
+ function lociForSelectionInStructure(structure, selection, hiddenChains) {
2202
+ var _selection$residues, _selection$chains;
2203
+ if (isEmptySelection(selection)) return void 0;
2204
+ const residues = new Set((_selection$residues = selection.residues) !== null && _selection$residues !== void 0 ? _selection$residues : []);
2205
+ const chains = new Set((_selection$chains = selection.chains) !== null && _selection$chains !== void 0 ? _selection$chains : []);
2206
+ return lociMatching(structure, (location) => {
2207
+ const chainId = StructureProperties.chain.auth_asym_id(location);
2208
+ if (hiddenChains === null || hiddenChains === void 0 ? void 0 : hiddenChains.has(chainId)) return false;
2209
+ return residues.has(StructureProperties.residue.key(location)) || chains.has(chainId);
2210
+ });
2211
+ }
2212
+ /**
2213
+ * Every atom of the residues between two 0-based indices, inclusive.
2214
+ *
2215
+ * A contiguous span is tested by comparison rather than by expanding it into a
2216
+ * set of indices: this runs on every residue a drag crosses, and a drag can
2217
+ * span the whole structure.
2218
+ */
2219
+ function lociForResidueRange(structure, min, max) {
2220
+ return lociMatching(structure, (location) => {
2221
+ const index = StructureProperties.residue.key(location);
2222
+ return index >= min && index <= max;
2223
+ });
2224
+ }
2225
+ /**
2226
+ * The same, across every structure loaded into a plugin. Results are merged
2227
+ * into the first structure that matched: a loci belongs to one structure, and
2228
+ * the residue index space already spans them as though they were one.
2229
+ */
2230
+ function lociForSelection(plugin, selection, hiddenChains) {
2231
+ for (const entry of plugin.managers.structure.hierarchy.current.structures) {
2232
+ var _entry$cell$obj;
2233
+ const structure = (_entry$cell$obj = entry.cell.obj) === null || _entry$cell$obj === void 0 ? void 0 : _entry$cell$obj.data;
2234
+ if (!structure) continue;
2235
+ const loci = lociForSelectionInStructure(structure, selection, hiddenChains);
2236
+ if (loci) return loci;
2237
+ }
2238
+ }
2239
+ //#endregion
2152
2240
  //#region src/core/ProteinStructureViewer/utils/residueRef.ts
2153
2241
  /**
2154
2242
  * Reads the residue a loci points at, or null when it points at nothing the
2155
2243
  * viewer can address.
2156
2244
  *
2157
2245
  * `index` comes from Mol*'s residue key, which counts residues in file order
2158
- * from zero and is the same counter `injectPlddtIntoPdb` walks. Deriving it
2246
+ * from zero and is the same counter `injectPlddt` walks. Deriving it
2159
2247
  * from the residue's *number* instead would tie it to how the file happens to
2160
2248
  * be numbered: a crop beginning at residue 200 would index `plddt` at 199, and
2161
2249
  * a file numbering each chain from 1 would give two residues the same index.
@@ -2939,32 +3027,6 @@ function neutralResidueColor(mode) {
2939
3027
  return Color.fromRgb(g, g, g);
2940
3028
  }
2941
3029
  //#endregion
2942
- //#region \0@oxc-project+runtime@0.134.0/helpers/esm/asyncToGenerator.js
2943
- function asyncGeneratorStep(n, t, e, r, o, a, c) {
2944
- try {
2945
- var i = n[a](c), u = i.value;
2946
- } catch (n) {
2947
- e(n);
2948
- return;
2949
- }
2950
- i.done ? t(u) : Promise.resolve(u).then(r, o);
2951
- }
2952
- function _asyncToGenerator(n) {
2953
- return function() {
2954
- var t = this, e = arguments;
2955
- return new Promise(function(r, o) {
2956
- var a = n.apply(t, e);
2957
- function _next(n) {
2958
- asyncGeneratorStep(a, r, o, _next, _throw, "next", n);
2959
- }
2960
- function _throw(n) {
2961
- asyncGeneratorStep(a, r, o, _next, _throw, "throw", n);
2962
- }
2963
- _next(void 0);
2964
- });
2965
- };
2966
- }
2967
- //#endregion
2968
3030
  //#region src/core/ProteinStructureViewer/utils/screenshot.ts
2969
3031
  /**
2970
3032
  * Each resolution as the Mol* preset that produces it: 1280x720, 1920x1080,
@@ -3303,6 +3365,25 @@ function focusResidue(plugin, loci) {
3303
3365
  return clipRatio;
3304
3366
  }
3305
3367
  /**
3368
+ * Frames the camera on a loci without cropping anything around it.
3369
+ *
3370
+ * The counterpart to `focusResidue` for something meant to be read in context
3371
+ * rather than peeled down to: the camera fits what was selected, and the depth
3372
+ * clip is left open to the whole scene, so the rest of the structure stays on
3373
+ * screen. A selected chain is shown by dimming the chains around it, and they
3374
+ * have to be there to be dimmed.
3375
+ */
3376
+ function frameLoci(plugin, loci) {
3377
+ var _plugin$canvas3d2;
3378
+ const camera = (_plugin$canvas3d2 = plugin.canvas3d) === null || _plugin$canvas3d2 === void 0 ? void 0 : _plugin$canvas3d2.camera;
3379
+ if (!camera) return;
3380
+ const sphere = Loci.getBoundingSphere(loci);
3381
+ if (!sphere) return;
3382
+ const framingRadius = Math.max(sphere.radius + FOCUS_EXTRA_RADIUS, FOCUS_MIN_RADIUS);
3383
+ const snapshot = camera.getFocus(sphere.center, framingRadius);
3384
+ camera.setState(_objectSpread2(_objectSpread2({}, snapshot), {}, { radius: camera.state.radiusMax }), FOCUS_DURATION_MS);
3385
+ }
3386
+ /**
3306
3387
  * Keeps the depth clip in sync with the zoom while a residue is focused.
3307
3388
  *
3308
3389
  * Mol*'s trackball zoom moves the camera without touching the clip radius, so
@@ -3312,15 +3393,15 @@ function focusResidue(plugin, loci) {
3312
3393
  * camera is mid-transition, so it does not fight the focus animation.
3313
3394
  */
3314
3395
  function syncClipToZoom(plugin, getClipRatio) {
3315
- var _plugin$canvas3d2;
3316
- const camera = (_plugin$canvas3d2 = plugin.canvas3d) === null || _plugin$canvas3d2 === void 0 ? void 0 : _plugin$canvas3d2.camera;
3396
+ var _plugin$canvas3d3;
3397
+ const camera = (_plugin$canvas3d3 = plugin.canvas3d) === null || _plugin$canvas3d3 === void 0 ? void 0 : _plugin$canvas3d3.camera;
3317
3398
  const clipRatio = getClipRatio();
3318
3399
  if (!camera || clipRatio === null || camera.transition.inTransition) return;
3319
3400
  const radius = clipRadiusForDistance(Vec3.distance(camera.state.position, camera.state.target), clipRatio, camera.state.radiusMax);
3320
3401
  if (Math.abs(camera.state.radius - radius) > CLIP_EPSILON) {
3321
- var _plugin$canvas3d3;
3402
+ var _plugin$canvas3d4;
3322
3403
  camera.setState({ radius });
3323
- (_plugin$canvas3d3 = plugin.canvas3d) === null || _plugin$canvas3d3 === void 0 || _plugin$canvas3d3.requestDraw();
3404
+ (_plugin$canvas3d4 = plugin.canvas3d) === null || _plugin$canvas3d4 === void 0 || _plugin$canvas3d4.requestDraw();
3324
3405
  }
3325
3406
  }
3326
3407
  //#endregion
@@ -3535,6 +3616,22 @@ function mergeMolstarSpec(base, override) {
3535
3616
  return merged;
3536
3617
  }
3537
3618
  //#endregion
3619
+ //#region src/core/ProteinStructureViewer/utils/structureFormat.ts
3620
+ /** How much of the file to inspect when guessing the format. */
3621
+ const DETECT_SAMPLE = 16384;
3622
+ /**
3623
+ * Infers whether `text` is PDB or mmCIF (PDBx).
3624
+ *
3625
+ * mmCIF documents start with a `data_` block and name their coordinates
3626
+ * `_atom_site.`. PDB never does either, so either marker is enough. A leading
3627
+ * comment (`# ...`) before `data_` is still mmCIF.
3628
+ */
3629
+ function detectStructureFormat(text) {
3630
+ const sample = text.slice(0, DETECT_SAMPLE);
3631
+ if (/(?:^|\n)\s*data_/.test(sample) || /_atom_site\./.test(sample)) return "mmcif";
3632
+ return "pdb";
3633
+ }
3634
+ //#endregion
3538
3635
  //#region src/core/ProteinStructureViewer/utils/chainColorTheme.ts
3539
3636
  /** Mol* color theme name under which per-chain coloring is registered. */
3540
3637
  const CHAIN_COLOR_THEME_NAME = "chain-color";
@@ -3673,9 +3770,183 @@ function injectPlddtIntoPdb(pdbData, plddtValues) {
3673
3770
  return paddedLine.substring(0, 60) + bfactorStr + paddedLine.substring(66);
3674
3771
  }).join("\n");
3675
3772
  }
3773
+ const ATOM_SITE_PREFIX = "_atom_site.";
3774
+ const B_FACTOR_COLUMN = "B_iso_or_equiv";
3775
+ function skipCifWhitespace(line, index) {
3776
+ let i = index;
3777
+ while (i < line.length && /\s/.test(line.charAt(i))) i += 1;
3778
+ return i;
3779
+ }
3780
+ function readCifToken(line, start) {
3781
+ const quote = line.charAt(start);
3782
+ if (quote === "'" || quote === "\"") {
3783
+ const end = line.indexOf(quote, start + 1);
3784
+ if (end < 0) return {
3785
+ next: line.length,
3786
+ token: line.slice(start + 1)
3787
+ };
3788
+ return {
3789
+ next: end + 1,
3790
+ token: line.slice(start + 1, end)
3791
+ };
3792
+ }
3793
+ let i = start;
3794
+ while (i < line.length && !/\s/.test(line.charAt(i))) i += 1;
3795
+ return {
3796
+ next: i,
3797
+ token: line.slice(start, i)
3798
+ };
3799
+ }
3800
+ /**
3801
+ * Splits one CIF line into tokens, honouring `'quoted'` and `"quoted"` values.
3802
+ * Atom-site rows are one line each in practice; multiline `;text;` fields are
3803
+ * not used there and are not handled.
3804
+ */
3805
+ function tokenizeCifLine(line) {
3806
+ const tokens = [];
3807
+ let i = skipCifWhitespace(line, 0);
3808
+ while (i < line.length) {
3809
+ const { next, token } = readCifToken(line, i);
3810
+ tokens.push(token);
3811
+ i = skipCifWhitespace(line, next);
3812
+ }
3813
+ return tokens;
3814
+ }
3815
+ function isCifNull(value) {
3816
+ return value === void 0 || value === "." || value === "?";
3817
+ }
3818
+ function columnIndex(columns, name) {
3819
+ return columns.indexOf(name);
3820
+ }
3821
+ /**
3822
+ * Residue identity matching `injectPlddtIntoPdb`: chain + sequence number +
3823
+ * insertion code. Prefers the `auth_*` columns Mol* reports on the parsed
3824
+ * model, falling back to `label_*` when a file omitted them. `.` / `?`
3825
+ * insertion codes count as empty, the way a blank PDB column 27 does.
3826
+ */
3827
+ function mmcifResidueKey(tokens, columns) {
3828
+ var _ref, _ref2, _ref3, _ref4;
3829
+ const authAsym = columnIndex(columns, "auth_asym_id");
3830
+ const labelAsym = columnIndex(columns, "label_asym_id");
3831
+ const authSeq = columnIndex(columns, "auth_seq_id");
3832
+ const labelSeq = columnIndex(columns, "label_seq_id");
3833
+ const ins = columnIndex(columns, "pdbx_PDB_ins_code");
3834
+ 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]}`;
3835
+ }
3836
+ function isAtomSiteRow(tokens, columns) {
3837
+ const group = columnIndex(columns, "group_PDB");
3838
+ if (group < 0) return true;
3839
+ const value = tokens[group];
3840
+ return value === "ATOM" || value === "HETATM";
3841
+ }
3842
+ function plddtBFactor(residueIndex, plddtValues) {
3843
+ var _plddtValues$residueI2;
3844
+ return (residueIndex < plddtValues.length ? ((_plddtValues$residueI2 = plddtValues[residueIndex]) !== null && _plddtValues$residueI2 !== void 0 ? _plddtValues$residueI2 : 0) * 100 : 50).toFixed(2);
3845
+ }
3846
+ function isLoopStart(line) {
3847
+ return line.trim() === "loop_" || line.trim().startsWith("loop_");
3848
+ }
3849
+ function isAtomSiteColumn(line) {
3850
+ return line.trim().startsWith(ATOM_SITE_PREFIX);
3851
+ }
3852
+ function endsAtomSiteLoop(line) {
3853
+ const trimmed = line.trim();
3854
+ if (trimmed === "") return false;
3855
+ if (trimmed.startsWith("#")) return true;
3856
+ if (isLoopStart(trimmed)) return true;
3857
+ if (trimmed.startsWith("data_") || trimmed.startsWith("save_")) return true;
3858
+ return trimmed.startsWith("_") && !trimmed.startsWith(ATOM_SITE_PREFIX);
3859
+ }
3860
+ function isAtomSiteLoop(lines, loopIndex) {
3861
+ let peek = loopIndex + 1;
3862
+ while (peek < lines.length && lines[peek].trim() === "") peek += 1;
3863
+ return peek < lines.length && isAtomSiteColumn(lines[peek]);
3864
+ }
3865
+ function ensureBFactorColumn(columns, headerLines) {
3866
+ const existing = columnIndex(columns, B_FACTOR_COLUMN);
3867
+ if (existing >= 0) return existing;
3868
+ columns.push(B_FACTOR_COLUMN);
3869
+ headerLines.push(`${ATOM_SITE_PREFIX}${B_FACTOR_COLUMN}`);
3870
+ return columns.length - 1;
3871
+ }
3872
+ function rewriteAtomSiteRow(dataLine, columns, bIndex, plddtValues, walk) {
3873
+ if (dataLine.trim() === "") return dataLine;
3874
+ const tokens = tokenizeCifLine(dataLine);
3875
+ if (tokens.length === 0) return dataLine;
3876
+ while (tokens.length < columns.length) tokens.push(".");
3877
+ if (isAtomSiteRow(tokens, columns)) {
3878
+ const residue = mmcifResidueKey(tokens, columns);
3879
+ if (residue !== walk.lastResidue) {
3880
+ walk.residueIndex += 1;
3881
+ walk.lastResidue = residue;
3882
+ }
3883
+ tokens[bIndex] = plddtBFactor(walk.residueIndex, plddtValues);
3884
+ }
3885
+ return tokens.join(" ");
3886
+ }
3887
+ function rewriteAtomSiteLoop(lines, loopIndex, plddtValues, walk) {
3888
+ const rows = [lines[loopIndex]];
3889
+ let i = loopIndex + 1;
3890
+ const columns = [];
3891
+ while (i < lines.length && isAtomSiteColumn(lines[i])) {
3892
+ columns.push(lines[i].trim().slice(11));
3893
+ rows.push(lines[i]);
3894
+ i += 1;
3895
+ }
3896
+ const bIndex = ensureBFactorColumn(columns, rows);
3897
+ while (i < lines.length && !endsAtomSiteLoop(lines[i])) {
3898
+ rows.push(rewriteAtomSiteRow(lines[i], columns, bIndex, plddtValues, walk));
3899
+ i += 1;
3900
+ }
3901
+ return {
3902
+ next: i,
3903
+ rows
3904
+ };
3905
+ }
3906
+ /**
3907
+ * Rewrites an mmCIF (PDBx) file's `_atom_site.B_iso_or_equiv` column with
3908
+ * pLDDT scores so Mol* can read them off the parsed model, the same way
3909
+ * `injectPlddtIntoPdb` rewrites the PDB B-factor column. Scores arrive on a
3910
+ * 0-1 scale and are stored on the conventional 0-100 one. Residues past the
3911
+ * end of `plddtValues` fall back to a mid-confidence 50.
3912
+ *
3913
+ * If the file has no B-factor column, one is appended to the atom-site loop
3914
+ * rather than leaving the scores behind.
3915
+ */
3916
+ function injectPlddtIntoMmcif(mmcifData, plddtValues) {
3917
+ const lines = mmcifData.split("\n");
3918
+ const result = [];
3919
+ const walk = {
3920
+ lastResidue: "",
3921
+ residueIndex: -1
3922
+ };
3923
+ let i = 0;
3924
+ let rewrittenLoop = false;
3925
+ while (i < lines.length) {
3926
+ const line = lines[i];
3927
+ if (!rewrittenLoop && isLoopStart(line) && isAtomSiteLoop(lines, i)) {
3928
+ const rewritten = rewriteAtomSiteLoop(lines, i, plddtValues, walk);
3929
+ result.push(...rewritten.rows);
3930
+ i = rewritten.next;
3931
+ rewrittenLoop = true;
3932
+ continue;
3933
+ }
3934
+ result.push(line);
3935
+ i += 1;
3936
+ }
3937
+ return result.join("\n");
3938
+ }
3939
+ /**
3940
+ * Rewrites a structure file's B-factors with pLDDT scores. Dispatches to
3941
+ * `injectPlddtIntoPdb` or `injectPlddtIntoMmcif` from `format`, or from
3942
+ * detecting the format of `data` when `format` is omitted.
3943
+ */
3944
+ function injectPlddt(data, plddtValues, format = detectStructureFormat(data)) {
3945
+ return format === "mmcif" ? injectPlddtIntoMmcif(data, plddtValues) : injectPlddtIntoPdb(data, plddtValues);
3946
+ }
3676
3947
  /**
3677
3948
  * Mol* color theme that reads pLDDT scores out of the B-factor column, paired
3678
- * with `injectPlddtIntoPdb`. Registered per plugin instance.
3949
+ * with `injectPlddt`. Registered per plugin instance.
3679
3950
  */
3680
3951
  const PlddtColoring = CustomElementProperty.create({
3681
3952
  coloring: {
@@ -3985,27 +4256,30 @@ const NOTHING_LOADED = {
3985
4256
  residuesByChain: /* @__PURE__ */ new Map()
3986
4257
  };
3987
4258
  /**
3988
- * Parses the PDB and draws each chain: a cartoon over its polymer, and
4259
+ * Parses the structure and draws each chain: a cartoon over its polymer, and
3989
4260
  * ball-and-stick over its ligands and ions.
3990
4261
  *
3991
4262
  * Mol*'s `default` hierarchy preset would be shorter, but it groups every
3992
4263
  * polymer chain into a single component, and a component is the unit Mol* can
3993
4264
  * hide. Building them per chain is what makes a chain individually hideable;
3994
4265
  * the refs handed back are what `setSubtreeVisibility` is later pointed at.
4266
+ *
4267
+ * The wrapper detects PDB vs mmCIF from the text; this just forwards that
4268
+ * guess to Mol*.
3995
4269
  */
3996
4270
  function loadStructure(_x9, _x10, _x11, _x12) {
3997
4271
  return _loadStructure.apply(this, arguments);
3998
4272
  }
3999
4273
  function _loadStructure() {
4000
- _loadStructure = _asyncToGenerator(function* (plugin, pdbData, usePlddtColoring, showAxes) {
4274
+ _loadStructure = _asyncToGenerator(function* (plugin, structureText, usePlddtColoring, showAxes) {
4001
4275
  try {
4002
4276
  var _plugin$canvas3d;
4003
4277
  yield plugin.clear();
4004
4278
  const data = yield plugin.builders.data.rawData({
4005
- data: pdbData,
4279
+ data: structureText,
4006
4280
  label: "Structure"
4007
4281
  });
4008
- const trajectory = yield plugin.builders.structure.parseTrajectory(data, "pdb");
4282
+ const trajectory = yield plugin.builders.structure.parseTrajectory(data, detectStructureFormat(structureText));
4009
4283
  const model = yield plugin.builders.structure.createModel(trajectory);
4010
4284
  const structure = yield plugin.builders.structure.createStructure(model, {
4011
4285
  name: "model",
@@ -4079,16 +4353,17 @@ function _applyColorTheme() {
4079
4353
  *
4080
4354
  * The plugin is built once and then mutated in place. Creating it is expensive
4081
4355
  * and destroys the camera, so prop changes that Mol* can absorb (background,
4082
- * axes, coloring, new PDB) are pushed in through the effects below and in the
4083
- * sibling hooks rather than by rebuilding.
4356
+ * axes, coloring, new structure) are pushed in through the effects below and in
4357
+ * the sibling hooks rather than by rebuilding.
4084
4358
  */
4085
- function useMolstarPlugin({ backgroundColor, chainColors, containerRef, download, edgeColor, hasPlddt, hiddenChains, highlightColor, mode, molstarSpec, onChainHover, onChainSelect, onChainToggle, onResidueClick, onResidueHover, onSelectionChange, onSelectionClear, pdb, selectedChains, sequenceViewerBackgroundColor, showAxes, showSequenceViewer }) {
4359
+ function useMolstarPlugin({ backgroundColor, chainColors, containerRef, download, edgeColor, hasPlddt, hiddenChains, highlightColor, mode, molstarSpec, onChainHover, onChainSelect, onChainToggle, onResidueClick, onResidueHover, onSelectionChange, onSelectionClear, selectedChains, sequenceViewerBackgroundColor, showAxes, showSequenceViewer, structure }) {
4086
4360
  const pluginRef = useRef(null);
4087
4361
  const residueValueThemeRef = useRef(null);
4088
4362
  const chainColorThemeRef = useRef(null);
4089
- const currentPdbRef = useRef(null);
4363
+ const currentStructureRef = useRef(null);
4090
4364
  const [isReady, setIsReady] = useState(false);
4091
4365
  const [chains, setChains] = useState([]);
4366
+ const [loadCount, setLoadCount] = useState(0);
4092
4367
  /**
4093
4368
  * Where each chain's components live in the state tree, which is what the
4094
4369
  * visibility effect points `setSubtreeVisibility` at. Rewritten by every
@@ -4145,6 +4420,7 @@ function useMolstarPlugin({ backgroundColor, chainColors, containerRef, download
4145
4420
  residuesByChainRef.current = loaded.residuesByChain;
4146
4421
  applyChainVisibility(plugin, loaded.componentRefs, hiddenChainsRef.current);
4147
4422
  setChains((prev) => chainsEqual(prev, loaded.chains) ? prev : loaded.chains);
4423
+ setLoadCount((count) => count + 1);
4148
4424
  }, []);
4149
4425
  const onResidueClickRef = useRef(onResidueClick);
4150
4426
  onResidueClickRef.current = onResidueClick;
@@ -4163,9 +4439,9 @@ function useMolstarPlugin({ backgroundColor, chainColors, containerRef, download
4163
4439
  highlightColor,
4164
4440
  mode,
4165
4441
  molstarSpec,
4166
- pdb,
4167
4442
  showAxes,
4168
- showSequenceViewer
4443
+ showSequenceViewer,
4444
+ structure
4169
4445
  });
4170
4446
  initialPropsRef.current = {
4171
4447
  backgroundColor,
@@ -4174,9 +4450,9 @@ function useMolstarPlugin({ backgroundColor, chainColors, containerRef, download
4174
4450
  highlightColor,
4175
4451
  mode,
4176
4452
  molstarSpec,
4177
- pdb,
4178
4453
  showAxes,
4179
- showSequenceViewer
4454
+ showSequenceViewer,
4455
+ structure
4180
4456
  };
4181
4457
  /**
4182
4458
  * The consumer's `canvas3d` overrides, keyed by content. Unlike the rest of
@@ -4231,8 +4507,8 @@ function useMolstarPlugin({ backgroundColor, chainColors, containerRef, download
4231
4507
  residueValueThemeRef.current = residueValueTheme;
4232
4508
  chainColorThemeRef.current = chainColorTheme;
4233
4509
  const latest = initialPropsRef.current;
4234
- const loaded = yield loadStructure(plugin, latest.pdb, latest.hasPlddt, latest.showAxes);
4235
- currentPdbRef.current = latest.pdb;
4510
+ const loaded = yield loadStructure(plugin, latest.structure, latest.hasPlddt, latest.showAxes);
4511
+ currentStructureRef.current = latest.structure;
4236
4512
  adoptLoadedStructure(plugin, loaded);
4237
4513
  /**
4238
4514
  * Mol*'s click behavior is a BehaviorSubject, so subscribing replays
@@ -4294,7 +4570,7 @@ function useMolstarPlugin({ backgroundColor, chainColors, containerRef, download
4294
4570
  chainColorThemeRef.current = null;
4295
4571
  componentRefsRef.current = /* @__PURE__ */ new Map();
4296
4572
  residuesByChainRef.current = /* @__PURE__ */ new Map();
4297
- currentPdbRef.current = null;
4573
+ currentStructureRef.current = null;
4298
4574
  clipRatioRef.current = null;
4299
4575
  setIsReady(false);
4300
4576
  };
@@ -4353,16 +4629,16 @@ function useMolstarPlugin({ backgroundColor, chainColors, containerRef, download
4353
4629
  useEffect(() => {
4354
4630
  const plugin = pluginRef.current;
4355
4631
  if (!plugin || !isReady) return;
4356
- if (pdb === currentPdbRef.current) return;
4357
- currentPdbRef.current = pdb;
4632
+ if (structure === currentStructureRef.current) return;
4633
+ currentStructureRef.current = structure;
4358
4634
  clipRatioRef.current = null;
4359
4635
  lastHoverRef.current = null;
4360
- loadStructure(plugin, pdb, hasPlddt, showAxes).then((loaded) => {
4636
+ loadStructure(plugin, structure, hasPlddt, showAxes).then((loaded) => {
4361
4637
  if (pluginRef.current !== plugin) return;
4362
4638
  adoptLoadedStructure(plugin, loaded);
4363
4639
  });
4364
4640
  }, [
4365
- pdb,
4641
+ structure,
4366
4642
  hasPlddt,
4367
4643
  isReady,
4368
4644
  adoptLoadedStructure
@@ -4376,6 +4652,7 @@ function useMolstarPlugin({ backgroundColor, chainColors, containerRef, download
4376
4652
  chainColorThemeRef,
4377
4653
  chains,
4378
4654
  isReady,
4655
+ loadCount,
4379
4656
  pluginRef,
4380
4657
  residuesByChainRef,
4381
4658
  residueValueThemeRef,
@@ -4491,10 +4768,14 @@ function focusWithShell(plugin, loci, includeNeighbours, currentShell, pluginRef
4491
4768
  /**
4492
4769
  * Makes `selection` the one thing that decides where the camera sits.
4493
4770
  *
4494
- * Setting the prop frames whatever it covers and marks it; clearing it drops
4495
- * the focus and zooms back out. The camera frames the selection's bounding
4496
- * sphere, so a range or a whole chain is fitted rather than approached as a
4497
- * point - one residue is just the smallest case of that.
4771
+ * Setting the prop frames whatever it covers; clearing it drops the focus and
4772
+ * zooms back out. The camera frames the selection's bounding sphere, so a range
4773
+ * or a whole chain is fitted rather than approached as a point - one residue is
4774
+ * just the smallest case of that.
4775
+ *
4776
+ * Residues are also marked, which is what draws Mol*'s ball-and-stick and
4777
+ * outline over them. Whole chains are not: those are shown by dimming the
4778
+ * chains around them instead, in `useChainHighlight`.
4498
4779
  *
4499
4780
  * The click subscription only reports the selection it was given, so a click
4500
4781
  * moves the camera only once the consumer accepts it and echoes it back -
@@ -4503,6 +4784,7 @@ function focusWithShell(plugin, loci, includeNeighbours, currentShell, pluginRef
4503
4784
  * responds to a click whether or not the consumer wires the prop up.
4504
4785
  */
4505
4786
  function useSelectionFocus({ hiddenChains, isReady, pluginRef, selection, setClipRatio }) {
4787
+ var _selection$chains, _selection$residues;
4506
4788
  /**
4507
4789
  * What this hook last pointed the camera at, as a key rather than the object
4508
4790
  * itself: a consumer echoing a selection back builds a new object each time,
@@ -4527,6 +4809,11 @@ function useSelectionFocus({ hiddenChains, isReady, pluginRef, selection, setCli
4527
4809
  * echoing an equal selection back from refocusing on every render.
4528
4810
  */
4529
4811
  const focusKey = selection ? `${selectionKey(selection)}#${[...hiddenChains].sort().join(",")}` : null;
4812
+ /**
4813
+ * Whether the selection names chains and nothing else, which is the shape
4814
+ * shown by dimming the chains around it rather than by focusing it.
4815
+ */
4816
+ const chainsOnly = Boolean((selection === null || selection === void 0 || (_selection$chains = selection.chains) === null || _selection$chains === void 0 ? void 0 : _selection$chains.length) && !((_selection$residues = selection.residues) === null || _selection$residues === void 0 ? void 0 : _selection$residues.length));
4530
4817
  useEffect(() => {
4531
4818
  var _chosen$chains;
4532
4819
  const plugin = pluginRef.current;
@@ -4547,6 +4834,12 @@ function useSelectionFocus({ hiddenChains, isReady, pluginRef, selection, setCli
4547
4834
  clearFocus(plugin, setClipRatio, false);
4548
4835
  return;
4549
4836
  }
4837
+ if (chainsOnly) {
4838
+ plugin.managers.structure.focus.clear();
4839
+ frameLoci(plugin, loci);
4840
+ setClipRatio(null);
4841
+ return;
4842
+ }
4550
4843
  return focusWithShell(plugin, loci, !((_chosen$chains = chosen.chains) === null || _chosen$chains === void 0 ? void 0 : _chosen$chains.length), neighbourhoodRef, pluginRef, setClipRatio);
4551
4844
  }, [
4552
4845
  focusKey,
@@ -4708,7 +5001,7 @@ const _excluded$1 = [
4708
5001
  "onResidueClick",
4709
5002
  "onResidueHover",
4710
5003
  "onSelectionChange",
4711
- "pdb",
5004
+ "structure",
4712
5005
  "plddt",
4713
5006
  "residueOverlay",
4714
5007
  "selection",
@@ -4776,14 +5069,14 @@ function resolveScaleProps(overlay, hasPlddt) {
4776
5069
  }
4777
5070
  var ProteinStructureViewer_default = memo(forwardRef((props, ref) => {
4778
5071
  var _selection$chains;
4779
- const { backgroundColor, chainColors: chainColorOverrides, disableChainHighlightOnHover = false, download, hiddenChains: hiddenChainsProp, onChainVisibilityChange, onChainsChange, molstarSpec, onResidueClick, onResidueHover, onSelectionChange, pdb, plddt, residueOverlay, selection = null, sequenceViewerBackgroundColor, showAxes = true, showChainLegend = true, showLegend = true, showSequenceViewer = true, stats } = props, rest = _objectWithoutProperties(props, _excluded$1);
5072
+ const { backgroundColor, chainColors: chainColorOverrides, disableChainHighlightOnHover = false, download, hiddenChains: hiddenChainsProp, onChainVisibilityChange, onChainsChange, molstarSpec, onResidueClick, onResidueHover, onSelectionChange, structure, plddt, residueOverlay, selection = null, sequenceViewerBackgroundColor, showAxes = true, showChainLegend = true, showLegend = true, showSequenceViewer = true, stats } = props, rest = _objectWithoutProperties(props, _excluded$1);
4780
5073
  const theme = useTheme();
4781
5074
  const mode = getMode({ theme });
4782
5075
  const semanticColors = getSemanticColors({ theme });
4783
5076
  const pluginMountRef = useRef(null);
4784
5077
  const hasPlddt = Boolean(plddt && plddt.length > 0);
4785
- const processedPdb = useMemo(() => hasPlddt ? injectPlddtIntoPdb(pdb, plddt) : pdb, [
4786
- pdb,
5078
+ const processedStructure = useMemo(() => hasPlddt ? injectPlddt(structure, plddt) : structure, [
5079
+ structure,
4787
5080
  plddt,
4788
5081
  hasPlddt
4789
5082
  ]);
@@ -4830,13 +5123,14 @@ var ProteinStructureViewer_default = memo(forwardRef((props, ref) => {
4830
5123
  onSelectionChange === null || onSelectionChange === void 0 || onSelectionChange(isOnlyThisChain ? null : { chains: [chainId] });
4831
5124
  }, [onSelectionChange, selection]);
4832
5125
  /**
4833
- * Lights a chain up while its name is pointed at, in the legend or in the
4834
- * sequence panel's captions.
5126
+ * Dims every other chain while a chain's name is pointed at, in the legend
5127
+ * or in the sequence panel's captions, and leaves the selected chain lit
5128
+ * once the pointer has gone.
4835
5129
  *
4836
- * Reached through a ref because the two ends need each other: lighting a
4837
- * chain up takes the plugin, which the hook below creates, and that hook
4838
- * has to be handed the callback to give the captions it renders. This one
4839
- * is stable and forwards to whatever the highlight hook installs once
5130
+ * Reached through a ref because the two ends need each other: dimming
5131
+ * other chains takes the plugin, which the hook below creates, and that
5132
+ * hook has to be handed the callback to give the captions it renders. This
5133
+ * one is stable and forwards to whatever the highlight hook installs once
4840
5134
  * there is a plugin for it to talk to.
4841
5135
  */
4842
5136
  const highlightChainRef = useRef(() => void 0);
@@ -4850,7 +5144,7 @@ var ProteinStructureViewer_default = memo(forwardRef((props, ref) => {
4850
5144
  hiddenChains: hiddenChainsProp,
4851
5145
  onChainVisibilityChange
4852
5146
  });
4853
- const { chainColorThemeRef, chains: loadedChains, isReady, pluginRef, residuesByChainRef, residueValueThemeRef, setClipRatio } = useMolstarPlugin({
5147
+ const { chainColorThemeRef, chains: loadedChains, isReady, loadCount, pluginRef, residuesByChainRef, residueValueThemeRef, setClipRatio } = useMolstarPlugin({
4854
5148
  backgroundColor: bgColor,
4855
5149
  chainColors,
4856
5150
  containerRef: pluginMountRef,
@@ -4868,15 +5162,18 @@ var ProteinStructureViewer_default = memo(forwardRef((props, ref) => {
4868
5162
  onResidueHover: handleResidueHover,
4869
5163
  onSelectionChange,
4870
5164
  onSelectionClear: handleSelectionClear,
4871
- pdb: processedPdb,
4872
5165
  selectedChains,
4873
5166
  sequenceViewerBackgroundColor,
4874
5167
  showAxes,
4875
- showSequenceViewer
5168
+ showSequenceViewer,
5169
+ structure: processedStructure
4876
5170
  });
4877
5171
  highlightChainRef.current = useChainHighlight({
4878
5172
  disabled: disableChainHighlightOnHover,
4879
- pluginRef
5173
+ hiddenChains,
5174
+ loadCount,
5175
+ pluginRef,
5176
+ selectedChains
4880
5177
  });
4881
5178
  useEffect(() => {
4882
5179
  setChains(loadedChains);
@@ -5421,4 +5718,4 @@ const StackedBarChart = (props) => {
5421
5718
  return /* @__PURE__ */ jsxs(ChartWrapper, _objectSpread2(_objectSpread2({ width }, rest), {}, { children: [/* @__PURE__ */ jsxs(TitleContainer, { children: [/* @__PURE__ */ jsx(ChartTitle, { children: title }), !hideBadge && displayBadge && /* @__PURE__ */ jsx(StyledBadge, { children: displayBadge })] }), chartContent] }));
5422
5719
  };
5423
5720
  //#endregion
5424
- export { DOWNLOAD_RESOLUTIONS, HeatmapChart_default as HeatmapChart, PLASMA_COLOR_SCALE, PLDDT_COLOR_SCALE, ProteinStructureViewer_default as ProteinStructureViewer, StackedBarChart, injectPlddtIntoPdb, sampleColorScale, toCssGradient };
5721
+ export { DOWNLOAD_RESOLUTIONS, HeatmapChart_default as HeatmapChart, PLASMA_COLOR_SCALE, PLDDT_COLOR_SCALE, ProteinStructureViewer_default as ProteinStructureViewer, StackedBarChart, detectStructureFormat, injectPlddt, injectPlddtIntoMmcif, injectPlddtIntoPdb, sampleColorScale, toCssGradient };