@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.cjs.js CHANGED
@@ -31,9 +31,8 @@ let react_jsx_runtime = require("react/jsx-runtime");
31
31
  let _czi_sds_components = require("@czi-sds/components");
32
32
  let _mui_material_styles = require("@mui/material/styles");
33
33
  let _phosphor_icons_react = require("@phosphor-icons/react");
34
- let molstar_lib_mol_util_color = require("molstar/lib/mol-util/color");
35
- let molstar_lib_mol_data_int = require("molstar/lib/mol-data/int");
36
34
  let molstar_lib_mol_model_structure = require("molstar/lib/mol-model/structure");
35
+ let molstar_lib_mol_plugin_state_helpers_structure_transparency = require("molstar/lib/mol-plugin-state/helpers/structure-transparency");
37
36
  let molstar_lib_mol_plugin_ui = require("molstar/lib/mol-plugin-ui");
38
37
  let molstar_lib_mol_plugin_ui_react18 = require("molstar/lib/mol-plugin-ui/react18");
39
38
  let molstar_lib_mol_plugin_ui_spec = require("molstar/lib/mol-plugin-ui/spec");
@@ -46,6 +45,7 @@ let molstar_lib_mol_model_loci = require("molstar/lib/mol-model/loci");
46
45
  let molstar_lib_mol_plugin_ui_base = require("molstar/lib/mol-plugin-ui/base");
47
46
  let molstar_lib_mol_util_input_input_observer = require("molstar/lib/mol-util/input/input-observer");
48
47
  let rxjs_operators = require("rxjs/operators");
48
+ let molstar_lib_mol_data_int = require("molstar/lib/mol-data/int");
49
49
  let _mui_material = require("@mui/material");
50
50
  let molstar_lib_mol_plugin_state_objects = require("molstar/lib/mol-plugin-state/objects");
51
51
  let molstar_lib_mol_plugin_ui_sequence = require("molstar/lib/mol-plugin-ui/sequence");
@@ -54,6 +54,7 @@ let molstar_lib_mol_util = require("molstar/lib/mol-util");
54
54
  let molstar_lib_mol_util_param_definition = require("molstar/lib/mol-util/param-definition");
55
55
  let molstar_lib_mol_plugin_ui_toast = require("molstar/lib/mol-plugin-ui/toast");
56
56
  let molstar_lib_mol_plugin_ui_viewport = require("molstar/lib/mol-plugin-ui/viewport");
57
+ let molstar_lib_mol_util_color = require("molstar/lib/mol-util/color");
57
58
  let molstar_lib_mol_plugin_commands = require("molstar/lib/mol-plugin/commands");
58
59
  let molstar_lib_mol_math_linear_algebra = require("molstar/lib/mol-math/linear-algebra");
59
60
  let molstar_lib_mol_model_structure_model_types = require("molstar/lib/mol-model/structure/model/types");
@@ -1231,249 +1232,183 @@ function StructureLegend({ chainBandColors, chainColors, chains = [], hiddenChai
1231
1232
  });
1232
1233
  }
1233
1234
  //#endregion
1234
- //#region src/core/ProteinStructureViewer/utils/selection.ts
1235
- /**
1236
- * A stable string for a selection, for comparing one against another.
1237
- *
1238
- * A consumer holding the selection in state hands back a new object on every
1239
- * render, so identity says nothing about whether the selection changed. Order
1240
- * is normalized too: `{ residues: [2, 1] }` and `{ residues: [1, 2] }` select
1241
- * the same residues and must not read as a change.
1242
- */
1243
- function selectionKey(selection) {
1244
- var _selection$residues, _selection$chains;
1245
- const residues = [...new Set((_selection$residues = selection.residues) !== null && _selection$residues !== void 0 ? _selection$residues : [])].sort((a, b) => a - b);
1246
- return `${[...new Set((_selection$chains = selection.chains) !== null && _selection$chains !== void 0 ? _selection$chains : [])].sort().join(",")}|${residues.join(",")}`;
1235
+ //#region \0@oxc-project+runtime@0.134.0/helpers/esm/asyncToGenerator.js
1236
+ function asyncGeneratorStep(n, t, e, r, o, a, c) {
1237
+ try {
1238
+ var i = n[a](c), u = i.value;
1239
+ } catch (n) {
1240
+ e(n);
1241
+ return;
1242
+ }
1243
+ i.done ? t(u) : Promise.resolve(u).then(r, o);
1247
1244
  }
1248
- /** True when the selection names nothing at all. */
1249
- function isEmptySelection(selection) {
1250
- var _selection$residues2, _selection$chains2;
1251
- if (!selection) return true;
1252
- 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);
1245
+ function _asyncToGenerator(n) {
1246
+ return function() {
1247
+ var t = this, e = arguments;
1248
+ return new Promise(function(r, o) {
1249
+ var a = n.apply(t, e);
1250
+ function _next(n) {
1251
+ asyncGeneratorStep(a, r, o, _next, _throw, "next", n);
1252
+ }
1253
+ function _throw(n) {
1254
+ asyncGeneratorStep(a, r, o, _next, _throw, "throw", n);
1255
+ }
1256
+ _next(void 0);
1257
+ });
1258
+ };
1253
1259
  }
1260
+ //#endregion
1261
+ //#region src/core/ProteinStructureViewer/hooks/useChainHighlight.ts
1254
1262
  /**
1255
- * The residues a selection covers, ascending and without repeats.
1263
+ * How transparent the other chains go while one is pointed at.
1256
1264
  *
1257
- * Chains are expanded through `residuesByChain`, which was read off the loaded
1258
- * structure, so a chain contributes exactly the residues it holds rather than
1259
- * the span between its ends. A residue named directly and also carried by a
1260
- * selected chain is counted once.
1265
+ * Enough that the chain under the pointer is the one being read, not enough
1266
+ * that the rest of the complex stops being context for it. 0 is opaque, 1 is
1267
+ * invisible.
1261
1268
  */
1262
- function selectionResidues(selection, residuesByChain) {
1263
- var _selection$residues3, _selection$chains3;
1264
- const residues = new Set((_selection$residues3 = selection.residues) !== null && _selection$residues3 !== void 0 ? _selection$residues3 : []);
1265
- for (const chainId of (_selection$chains3 = selection.chains) !== null && _selection$chains3 !== void 0 ? _selection$chains3 : []) {
1266
- var _residuesByChain$get;
1267
- for (const index of (_residuesByChain$get = residuesByChain.get(chainId)) !== null && _residuesByChain$get !== void 0 ? _residuesByChain$get : []) residues.add(index);
1268
- }
1269
- return [...residues].sort((a, b) => a - b);
1270
- }
1269
+ const CHAIN_DIM_TRANSPARENCY = .65;
1271
1270
  /**
1272
- * Names a selection for the legend's readout.
1271
+ * The chain id in a component key.
1273
1272
  *
1274
- * A single chain is named as one, since "Chain A" says more than the number of
1275
- * residues it happens to hold. Anything else is reported as a count, which is
1276
- * the only thing true of every shape a selection can take. A lone residue is
1277
- * named by the caller instead, which reads it back off the structure to get its
1278
- * three-letter code and the number the file gives it.
1273
+ * Chain components are tagged `structure-component-<part>-<chainId>`, and a
1274
+ * key is the component's tags sorted and joined with commas. Anchoring on the
1275
+ * part name and stopping at the comma is what keeps `A` from also matching the
1276
+ * chain named `AA`.
1279
1277
  */
1280
- function selectionLabel(selection, residueCount) {
1281
- var _selection$chains4, _selection$residues4;
1282
- const chains = (_selection$chains4 = selection.chains) !== null && _selection$chains4 !== void 0 ? _selection$chains4 : [];
1283
- if (chains.length === 1 && !((_selection$residues4 = selection.residues) === null || _selection$residues4 === void 0 ? void 0 : _selection$residues4.length)) return `Chain ${chains[0]}`;
1284
- return `${residueCount} residues`;
1278
+ const COMPONENT_CHAIN_ID = /structure-component-(?:polymer|ligand)-([^,]+)/;
1279
+ /** The chain a component was built for, or undefined if it is not a chain's. */
1280
+ function componentChainId(component) {
1281
+ var _component$key, _COMPONENT_CHAIN_ID$e;
1282
+ const key = (_component$key = component.key) !== null && _component$key !== void 0 ? _component$key : component.cell.transform.ref;
1283
+ return (_COMPONENT_CHAIN_ID$e = COMPONENT_CHAIN_ID.exec(key)) === null || _COMPONENT_CHAIN_ID$e === void 0 ? void 0 : _COMPONENT_CHAIN_ID$e[1];
1285
1284
  }
1286
- /** The mean of the values `read` returns, ignoring residues without one. */
1287
- function meanOverResidues(residues, read) {
1288
- let sum = 0;
1289
- let count = 0;
1290
- for (const index of residues) {
1291
- const value = read(index);
1292
- if (value === null || value === void 0) continue;
1293
- sum += value;
1294
- count++;
1295
- }
1296
- return count === 0 ? null : sum / count;
1285
+ /** Every chain component of every loaded structure, polymers and ligands. */
1286
+ function structureComponents(plugin) {
1287
+ return plugin.managers.structure.hierarchy.current.structures.flatMap((entry) => entry.components);
1297
1288
  }
1298
- //#endregion
1299
- //#region src/core/ProteinStructureViewer/utils/residueLoci.ts
1300
- /**
1301
- * Every atom the predicate accepts, as geometry Mol* can focus, frame and mark,
1302
- * or undefined when it accepts none.
1303
- *
1304
- * One pass over the structure rather than a pass per residue: a whole-chain
1305
- * selection names hundreds of residues, and resolving them one at a time would
1306
- * walk the structure once for each.
1307
- *
1308
- * Atoms are bucketed by the unit they belong to because element indices are
1309
- * unit-local. That bucketing is also what lets a result span a chain break: it
1310
- * carries an entry per unit, which is what a range across two chains needs and
1311
- * what a single unit-local entry cannot express.
1312
- *
1313
- * Whole residues are collected rather than one atom each, since the caller
1314
- * frames the camera on the loci's bounding sphere and a lone atom would zoom
1315
- * far closer than the residue warrants.
1316
- */
1317
- function lociMatching(structure, matches) {
1318
- const location = molstar_lib_mol_model_structure.StructureElement.Location.create(structure);
1319
- const elements = [];
1320
- for (const unit of structure.units) {
1321
- location.unit = unit;
1322
- const indices = [];
1323
- for (let i = 0; i < unit.elements.length; i++) {
1324
- location.element = unit.elements[i];
1325
- if (matches(location)) indices.push(i);
1326
- }
1327
- if (indices.length > 0) elements.push({
1328
- indices,
1329
- unit
1330
- });
1331
- }
1332
- if (elements.length === 0) return void 0;
1333
- return molstar_lib_mol_model_structure.StructureElement.Loci(structure, elements.map(({ indices, unit }) => ({
1334
- indices: molstar_lib_mol_data_int.OrderedSet.ofSortedArray(indices),
1335
- unit
1336
- })));
1289
+ /** A set of chain ids as one comparable string. */
1290
+ function chainsKey(chains) {
1291
+ return [...chains].sort().join("\0");
1337
1292
  }
1338
1293
  /**
1339
- * Every atom of the residues a selection covers, within one structure.
1294
+ * Makes every chain outside `lit` transparent, or all of them opaque again when
1295
+ * nothing is lit.
1340
1296
  *
1341
- * `hiddenChains` is subtracted from the result. A hidden chain is not drawn, so
1342
- * geometry built from it would have nothing to sit on: Mol*'s focus
1343
- * representation draws its own ball-and-stick from whatever is focused, in a
1344
- * part of the state tree the chain's own visibility does not reach, and a
1345
- * hidden chain left in the loci would keep showing up there after its cartoon
1346
- * had gone.
1347
- */
1348
- function lociForSelectionInStructure(structure, selection, hiddenChains) {
1349
- var _selection$residues, _selection$chains;
1350
- if (isEmptySelection(selection)) return void 0;
1351
- const residues = new Set((_selection$residues = selection.residues) !== null && _selection$residues !== void 0 ? _selection$residues : []);
1352
- const chains = new Set((_selection$chains = selection.chains) !== null && _selection$chains !== void 0 ? _selection$chains : []);
1353
- return lociMatching(structure, (location) => {
1354
- const chainId = molstar_lib_mol_model_structure.StructureProperties.chain.auth_asym_id(location);
1355
- if (hiddenChains === null || hiddenChains === void 0 ? void 0 : hiddenChains.has(chainId)) return false;
1356
- return residues.has(molstar_lib_mol_model_structure.StructureProperties.residue.key(location)) || chains.has(chainId);
1357
- });
1358
- }
1359
- /**
1360
- * Every atom of one chain's polymer, in the first structure that has one.
1297
+ * Transparency layers accumulate, so what is already there is cleared before
1298
+ * the next chain is dimmed rather than added to - otherwise moving along the
1299
+ * list would leave every chain it passed dimmed behind it.
1361
1300
  *
1362
- * The polymer alone, so pointing at a chain's name lights up the cartoon the
1363
- * name is captioning. The ligands sitting on the chain belong to it but are
1364
- * not what it is named for, and they are drawn by a representation of their
1365
- * own.
1301
+ * False when there is no structure to write to yet, which is what a selection
1302
+ * arriving before the first load looks like. It is applied again after the load
1303
+ * rather than being taken as done.
1366
1304
  */
1367
- function lociForChainPolymer(plugin, chainId) {
1368
- for (const entry of plugin.managers.structure.hierarchy.current.structures) {
1369
- var _entry$cell$obj;
1370
- const structure = (_entry$cell$obj = entry.cell.obj) === null || _entry$cell$obj === void 0 ? void 0 : _entry$cell$obj.data;
1371
- if (!structure) continue;
1372
- const loci = lociMatching(structure, (location) => molstar_lib_mol_model_structure.StructureProperties.chain.auth_asym_id(location) === chainId && molstar_lib_mol_model_structure.StructureProperties.entity.type(location) === "polymer");
1373
- if (loci) return loci;
1374
- }
1305
+ function dimChainsOutside(_x2, _x3) {
1306
+ return _dimChainsOutside.apply(this, arguments);
1375
1307
  }
1376
- /**
1377
- * Every atom of the residues between two 0-based indices, inclusive.
1378
- *
1379
- * A contiguous span is tested by comparison rather than by expanding it into a
1380
- * set of indices: this runs on every residue a drag crosses, and a drag can
1381
- * span the whole structure.
1382
- */
1383
- function lociForResidueRange(structure, min, max) {
1384
- return lociMatching(structure, (location) => {
1385
- const index = molstar_lib_mol_model_structure.StructureProperties.residue.key(location);
1386
- return index >= min && index <= max;
1308
+ function _dimChainsOutside() {
1309
+ _dimChainsOutside = _asyncToGenerator(function* (plugin, lit) {
1310
+ const components = structureComponents(plugin);
1311
+ if (components.length === 0) return false;
1312
+ yield (0, molstar_lib_mol_plugin_state_helpers_structure_transparency.clearStructureTransparency)(plugin, components);
1313
+ if (lit.size === 0) return true;
1314
+ const dimmed = components.filter((component) => {
1315
+ const chainId = componentChainId(component);
1316
+ return chainId !== void 0 && !lit.has(chainId);
1317
+ });
1318
+ if (dimmed.length === 0) return true;
1319
+ yield (0, molstar_lib_mol_plugin_state_helpers_structure_transparency.setStructureTransparency)(plugin, dimmed, CHAIN_DIM_TRANSPARENCY, function() {
1320
+ var _ref = _asyncToGenerator(function* (structure) {
1321
+ return molstar_lib_mol_model_structure.Structure.toStructureElementLoci(structure);
1322
+ });
1323
+ return function(_x) {
1324
+ return _ref.apply(this, arguments);
1325
+ };
1326
+ }());
1327
+ return true;
1387
1328
  });
1329
+ return _dimChainsOutside.apply(this, arguments);
1388
1330
  }
1389
1331
  /**
1390
- * The same, across every structure loaded into a plugin. Results are merged
1391
- * into the first structure that matched: a loci belongs to one structure, and
1392
- * the residue index space already spans them as though they were one.
1393
- */
1394
- function lociForSelection(plugin, selection, hiddenChains) {
1395
- for (const entry of plugin.managers.structure.hierarchy.current.structures) {
1396
- var _entry$cell$obj2;
1397
- const structure = (_entry$cell$obj2 = entry.cell.obj) === null || _entry$cell$obj2 === void 0 ? void 0 : _entry$cell$obj2.data;
1398
- if (!structure) continue;
1399
- const loci = lociForSelectionInStructure(structure, selection, hiddenChains);
1400
- if (loci) return loci;
1401
- }
1402
- }
1403
- //#endregion
1404
- //#region src/core/ProteinStructureViewer/hooks/useChainHighlight.ts
1405
- /**
1406
- * What a chain lights up in while its name is under the pointer.
1332
+ * Leaves one chain lit and dims the rest: the chain whose name is pointed at,
1333
+ * in the sequence panel's captions or the chain legend's rows, or the chain the
1334
+ * selection covers once the pointer has gone.
1407
1335
  *
1408
- * Its own color rather than the theme's hover tint, because it answers a
1409
- * different question. Hovering a residue asks "what is this?" and a tint is
1410
- * enough; pointing at a chain's name asks "where is this chain?" of a
1411
- * structure where the answer may be half the screen, and has to carry across
1412
- * a cartoon already painted in the chain palette, in pLDDT bands, or in an
1413
- * overlay. A saturated pink is in none of those, so it cannot be mistaken for
1414
- * the structure's own coloring.
1415
- */
1416
- const CHAIN_HIGHLIGHT_COLOR = molstar_lib_mol_util_color.Color.fromRgb(200, 45, 149);
1417
- /**
1418
- * Stronger than the residue hover's tint, for the same reason. A whole chain
1419
- * washed in 20% pink reads as a rendering artifact; at this strength it reads
1420
- * as deliberate.
1421
- */
1422
- const CHAIN_HIGHLIGHT_STRENGTH = .7;
1423
- /**
1424
- * Lights up a whole chain in the 3D view while its name is pointed at, in the
1425
- * sequence panel's captions or the chain legend's rows.
1336
+ * The chain left lit is exactly as it was - in the chain palette, in pLDDT
1337
+ * bands, or in an overlay - and the others recede around it. Nothing is painted
1338
+ * over the top, so what the reader is being pointed at is still the color the
1339
+ * legend beside it is describing, and a selected chain reads the same as a
1340
+ * hovered one rather than sprouting a highlight of its own.
1426
1341
  *
1427
- * Mol*'s marking system is what draws it, so the chain is tinted and outlined
1428
- * exactly as a hovered residue is, with no geometry added to the state tree
1429
- * and nothing to tear down. Marking colors are a property of the renderer
1430
- * rather than of the loci, though, so the pink is swapped in for the length of
1431
- * the hover and the theme's own colors handed back on the way out.
1342
+ * A hover outranks the selection while it lasts, since pointing at a chain is a
1343
+ * question about that chain, and hands it back on the way out.
1432
1344
  *
1433
- * Those colors are read off the canvas at that moment rather than passed in.
1434
- * They are already derived from the theme in two places, and a third copy kept
1435
- * in step by hand would be the one to drift.
1436
- */
1437
- function useChainHighlight({ disabled, pluginRef }) {
1438
- const borrowedRef = (0, react.useRef)(null);
1439
- const highlightChain = (0, react.useCallback)((name) => {
1440
- var _borrowedRef$current;
1441
- const plugin = pluginRef.current;
1442
- const canvas3d = plugin === null || plugin === void 0 ? void 0 : plugin.canvas3d;
1443
- if (!plugin || !canvas3d) return;
1444
- const chainId = disabled ? null : name;
1445
- if (chainId === null) {
1446
- plugin.managers.interactivity.lociHighlights.clearHighlights();
1447
- const borrowed = borrowedRef.current;
1448
- if (borrowed) {
1449
- canvas3d.setProps({
1450
- marking: { highlightEdgeColor: borrowed.highlightEdgeColor },
1451
- renderer: {
1452
- highlightColor: borrowed.highlightColor,
1453
- highlightStrength: borrowed.highlightStrength
1454
- }
1455
- });
1456
- borrowedRef.current = null;
1345
+ * Dimming is a state-tree commit rather than a renderer setting, so it is
1346
+ * applied one at a time and coalesced: a pointer crossing the list faster than
1347
+ * a commit finishes skips the chains it passed over and settles on the one it
1348
+ * came to rest on, instead of working through a queue of hovers that have
1349
+ * already been left.
1350
+ */
1351
+ function useChainHighlight({ disabled, hiddenChains, loadCount, pluginRef, selectedChains }) {
1352
+ const hoveredRef = (0, react.useRef)(null);
1353
+ /**
1354
+ * The chains left lit on the canvas as it stands. A freshly loaded structure
1355
+ * carries no dimming at all, which is what the empty key means.
1356
+ */
1357
+ const shownRef = (0, react.useRef)(chainsKey(/* @__PURE__ */ new Set()));
1358
+ const committingRef = (0, react.useRef)(false);
1359
+ /**
1360
+ * Read when a commit comes round rather than closed over, so one already in
1361
+ * flight settles on the current props instead of the ones it started under.
1362
+ */
1363
+ const propsRef = (0, react.useRef)({
1364
+ disabled,
1365
+ hiddenChains,
1366
+ selectedChains
1367
+ });
1368
+ propsRef.current = {
1369
+ disabled,
1370
+ hiddenChains,
1371
+ selectedChains
1372
+ };
1373
+ const litChains = (0, react.useCallback)(() => {
1374
+ const { disabled: hoverOff, hiddenChains: hidden, selectedChains: selected } = propsRef.current;
1375
+ const hovered = hoveredRef.current;
1376
+ if (!hoverOff && hovered !== null && !hidden.has(hovered)) return new Set([hovered]);
1377
+ return new Set([...selected].filter((chainId) => !hidden.has(chainId)));
1378
+ }, []);
1379
+ const settle = (0, react.useCallback)(_asyncToGenerator(function* () {
1380
+ if (committingRef.current) return;
1381
+ committingRef.current = true;
1382
+ try {
1383
+ while (pluginRef.current) {
1384
+ const lit = litChains();
1385
+ const key = chainsKey(lit);
1386
+ if (key === shownRef.current) break;
1387
+ if (!(yield dimChainsOutside(pluginRef.current, lit))) break;
1388
+ shownRef.current = key;
1457
1389
  }
1458
- return;
1390
+ } catch (error) {
1391
+ console.error("Failed to dim chains:", error);
1392
+ } finally {
1393
+ committingRef.current = false;
1459
1394
  }
1460
- const loci = lociForChainPolymer(plugin, chainId);
1461
- if (!loci) return;
1462
- (_borrowedRef$current = borrowedRef.current) !== null && _borrowedRef$current !== void 0 || (borrowedRef.current = {
1463
- highlightColor: canvas3d.props.renderer.highlightColor,
1464
- highlightEdgeColor: canvas3d.props.marking.highlightEdgeColor,
1465
- highlightStrength: canvas3d.props.renderer.highlightStrength
1466
- });
1467
- canvas3d.setProps({
1468
- marking: { highlightEdgeColor: CHAIN_HIGHLIGHT_COLOR },
1469
- renderer: {
1470
- highlightColor: CHAIN_HIGHLIGHT_COLOR,
1471
- highlightStrength: CHAIN_HIGHLIGHT_STRENGTH
1472
- }
1473
- });
1474
- plugin.managers.interactivity.lociHighlights.highlightOnly({ loci });
1475
- }, [disabled, pluginRef]);
1476
- (0, react.useEffect)(() => () => void (borrowedRef.current = null), []);
1395
+ }), [litChains, pluginRef]);
1396
+ const highlightChain = (0, react.useCallback)((chainId) => {
1397
+ hoveredRef.current = chainId;
1398
+ settle();
1399
+ }, [settle]);
1400
+ (0, react.useEffect)(() => {
1401
+ settle();
1402
+ }, [
1403
+ disabled,
1404
+ hiddenChains,
1405
+ selectedChains,
1406
+ settle
1407
+ ]);
1408
+ (0, react.useEffect)(() => {
1409
+ shownRef.current = chainsKey(/* @__PURE__ */ new Set());
1410
+ settle();
1411
+ }, [loadCount, settle]);
1477
1412
  return highlightChain;
1478
1413
  }
1479
1414
  //#endregion
@@ -2174,13 +2109,166 @@ function useResidueMarkers({ containerRef, residueColors, sequenceWrapper }) {
2174
2109
  });
2175
2110
  }
2176
2111
  //#endregion
2112
+ //#region src/core/ProteinStructureViewer/utils/selection.ts
2113
+ /**
2114
+ * A stable string for a selection, for comparing one against another.
2115
+ *
2116
+ * A consumer holding the selection in state hands back a new object on every
2117
+ * render, so identity says nothing about whether the selection changed. Order
2118
+ * is normalized too: `{ residues: [2, 1] }` and `{ residues: [1, 2] }` select
2119
+ * the same residues and must not read as a change.
2120
+ */
2121
+ function selectionKey(selection) {
2122
+ var _selection$residues, _selection$chains;
2123
+ const residues = [...new Set((_selection$residues = selection.residues) !== null && _selection$residues !== void 0 ? _selection$residues : [])].sort((a, b) => a - b);
2124
+ return `${[...new Set((_selection$chains = selection.chains) !== null && _selection$chains !== void 0 ? _selection$chains : [])].sort().join(",")}|${residues.join(",")}`;
2125
+ }
2126
+ /** True when the selection names nothing at all. */
2127
+ function isEmptySelection(selection) {
2128
+ var _selection$residues2, _selection$chains2;
2129
+ if (!selection) return true;
2130
+ 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);
2131
+ }
2132
+ /**
2133
+ * The residues a selection covers, ascending and without repeats.
2134
+ *
2135
+ * Chains are expanded through `residuesByChain`, which was read off the loaded
2136
+ * structure, so a chain contributes exactly the residues it holds rather than
2137
+ * the span between its ends. A residue named directly and also carried by a
2138
+ * selected chain is counted once.
2139
+ */
2140
+ function selectionResidues(selection, residuesByChain) {
2141
+ var _selection$residues3, _selection$chains3;
2142
+ const residues = new Set((_selection$residues3 = selection.residues) !== null && _selection$residues3 !== void 0 ? _selection$residues3 : []);
2143
+ for (const chainId of (_selection$chains3 = selection.chains) !== null && _selection$chains3 !== void 0 ? _selection$chains3 : []) {
2144
+ var _residuesByChain$get;
2145
+ for (const index of (_residuesByChain$get = residuesByChain.get(chainId)) !== null && _residuesByChain$get !== void 0 ? _residuesByChain$get : []) residues.add(index);
2146
+ }
2147
+ return [...residues].sort((a, b) => a - b);
2148
+ }
2149
+ /**
2150
+ * Names a selection for the legend's readout.
2151
+ *
2152
+ * A single chain is named as one, since "Chain A" says more than the number of
2153
+ * residues it happens to hold. Anything else is reported as a count, which is
2154
+ * the only thing true of every shape a selection can take. A lone residue is
2155
+ * named by the caller instead, which reads it back off the structure to get its
2156
+ * three-letter code and the number the file gives it.
2157
+ */
2158
+ function selectionLabel(selection, residueCount) {
2159
+ var _selection$chains4, _selection$residues4;
2160
+ const chains = (_selection$chains4 = selection.chains) !== null && _selection$chains4 !== void 0 ? _selection$chains4 : [];
2161
+ if (chains.length === 1 && !((_selection$residues4 = selection.residues) === null || _selection$residues4 === void 0 ? void 0 : _selection$residues4.length)) return `Chain ${chains[0]}`;
2162
+ return `${residueCount} residues`;
2163
+ }
2164
+ /** The mean of the values `read` returns, ignoring residues without one. */
2165
+ function meanOverResidues(residues, read) {
2166
+ let sum = 0;
2167
+ let count = 0;
2168
+ for (const index of residues) {
2169
+ const value = read(index);
2170
+ if (value === null || value === void 0) continue;
2171
+ sum += value;
2172
+ count++;
2173
+ }
2174
+ return count === 0 ? null : sum / count;
2175
+ }
2176
+ //#endregion
2177
+ //#region src/core/ProteinStructureViewer/utils/residueLoci.ts
2178
+ /**
2179
+ * Every atom the predicate accepts, as geometry Mol* can focus, frame and mark,
2180
+ * or undefined when it accepts none.
2181
+ *
2182
+ * One pass over the structure rather than a pass per residue: a whole-chain
2183
+ * selection names hundreds of residues, and resolving them one at a time would
2184
+ * walk the structure once for each.
2185
+ *
2186
+ * Atoms are bucketed by the unit they belong to because element indices are
2187
+ * unit-local. That bucketing is also what lets a result span a chain break: it
2188
+ * carries an entry per unit, which is what a range across two chains needs and
2189
+ * what a single unit-local entry cannot express.
2190
+ *
2191
+ * Whole residues are collected rather than one atom each, since the caller
2192
+ * frames the camera on the loci's bounding sphere and a lone atom would zoom
2193
+ * far closer than the residue warrants.
2194
+ */
2195
+ function lociMatching(structure, matches) {
2196
+ const location = molstar_lib_mol_model_structure.StructureElement.Location.create(structure);
2197
+ const elements = [];
2198
+ for (const unit of structure.units) {
2199
+ location.unit = unit;
2200
+ const indices = [];
2201
+ for (let i = 0; i < unit.elements.length; i++) {
2202
+ location.element = unit.elements[i];
2203
+ if (matches(location)) indices.push(i);
2204
+ }
2205
+ if (indices.length > 0) elements.push({
2206
+ indices,
2207
+ unit
2208
+ });
2209
+ }
2210
+ if (elements.length === 0) return void 0;
2211
+ return molstar_lib_mol_model_structure.StructureElement.Loci(structure, elements.map(({ indices, unit }) => ({
2212
+ indices: molstar_lib_mol_data_int.OrderedSet.ofSortedArray(indices),
2213
+ unit
2214
+ })));
2215
+ }
2216
+ /**
2217
+ * Every atom of the residues a selection covers, within one structure.
2218
+ *
2219
+ * `hiddenChains` is subtracted from the result. A hidden chain is not drawn, so
2220
+ * geometry built from it would have nothing to sit on: Mol*'s focus
2221
+ * representation draws its own ball-and-stick from whatever is focused, in a
2222
+ * part of the state tree the chain's own visibility does not reach, and a
2223
+ * hidden chain left in the loci would keep showing up there after its cartoon
2224
+ * had gone.
2225
+ */
2226
+ function lociForSelectionInStructure(structure, selection, hiddenChains) {
2227
+ var _selection$residues, _selection$chains;
2228
+ if (isEmptySelection(selection)) return void 0;
2229
+ const residues = new Set((_selection$residues = selection.residues) !== null && _selection$residues !== void 0 ? _selection$residues : []);
2230
+ const chains = new Set((_selection$chains = selection.chains) !== null && _selection$chains !== void 0 ? _selection$chains : []);
2231
+ return lociMatching(structure, (location) => {
2232
+ const chainId = molstar_lib_mol_model_structure.StructureProperties.chain.auth_asym_id(location);
2233
+ if (hiddenChains === null || hiddenChains === void 0 ? void 0 : hiddenChains.has(chainId)) return false;
2234
+ return residues.has(molstar_lib_mol_model_structure.StructureProperties.residue.key(location)) || chains.has(chainId);
2235
+ });
2236
+ }
2237
+ /**
2238
+ * Every atom of the residues between two 0-based indices, inclusive.
2239
+ *
2240
+ * A contiguous span is tested by comparison rather than by expanding it into a
2241
+ * set of indices: this runs on every residue a drag crosses, and a drag can
2242
+ * span the whole structure.
2243
+ */
2244
+ function lociForResidueRange(structure, min, max) {
2245
+ return lociMatching(structure, (location) => {
2246
+ const index = molstar_lib_mol_model_structure.StructureProperties.residue.key(location);
2247
+ return index >= min && index <= max;
2248
+ });
2249
+ }
2250
+ /**
2251
+ * The same, across every structure loaded into a plugin. Results are merged
2252
+ * into the first structure that matched: a loci belongs to one structure, and
2253
+ * the residue index space already spans them as though they were one.
2254
+ */
2255
+ function lociForSelection(plugin, selection, hiddenChains) {
2256
+ for (const entry of plugin.managers.structure.hierarchy.current.structures) {
2257
+ var _entry$cell$obj;
2258
+ const structure = (_entry$cell$obj = entry.cell.obj) === null || _entry$cell$obj === void 0 ? void 0 : _entry$cell$obj.data;
2259
+ if (!structure) continue;
2260
+ const loci = lociForSelectionInStructure(structure, selection, hiddenChains);
2261
+ if (loci) return loci;
2262
+ }
2263
+ }
2264
+ //#endregion
2177
2265
  //#region src/core/ProteinStructureViewer/utils/residueRef.ts
2178
2266
  /**
2179
2267
  * Reads the residue a loci points at, or null when it points at nothing the
2180
2268
  * viewer can address.
2181
2269
  *
2182
2270
  * `index` comes from Mol*'s residue key, which counts residues in file order
2183
- * from zero and is the same counter `injectPlddtIntoPdb` walks. Deriving it
2271
+ * from zero and is the same counter `injectPlddt` walks. Deriving it
2184
2272
  * from the residue's *number* instead would tie it to how the file happens to
2185
2273
  * be numbered: a crop beginning at residue 200 would index `plddt` at 199, and
2186
2274
  * a file numbering each chain from 1 would give two residues the same index.
@@ -2964,32 +3052,6 @@ function neutralResidueColor(mode) {
2964
3052
  return molstar_lib_mol_util_color.Color.fromRgb(g, g, g);
2965
3053
  }
2966
3054
  //#endregion
2967
- //#region \0@oxc-project+runtime@0.134.0/helpers/esm/asyncToGenerator.js
2968
- function asyncGeneratorStep(n, t, e, r, o, a, c) {
2969
- try {
2970
- var i = n[a](c), u = i.value;
2971
- } catch (n) {
2972
- e(n);
2973
- return;
2974
- }
2975
- i.done ? t(u) : Promise.resolve(u).then(r, o);
2976
- }
2977
- function _asyncToGenerator(n) {
2978
- return function() {
2979
- var t = this, e = arguments;
2980
- return new Promise(function(r, o) {
2981
- var a = n.apply(t, e);
2982
- function _next(n) {
2983
- asyncGeneratorStep(a, r, o, _next, _throw, "next", n);
2984
- }
2985
- function _throw(n) {
2986
- asyncGeneratorStep(a, r, o, _next, _throw, "throw", n);
2987
- }
2988
- _next(void 0);
2989
- });
2990
- };
2991
- }
2992
- //#endregion
2993
3055
  //#region src/core/ProteinStructureViewer/utils/screenshot.ts
2994
3056
  /**
2995
3057
  * Each resolution as the Mol* preset that produces it: 1280x720, 1920x1080,
@@ -3328,6 +3390,25 @@ function focusResidue(plugin, loci) {
3328
3390
  return clipRatio;
3329
3391
  }
3330
3392
  /**
3393
+ * Frames the camera on a loci without cropping anything around it.
3394
+ *
3395
+ * The counterpart to `focusResidue` for something meant to be read in context
3396
+ * rather than peeled down to: the camera fits what was selected, and the depth
3397
+ * clip is left open to the whole scene, so the rest of the structure stays on
3398
+ * screen. A selected chain is shown by dimming the chains around it, and they
3399
+ * have to be there to be dimmed.
3400
+ */
3401
+ function frameLoci(plugin, loci) {
3402
+ var _plugin$canvas3d2;
3403
+ const camera = (_plugin$canvas3d2 = plugin.canvas3d) === null || _plugin$canvas3d2 === void 0 ? void 0 : _plugin$canvas3d2.camera;
3404
+ if (!camera) return;
3405
+ const sphere = molstar_lib_mol_model_loci.Loci.getBoundingSphere(loci);
3406
+ if (!sphere) return;
3407
+ const framingRadius = Math.max(sphere.radius + FOCUS_EXTRA_RADIUS, FOCUS_MIN_RADIUS);
3408
+ const snapshot = camera.getFocus(sphere.center, framingRadius);
3409
+ camera.setState(_objectSpread2(_objectSpread2({}, snapshot), {}, { radius: camera.state.radiusMax }), FOCUS_DURATION_MS);
3410
+ }
3411
+ /**
3331
3412
  * Keeps the depth clip in sync with the zoom while a residue is focused.
3332
3413
  *
3333
3414
  * Mol*'s trackball zoom moves the camera without touching the clip radius, so
@@ -3337,15 +3418,15 @@ function focusResidue(plugin, loci) {
3337
3418
  * camera is mid-transition, so it does not fight the focus animation.
3338
3419
  */
3339
3420
  function syncClipToZoom(plugin, getClipRatio) {
3340
- var _plugin$canvas3d2;
3341
- const camera = (_plugin$canvas3d2 = plugin.canvas3d) === null || _plugin$canvas3d2 === void 0 ? void 0 : _plugin$canvas3d2.camera;
3421
+ var _plugin$canvas3d3;
3422
+ const camera = (_plugin$canvas3d3 = plugin.canvas3d) === null || _plugin$canvas3d3 === void 0 ? void 0 : _plugin$canvas3d3.camera;
3342
3423
  const clipRatio = getClipRatio();
3343
3424
  if (!camera || clipRatio === null || camera.transition.inTransition) return;
3344
3425
  const radius = clipRadiusForDistance(molstar_lib_mol_math_linear_algebra.Vec3.distance(camera.state.position, camera.state.target), clipRatio, camera.state.radiusMax);
3345
3426
  if (Math.abs(camera.state.radius - radius) > CLIP_EPSILON) {
3346
- var _plugin$canvas3d3;
3427
+ var _plugin$canvas3d4;
3347
3428
  camera.setState({ radius });
3348
- (_plugin$canvas3d3 = plugin.canvas3d) === null || _plugin$canvas3d3 === void 0 || _plugin$canvas3d3.requestDraw();
3429
+ (_plugin$canvas3d4 = plugin.canvas3d) === null || _plugin$canvas3d4 === void 0 || _plugin$canvas3d4.requestDraw();
3349
3430
  }
3350
3431
  }
3351
3432
  //#endregion
@@ -3560,6 +3641,22 @@ function mergeMolstarSpec(base, override) {
3560
3641
  return merged;
3561
3642
  }
3562
3643
  //#endregion
3644
+ //#region src/core/ProteinStructureViewer/utils/structureFormat.ts
3645
+ /** How much of the file to inspect when guessing the format. */
3646
+ const DETECT_SAMPLE = 16384;
3647
+ /**
3648
+ * Infers whether `text` is PDB or mmCIF (PDBx).
3649
+ *
3650
+ * mmCIF documents start with a `data_` block and name their coordinates
3651
+ * `_atom_site.`. PDB never does either, so either marker is enough. A leading
3652
+ * comment (`# ...`) before `data_` is still mmCIF.
3653
+ */
3654
+ function detectStructureFormat(text) {
3655
+ const sample = text.slice(0, DETECT_SAMPLE);
3656
+ if (/(?:^|\n)\s*data_/.test(sample) || /_atom_site\./.test(sample)) return "mmcif";
3657
+ return "pdb";
3658
+ }
3659
+ //#endregion
3563
3660
  //#region src/core/ProteinStructureViewer/utils/chainColorTheme.ts
3564
3661
  /** Mol* color theme name under which per-chain coloring is registered. */
3565
3662
  const CHAIN_COLOR_THEME_NAME = "chain-color";
@@ -3698,9 +3795,183 @@ function injectPlddtIntoPdb(pdbData, plddtValues) {
3698
3795
  return paddedLine.substring(0, 60) + bfactorStr + paddedLine.substring(66);
3699
3796
  }).join("\n");
3700
3797
  }
3798
+ const ATOM_SITE_PREFIX = "_atom_site.";
3799
+ const B_FACTOR_COLUMN = "B_iso_or_equiv";
3800
+ function skipCifWhitespace(line, index) {
3801
+ let i = index;
3802
+ while (i < line.length && /\s/.test(line.charAt(i))) i += 1;
3803
+ return i;
3804
+ }
3805
+ function readCifToken(line, start) {
3806
+ const quote = line.charAt(start);
3807
+ if (quote === "'" || quote === "\"") {
3808
+ const end = line.indexOf(quote, start + 1);
3809
+ if (end < 0) return {
3810
+ next: line.length,
3811
+ token: line.slice(start + 1)
3812
+ };
3813
+ return {
3814
+ next: end + 1,
3815
+ token: line.slice(start + 1, end)
3816
+ };
3817
+ }
3818
+ let i = start;
3819
+ while (i < line.length && !/\s/.test(line.charAt(i))) i += 1;
3820
+ return {
3821
+ next: i,
3822
+ token: line.slice(start, i)
3823
+ };
3824
+ }
3825
+ /**
3826
+ * Splits one CIF line into tokens, honouring `'quoted'` and `"quoted"` values.
3827
+ * Atom-site rows are one line each in practice; multiline `;text;` fields are
3828
+ * not used there and are not handled.
3829
+ */
3830
+ function tokenizeCifLine(line) {
3831
+ const tokens = [];
3832
+ let i = skipCifWhitespace(line, 0);
3833
+ while (i < line.length) {
3834
+ const { next, token } = readCifToken(line, i);
3835
+ tokens.push(token);
3836
+ i = skipCifWhitespace(line, next);
3837
+ }
3838
+ return tokens;
3839
+ }
3840
+ function isCifNull(value) {
3841
+ return value === void 0 || value === "." || value === "?";
3842
+ }
3843
+ function columnIndex(columns, name) {
3844
+ return columns.indexOf(name);
3845
+ }
3846
+ /**
3847
+ * Residue identity matching `injectPlddtIntoPdb`: chain + sequence number +
3848
+ * insertion code. Prefers the `auth_*` columns Mol* reports on the parsed
3849
+ * model, falling back to `label_*` when a file omitted them. `.` / `?`
3850
+ * insertion codes count as empty, the way a blank PDB column 27 does.
3851
+ */
3852
+ function mmcifResidueKey(tokens, columns) {
3853
+ var _ref, _ref2, _ref3, _ref4;
3854
+ const authAsym = columnIndex(columns, "auth_asym_id");
3855
+ const labelAsym = columnIndex(columns, "label_asym_id");
3856
+ const authSeq = columnIndex(columns, "auth_seq_id");
3857
+ const labelSeq = columnIndex(columns, "label_seq_id");
3858
+ const ins = columnIndex(columns, "pdbx_PDB_ins_code");
3859
+ 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]}`;
3860
+ }
3861
+ function isAtomSiteRow(tokens, columns) {
3862
+ const group = columnIndex(columns, "group_PDB");
3863
+ if (group < 0) return true;
3864
+ const value = tokens[group];
3865
+ return value === "ATOM" || value === "HETATM";
3866
+ }
3867
+ function plddtBFactor(residueIndex, plddtValues) {
3868
+ var _plddtValues$residueI2;
3869
+ return (residueIndex < plddtValues.length ? ((_plddtValues$residueI2 = plddtValues[residueIndex]) !== null && _plddtValues$residueI2 !== void 0 ? _plddtValues$residueI2 : 0) * 100 : 50).toFixed(2);
3870
+ }
3871
+ function isLoopStart(line) {
3872
+ return line.trim() === "loop_" || line.trim().startsWith("loop_");
3873
+ }
3874
+ function isAtomSiteColumn(line) {
3875
+ return line.trim().startsWith(ATOM_SITE_PREFIX);
3876
+ }
3877
+ function endsAtomSiteLoop(line) {
3878
+ const trimmed = line.trim();
3879
+ if (trimmed === "") return false;
3880
+ if (trimmed.startsWith("#")) return true;
3881
+ if (isLoopStart(trimmed)) return true;
3882
+ if (trimmed.startsWith("data_") || trimmed.startsWith("save_")) return true;
3883
+ return trimmed.startsWith("_") && !trimmed.startsWith(ATOM_SITE_PREFIX);
3884
+ }
3885
+ function isAtomSiteLoop(lines, loopIndex) {
3886
+ let peek = loopIndex + 1;
3887
+ while (peek < lines.length && lines[peek].trim() === "") peek += 1;
3888
+ return peek < lines.length && isAtomSiteColumn(lines[peek]);
3889
+ }
3890
+ function ensureBFactorColumn(columns, headerLines) {
3891
+ const existing = columnIndex(columns, B_FACTOR_COLUMN);
3892
+ if (existing >= 0) return existing;
3893
+ columns.push(B_FACTOR_COLUMN);
3894
+ headerLines.push(`${ATOM_SITE_PREFIX}${B_FACTOR_COLUMN}`);
3895
+ return columns.length - 1;
3896
+ }
3897
+ function rewriteAtomSiteRow(dataLine, columns, bIndex, plddtValues, walk) {
3898
+ if (dataLine.trim() === "") return dataLine;
3899
+ const tokens = tokenizeCifLine(dataLine);
3900
+ if (tokens.length === 0) return dataLine;
3901
+ while (tokens.length < columns.length) tokens.push(".");
3902
+ if (isAtomSiteRow(tokens, columns)) {
3903
+ const residue = mmcifResidueKey(tokens, columns);
3904
+ if (residue !== walk.lastResidue) {
3905
+ walk.residueIndex += 1;
3906
+ walk.lastResidue = residue;
3907
+ }
3908
+ tokens[bIndex] = plddtBFactor(walk.residueIndex, plddtValues);
3909
+ }
3910
+ return tokens.join(" ");
3911
+ }
3912
+ function rewriteAtomSiteLoop(lines, loopIndex, plddtValues, walk) {
3913
+ const rows = [lines[loopIndex]];
3914
+ let i = loopIndex + 1;
3915
+ const columns = [];
3916
+ while (i < lines.length && isAtomSiteColumn(lines[i])) {
3917
+ columns.push(lines[i].trim().slice(11));
3918
+ rows.push(lines[i]);
3919
+ i += 1;
3920
+ }
3921
+ const bIndex = ensureBFactorColumn(columns, rows);
3922
+ while (i < lines.length && !endsAtomSiteLoop(lines[i])) {
3923
+ rows.push(rewriteAtomSiteRow(lines[i], columns, bIndex, plddtValues, walk));
3924
+ i += 1;
3925
+ }
3926
+ return {
3927
+ next: i,
3928
+ rows
3929
+ };
3930
+ }
3931
+ /**
3932
+ * Rewrites an mmCIF (PDBx) file's `_atom_site.B_iso_or_equiv` column with
3933
+ * pLDDT scores so Mol* can read them off the parsed model, the same way
3934
+ * `injectPlddtIntoPdb` rewrites the PDB B-factor column. Scores arrive on a
3935
+ * 0-1 scale and are stored on the conventional 0-100 one. Residues past the
3936
+ * end of `plddtValues` fall back to a mid-confidence 50.
3937
+ *
3938
+ * If the file has no B-factor column, one is appended to the atom-site loop
3939
+ * rather than leaving the scores behind.
3940
+ */
3941
+ function injectPlddtIntoMmcif(mmcifData, plddtValues) {
3942
+ const lines = mmcifData.split("\n");
3943
+ const result = [];
3944
+ const walk = {
3945
+ lastResidue: "",
3946
+ residueIndex: -1
3947
+ };
3948
+ let i = 0;
3949
+ let rewrittenLoop = false;
3950
+ while (i < lines.length) {
3951
+ const line = lines[i];
3952
+ if (!rewrittenLoop && isLoopStart(line) && isAtomSiteLoop(lines, i)) {
3953
+ const rewritten = rewriteAtomSiteLoop(lines, i, plddtValues, walk);
3954
+ result.push(...rewritten.rows);
3955
+ i = rewritten.next;
3956
+ rewrittenLoop = true;
3957
+ continue;
3958
+ }
3959
+ result.push(line);
3960
+ i += 1;
3961
+ }
3962
+ return result.join("\n");
3963
+ }
3964
+ /**
3965
+ * Rewrites a structure file's B-factors with pLDDT scores. Dispatches to
3966
+ * `injectPlddtIntoPdb` or `injectPlddtIntoMmcif` from `format`, or from
3967
+ * detecting the format of `data` when `format` is omitted.
3968
+ */
3969
+ function injectPlddt(data, plddtValues, format = detectStructureFormat(data)) {
3970
+ return format === "mmcif" ? injectPlddtIntoMmcif(data, plddtValues) : injectPlddtIntoPdb(data, plddtValues);
3971
+ }
3701
3972
  /**
3702
3973
  * Mol* color theme that reads pLDDT scores out of the B-factor column, paired
3703
- * with `injectPlddtIntoPdb`. Registered per plugin instance.
3974
+ * with `injectPlddt`. Registered per plugin instance.
3704
3975
  */
3705
3976
  const PlddtColoring = molstar_lib_mol_model_props_common_custom_element_property.CustomElementProperty.create({
3706
3977
  coloring: {
@@ -4010,27 +4281,30 @@ const NOTHING_LOADED = {
4010
4281
  residuesByChain: /* @__PURE__ */ new Map()
4011
4282
  };
4012
4283
  /**
4013
- * Parses the PDB and draws each chain: a cartoon over its polymer, and
4284
+ * Parses the structure and draws each chain: a cartoon over its polymer, and
4014
4285
  * ball-and-stick over its ligands and ions.
4015
4286
  *
4016
4287
  * Mol*'s `default` hierarchy preset would be shorter, but it groups every
4017
4288
  * polymer chain into a single component, and a component is the unit Mol* can
4018
4289
  * hide. Building them per chain is what makes a chain individually hideable;
4019
4290
  * the refs handed back are what `setSubtreeVisibility` is later pointed at.
4291
+ *
4292
+ * The wrapper detects PDB vs mmCIF from the text; this just forwards that
4293
+ * guess to Mol*.
4020
4294
  */
4021
4295
  function loadStructure(_x9, _x10, _x11, _x12) {
4022
4296
  return _loadStructure.apply(this, arguments);
4023
4297
  }
4024
4298
  function _loadStructure() {
4025
- _loadStructure = _asyncToGenerator(function* (plugin, pdbData, usePlddtColoring, showAxes) {
4299
+ _loadStructure = _asyncToGenerator(function* (plugin, structureText, usePlddtColoring, showAxes) {
4026
4300
  try {
4027
4301
  var _plugin$canvas3d;
4028
4302
  yield plugin.clear();
4029
4303
  const data = yield plugin.builders.data.rawData({
4030
- data: pdbData,
4304
+ data: structureText,
4031
4305
  label: "Structure"
4032
4306
  });
4033
- const trajectory = yield plugin.builders.structure.parseTrajectory(data, "pdb");
4307
+ const trajectory = yield plugin.builders.structure.parseTrajectory(data, detectStructureFormat(structureText));
4034
4308
  const model = yield plugin.builders.structure.createModel(trajectory);
4035
4309
  const structure = yield plugin.builders.structure.createStructure(model, {
4036
4310
  name: "model",
@@ -4104,16 +4378,17 @@ function _applyColorTheme() {
4104
4378
  *
4105
4379
  * The plugin is built once and then mutated in place. Creating it is expensive
4106
4380
  * and destroys the camera, so prop changes that Mol* can absorb (background,
4107
- * axes, coloring, new PDB) are pushed in through the effects below and in the
4108
- * sibling hooks rather than by rebuilding.
4381
+ * axes, coloring, new structure) are pushed in through the effects below and in
4382
+ * the sibling hooks rather than by rebuilding.
4109
4383
  */
4110
- function useMolstarPlugin({ backgroundColor, chainColors, containerRef, download, edgeColor, hasPlddt, hiddenChains, highlightColor, mode, molstarSpec, onChainHover, onChainSelect, onChainToggle, onResidueClick, onResidueHover, onSelectionChange, onSelectionClear, pdb, selectedChains, sequenceViewerBackgroundColor, showAxes, showSequenceViewer }) {
4384
+ function useMolstarPlugin({ backgroundColor, chainColors, containerRef, download, edgeColor, hasPlddt, hiddenChains, highlightColor, mode, molstarSpec, onChainHover, onChainSelect, onChainToggle, onResidueClick, onResidueHover, onSelectionChange, onSelectionClear, selectedChains, sequenceViewerBackgroundColor, showAxes, showSequenceViewer, structure }) {
4111
4385
  const pluginRef = (0, react.useRef)(null);
4112
4386
  const residueValueThemeRef = (0, react.useRef)(null);
4113
4387
  const chainColorThemeRef = (0, react.useRef)(null);
4114
- const currentPdbRef = (0, react.useRef)(null);
4388
+ const currentStructureRef = (0, react.useRef)(null);
4115
4389
  const [isReady, setIsReady] = (0, react.useState)(false);
4116
4390
  const [chains, setChains] = (0, react.useState)([]);
4391
+ const [loadCount, setLoadCount] = (0, react.useState)(0);
4117
4392
  /**
4118
4393
  * Where each chain's components live in the state tree, which is what the
4119
4394
  * visibility effect points `setSubtreeVisibility` at. Rewritten by every
@@ -4170,6 +4445,7 @@ function useMolstarPlugin({ backgroundColor, chainColors, containerRef, download
4170
4445
  residuesByChainRef.current = loaded.residuesByChain;
4171
4446
  applyChainVisibility(plugin, loaded.componentRefs, hiddenChainsRef.current);
4172
4447
  setChains((prev) => chainsEqual(prev, loaded.chains) ? prev : loaded.chains);
4448
+ setLoadCount((count) => count + 1);
4173
4449
  }, []);
4174
4450
  const onResidueClickRef = (0, react.useRef)(onResidueClick);
4175
4451
  onResidueClickRef.current = onResidueClick;
@@ -4188,9 +4464,9 @@ function useMolstarPlugin({ backgroundColor, chainColors, containerRef, download
4188
4464
  highlightColor,
4189
4465
  mode,
4190
4466
  molstarSpec,
4191
- pdb,
4192
4467
  showAxes,
4193
- showSequenceViewer
4468
+ showSequenceViewer,
4469
+ structure
4194
4470
  });
4195
4471
  initialPropsRef.current = {
4196
4472
  backgroundColor,
@@ -4199,9 +4475,9 @@ function useMolstarPlugin({ backgroundColor, chainColors, containerRef, download
4199
4475
  highlightColor,
4200
4476
  mode,
4201
4477
  molstarSpec,
4202
- pdb,
4203
4478
  showAxes,
4204
- showSequenceViewer
4479
+ showSequenceViewer,
4480
+ structure
4205
4481
  };
4206
4482
  /**
4207
4483
  * The consumer's `canvas3d` overrides, keyed by content. Unlike the rest of
@@ -4256,8 +4532,8 @@ function useMolstarPlugin({ backgroundColor, chainColors, containerRef, download
4256
4532
  residueValueThemeRef.current = residueValueTheme;
4257
4533
  chainColorThemeRef.current = chainColorTheme;
4258
4534
  const latest = initialPropsRef.current;
4259
- const loaded = yield loadStructure(plugin, latest.pdb, latest.hasPlddt, latest.showAxes);
4260
- currentPdbRef.current = latest.pdb;
4535
+ const loaded = yield loadStructure(plugin, latest.structure, latest.hasPlddt, latest.showAxes);
4536
+ currentStructureRef.current = latest.structure;
4261
4537
  adoptLoadedStructure(plugin, loaded);
4262
4538
  /**
4263
4539
  * Mol*'s click behavior is a BehaviorSubject, so subscribing replays
@@ -4319,7 +4595,7 @@ function useMolstarPlugin({ backgroundColor, chainColors, containerRef, download
4319
4595
  chainColorThemeRef.current = null;
4320
4596
  componentRefsRef.current = /* @__PURE__ */ new Map();
4321
4597
  residuesByChainRef.current = /* @__PURE__ */ new Map();
4322
- currentPdbRef.current = null;
4598
+ currentStructureRef.current = null;
4323
4599
  clipRatioRef.current = null;
4324
4600
  setIsReady(false);
4325
4601
  };
@@ -4378,16 +4654,16 @@ function useMolstarPlugin({ backgroundColor, chainColors, containerRef, download
4378
4654
  (0, react.useEffect)(() => {
4379
4655
  const plugin = pluginRef.current;
4380
4656
  if (!plugin || !isReady) return;
4381
- if (pdb === currentPdbRef.current) return;
4382
- currentPdbRef.current = pdb;
4657
+ if (structure === currentStructureRef.current) return;
4658
+ currentStructureRef.current = structure;
4383
4659
  clipRatioRef.current = null;
4384
4660
  lastHoverRef.current = null;
4385
- loadStructure(plugin, pdb, hasPlddt, showAxes).then((loaded) => {
4661
+ loadStructure(plugin, structure, hasPlddt, showAxes).then((loaded) => {
4386
4662
  if (pluginRef.current !== plugin) return;
4387
4663
  adoptLoadedStructure(plugin, loaded);
4388
4664
  });
4389
4665
  }, [
4390
- pdb,
4666
+ structure,
4391
4667
  hasPlddt,
4392
4668
  isReady,
4393
4669
  adoptLoadedStructure
@@ -4401,6 +4677,7 @@ function useMolstarPlugin({ backgroundColor, chainColors, containerRef, download
4401
4677
  chainColorThemeRef,
4402
4678
  chains,
4403
4679
  isReady,
4680
+ loadCount,
4404
4681
  pluginRef,
4405
4682
  residuesByChainRef,
4406
4683
  residueValueThemeRef,
@@ -4516,10 +4793,14 @@ function focusWithShell(plugin, loci, includeNeighbours, currentShell, pluginRef
4516
4793
  /**
4517
4794
  * Makes `selection` the one thing that decides where the camera sits.
4518
4795
  *
4519
- * Setting the prop frames whatever it covers and marks it; clearing it drops
4520
- * the focus and zooms back out. The camera frames the selection's bounding
4521
- * sphere, so a range or a whole chain is fitted rather than approached as a
4522
- * point - one residue is just the smallest case of that.
4796
+ * Setting the prop frames whatever it covers; clearing it drops the focus and
4797
+ * zooms back out. The camera frames the selection's bounding sphere, so a range
4798
+ * or a whole chain is fitted rather than approached as a point - one residue is
4799
+ * just the smallest case of that.
4800
+ *
4801
+ * Residues are also marked, which is what draws Mol*'s ball-and-stick and
4802
+ * outline over them. Whole chains are not: those are shown by dimming the
4803
+ * chains around them instead, in `useChainHighlight`.
4523
4804
  *
4524
4805
  * The click subscription only reports the selection it was given, so a click
4525
4806
  * moves the camera only once the consumer accepts it and echoes it back -
@@ -4528,6 +4809,7 @@ function focusWithShell(plugin, loci, includeNeighbours, currentShell, pluginRef
4528
4809
  * responds to a click whether or not the consumer wires the prop up.
4529
4810
  */
4530
4811
  function useSelectionFocus({ hiddenChains, isReady, pluginRef, selection, setClipRatio }) {
4812
+ var _selection$chains, _selection$residues;
4531
4813
  /**
4532
4814
  * What this hook last pointed the camera at, as a key rather than the object
4533
4815
  * itself: a consumer echoing a selection back builds a new object each time,
@@ -4552,6 +4834,11 @@ function useSelectionFocus({ hiddenChains, isReady, pluginRef, selection, setCli
4552
4834
  * echoing an equal selection back from refocusing on every render.
4553
4835
  */
4554
4836
  const focusKey = selection ? `${selectionKey(selection)}#${[...hiddenChains].sort().join(",")}` : null;
4837
+ /**
4838
+ * Whether the selection names chains and nothing else, which is the shape
4839
+ * shown by dimming the chains around it rather than by focusing it.
4840
+ */
4841
+ 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));
4555
4842
  (0, react.useEffect)(() => {
4556
4843
  var _chosen$chains;
4557
4844
  const plugin = pluginRef.current;
@@ -4572,6 +4859,12 @@ function useSelectionFocus({ hiddenChains, isReady, pluginRef, selection, setCli
4572
4859
  clearFocus(plugin, setClipRatio, false);
4573
4860
  return;
4574
4861
  }
4862
+ if (chainsOnly) {
4863
+ plugin.managers.structure.focus.clear();
4864
+ frameLoci(plugin, loci);
4865
+ setClipRatio(null);
4866
+ return;
4867
+ }
4575
4868
  return focusWithShell(plugin, loci, !((_chosen$chains = chosen.chains) === null || _chosen$chains === void 0 ? void 0 : _chosen$chains.length), neighbourhoodRef, pluginRef, setClipRatio);
4576
4869
  }, [
4577
4870
  focusKey,
@@ -4733,7 +5026,7 @@ const _excluded$1 = [
4733
5026
  "onResidueClick",
4734
5027
  "onResidueHover",
4735
5028
  "onSelectionChange",
4736
- "pdb",
5029
+ "structure",
4737
5030
  "plddt",
4738
5031
  "residueOverlay",
4739
5032
  "selection",
@@ -4810,14 +5103,14 @@ function resolveScaleProps(overlay, hasPlddt) {
4810
5103
  */
4811
5104
  const ProteinStructureViewer = (0, react.forwardRef)((props, ref) => {
4812
5105
  var _selection$chains;
4813
- 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);
5106
+ 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);
4814
5107
  const theme = (0, _mui_material_styles.useTheme)();
4815
5108
  const mode = (0, _czi_sds_components.getMode)({ theme });
4816
5109
  const semanticColors = (0, _czi_sds_components.getSemanticColors)({ theme });
4817
5110
  const pluginMountRef = (0, react.useRef)(null);
4818
5111
  const hasPlddt = Boolean(plddt && plddt.length > 0);
4819
- const processedPdb = (0, react.useMemo)(() => hasPlddt ? injectPlddtIntoPdb(pdb, plddt) : pdb, [
4820
- pdb,
5112
+ const processedStructure = (0, react.useMemo)(() => hasPlddt ? injectPlddt(structure, plddt) : structure, [
5113
+ structure,
4821
5114
  plddt,
4822
5115
  hasPlddt
4823
5116
  ]);
@@ -4864,13 +5157,14 @@ const ProteinStructureViewer = (0, react.forwardRef)((props, ref) => {
4864
5157
  onSelectionChange === null || onSelectionChange === void 0 || onSelectionChange(isOnlyThisChain ? null : { chains: [chainId] });
4865
5158
  }, [onSelectionChange, selection]);
4866
5159
  /**
4867
- * Lights a chain up while its name is pointed at, in the legend or in the
4868
- * sequence panel's captions.
5160
+ * Dims every other chain while a chain's name is pointed at, in the legend
5161
+ * or in the sequence panel's captions, and leaves the selected chain lit
5162
+ * once the pointer has gone.
4869
5163
  *
4870
- * Reached through a ref because the two ends need each other: lighting a
4871
- * chain up takes the plugin, which the hook below creates, and that hook
4872
- * has to be handed the callback to give the captions it renders. This one
4873
- * is stable and forwards to whatever the highlight hook installs once
5164
+ * Reached through a ref because the two ends need each other: dimming
5165
+ * other chains takes the plugin, which the hook below creates, and that
5166
+ * hook has to be handed the callback to give the captions it renders. This
5167
+ * one is stable and forwards to whatever the highlight hook installs once
4874
5168
  * there is a plugin for it to talk to.
4875
5169
  */
4876
5170
  const highlightChainRef = (0, react.useRef)(() => void 0);
@@ -4884,7 +5178,7 @@ const ProteinStructureViewer = (0, react.forwardRef)((props, ref) => {
4884
5178
  hiddenChains: hiddenChainsProp,
4885
5179
  onChainVisibilityChange
4886
5180
  });
4887
- const { chainColorThemeRef, chains: loadedChains, isReady, pluginRef, residuesByChainRef, residueValueThemeRef, setClipRatio } = useMolstarPlugin({
5181
+ const { chainColorThemeRef, chains: loadedChains, isReady, loadCount, pluginRef, residuesByChainRef, residueValueThemeRef, setClipRatio } = useMolstarPlugin({
4888
5182
  backgroundColor: bgColor,
4889
5183
  chainColors,
4890
5184
  containerRef: pluginMountRef,
@@ -4902,15 +5196,18 @@ const ProteinStructureViewer = (0, react.forwardRef)((props, ref) => {
4902
5196
  onResidueHover: handleResidueHover,
4903
5197
  onSelectionChange,
4904
5198
  onSelectionClear: handleSelectionClear,
4905
- pdb: processedPdb,
4906
5199
  selectedChains,
4907
5200
  sequenceViewerBackgroundColor,
4908
5201
  showAxes,
4909
- showSequenceViewer
5202
+ showSequenceViewer,
5203
+ structure: processedStructure
4910
5204
  });
4911
5205
  highlightChainRef.current = useChainHighlight({
4912
5206
  disabled: disableChainHighlightOnHover,
4913
- pluginRef
5207
+ hiddenChains,
5208
+ loadCount,
5209
+ pluginRef,
5210
+ selectedChains
4914
5211
  });
4915
5212
  (0, react.useEffect)(() => {
4916
5213
  setChains(loadedChains);
@@ -5462,6 +5759,9 @@ exports.PLASMA_COLOR_SCALE = PLASMA_COLOR_SCALE;
5462
5759
  exports.PLDDT_COLOR_SCALE = PLDDT_COLOR_SCALE;
5463
5760
  exports.ProteinStructureViewer = ProteinStructureViewer_default;
5464
5761
  exports.StackedBarChart = StackedBarChart;
5762
+ exports.detectStructureFormat = detectStructureFormat;
5763
+ exports.injectPlddt = injectPlddt;
5764
+ exports.injectPlddtIntoMmcif = injectPlddtIntoMmcif;
5465
5765
  exports.injectPlddtIntoPdb = injectPlddtIntoPdb;
5466
5766
  exports.sampleColorScale = sampleColorScale;
5467
5767
  exports.toCssGradient = toCssGradient;