@czi-sds/data-viz 2.4.3 → 2.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,27 +1,28 @@
1
1
  "use client";
2
2
  import { n as _objectWithoutProperties, t as _objectSpread2 } from "./objectSpread2.esm.js";
3
- import { i as toCssGradient, n as sampleColorScale, r as sampleSteppedScale, t as PLASMA_COLOR_SCALE } from "./colorScales.esm.js";
3
+ import { A as isEmptySelection, C as resolveSelectionAddresses, D as lociForResidueRange, E as readCameraState, M as selectionKey, N as selectionLabel, O as lociForSelection, P as selectionResidues, S as residueAddressesKey, T as camerasEqual, _ as chainColorMap, b as _asyncToGenerator, c as resolveColorBy, g as themeColor, h as highlightsKey, i as AXES_ON, j as meanOverResidues, k as lociOutsideChains, l as PLDDT_BAND_COLORS, n as exportStructureImage, o as StructureScene, r as AXES_OFF, s as chainSetKey, u as PLDDT_COLOR_SCALE, v as NOTHING_LOADED, w as chainsEqual, y as loadInfo } from "./renderStructureImage.esm.js";
4
+ import { i as toCssGradient, t as PLASMA_COLOR_SCALE } from "./colorScales.esm.js";
4
5
  import { Fragment, createContext, forwardRef, memo, useCallback, useContext, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from "react";
5
6
  import styled from "@emotion/styled";
6
7
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
8
+ import { Vec3 } from "molstar/lib/mol-math/linear-algebra";
9
+ import { EmptyLoci, EveryLoci, Loci } from "molstar/lib/mol-model/loci";
10
+ import { OrderedSet } from "molstar/lib/mol-data/int";
11
+ import { Structure, StructureElement, StructureProperties, Unit } from "molstar/lib/mol-model/structure";
7
12
  import { Button, Theme, Tooltip, defaultTheme, fontBodyXs, fontBodyXxxs, fontCodeXs, fontHeaderM, getMode, getSemanticColors, getSpaces } from "@czi-sds/components";
8
13
  import { ThemeProvider, useTheme } from "@mui/material/styles";
9
14
  import { ApertureIcon, ArrowCounterClockwiseIcon, CheckIcon, CopyIcon, EyeIcon, EyeSlashIcon, QuestionIcon } from "@phosphor-icons/react";
10
- import { Structure, StructureElement, StructureProperties, Unit } from "molstar/lib/mol-model/structure";
11
15
  import { clearStructureTransparency, setStructureTransparency } from "molstar/lib/mol-plugin-state/helpers/structure-transparency";
12
16
  import { createPluginUI } from "molstar/lib/mol-plugin-ui";
13
17
  import { renderReact18 } from "molstar/lib/mol-plugin-ui/react18";
14
18
  import { DefaultPluginUISpec } from "molstar/lib/mol-plugin-ui/spec";
15
19
  import { PluginBehaviors } from "molstar/lib/mol-plugin/behavior";
16
- import { setSubtreeVisibility } from "molstar/lib/mol-plugin/behavior/static/state";
17
20
  import { PluginConfig } from "molstar/lib/mol-plugin/config";
18
21
  import { Representation } from "molstar/lib/mol-repr/representation";
19
22
  import { BehaviorSubject, Subject } from "rxjs";
20
- import { EveryLoci, Loci } from "molstar/lib/mol-model/loci";
21
23
  import { PluginReactContext } from "molstar/lib/mol-plugin-ui/base";
22
24
  import { ButtonsType, getButton, getButtons, getModifiers } from "molstar/lib/mol-util/input/input-observer";
23
25
  import { throttleTime } from "rxjs/operators";
24
- import { OrderedSet } from "molstar/lib/mol-data/int";
25
26
  import { useTheme as useTheme$1 } from "@mui/material";
26
27
  import { PluginStateObject } from "molstar/lib/mol-plugin-state/objects";
27
28
  import { getChainOptions, getModelEntityOptions, getOperatorOptions, getSequenceWrapper, getStructureOptions } from "molstar/lib/mol-plugin-ui/sequence";
@@ -30,13 +31,7 @@ import { arrayEqual } from "molstar/lib/mol-util";
30
31
  import { ParamDefinition } from "molstar/lib/mol-util/param-definition";
31
32
  import { Toasts } from "molstar/lib/mol-plugin-ui/toast";
32
33
  import { Viewport } from "molstar/lib/mol-plugin-ui/viewport";
33
- import { Color } from "molstar/lib/mol-util/color";
34
34
  import { PluginCommands } from "molstar/lib/mol-plugin/commands";
35
- import { Vec3 } from "molstar/lib/mol-math/linear-algebra";
36
- import { BondType } from "molstar/lib/mol-model/structure/model/types";
37
- import { elementLabel } from "molstar/lib/mol-theme/label";
38
- import { MolScriptBuilder } from "molstar/lib/mol-script/language/builder";
39
- import { CustomElementProperty } from "molstar/lib/mol-model-props/common/custom-element-property";
40
35
  import { StructureFocusRepresentation } from "molstar/lib/mol-plugin/behavior/dynamic/selection/structure-focus-representation";
41
36
  //#region src/core/ProteinStructureViewer/components/ChainLegend/style.ts
42
37
  /** Opacity applied to a hidden chain's row, so it reads as off but reachable. */
@@ -295,17 +290,35 @@ const MIN_WIDTH = 200;
295
290
  */
296
291
  const VIEWER_CONTAINER_NAME = "sds-structure-viewer";
297
292
  /**
298
- * Height reserved for the sequence panel along the bottom, and the viewer
299
- * width above which it grows. Measured against the container above rather than
300
- * the page, so a viewer embedded in a wider page sizes to the room it has.
293
+ * Height reserved for the sequence panel along the bottom when the consumer
294
+ * sets none, and the viewer width above which it grows. Measured against the
295
+ * container above rather than the page, so a viewer embedded in a wider page
296
+ * sizes to the room it has.
301
297
  *
302
- * The legend sits directly on top of the panel and offsets itself by these
303
- * same three values, so it reads them from here rather than restating them.
304
- * Copies that drifted would leave the legend floating off the panel.
298
+ * The legend sits directly on top of the panel and offsets itself by the same
299
+ * heights at the same breakpoint, so it reads them from here rather than
300
+ * restating them. Copies that drifted would leave the legend floating off the
301
+ * panel.
305
302
  */
306
303
  const SEQUENCE_HEIGHT = "max(104px, 30%)";
307
304
  const SEQUENCE_HEIGHT_WIDE = "max(134px, 32%)";
308
305
  /**
306
+ * The sequence panel's height below `WIDE_VIEWER` and at or above it. A height
307
+ * the consumer sets holds at every width, a number is read as pixels, and an
308
+ * empty string leaves the defaults in place, as it would in a React style.
309
+ */
310
+ function sequenceHeights(height) {
311
+ if (height === void 0 || height === "") return {
312
+ base: SEQUENCE_HEIGHT,
313
+ wide: SEQUENCE_HEIGHT_WIDE
314
+ };
315
+ const css = typeof height === "number" ? `${height}px` : height;
316
+ return {
317
+ base: css,
318
+ wide: css
319
+ };
320
+ }
321
+ /**
309
322
  * The element Mol* mounts into.
310
323
  *
311
324
  * This has to be its own node rather than the root: Mol* takes ownership of its
@@ -382,17 +395,20 @@ const ViewerRoot = styled("div")`
382
395
  /*
383
396
  * Mol* puts the sequence panel in its "top" region; move it to the bottom and
384
397
  * shrink the main viewport region to match, so the structure sits above the
385
- * sequence rather than behind it. The panel grows on wider viewers, and the
386
- * viewport region's offset follows it.
398
+ * sequence rather than behind it. The panel grows on wider viewers unless the
399
+ * consumer has set its height, and the viewport region's offset follows it.
387
400
  */
388
401
  ${(props) => {
389
- const { showSequenceViewer } = props;
402
+ const { sequenceViewerHeight, showSequenceViewer } = props;
403
+ const height = sequenceHeights(sequenceViewerHeight);
404
+ const mainBottom = showSequenceViewer ? height.base : "0";
405
+ const mainBottomWide = showSequenceViewer ? height.wide : "0";
390
406
  return `
391
407
  .msp-plugin .msp-layout-region.msp-layout-top {
392
408
  display: ${showSequenceViewer ? "block" : "none"} !important;
393
409
  position: absolute !important;
394
410
  inset: auto 0 0 0 !important;
395
- height: ${SEQUENCE_HEIGHT} !important;
411
+ height: ${height.base} !important;
396
412
  }
397
413
 
398
414
  .msp-plugin .msp-layout-region.msp-layout-main {
@@ -400,16 +416,16 @@ const ViewerRoot = styled("div")`
400
416
  top: 0 !important;
401
417
  left: 0 !important;
402
418
  right: 0 !important;
403
- bottom: ${showSequenceViewer ? SEQUENCE_HEIGHT : "0"} !important;
419
+ bottom: ${mainBottom} !important;
404
420
  }
405
421
 
406
422
  @container ${VIEWER_CONTAINER_NAME} (min-width: 880px) {
407
423
  .msp-plugin .msp-layout-region.msp-layout-top {
408
- height: ${SEQUENCE_HEIGHT_WIDE} !important;
424
+ height: ${height.wide} !important;
409
425
  }
410
426
 
411
427
  .msp-plugin .msp-layout-region.msp-layout-main {
412
- bottom: ${showSequenceViewer ? SEQUENCE_HEIGHT_WIDE : "0"} !important;
428
+ bottom: ${mainBottomWide} !important;
413
429
  }
414
430
  }
415
431
  `;
@@ -451,12 +467,13 @@ const LegendOverlay = styled("div")`
451
467
 
452
468
  ${(props) => {
453
469
  const spaces = getSpaces(props);
454
- const { showSequenceViewer } = props;
470
+ const { sequenceViewerHeight, showSequenceViewer } = props;
471
+ const height = sequenceHeights(sequenceViewerHeight);
455
472
  return `
456
473
  right: ${spaces === null || spaces === void 0 ? void 0 : spaces.m}px;
457
474
  gap: ${spaces === null || spaces === void 0 ? void 0 : spaces.xs}px;
458
475
  padding-bottom: ${spaces === null || spaces === void 0 ? void 0 : spaces.m}px;
459
- bottom: ${showSequenceViewer ? SEQUENCE_HEIGHT : "0"};
476
+ bottom: ${showSequenceViewer ? height.base : "0"};
460
477
 
461
478
  /*
462
479
  * Track the sequence panel as it grows on wider viewers. The panel is
@@ -464,7 +481,7 @@ const LegendOverlay = styled("div")`
464
481
  * would part the legend from it on a viewer narrower than its page.
465
482
  */
466
483
  @container ${VIEWER_CONTAINER_NAME} (min-width: 880px) {
467
- bottom: ${showSequenceViewer ? SEQUENCE_HEIGHT_WIDE : "0"};
484
+ bottom: ${showSequenceViewer ? height.wide : "0"};
468
485
  }
469
486
 
470
487
  /* Spacer balancing the axes widget pinned in the opposite corner. */
@@ -640,7 +657,7 @@ function readoutSlot(label, value, residueCount) {
640
657
  * residue takes priority so the user can inspect others without losing their
641
658
  * selection.
642
659
  */
643
- function StructureLegend({ chainBandColors, chainColors, chains = [], hiddenChains, hoveredResidue = null, onChainHover, onChainSelect, onChainToggle, scale, scaleLabel, scaleMax = null, scaleMin = 0, scaleTooltip, scaleTooltipProps, selectedChains, selectedResidue = null, showSequenceViewer, stats, valueLabel }) {
660
+ function StructureLegend({ chainBandColors, chainColors, chains = [], hiddenChains, hoveredResidue = null, onChainHover, onChainSelect, onChainToggle, scale, scaleLabel, scaleMax = null, scaleMin = 0, scaleTooltip, scaleTooltipProps, selectedChains, selectedResidue = null, sequenceViewerHeight, showSequenceViewer, stats, valueLabel }) {
644
661
  var _activeResidue$residu, _stats$, _stats$2, _stats$3;
645
662
  const activeResidue = hoveredResidue !== null && hoveredResidue !== void 0 ? hoveredResidue : selectedResidue;
646
663
  const showResidue = activeResidue !== null;
@@ -655,6 +672,7 @@ function StructureLegend({ chainBandColors, chainColors, chains = [], hiddenChai
655
672
  showResidue ? readoutSlot("pLDDT", activeResidue.plddt, residueCount) : (_stats$3 = stats[2]) !== null && _stats$3 !== void 0 ? _stats$3 : null
656
673
  ];
657
674
  return /* @__PURE__ */ jsxs(LegendOverlay, {
675
+ sequenceViewerHeight,
658
676
  showSequenceViewer,
659
677
  children: [/* @__PURE__ */ jsx(StatsGrid, { children: Array.from({ length: SLOT_COUNT }, (_, i) => {
660
678
  var _slots$i;
@@ -689,30 +707,71 @@ function StructureLegend({ chainBandColors, chainColors, chains = [], hiddenChai
689
707
  });
690
708
  }
691
709
  //#endregion
692
- //#region \0@oxc-project+runtime@0.134.0/helpers/esm/asyncToGenerator.js
693
- function asyncGeneratorStep(n, t, e, r, o, a, c) {
694
- try {
695
- var i = n[a](c), u = i.value;
696
- } catch (n) {
697
- e(n);
698
- return;
699
- }
700
- i.done ? t(u) : Promise.resolve(u).then(r, o);
701
- }
702
- function _asyncToGenerator(n) {
703
- return function() {
704
- var t = this, e = arguments;
705
- return new Promise(function(r, o) {
706
- var a = n.apply(t, e);
707
- function _next(n) {
708
- asyncGeneratorStep(a, r, o, _next, _throw, "next", n);
709
- }
710
- function _throw(n) {
711
- asyncGeneratorStep(a, r, o, _next, _throw, "throw", n);
712
- }
713
- _next(void 0);
710
+ //#region src/core/ProteinStructureViewer/hooks/useCamera.ts
711
+ /**
712
+ * How long the camera has to stay put before it counts as having come to
713
+ * rest, in ms. Mol* draws on demand, so a camera that has stopped stops the
714
+ * draws, and a trackball still coasting after a drag keeps pushing this back.
715
+ */
716
+ const CAMERA_SETTLE_MS = 150;
717
+ /**
718
+ * Keeps the camera in step with the camera props after a structure is up:
719
+ * the projection, the orientation, and reporting where the camera comes to
720
+ * rest. Where the camera starts on a structure is decided by the load, which
721
+ * knows when there is something drawn to place it against.
722
+ */
723
+ function useCamera({ framedOrientationRef, highlights, isReady, onCameraChange, orientation, pluginRef, projection, sceneRef }) {
724
+ const highlightsRef = useRef(highlights);
725
+ highlightsRef.current = highlights;
726
+ const onCameraChangeRef = useRef(onCameraChange);
727
+ onCameraChangeRef.current = onCameraChange;
728
+ useEffect(() => {
729
+ var _pluginRef$current;
730
+ const canvas3d = (_pluginRef$current = pluginRef.current) === null || _pluginRef$current === void 0 ? void 0 : _pluginRef$current.canvas3d;
731
+ if (!canvas3d || !isReady || !projection) return;
732
+ if (canvas3d.props.camera.mode === projection) return;
733
+ canvas3d.setProps({ camera: { mode: projection } });
734
+ }, [
735
+ isReady,
736
+ pluginRef,
737
+ projection
738
+ ]);
739
+ useEffect(() => {
740
+ const scene = sceneRef.current;
741
+ if (!scene || !isReady) return;
742
+ if (orientation === framedOrientationRef.current) return;
743
+ framedOrientationRef.current = orientation;
744
+ if (!orientation) return;
745
+ scene.orient(orientation, highlightsRef.current, 250);
746
+ }, [
747
+ framedOrientationRef,
748
+ isReady,
749
+ orientation,
750
+ sceneRef
751
+ ]);
752
+ useEffect(() => {
753
+ var _pluginRef$current2;
754
+ const canvas3d = (_pluginRef$current2 = pluginRef.current) === null || _pluginRef$current2 === void 0 ? void 0 : _pluginRef$current2.canvas3d;
755
+ if (!canvas3d || !isReady) return;
756
+ let timer;
757
+ let reported = null;
758
+ const subscription = canvas3d.didDraw.subscribe(() => {
759
+ if (!onCameraChangeRef.current) return;
760
+ clearTimeout(timer);
761
+ timer = setTimeout(() => {
762
+ var _onCameraChangeRef$cu;
763
+ if (canvas3d.camera.transition.inTransition) return;
764
+ const state = readCameraState(canvas3d);
765
+ if (reported && camerasEqual(reported, state)) return;
766
+ reported = state;
767
+ (_onCameraChangeRef$cu = onCameraChangeRef.current) === null || _onCameraChangeRef$cu === void 0 || _onCameraChangeRef$cu.call(onCameraChangeRef, state);
768
+ }, CAMERA_SETTLE_MS);
714
769
  });
715
- };
770
+ return () => {
771
+ subscription.unsubscribe();
772
+ clearTimeout(timer);
773
+ };
774
+ }, [isReady, pluginRef]);
716
775
  }
717
776
  //#endregion
718
777
  //#region src/core/ProteinStructureViewer/hooks/useChainHighlight.ts
@@ -732,13 +791,20 @@ const CHAIN_DIM_TRANSPARENCY = .65;
732
791
  * part name and stopping at the comma is what keeps `A` from also matching the
733
792
  * chain named `AA`.
734
793
  */
735
- const COMPONENT_CHAIN_ID = /structure-component-(?:polymer|ligand)-([^,]+)/;
794
+ const COMPONENT_CHAIN_ID = /structure-component-(?:polymer|ligand|highlight)-([^,]+)/;
795
+ /** The one component spanning chains: the surface over the visible ones. */
796
+ const SURFACE_COMPONENT = /structure-component-surface(?:,|$)/;
736
797
  /** The chain a component was built for, or undefined if it is not a chain's. */
737
798
  function componentChainId(component) {
738
799
  var _component$key, _COMPONENT_CHAIN_ID$e;
739
800
  const key = (_component$key = component.key) !== null && _component$key !== void 0 ? _component$key : component.cell.transform.ref;
740
801
  return (_COMPONENT_CHAIN_ID$e = COMPONENT_CHAIN_ID.exec(key)) === null || _COMPONENT_CHAIN_ID$e === void 0 ? void 0 : _COMPONENT_CHAIN_ID$e[1];
741
802
  }
803
+ /** True for the surface, which draws every visible chain in one component. */
804
+ function isSurfaceComponent(component) {
805
+ var _component$key2;
806
+ return SURFACE_COMPONENT.test((_component$key2 = component.key) !== null && _component$key2 !== void 0 ? _component$key2 : component.cell.transform.ref);
807
+ }
742
808
  /** Every chain component of every loaded structure, polymers and ligands. */
743
809
  function structureComponents(plugin) {
744
810
  return plugin.managers.structure.hierarchy.current.structures.flatMap((entry) => entry.components);
@@ -759,7 +825,7 @@ function chainsKey(chains) {
759
825
  * arriving before the first load looks like. It is applied again after the load
760
826
  * rather than being taken as done.
761
827
  */
762
- function dimChainsOutside(_x2, _x3) {
828
+ function dimChainsOutside(_x3, _x4) {
763
829
  return _dimChainsOutside.apply(this, arguments);
764
830
  }
765
831
  function _dimChainsOutside() {
@@ -772,8 +838,7 @@ function _dimChainsOutside() {
772
838
  const chainId = componentChainId(component);
773
839
  return chainId !== void 0 && !lit.has(chainId);
774
840
  });
775
- if (dimmed.length === 0) return true;
776
- yield setStructureTransparency(plugin, dimmed, CHAIN_DIM_TRANSPARENCY, function() {
841
+ if (dimmed.length > 0) yield setStructureTransparency(plugin, dimmed, CHAIN_DIM_TRANSPARENCY, function() {
777
842
  var _ref = _asyncToGenerator(function* (structure) {
778
843
  return Structure.toStructureElementLoci(structure);
779
844
  });
@@ -781,6 +846,16 @@ function _dimChainsOutside() {
781
846
  return _ref.apply(this, arguments);
782
847
  };
783
848
  }());
849
+ const surfaces = components.filter(isSurfaceComponent);
850
+ if (surfaces.length > 0) yield setStructureTransparency(plugin, surfaces, CHAIN_DIM_TRANSPARENCY, function() {
851
+ var _ref2 = _asyncToGenerator(function* (structure) {
852
+ var _lociOutsideChains;
853
+ return (_lociOutsideChains = lociOutsideChains(structure, lit)) !== null && _lociOutsideChains !== void 0 ? _lociOutsideChains : EmptyLoci;
854
+ });
855
+ return function(_x2) {
856
+ return _ref2.apply(this, arguments);
857
+ };
858
+ }());
784
859
  return true;
785
860
  });
786
861
  return _dimChainsOutside.apply(this, arguments);
@@ -805,7 +880,7 @@ function _dimChainsOutside() {
805
880
  * came to rest on, instead of working through a queue of hovers that have
806
881
  * already been left.
807
882
  */
808
- function useChainHighlight({ disabled, hiddenChains, loadCount, pluginRef, selectedChains }) {
883
+ function useChainHighlight({ disabled, hiddenChains, loadCount, pluginRef, sceneVersion, selectedChains }) {
809
884
  const hoveredRef = useRef(null);
810
885
  /**
811
886
  * The chains left lit on the canvas as it stands. A freshly loaded structure
@@ -865,51 +940,14 @@ function useChainHighlight({ disabled, hiddenChains, loadCount, pluginRef, selec
865
940
  useEffect(() => {
866
941
  shownRef.current = chainsKey(/* @__PURE__ */ new Set());
867
942
  settle();
868
- }, [loadCount, settle]);
943
+ }, [
944
+ loadCount,
945
+ sceneVersion,
946
+ settle
947
+ ]);
869
948
  return highlightChain;
870
949
  }
871
950
  //#endregion
872
- //#region src/common/chainColors.ts
873
- /**
874
- * Default colors for telling chains apart, in assignment order.
875
- *
876
- * The Okabe-Ito qualitative palette, which is built to stay distinguishable
877
- * under every common form of color blindness - the reason to prefer it over a
878
- * hand-picked set, since a complex is read by which chain is which and a
879
- * viewer that collapses two of them into one color stops answering that.
880
- *
881
- * Its black is left out: the canvas inverts with the theme, so a chain painted
882
- * black would vanish against the dark background. The rest carry enough
883
- * lightness to hold up against both.
884
- *
885
- * Chains past the end of the palette wrap around to the start.
886
- */
887
- const CHAIN_COLOR_PALETTE = [
888
- "#009E73",
889
- "#0072B2",
890
- "#E69F00",
891
- "#CC79A7",
892
- "#56B4E9",
893
- "#D55E00",
894
- "#F0E442"
895
- ];
896
- /**
897
- * Assigns each chain a color, in the order given, with `overrides` taking
898
- * precedence by `chainId`.
899
- *
900
- * Palette positions are spent on every chain whether or not it is overridden,
901
- * so overriding one chain does not shift the colors of the others.
902
- */
903
- function chainColorMap(chainIds, overrides) {
904
- const colors = /* @__PURE__ */ new Map();
905
- chainIds.forEach((chainId, i) => {
906
- var _overrides$chainId;
907
- const fallback = CHAIN_COLOR_PALETTE[i % CHAIN_COLOR_PALETTE.length];
908
- colors.set(chainId, (_overrides$chainId = overrides === null || overrides === void 0 ? void 0 : overrides[chainId]) !== null && _overrides$chainId !== void 0 ? _overrides$chainId : fallback);
909
- });
910
- return colors;
911
- }
912
- //#endregion
913
951
  //#region src/core/ProteinStructureViewer/hooks/useChains.ts
914
952
  /**
915
953
  * Reconciles the chains found in the structure with the visibility and coloring
@@ -1577,159 +1615,6 @@ function useResidueMarkers({ containerRef, residueColors, sequenceWrapper }) {
1577
1615
  });
1578
1616
  }
1579
1617
  //#endregion
1580
- //#region src/core/ProteinStructureViewer/utils/selection.ts
1581
- /**
1582
- * A stable string for a selection, for comparing one against another.
1583
- *
1584
- * A consumer holding the selection in state hands back a new object on every
1585
- * render, so identity says nothing about whether the selection changed. Order
1586
- * is normalized too: `{ residues: [2, 1] }` and `{ residues: [1, 2] }` select
1587
- * the same residues and must not read as a change.
1588
- */
1589
- function selectionKey(selection) {
1590
- var _selection$residues, _selection$chains;
1591
- const residues = [...new Set((_selection$residues = selection.residues) !== null && _selection$residues !== void 0 ? _selection$residues : [])].sort((a, b) => a - b);
1592
- return `${[...new Set((_selection$chains = selection.chains) !== null && _selection$chains !== void 0 ? _selection$chains : [])].sort().join(",")}|${residues.join(",")}`;
1593
- }
1594
- /** True when the selection names nothing at all. */
1595
- function isEmptySelection(selection) {
1596
- var _selection$residues2, _selection$chains2;
1597
- if (!selection) return true;
1598
- 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);
1599
- }
1600
- /**
1601
- * The residues a selection covers, ascending and without repeats.
1602
- *
1603
- * Chains are expanded through `residuesByChain`, which was read off the loaded
1604
- * structure, so a chain contributes exactly the residues it holds rather than
1605
- * the span between its ends. A residue named directly and also carried by a
1606
- * selected chain is counted once.
1607
- */
1608
- function selectionResidues(selection, residuesByChain) {
1609
- var _selection$residues3, _selection$chains3;
1610
- const residues = new Set((_selection$residues3 = selection.residues) !== null && _selection$residues3 !== void 0 ? _selection$residues3 : []);
1611
- for (const chainId of (_selection$chains3 = selection.chains) !== null && _selection$chains3 !== void 0 ? _selection$chains3 : []) {
1612
- var _residuesByChain$get;
1613
- for (const index of (_residuesByChain$get = residuesByChain.get(chainId)) !== null && _residuesByChain$get !== void 0 ? _residuesByChain$get : []) residues.add(index);
1614
- }
1615
- return [...residues].sort((a, b) => a - b);
1616
- }
1617
- /**
1618
- * Names a selection for the legend's readout.
1619
- *
1620
- * A single chain is named as one, since "Chain A" says more than the number of
1621
- * residues it happens to hold. Anything else is reported as a count, which is
1622
- * the only thing true of every shape a selection can take. A lone residue is
1623
- * named by the caller instead, which reads it back off the structure to get its
1624
- * three-letter code and the number the file gives it.
1625
- */
1626
- function selectionLabel(selection, residueCount) {
1627
- var _selection$chains4, _selection$residues4;
1628
- const chains = (_selection$chains4 = selection.chains) !== null && _selection$chains4 !== void 0 ? _selection$chains4 : [];
1629
- if (chains.length === 1 && !((_selection$residues4 = selection.residues) === null || _selection$residues4 === void 0 ? void 0 : _selection$residues4.length)) return `Chain ${chains[0]}`;
1630
- return `${residueCount} residues`;
1631
- }
1632
- /** The mean of the values `read` returns, ignoring residues without one. */
1633
- function meanOverResidues(residues, read) {
1634
- let sum = 0;
1635
- let count = 0;
1636
- for (const index of residues) {
1637
- const value = read(index);
1638
- if (value === null || value === void 0) continue;
1639
- sum += value;
1640
- count++;
1641
- }
1642
- return count === 0 ? null : sum / count;
1643
- }
1644
- //#endregion
1645
- //#region src/core/ProteinStructureViewer/utils/residueLoci.ts
1646
- /**
1647
- * Every atom the predicate accepts, as geometry Mol* can focus, frame and mark,
1648
- * or undefined when it accepts none.
1649
- *
1650
- * One pass over the structure rather than a pass per residue: a whole-chain
1651
- * selection names hundreds of residues, and resolving them one at a time would
1652
- * walk the structure once for each.
1653
- *
1654
- * Atoms are bucketed by the unit they belong to because element indices are
1655
- * unit-local. That bucketing is also what lets a result span a chain break: it
1656
- * carries an entry per unit, which is what a range across two chains needs and
1657
- * what a single unit-local entry cannot express.
1658
- *
1659
- * Whole residues are collected rather than one atom each, since the caller
1660
- * frames the camera on the loci's bounding sphere and a lone atom would zoom
1661
- * far closer than the residue warrants.
1662
- */
1663
- function lociMatching(structure, matches) {
1664
- const location = StructureElement.Location.create(structure);
1665
- const elements = [];
1666
- for (const unit of structure.units) {
1667
- location.unit = unit;
1668
- const indices = [];
1669
- for (let i = 0; i < unit.elements.length; i++) {
1670
- location.element = unit.elements[i];
1671
- if (matches(location)) indices.push(i);
1672
- }
1673
- if (indices.length > 0) elements.push({
1674
- indices,
1675
- unit
1676
- });
1677
- }
1678
- if (elements.length === 0) return void 0;
1679
- return StructureElement.Loci(structure, elements.map(({ indices, unit }) => ({
1680
- indices: OrderedSet.ofSortedArray(indices),
1681
- unit
1682
- })));
1683
- }
1684
- /**
1685
- * Every atom of the residues a selection covers, within one structure.
1686
- *
1687
- * `hiddenChains` is subtracted from the result. A hidden chain is not drawn, so
1688
- * geometry built from it would have nothing to sit on: Mol*'s focus
1689
- * representation draws its own ball-and-stick from whatever is focused, in a
1690
- * part of the state tree the chain's own visibility does not reach, and a
1691
- * hidden chain left in the loci would keep showing up there after its cartoon
1692
- * had gone.
1693
- */
1694
- function lociForSelectionInStructure(structure, selection, hiddenChains) {
1695
- var _selection$residues, _selection$chains;
1696
- if (isEmptySelection(selection)) return void 0;
1697
- const residues = new Set((_selection$residues = selection.residues) !== null && _selection$residues !== void 0 ? _selection$residues : []);
1698
- const chains = new Set((_selection$chains = selection.chains) !== null && _selection$chains !== void 0 ? _selection$chains : []);
1699
- return lociMatching(structure, (location) => {
1700
- const chainId = StructureProperties.chain.auth_asym_id(location);
1701
- if (hiddenChains === null || hiddenChains === void 0 ? void 0 : hiddenChains.has(chainId)) return false;
1702
- return residues.has(StructureProperties.residue.key(location)) || chains.has(chainId);
1703
- });
1704
- }
1705
- /**
1706
- * Every atom of the residues between two 0-based indices, inclusive.
1707
- *
1708
- * A contiguous span is tested by comparison rather than by expanding it into a
1709
- * set of indices: this runs on every residue a drag crosses, and a drag can
1710
- * span the whole structure.
1711
- */
1712
- function lociForResidueRange(structure, min, max) {
1713
- return lociMatching(structure, (location) => {
1714
- const index = StructureProperties.residue.key(location);
1715
- return index >= min && index <= max;
1716
- });
1717
- }
1718
- /**
1719
- * The same, across every structure loaded into a plugin. Results are merged
1720
- * into the first structure that matched: a loci belongs to one structure, and
1721
- * the residue index space already spans them as though they were one.
1722
- */
1723
- function lociForSelection(plugin, selection, hiddenChains) {
1724
- for (const entry of plugin.managers.structure.hierarchy.current.structures) {
1725
- var _entry$cell$obj;
1726
- const structure = (_entry$cell$obj = entry.cell.obj) === null || _entry$cell$obj === void 0 ? void 0 : _entry$cell$obj.data;
1727
- if (!structure) continue;
1728
- const loci = lociForSelectionInStructure(structure, selection, hiddenChains);
1729
- if (loci) return loci;
1730
- }
1731
- }
1732
- //#endregion
1733
1618
  //#region src/core/ProteinStructureViewer/utils/residueRef.ts
1734
1619
  /**
1735
1620
  * Reads the residue a loci points at, or null when it points at nothing the
@@ -2458,138 +2343,6 @@ function createSequenceView(viewSettings) {
2458
2343
  };
2459
2344
  }
2460
2345
  //#endregion
2461
- //#region src/core/ProteinStructureViewer/utils/axes.ts
2462
- /**
2463
- * The orientation axes widget, as Mol* parameterizes it.
2464
- *
2465
- * Shared rather than declared where it is used, because two different parts of
2466
- * Mol* take the same shape: the live canvas takes it as
2467
- * `camera.helper.axes`, and the screenshot helper takes it as its own `axes`,
2468
- * so an image can carry the widget or leave it out independently of the view
2469
- * it was captured from.
2470
- */
2471
- const AXES_ON = {
2472
- name: "on",
2473
- params: {
2474
- alpha: .51,
2475
- colorX: 16711680,
2476
- colorY: 32768,
2477
- colorZ: 255,
2478
- labelColorX: 8421504,
2479
- labelColorY: 8421504,
2480
- labelColorZ: 8421504,
2481
- labelOpacity: 1,
2482
- labelScale: .25,
2483
- labelX: "X",
2484
- labelY: "Y",
2485
- labelZ: "Z",
2486
- location: "bottom-left",
2487
- locationOffsetX: 0,
2488
- locationOffsetY: 0,
2489
- originColor: 8421504,
2490
- planeColorXY: 8421504,
2491
- planeColorXZ: 8421504,
2492
- planeColorYZ: 8421504,
2493
- radiusScale: .075,
2494
- scale: .15,
2495
- showLabels: false,
2496
- showPlanes: true
2497
- }
2498
- };
2499
- const AXES_OFF = {
2500
- name: "off",
2501
- params: {}
2502
- };
2503
- //#endregion
2504
- //#region src/core/ProteinStructureViewer/utils/color.ts
2505
- /** Parses `#RRGGBB` (or `RRGGBB`) into a Mol* `Color`. */
2506
- function parseHexColor(hex) {
2507
- const h = hex.replace("#", "");
2508
- return Color.fromRgb(parseInt(h.substring(0, 2), 16), parseInt(h.substring(2, 4), 16), parseInt(h.substring(4, 6), 16));
2509
- }
2510
- /**
2511
- * Parses a theme color into a Mol* `Color`, falling back when the token is
2512
- * missing or is a format Mol* cannot consume (theme values are hex in practice,
2513
- * but the palette types allow any CSS color string).
2514
- */
2515
- function themeColor(value, fallback) {
2516
- const candidate = value === null || value === void 0 ? void 0 : value.trim();
2517
- return parseHexColor(candidate !== void 0 && /^#?[0-9a-f]{6}$/i.test(candidate) ? candidate : fallback);
2518
- }
2519
- /**
2520
- * Neutral fill for residues with no overlay value. Sits between the light and
2521
- * dark backgrounds so unhighlighted geometry stays legible in both modes.
2522
- */
2523
- function neutralResidueColor(mode) {
2524
- const g = mode === "dark" ? 100 : 180;
2525
- return Color.fromRgb(g, g, g);
2526
- }
2527
- //#endregion
2528
- //#region src/core/ProteinStructureViewer/utils/screenshot.ts
2529
- /**
2530
- * Each resolution as the Mol* preset that produces it: 1280x720, 1920x1080,
2531
- * 3840x2160, 7680x4320. Named here rather than given as dimensions so the
2532
- * aspect stays Mol*'s to choose.
2533
- *
2534
- * Mol*'s own `viewport` preset - the canvas at its current size - is left out
2535
- * deliberately: it makes the image depend on how big the viewer happens to be
2536
- * on screen, which is not something a caller asking for "high" is asking for.
2537
- */
2538
- const RESOLUTION_PRESETS = {
2539
- high: "ultra-hd",
2540
- low: "hd",
2541
- maximum: "8k-ultra-hd",
2542
- medium: "full-hd"
2543
- };
2544
- /** Painted behind the structure when a background is asked for but unreadable. */
2545
- const FALLBACK_BACKGROUND = "#ffffff";
2546
- /**
2547
- * Downloads the structure as a PNG.
2548
- *
2549
- * Mol*'s screenshot helper renders its own pass rather than reading the canvas
2550
- * back, so the image is drawn fresh at the requested size and carries its own
2551
- * transparency and axes settings - none of which disturb the view on screen.
2552
- *
2553
- * A named background is the one exception. The helper only chooses between
2554
- * transparent and whatever the renderer is already painting, so a color has to
2555
- * be put on the canvas for the length of the capture and taken off afterwards.
2556
- * `finally` is what guarantees the second half: leaving a figure's background
2557
- * behind on the viewer would be a worse bug than a failed download.
2558
- */
2559
- function downloadStructureImage(_x) {
2560
- return _downloadStructureImage.apply(this, arguments);
2561
- }
2562
- function _downloadStructureImage() {
2563
- _downloadStructureImage = _asyncToGenerator(function* (plugin, options = {}) {
2564
- const helper = plugin.helpers.viewportScreenshot;
2565
- if (!helper) return;
2566
- const { backgroundColor, filename, resolution = "medium", showAxes } = options;
2567
- const transparent = backgroundColor === void 0;
2568
- helper.behaviors.values.next(_objectSpread2(_objectSpread2({}, helper.behaviors.values.value), {}, {
2569
- axes: showAxes ? AXES_ON : AXES_OFF,
2570
- format: {
2571
- name: "png",
2572
- params: {}
2573
- },
2574
- resolution: {
2575
- name: RESOLUTION_PRESETS[resolution],
2576
- params: {}
2577
- },
2578
- transparent
2579
- }));
2580
- const canvas3d = plugin.canvas3d;
2581
- const painted = canvas3d === null || canvas3d === void 0 ? void 0 : canvas3d.props.renderer.backgroundColor;
2582
- const overrideBackground = !transparent && canvas3d !== void 0;
2583
- if (overrideBackground) canvas3d === null || canvas3d === void 0 || canvas3d.setProps({ renderer: { backgroundColor: themeColor(backgroundColor, FALLBACK_BACKGROUND) } });
2584
- try {
2585
- yield helper.download(filename);
2586
- } finally {
2587
- if (overrideBackground && painted !== void 0) canvas3d === null || canvas3d === void 0 || canvas3d.setProps({ renderer: { backgroundColor: painted } });
2588
- }
2589
- });
2590
- return _downloadStructureImage.apply(this, arguments);
2591
- }
2592
- //#endregion
2593
2346
  //#region src/core/ProteinStructureViewer/components/Viewport/constants.ts
2594
2347
  /**
2595
2348
  * Fraction of the bounding sphere radius within which the camera still counts
@@ -2752,6 +2505,11 @@ function createViewportView(viewSettings) {
2752
2505
  return (_s$showAxes = s.showAxes) !== null && _s$showAxes !== void 0 ? _s$showAxes : true;
2753
2506
  });
2754
2507
  const download = useViewSetting(viewSettings, (s) => s.download);
2508
+ const onError = useViewSetting(viewSettings, (s) => s.onError);
2509
+ const sceneBusy = useViewSetting(viewSettings, (s) => {
2510
+ var _s$sceneBusy;
2511
+ return (_s$sceneBusy = s.sceneBusy) !== null && _s$sceneBusy !== void 0 ? _s$sceneBusy : false;
2512
+ });
2755
2513
  /**
2756
2514
  * Disabled while a capture is in flight. A high resolution takes long
2757
2515
  * enough to click twice through, and each click renders its own pass.
@@ -2761,13 +2519,18 @@ function createViewportView(viewSettings) {
2761
2519
  if (!download) return;
2762
2520
  setDownloading(true);
2763
2521
  try {
2764
- yield downloadStructureImage(plugin, download);
2522
+ yield exportStructureImage(plugin, download);
2765
2523
  } catch (error) {
2766
- console.error("Failed to download the structure image:", error);
2524
+ if (onError) onError(error, "capture");
2525
+ else console.error("Failed to download the structure image:", error);
2767
2526
  } finally {
2768
2527
  setDownloading(false);
2769
2528
  }
2770
- }), [download, plugin]);
2529
+ }), [
2530
+ download,
2531
+ onError,
2532
+ plugin
2533
+ ]);
2771
2534
  return /* @__PURE__ */ jsxs(Fragment$1, { children: [
2772
2535
  /* @__PURE__ */ jsx(Viewport, {}),
2773
2536
  /* @__PURE__ */ jsx("div", {
@@ -2801,7 +2564,7 @@ function createViewportView(viewSettings) {
2801
2564
  textAlign: "left",
2802
2565
  children: /* @__PURE__ */ jsx(Button, {
2803
2566
  "aria-label": "Download image of the structure",
2804
- disabled: downloading,
2567
+ disabled: downloading || sceneBusy,
2805
2568
  onClick: captureImage,
2806
2569
  sdsStyle: "minimal",
2807
2570
  sdsType: "secondary",
@@ -2908,150 +2671,6 @@ function syncClipToZoom(plugin, getClipRatio) {
2908
2671
  }
2909
2672
  }
2910
2673
  //#endregion
2911
- //#region src/core/ProteinStructureViewer/utils/chains.ts
2912
- /**
2913
- * The polymer of one chain, as named in the file. What each chain's cartoon is
2914
- * built from, and so what makes a chain a thing Mol* can hide on its own.
2915
- *
2916
- * Paired with `chainLigandExpression`: between them they cover everything on a
2917
- * chain the viewer draws, split by what it draws it as. The polymer
2918
- * restriction is the one Mol*'s `polymer-cartoon` preset applied, and is what
2919
- * keeps a heme from being handed to a representation that can only trace a
2920
- * backbone.
2921
- */
2922
- function chainPolymerExpression(chainId) {
2923
- return MolScriptBuilder.struct.generator.atomGroups({
2924
- "chain-test": MolScriptBuilder.core.rel.eq([MolScriptBuilder.ammp("auth_asym_id"), chainId]),
2925
- "entity-test": MolScriptBuilder.core.rel.eq([MolScriptBuilder.ammp("entityType"), "polymer"])
2926
- });
2927
- }
2928
- /**
2929
- * Bonds worth following out of a ligand: the ones holding it to the protein.
2930
- *
2931
- * Mol*'s own pair, and metallic coordination is the half that matters most -
2932
- * a heme's iron is held to its histidine by a coordination bond and not a
2933
- * covalent one, so covalent alone would leave every metalloprotein's cofactor
2934
- * looking dropped in rather than bound.
2935
- */
2936
- const CONNECTING_BOND = MolScriptBuilder.core.flags.hasAny([MolScriptBuilder.struct.bondProperty.flags(), MolScriptBuilder.core.type.bitflags([BondType.fromNames(["covalent", "metal-coordination"])])]);
2937
- /**
2938
- * The heteroatoms of one chain - ligands, ions, glycans, lipids - along with
2939
- * the residues holding them, which is what its ball-and-stick is built from.
2940
- *
2941
- * `non-polymer` is the one entity type a PDB's HETATM records land under,
2942
- * ligands and ions alike, so there is nothing finer to test here: the
2943
- * distinction Mol*'s presets draw between their `ligand`, `ion` and `lipid`
2944
- * components is a residue-name lookup made to give each a slightly different
2945
- * style, and all three are sticks. `branched` is the separate type glycans
2946
- * arrive as.
2947
- *
2948
- * Water is the third type and is left out of the seed. Every solvent molecule
2949
- * drawn as sticks buries the structure they surround, which is why Mol* draws
2950
- * them at 60% alpha rather than plain, and none of them is what "show the
2951
- * ligands" asks for.
2952
- *
2953
- * One layer of bonds out from there, taken as whole residues, is what brings
2954
- * the binding residue along - myoglobin's His93, say. Without it the heme
2955
- * floats unattached in the middle of a cartoon it is visibly bonded to, and
2956
- * the bond drawn to its iron ends in mid-air. One layer and no more: the
2957
- * residue that holds the ligand, not that residue's own neighbours.
2958
- *
2959
- * Intersected back with the chain, so everything a chain is drawn with belongs
2960
- * to that chain. A ligand bonded across an interface would otherwise leave the
2961
- * partner chain's residue in this chain's component, somewhere hiding the
2962
- * partner chain could not reach.
2963
- */
2964
- function chainLigandExpression(chainId) {
2965
- const onThisChain = MolScriptBuilder.core.rel.eq([MolScriptBuilder.ammp("auth_asym_id"), chainId]);
2966
- return MolScriptBuilder.struct.modifier.union([MolScriptBuilder.struct.modifier.intersectBy({
2967
- 0: MolScriptBuilder.struct.modifier.includeConnected({
2968
- 0: MolScriptBuilder.struct.generator.atomGroups({
2969
- "chain-test": onThisChain,
2970
- "entity-test": MolScriptBuilder.core.set.has([MolScriptBuilder.set("non-polymer", "branched"), MolScriptBuilder.ammp("entityType")])
2971
- }),
2972
- "as-whole-residues": true,
2973
- "bond-test": CONNECTING_BOND,
2974
- "layer-count": 1
2975
- }),
2976
- by: MolScriptBuilder.struct.generator.atomGroups({ "chain-test": onThisChain })
2977
- })]);
2978
- }
2979
- /**
2980
- * Matches how the sequence panel captions its chains, which reaches the same
2981
- * label through Mol*'s own `getChainOptions`. Deriving it the same way is what
2982
- * keeps a chain from going by one name in the legend and another in the panel.
2983
- */
2984
- const CHAIN_LABEL_OPTIONS = {
2985
- granularity: "chain",
2986
- hidePrefix: true,
2987
- htmlStyling: false
2988
- };
2989
- /**
2990
- * Reads the chains of a structure and the polymer residues on each.
2991
- *
2992
- * Residue indices are Mol*'s residue keys, counting residues in file order
2993
- * across the whole structure, so what is reported here addresses the same
2994
- * residues as `plddt`, `residueOverlay` and `selection`.
2995
- *
2996
- * Heteroatoms are drawn (see `chainLigandExpression`) but are not counted
2997
- * here. A heme is a residue to the file and not to a reader: it is no part of
2998
- * the sequence the panel lists, and averaging a confidence score over it would
2999
- * divide by a residue that has none.
3000
- *
3001
- * Residues are gathered into a set rather than inferred from the ends: a chain
3002
- * carrying several symmetry operators repeats each of its residues once per
3003
- * operator, and a structure whose chains interleave leaves a chain's indices
3004
- * non-contiguous. Neither is what a predicted complex looks like, but a range
3005
- * would misread both.
3006
- */
3007
- function scanChains(structure) {
3008
- const accumulators = /* @__PURE__ */ new Map();
3009
- const location = StructureElement.Location.create(structure);
3010
- for (const unit of structure.units) {
3011
- location.unit = unit;
3012
- for (let i = 0; i < unit.elements.length; i++) {
3013
- location.element = unit.elements[i];
3014
- const chainId = StructureProperties.chain.auth_asym_id(location);
3015
- let chain = accumulators.get(chainId);
3016
- if (!chain) {
3017
- chain = {
3018
- chainId,
3019
- label: elementLabel(location, CHAIN_LABEL_OPTIONS),
3020
- residues: /* @__PURE__ */ new Set()
3021
- };
3022
- accumulators.set(chainId, chain);
3023
- }
3024
- if (StructureProperties.entity.type(location) !== "polymer") continue;
3025
- chain.residues.add(StructureProperties.residue.key(location));
3026
- }
3027
- }
3028
- const ordered = [...accumulators.values()].filter((chain) => chain.residues.size > 0).map((chain) => _objectSpread2(_objectSpread2({}, chain), {}, { residues: [...chain.residues].sort((a, b) => a - b) })).sort((a, b) => {
3029
- var _a$residues$, _b$residues$;
3030
- return ((_a$residues$ = a.residues[0]) !== null && _a$residues$ !== void 0 ? _a$residues$ : 0) - ((_b$residues$ = b.residues[0]) !== null && _b$residues$ !== void 0 ? _b$residues$ : 0);
3031
- });
3032
- return {
3033
- chainLabels: new Map([...accumulators.values()].map(({ chainId, label }) => [chainId, label])),
3034
- chains: ordered.map(({ chainId, label, residues }) => {
3035
- var _residues, _residues$;
3036
- return {
3037
- chainId,
3038
- endIndex: (_residues = residues[residues.length - 1]) !== null && _residues !== void 0 ? _residues : 0,
3039
- label,
3040
- residueCount: residues.length,
3041
- startIndex: (_residues$ = residues[0]) !== null && _residues$ !== void 0 ? _residues$ : 0
3042
- };
3043
- }),
3044
- residuesByChain: new Map(ordered.map(({ chainId, residues }) => [chainId, residues]))
3045
- };
3046
- }
3047
- /** True when the two lists name the same chains, in the same order. */
3048
- function chainsEqual(a, b) {
3049
- return a.length === b.length && a.every((chain, i) => {
3050
- const other = b[i];
3051
- return other !== void 0 && chain.chainId === other.chainId && chain.startIndex === other.startIndex && chain.endIndex === other.endIndex && chain.residueCount === other.residueCount;
3052
- });
3053
- }
3054
- //#endregion
3055
2674
  //#region src/core/ProteinStructureViewer/utils/molstarSpec.ts
3056
2675
  /**
3057
2676
  * The spec's list-valued keys. These are appended rather than merged: Mol*
@@ -3119,421 +2738,16 @@ function mergeMolstarSpec(base, override) {
3119
2738
  return merged;
3120
2739
  }
3121
2740
  //#endregion
3122
- //#region src/core/ProteinStructureViewer/utils/structureFormat.ts
3123
- /** How much of the file to inspect when guessing the format. */
3124
- const DETECT_SAMPLE = 16384;
3125
- /**
3126
- * Infers whether `text` is PDB or mmCIF (PDBx).
3127
- *
3128
- * mmCIF documents start with a `data_` block and name their coordinates
3129
- * `_atom_site.`. PDB never does either, so either marker is enough. A leading
3130
- * comment (`# ...`) before `data_` is still mmCIF.
3131
- */
3132
- function detectStructureFormat(text) {
3133
- const sample = text.slice(0, DETECT_SAMPLE);
3134
- if (/(?:^|\n)\s*data_/.test(sample) || /_atom_site\./.test(sample)) return "mmcif";
3135
- return "pdb";
3136
- }
3137
- //#endregion
3138
- //#region src/core/ProteinStructureViewer/utils/chainColorTheme.ts
3139
- /** Mol* color theme name under which per-chain coloring is registered. */
3140
- const CHAIN_COLOR_THEME_NAME = "chain-color";
3141
- /** Painted for a chain with no color assigned, or one assigned a bad value. */
3142
- const DEFAULT_CHAIN_COLOR_HEX = "#808080";
3143
- const DEFAULT_CHAIN_COLOR = Color.fromRgb(128, 128, 128);
3144
- /**
3145
- * Builds a Mol* color theme that paints each chain its own color.
3146
- *
3147
- * This stands in for Mol*'s built-in `chain-id` theme, which picks its own
3148
- * colors from a palette we cannot read back. Owning the assignment is what
3149
- * lets the chain legend show a swatch that matches the structure, rather than
3150
- * a key for colors it has to guess at.
3151
- *
3152
- * Created per plugin instance, and its state read live inside `color()`, for
3153
- * the same reasons as the residue value theme beside it: several viewers can
3154
- * color chains differently on one page, and recoloring does not rebuild the
3155
- * structure.
3156
- */
3157
- function createChainColorTheme() {
3158
- const state = {
3159
- colors: /* @__PURE__ */ new Map(),
3160
- version: 0
3161
- };
3162
- const provider = {
3163
- category: "chain property",
3164
- defaultValues: {},
3165
- factory() {
3166
- const { colors } = state;
3167
- const parsed = /* @__PURE__ */ new Map();
3168
- for (const [chainId, hex] of colors) parsed.set(chainId, themeColor(hex, DEFAULT_CHAIN_COLOR_HEX));
3169
- return {
3170
- color(location) {
3171
- var _parsed$get;
3172
- if (!StructureElement.Location.is(location)) return DEFAULT_CHAIN_COLOR;
3173
- const chainId = StructureProperties.chain.auth_asym_id(location);
3174
- return (_parsed$get = parsed.get(chainId)) !== null && _parsed$get !== void 0 ? _parsed$get : DEFAULT_CHAIN_COLOR;
3175
- },
3176
- contextHash: state.version,
3177
- description: "Color residues by the chain they sit on",
3178
- factory: provider.factory,
3179
- granularity: "group",
3180
- props: {}
3181
- };
3182
- },
3183
- getParams: () => ({}),
3184
- isApplicable: () => true,
3185
- label: "Chain Color",
3186
- name: CHAIN_COLOR_THEME_NAME
3187
- };
3188
- return {
3189
- provider,
3190
- setState(colors) {
3191
- state.colors = colors;
3192
- state.version++;
3193
- }
3194
- };
3195
- }
3196
- //#endregion
3197
- //#region src/core/ProteinStructureViewer/utils/plddt.ts
3198
- /** Mol* color theme name under which pLDDT coloring is registered. */
3199
- const PLDDT_THEME_NAME = "plddt-bfactor";
3200
- /**
3201
- * AlphaFold's pLDDT confidence bands. Thresholds are on the 0-1 scale that
3202
- * `plddt` values use, ascending from lowest to highest confidence.
3203
- */
3204
- const PLDDT_COLOR_SCALE = {
3205
- kind: "stepped",
3206
- stops: [
3207
- {
3208
- color: "#FF7C45",
3209
- label: "0.5",
3210
- threshold: .5
3211
- },
3212
- {
3213
- color: "#FFDB11",
3214
- label: "0.7",
3215
- threshold: .7
3216
- },
3217
- {
3218
- color: "#64CBF3",
3219
- label: "0.9",
3220
- threshold: .9
3221
- },
3222
- {
3223
- color: "#0053D5",
3224
- label: "1.0",
3225
- threshold: 1
3226
- }
3227
- ]
3228
- };
3229
- const PLDDT_BANDS = PLDDT_COLOR_SCALE.kind === "stepped" ? PLDDT_COLOR_SCALE.stops : [];
3230
- /**
3231
- * The band colors alone, ascending from least to most confident.
3232
- *
3233
- * What the chain legend shows in place of a single color while pLDDT is
3234
- * painting the structure: no one color stands for a chain then, so the swatch
3235
- * carries the whole key instead.
3236
- */
3237
- const PLDDT_BAND_COLORS = PLDDT_BANDS.map((stop) => stop.color);
3238
- const DEFAULT_PLDDT_COLOR = Color.fromRgb(128, 128, 128);
3239
- /**
3240
- * Maps a pLDDT confidence score on the 0-100 B-factor scale to its band color.
3241
- * `PLDDT_COLOR_SCALE` thresholds are on the 0-1 scale, so the score is
3242
- * normalized before it is sampled.
3243
- */
3244
- function plddtToColor(value) {
3245
- const [r, g, b] = sampleSteppedScale(PLDDT_BANDS, value / 100);
3246
- return Color.fromRgb(r, g, b);
3247
- }
3248
- /**
3249
- * Rewrites a PDB file's B-factor column with pLDDT scores so Mol* can read them
3250
- * off the parsed model. B-factors occupy columns 60-66 of ATOM/HETATM lines;
3251
- * scores arrive on a 0-1 scale and are stored on the conventional 0-100 one.
3252
- * Residues past the end of `plddtValues` fall back to a mid-confidence 50.
3253
- *
3254
- * A residue is identified by chain, sequence number and insertion code. The
3255
- * code has to be part of that: `10` and `10A` are two residues, Mol* counts
3256
- * them as two, and treating them as one here would hand every residue after
3257
- * them its neighbour's score.
3258
- */
3259
- function injectPlddtIntoPdb(pdbData, plddtValues) {
3260
- const lines = pdbData.split("\n");
3261
- let residueIndex = -1;
3262
- let lastResidue = "";
3263
- return lines.map((line) => {
3264
- var _plddtValues$residueI;
3265
- if (!line.startsWith("ATOM") && !line.startsWith("HETATM")) return line;
3266
- const residue = line.substring(21, 27);
3267
- if (residue !== lastResidue) {
3268
- residueIndex++;
3269
- lastResidue = residue;
3270
- }
3271
- const bfactorStr = (residueIndex < plddtValues.length ? ((_plddtValues$residueI = plddtValues[residueIndex]) !== null && _plddtValues$residueI !== void 0 ? _plddtValues$residueI : 0) * 100 : 50).toFixed(2).padStart(6, " ");
3272
- const paddedLine = line.padEnd(66, " ");
3273
- return paddedLine.substring(0, 60) + bfactorStr + paddedLine.substring(66);
3274
- }).join("\n");
3275
- }
3276
- const ATOM_SITE_PREFIX = "_atom_site.";
3277
- const B_FACTOR_COLUMN = "B_iso_or_equiv";
3278
- function skipCifWhitespace(line, index) {
3279
- let i = index;
3280
- while (i < line.length && /\s/.test(line.charAt(i))) i += 1;
3281
- return i;
3282
- }
3283
- function readCifToken(line, start) {
3284
- const quote = line.charAt(start);
3285
- if (quote === "'" || quote === "\"") {
3286
- const end = line.indexOf(quote, start + 1);
3287
- if (end < 0) return {
3288
- next: line.length,
3289
- token: line.slice(start + 1)
3290
- };
3291
- return {
3292
- next: end + 1,
3293
- token: line.slice(start + 1, end)
3294
- };
3295
- }
3296
- let i = start;
3297
- while (i < line.length && !/\s/.test(line.charAt(i))) i += 1;
3298
- return {
3299
- next: i,
3300
- token: line.slice(start, i)
3301
- };
3302
- }
3303
- /**
3304
- * Splits one CIF line into tokens, honouring `'quoted'` and `"quoted"` values.
3305
- * Atom-site rows are one line each in practice; multiline `;text;` fields are
3306
- * not used there and are not handled.
3307
- */
3308
- function tokenizeCifLine(line) {
3309
- const tokens = [];
3310
- let i = skipCifWhitespace(line, 0);
3311
- while (i < line.length) {
3312
- const { next, token } = readCifToken(line, i);
3313
- tokens.push(token);
3314
- i = skipCifWhitespace(line, next);
3315
- }
3316
- return tokens;
3317
- }
3318
- function isCifNull(value) {
3319
- return value === void 0 || value === "." || value === "?";
3320
- }
3321
- function columnIndex(columns, name) {
3322
- return columns.indexOf(name);
3323
- }
3324
- /**
3325
- * Residue identity matching `injectPlddtIntoPdb`: chain + sequence number +
3326
- * insertion code. Prefers the `auth_*` columns Mol* reports on the parsed
3327
- * model, falling back to `label_*` when a file omitted them. `.` / `?`
3328
- * insertion codes count as empty, the way a blank PDB column 27 does.
3329
- */
3330
- function mmcifResidueKey(tokens, columns) {
3331
- var _ref, _ref2, _ref3, _ref4;
3332
- const authAsym = columnIndex(columns, "auth_asym_id");
3333
- const labelAsym = columnIndex(columns, "label_asym_id");
3334
- const authSeq = columnIndex(columns, "auth_seq_id");
3335
- const labelSeq = columnIndex(columns, "label_seq_id");
3336
- const ins = columnIndex(columns, "pdbx_PDB_ins_code");
3337
- 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]}`;
3338
- }
3339
- function isAtomSiteRow(tokens, columns) {
3340
- const group = columnIndex(columns, "group_PDB");
3341
- if (group < 0) return true;
3342
- const value = tokens[group];
3343
- return value === "ATOM" || value === "HETATM";
3344
- }
3345
- function plddtBFactor(residueIndex, plddtValues) {
3346
- var _plddtValues$residueI2;
3347
- return (residueIndex < plddtValues.length ? ((_plddtValues$residueI2 = plddtValues[residueIndex]) !== null && _plddtValues$residueI2 !== void 0 ? _plddtValues$residueI2 : 0) * 100 : 50).toFixed(2);
3348
- }
3349
- function isLoopStart(line) {
3350
- return line.trim() === "loop_" || line.trim().startsWith("loop_");
3351
- }
3352
- function isAtomSiteColumn(line) {
3353
- return line.trim().startsWith(ATOM_SITE_PREFIX);
3354
- }
3355
- function endsAtomSiteLoop(line) {
3356
- const trimmed = line.trim();
3357
- if (trimmed === "") return false;
3358
- if (trimmed.startsWith("#")) return true;
3359
- if (isLoopStart(trimmed)) return true;
3360
- if (trimmed.startsWith("data_") || trimmed.startsWith("save_")) return true;
3361
- return trimmed.startsWith("_") && !trimmed.startsWith(ATOM_SITE_PREFIX);
3362
- }
3363
- function isAtomSiteLoop(lines, loopIndex) {
3364
- let peek = loopIndex + 1;
3365
- while (peek < lines.length && lines[peek].trim() === "") peek += 1;
3366
- return peek < lines.length && isAtomSiteColumn(lines[peek]);
3367
- }
3368
- function ensureBFactorColumn(columns, headerLines) {
3369
- const existing = columnIndex(columns, B_FACTOR_COLUMN);
3370
- if (existing >= 0) return existing;
3371
- columns.push(B_FACTOR_COLUMN);
3372
- headerLines.push(`${ATOM_SITE_PREFIX}${B_FACTOR_COLUMN}`);
3373
- return columns.length - 1;
3374
- }
3375
- function rewriteAtomSiteRow(dataLine, columns, bIndex, plddtValues, walk) {
3376
- if (dataLine.trim() === "") return dataLine;
3377
- const tokens = tokenizeCifLine(dataLine);
3378
- if (tokens.length === 0) return dataLine;
3379
- while (tokens.length < columns.length) tokens.push(".");
3380
- if (isAtomSiteRow(tokens, columns)) {
3381
- const residue = mmcifResidueKey(tokens, columns);
3382
- if (residue !== walk.lastResidue) {
3383
- walk.residueIndex += 1;
3384
- walk.lastResidue = residue;
3385
- }
3386
- tokens[bIndex] = plddtBFactor(walk.residueIndex, plddtValues);
3387
- }
3388
- return tokens.join(" ");
3389
- }
3390
- function rewriteAtomSiteLoop(lines, loopIndex, plddtValues, walk) {
3391
- const rows = [lines[loopIndex]];
3392
- let i = loopIndex + 1;
3393
- const columns = [];
3394
- while (i < lines.length && isAtomSiteColumn(lines[i])) {
3395
- columns.push(lines[i].trim().slice(11));
3396
- rows.push(lines[i]);
3397
- i += 1;
3398
- }
3399
- const bIndex = ensureBFactorColumn(columns, rows);
3400
- while (i < lines.length && !endsAtomSiteLoop(lines[i])) {
3401
- rows.push(rewriteAtomSiteRow(lines[i], columns, bIndex, plddtValues, walk));
3402
- i += 1;
3403
- }
3404
- return {
3405
- next: i,
3406
- rows
3407
- };
3408
- }
3409
- /**
3410
- * Rewrites an mmCIF (PDBx) file's `_atom_site.B_iso_or_equiv` column with
3411
- * pLDDT scores so Mol* can read them off the parsed model, the same way
3412
- * `injectPlddtIntoPdb` rewrites the PDB B-factor column. Scores arrive on a
3413
- * 0-1 scale and are stored on the conventional 0-100 one. Residues past the
3414
- * end of `plddtValues` fall back to a mid-confidence 50.
3415
- *
3416
- * If the file has no B-factor column, one is appended to the atom-site loop
3417
- * rather than leaving the scores behind.
3418
- */
3419
- function injectPlddtIntoMmcif(mmcifData, plddtValues) {
3420
- const lines = mmcifData.split("\n");
3421
- const result = [];
3422
- const walk = {
3423
- lastResidue: "",
3424
- residueIndex: -1
3425
- };
3426
- let i = 0;
3427
- let rewrittenLoop = false;
3428
- while (i < lines.length) {
3429
- const line = lines[i];
3430
- if (!rewrittenLoop && isLoopStart(line) && isAtomSiteLoop(lines, i)) {
3431
- const rewritten = rewriteAtomSiteLoop(lines, i, plddtValues, walk);
3432
- result.push(...rewritten.rows);
3433
- i = rewritten.next;
3434
- rewrittenLoop = true;
3435
- continue;
3436
- }
3437
- result.push(line);
3438
- i += 1;
3439
- }
3440
- return result.join("\n");
3441
- }
3442
- /**
3443
- * Rewrites a structure file's B-factors with pLDDT scores. Dispatches to
3444
- * `injectPlddtIntoPdb` or `injectPlddtIntoMmcif` from `format`, or from
3445
- * detecting the format of `data` when `format` is omitted.
3446
- */
3447
- function injectPlddt(data, plddtValues, format = detectStructureFormat(data)) {
3448
- return format === "mmcif" ? injectPlddtIntoMmcif(data, plddtValues) : injectPlddtIntoPdb(data, plddtValues);
3449
- }
3450
- /**
3451
- * Mol* color theme that reads pLDDT scores out of the B-factor column, paired
3452
- * with `injectPlddt`. Registered per plugin instance.
3453
- */
3454
- const PlddtColoring = CustomElementProperty.create({
3455
- coloring: {
3456
- defaultColor: DEFAULT_PLDDT_COLOR,
3457
- getColor(e) {
3458
- return plddtToColor(e);
3459
- }
3460
- },
3461
- getData(model) {
3462
- const map = /* @__PURE__ */ new Map();
3463
- const atomCount = model.atomicHierarchy.atoms._rowCount;
3464
- for (let i = 0; i < atomCount; i++) map.set(i, model.atomicConformation.B_iso_or_equiv.value(i));
3465
- return { value: map };
3466
- },
3467
- label: "pLDDT",
3468
- name: PLDDT_THEME_NAME
3469
- });
3470
- //#endregion
3471
- //#region src/core/ProteinStructureViewer/utils/residueValueTheme.ts
3472
- /** Mol* color theme name under which residue value overlays are registered. */
3473
- const RESIDUE_VALUE_THEME_NAME = "residue-value";
3474
- /**
3475
- * Builds a Mol* color theme that paints residues by an arbitrary per-residue
3476
- * value.
3477
- *
3478
- * The theme is created per plugin instance rather than shared at module scope,
3479
- * so several viewers can render different overlays on the same page. Its state
3480
- * lives in the closure below and is read live inside `color()`, which is what
3481
- * lets an overlay update recolor the structure without rebuilding it.
3482
- */
3483
- function createResidueValueTheme(mode) {
3484
- const state = {
3485
- colorScale: PLASMA_COLOR_SCALE,
3486
- max: 1,
3487
- min: 0,
3488
- mode,
3489
- values: /* @__PURE__ */ new Map(),
3490
- version: 0
3491
- };
3492
- const provider = {
3493
- category: "residue property",
3494
- defaultValues: {},
3495
- factory() {
3496
- const { colorScale, max, min, mode: currentMode, values } = state;
3497
- const neutral = neutralResidueColor(currentMode);
3498
- return {
3499
- color(location) {
3500
- if (!StructureElement.Location.is(location)) return neutral;
3501
- const value = values.get(StructureProperties.residue.key(location));
3502
- if (value === void 0) return neutral;
3503
- const rgb = sampleColorScale(colorScale, value, max, min);
3504
- return rgb ? Color.fromRgb(rgb[0], rgb[1], rgb[2]) : neutral;
3505
- },
3506
- contextHash: state.version,
3507
- description: "Color residues by a per-residue value",
3508
- factory: provider.factory,
3509
- granularity: "group",
3510
- props: {}
3511
- };
3512
- },
3513
- getParams: () => ({}),
3514
- isApplicable: () => true,
3515
- label: "Residue Value",
3516
- name: RESIDUE_VALUE_THEME_NAME
3517
- };
3518
- return {
3519
- provider,
3520
- setState(next) {
3521
- Object.assign(state, next);
3522
- state.version++;
3523
- }
3524
- };
3525
- }
3526
- //#endregion
3527
2741
  //#region src/core/ProteinStructureViewer/hooks/useMolstarPlugin.ts
3528
- /**
3529
- * Theme used when neither pLDDT scores nor an overlay are supplied. Our own
3530
- * per-chain theme rather than Mol*'s built-in `chain-id`, so the chain legend
3531
- * can show swatches that match the structure instead of guessing at the colors
3532
- * Mol* picked.
3533
- */
3534
- const FALLBACK_THEME_NAME = CHAIN_COLOR_THEME_NAME;
3535
2742
  /** Delay before retrying initialization while the container has no size. */
3536
2743
  const LAYOUT_RETRY_MS = 100;
2744
+ /** What the viewer logs for a failure when no `onError` is listening. */
2745
+ const UNHANDLED_ERROR_MESSAGES = {
2746
+ capture: "Failed to download the structure image:",
2747
+ init: "Failed to initialize Mol* viewer:",
2748
+ load: "Failed to load structure:",
2749
+ representation: "Failed to draw the structure:"
2750
+ };
3537
2751
  /** How long to wait for a frame before starting without one. */
3538
2752
  const FRAME_WAIT_MS = 100;
3539
2753
  /**
@@ -3593,7 +2807,7 @@ function createViewer(_x) {
3593
2807
  return _createViewer.apply(this, arguments);
3594
2808
  }
3595
2809
  function _createViewer() {
3596
- _createViewer = _asyncToGenerator(function* ({ backgroundColor, chainColorTheme, edgeColor, highlightColor, molstarSpec, residueValueTheme, root, showAxes, showSequenceViewer, viewSettings }) {
2810
+ _createViewer = _asyncToGenerator(function* ({ backgroundColor, edgeColor, highlightColor, molstarSpec, root, showAxes, showSequenceViewer, viewSettings }) {
3597
2811
  var _spec$config;
3598
2812
  const spec = DefaultPluginUISpec();
3599
2813
  const behaviors = spec.behaviors.filter((b) => b.transformer !== PluginBehaviors.Camera.FocusLoci);
@@ -3648,10 +2862,6 @@ function _createViewer() {
3648
2862
  }), molstarSpec),
3649
2863
  target: root
3650
2864
  });
3651
- const registry = plugin.representation.structure.themes.colorThemeRegistry;
3652
- if (PlddtColoring.colorThemeProvider) registry.add(PlddtColoring.colorThemeProvider);
3653
- registry.add(residueValueTheme.provider);
3654
- registry.add(chainColorTheme.provider);
3655
2865
  for (const p of [...plugin.managers.lociLabels.providers]) plugin.managers.lociLabels.removeProvider(p);
3656
2866
  plugin.managers.structure.focus.behaviors.current.subscribe((entry) => {
3657
2867
  const select = plugin.managers.interactivity.lociSelects;
@@ -3663,232 +2873,59 @@ function _createViewer() {
3663
2873
  return _createViewer.apply(this, arguments);
3664
2874
  }
3665
2875
  /**
3666
- * The representation parameters Mol*'s own presets compute, read from the same
3667
- * manager state they read. Ported rather than hardcoded so a chain drawn here
3668
- * looks exactly like the one the preset used to draw. Mol* hands the same set
3669
- * to its cartoons and its ball-and-sticks, and so does this.
3670
- */
3671
- function representationTypeParams(plugin) {
3672
- const { hydrogens, ignoreLight, visualQuality } = plugin.managers.structure.component.state.options;
3673
- return {
3674
- ignoreHydrogens: hydrogens !== "all",
3675
- ignoreHydrogensVariant: hydrogens === "only-polar" ? "non-polar" : "all",
3676
- ignoreLight,
3677
- quality: visualQuality
3678
- };
3679
- }
3680
- /**
3681
- * The two halves of a chain the viewer draws. Each names the component its
3682
- * half is built under and tags the representation drawn over it, and it is
3683
- * that tag `applyColorTheme` reads back to tell the two apart.
3684
- */
3685
- const POLYMER_PART = "polymer";
3686
- const LIGAND_PART = "ligand";
3687
- /**
3688
- * True for the representation drawing a chain's heteroatoms.
3689
- *
3690
- * Read off the tag put on the representation when it was built, rather than
3691
- * off the component's key. Mol* does not store the key as it was given: it
3692
- * files components under `structure-component-<key>`, so a test against the
3693
- * key as written here silently matches nothing, and every ligand quietly takes
3694
- * the structure-wide theme instead. The tag survives untouched.
3695
- */
3696
- function isLigandRepresentation(representation) {
3697
- var _representation$cell$;
3698
- return ((_representation$cell$ = representation.cell.transform.tags) === null || _representation$cell$ === void 0 ? void 0 : _representation$cell$.includes(LIGAND_PART)) === true;
3699
- }
3700
- /**
3701
- * How heteroatoms are colored: the theme Mol* puts on a ball-and-stick when
3702
- * left to itself, with the parameters it chooses too.
3703
- *
3704
- * Named rather than left blank only so that it survives `applyColorTheme`,
3705
- * which would otherwise repaint the ligands along with everything else. The
3706
- * structure-wide theme is the wrong answer for them twice over: a heme reads
3707
- * as a heme because its iron is orange and its nitrogens are blue, which one
3708
- * flat color throws away, and pLDDT and residue overlays have no value for a
3709
- * HETATM to be colored by at all.
3710
- */
3711
- const ELEMENT_THEME = "element-symbol";
3712
- /**
3713
- * Draws one chain: its polymer as a cartoon, its ligands and ions as
3714
- * ball-and-stick.
3715
- *
3716
- * Two components rather than one, because a component is what a representation
3717
- * attaches to and neither representation suits both halves - a cartoon has no
3718
- * backbone to trace through a heme, and a ball-and-stick over the whole chain
3719
- * would draw every protein atom as a sphere.
3720
- *
3721
- * Hands back the refs of whichever components the chain turned out to have,
3722
- * which is what `setSubtreeVisibility` is later pointed at. Grouping them
3723
- * under the chain is what makes hiding a chain take its ligands with it.
3724
- */
3725
- function buildChainComponents(_x5, _x6, _x7, _x8) {
3726
- return _buildChainComponents.apply(this, arguments);
3727
- }
3728
- function _buildChainComponents() {
3729
- _buildChainComponents = _asyncToGenerator(function* (plugin, structure, chain, typeParams) {
3730
- const builders = plugin.builders.structure;
3731
- const refs = [];
3732
- const addPart = function() {
3733
- var _ref = _asyncToGenerator(function* (part, expression, props) {
3734
- const component = yield builders.tryCreateComponentFromExpression(structure, expression, `${part}-${chain.chainId}`, { label: chain.label });
3735
- if (!component) return;
3736
- yield builders.representation.addRepresentation(component, props, { tag: part });
3737
- refs.push(component.ref);
3738
- });
3739
- return function addPart(_x2, _x3, _x4) {
3740
- return _ref.apply(this, arguments);
3741
- };
3742
- }();
3743
- yield addPart(POLYMER_PART, chainPolymerExpression(chain.chainId), {
3744
- type: "cartoon",
3745
- typeParams
3746
- });
3747
- yield addPart(LIGAND_PART, chainLigandExpression(chain.chainId), {
3748
- color: ELEMENT_THEME,
3749
- type: "ball-and-stick",
3750
- typeParams
3751
- });
3752
- return refs;
3753
- });
3754
- return _buildChainComponents.apply(this, arguments);
3755
- }
3756
- const NOTHING_LOADED = {
3757
- chains: [],
3758
- componentRefs: /* @__PURE__ */ new Map(),
3759
- residuesByChain: /* @__PURE__ */ new Map()
3760
- };
3761
- /**
3762
- * Parses the structure and draws each chain: a cartoon over its polymer, and
3763
- * ball-and-stick over its ligands and ions.
3764
- *
3765
- * Mol*'s `default` hierarchy preset would be shorter, but it groups every
3766
- * polymer chain into a single component, and a component is the unit Mol* can
3767
- * hide. Building them per chain is what makes a chain individually hideable;
3768
- * the refs handed back are what `setSubtreeVisibility` is later pointed at.
3769
- *
3770
- * The wrapper detects PDB vs mmCIF from the text; this just forwards that
3771
- * guess to Mol*.
3772
- */
3773
- function loadStructure(_x9, _x10, _x11, _x12) {
3774
- return _loadStructure.apply(this, arguments);
3775
- }
3776
- function _loadStructure() {
3777
- _loadStructure = _asyncToGenerator(function* (plugin, structureText, usePlddtColoring, showAxes) {
3778
- try {
3779
- var _plugin$canvas3d;
3780
- yield plugin.clear();
3781
- const data = yield plugin.builders.data.rawData({
3782
- data: structureText,
3783
- label: "Structure"
3784
- });
3785
- const trajectory = yield plugin.builders.structure.parseTrajectory(data, detectStructureFormat(structureText));
3786
- const model = yield plugin.builders.structure.createModel(trajectory);
3787
- const structure = yield plugin.builders.structure.createStructure(model, {
3788
- name: "model",
3789
- params: {}
3790
- });
3791
- const data3d = structure.data;
3792
- if (!data3d) return NOTHING_LOADED;
3793
- const { chainLabels, chains, residuesByChain } = scanChains(data3d);
3794
- const typeParams = representationTypeParams(plugin);
3795
- const componentRefs = /* @__PURE__ */ new Map();
3796
- for (const [chainId, label] of chainLabels) {
3797
- const refs = yield buildChainComponents(plugin, structure, {
3798
- chainId,
3799
- label
3800
- }, typeParams);
3801
- if (refs.length > 0) componentRefs.set(chainId, refs);
3802
- }
3803
- if (usePlddtColoring) yield applyColorTheme(plugin, PLDDT_THEME_NAME);
3804
- (_plugin$canvas3d = plugin.canvas3d) === null || _plugin$canvas3d === void 0 || _plugin$canvas3d.requestCameraReset();
3805
- setAxes(plugin, showAxes);
3806
- return {
3807
- chains,
3808
- componentRefs,
3809
- residuesByChain
3810
- };
3811
- } catch (error) {
3812
- console.error("Failed to load structure:", error);
3813
- return NOTHING_LOADED;
3814
- }
3815
- });
3816
- return _loadStructure.apply(this, arguments);
3817
- }
3818
- /**
3819
- * Shows or hides everything each chain is drawn with, cartoon and sticks
3820
- * alike. Every component under the chain, so hiding it takes its ligands with
3821
- * it rather than leaving a heme floating where its protein used to be.
3822
- *
3823
- * The whole subtree of each, not just the component cell: visibility is
3824
- * per-cell, and it is the representation underneath that actually draws.
3825
- *
3826
- * Set outright rather than through the component manager's `toggleVisibility`,
3827
- * which flips whatever it finds: the hidden set is derived from props on every
3828
- * pass, so it has to be applied as a statement of what should be true and not
3829
- * as a change to what already is.
3830
- */
3831
- function applyChainVisibility(plugin, componentRefs, hidden) {
3832
- for (const [chainId, refs] of componentRefs) for (const ref of refs) setSubtreeVisibility(plugin.state.data, ref, hidden.has(chainId));
3833
- }
3834
- /**
3835
- * Recolors every loaded structure's representations with the named theme,
3836
- * leaving the heteroatoms on `ELEMENT_THEME`.
3837
- *
3838
- * The per-representation form of `updateRepresentationsTheme`, so the two can
3839
- * be told apart in one pass; handing it a filtered list instead would leave
3840
- * the ligands on whichever theme happened to be current when they were built.
3841
- */
3842
- function applyColorTheme(_x13, _x14) {
3843
- return _applyColorTheme.apply(this, arguments);
3844
- }
3845
- function _applyColorTheme() {
3846
- _applyColorTheme = _asyncToGenerator(function* (plugin, colorTheme) {
3847
- yield plugin.dataTransaction(_asyncToGenerator(function* () {
3848
- for (const s of plugin.managers.structure.hierarchy.current.structures) yield plugin.managers.structure.component.updateRepresentationsTheme(s.components, (_component, representation) => ({ color: isLigandRepresentation(representation) ? ELEMENT_THEME : colorTheme }));
3849
- }));
3850
- });
3851
- return _applyColorTheme.apply(this, arguments);
3852
- }
3853
- /**
3854
2876
  * Owns the Mol* plugin for one viewer: creates it, loads the structure, wires
3855
2877
  * interaction callbacks, and disposes of it on unmount.
3856
2878
  *
3857
2879
  * The plugin is built once and then mutated in place. Creating it is expensive
3858
2880
  * and destroys the camera, so prop changes that Mol* can absorb (background,
3859
- * axes, coloring, new structure) are pushed in through the effects below and in
3860
- * the sibling hooks rather than by rebuilding.
2881
+ * axes, coloring, representation, new structure) are pushed in through the
2882
+ * effects below and in the sibling hooks rather than by rebuilding.
3861
2883
  */
3862
- function useMolstarPlugin({ backgroundColor, chainColors, containerRef, download, edgeColor, hasPlddt, hiddenChains, highlightColor, mode, molstarSpec, onChainHover, onChainSelect, onChainToggle, onResidueClick, onResidueHover, onSelectionChange, onSelectionClear, selectedChains, sequenceViewerBackgroundColor, showAxes, showSequenceViewer, structure }) {
2884
+ function useMolstarPlugin({ backgroundColor, containerRef, download, edgeColor, hiddenChains, highlightColor, mode, molstarSpec, onChainHover, onChainSelect, onChainToggle, onDispose, onError, onReady, onResidueClick, onResidueHover, onSelectionChange, onSelectionClear, onStructureLoad, sceneMode, sceneProps, selectedChains, sequenceViewerBackgroundColor, showAxes, showSequenceViewer, structure }) {
2885
+ var _sceneProps$chainColo;
3863
2886
  const pluginRef = useRef(null);
3864
- const residueValueThemeRef = useRef(null);
3865
- const chainColorThemeRef = useRef(null);
2887
+ const sceneRef = useRef(null);
3866
2888
  const currentStructureRef = useRef(null);
3867
2889
  const [isReady, setIsReady] = useState(false);
3868
2890
  const [chains, setChains] = useState([]);
3869
2891
  const [loadCount, setLoadCount] = useState(0);
3870
- /**
3871
- * Where each chain's components live in the state tree, which is what the
3872
- * visibility effect points `setSubtreeVisibility` at. Rewritten by every
3873
- * load, since the refs do not survive `plugin.clear()`.
3874
- */
3875
- const componentRefsRef = useRef(/* @__PURE__ */ new Map());
2892
+ const [sceneVersion, setSceneVersion] = useState(0);
2893
+ const [sceneBusy, setSceneBusy] = useState(false);
3876
2894
  /**
3877
2895
  * Which residues sit on each chain, for the readout to average a score over
3878
2896
  * a whole-chain selection. A ref rather than state: nothing renders from it
3879
2897
  * directly, and it is rewritten in step with `chains`, which does.
3880
2898
  */
3881
2899
  const residuesByChainRef = useRef(/* @__PURE__ */ new Map());
2900
+ const addressIndexRef = useRef(/* @__PURE__ */ new Map());
2901
+ const framedOrientationRef = useRef(void 0);
3882
2902
  /**
3883
- * The visibility and coloring to apply as soon as a structure exists. Read
3884
- * through refs because both arrive as fresh objects on every render, and a
3885
- * load has to be able to consult them without the load effect depending on
3886
- * them - a new Set each render would reload the structure on every pass.
2903
+ * The scene as the props describe it, read by a load when it comes to draw.
2904
+ * Through a ref because a load outlasts renders and has to draw what the
2905
+ * props say when it gets there, not what they said when it began.
3887
2906
  */
3888
- const hiddenChainsRef = useRef(hiddenChains);
3889
- hiddenChainsRef.current = hiddenChains;
3890
- const chainColorsRef = useRef(chainColors);
3891
- chainColorsRef.current = chainColors;
2907
+ const scenePropsRef = useRef(sceneProps);
2908
+ scenePropsRef.current = sceneProps;
2909
+ const initialPropsRef = useRef({
2910
+ backgroundColor,
2911
+ edgeColor,
2912
+ highlightColor,
2913
+ mode,
2914
+ molstarSpec,
2915
+ showAxes,
2916
+ showSequenceViewer,
2917
+ structure
2918
+ });
2919
+ initialPropsRef.current = {
2920
+ backgroundColor,
2921
+ edgeColor,
2922
+ highlightColor,
2923
+ mode,
2924
+ molstarSpec,
2925
+ showAxes,
2926
+ showSequenceViewer,
2927
+ structure
2928
+ };
3892
2929
  /**
3893
2930
  * Carries the theme-dependent props into the views Mol* renders in its own
3894
2931
  * React root. They cannot read them from context, and rebuilding the plugin
@@ -3910,18 +2947,16 @@ function useMolstarPlugin({ backgroundColor, chainColors, containerRef, download
3910
2947
  clipRatioRef.current = ratio;
3911
2948
  }, []);
3912
2949
  /**
3913
- * Takes up what a load produced: the component refs the chain-keyed props
3914
- * address, the visibility they were already asking for, and the chain list
3915
- * the consumer is told about.
2950
+ * Takes up what a load produced: what the chain-keyed props and addresses
2951
+ * are resolved against, and the chain list the consumer is told about.
3916
2952
  *
3917
2953
  * Chains are published only when they actually differ, so reloading the same
3918
- * structure - a theme switch that happens to reload, a parent handing back an
3919
- * equal PDB string - does not hand every consumer a new array to react to.
2954
+ * structure - a parent handing back an equal PDB string - does not hand every
2955
+ * consumer a new array to react to.
3920
2956
  */
3921
- const adoptLoadedStructure = useCallback((plugin, loaded) => {
3922
- componentRefsRef.current = loaded.componentRefs;
2957
+ const adoptLoadedStructure = useCallback((loaded) => {
3923
2958
  residuesByChainRef.current = loaded.residuesByChain;
3924
- applyChainVisibility(plugin, loaded.componentRefs, hiddenChainsRef.current);
2959
+ addressIndexRef.current = loaded.addressIndex;
3925
2960
  setChains((prev) => chainsEqual(prev, loaded.chains) ? prev : loaded.chains);
3926
2961
  setLoadCount((count) => count + 1);
3927
2962
  }, []);
@@ -3933,30 +2968,113 @@ function useMolstarPlugin({ backgroundColor, chainColors, containerRef, download
3933
2968
  onSelectionChangeRef.current = onSelectionChange;
3934
2969
  const onResidueHoverRef = useRef(onResidueHover);
3935
2970
  onResidueHoverRef.current = onResidueHover;
2971
+ const onReadyRef = useRef(onReady);
2972
+ onReadyRef.current = onReady;
2973
+ const onErrorRef = useRef(onError);
2974
+ onErrorRef.current = onError;
2975
+ const onDisposeRef = useRef(onDispose);
2976
+ onDisposeRef.current = onDispose;
2977
+ const onStructureLoadRef = useRef(onStructureLoad);
2978
+ onStructureLoadRef.current = onStructureLoad;
2979
+ /**
2980
+ * Fixed at the first render: the load path draws or declines to draw by it,
2981
+ * so a mode that changed under a live plugin would leave its scene half one
2982
+ * and half the other.
2983
+ */
2984
+ const sceneModeRef = useRef(sceneMode);
2985
+ /**
2986
+ * The plugin last handed to `onReady`, so that `onDispose` is only called for
2987
+ * one a consumer has actually been given - and is called for it even while
2988
+ * its `onReady` is still running.
2989
+ */
2990
+ const handedOverRef = useRef(null);
3936
2991
  /** Last residue reported to `onResidueHover`, to suppress repeats. */
3937
2992
  const lastHoverRef = useRef(null);
3938
- const initialPropsRef = useRef({
3939
- backgroundColor,
3940
- edgeColor,
3941
- hasPlddt,
3942
- highlightColor,
3943
- mode,
3944
- molstarSpec,
3945
- showAxes,
3946
- showSequenceViewer,
3947
- structure
3948
- });
3949
- initialPropsRef.current = {
3950
- backgroundColor,
3951
- edgeColor,
3952
- hasPlddt,
3953
- highlightColor,
3954
- mode,
3955
- molstarSpec,
3956
- showAxes,
3957
- showSequenceViewer,
3958
- structure
3959
- };
2993
+ const reportError = useCallback((error, phase) => {
2994
+ const handler = onErrorRef.current;
2995
+ if (handler) handler(error, phase);
2996
+ else console.error(UNHANDLED_ERROR_MESSAGES[phase], error);
2997
+ }, []);
2998
+ /**
2999
+ * Hands a loaded structure to `onReady` and waits for it.
3000
+ *
3001
+ * `isCurrent` says whether the plugin is still the viewer's own by the time
3002
+ * the consumer is done with it. A failure after that is not reported: the
3003
+ * plugin it happened on is gone, and the consumer has been told so through
3004
+ * `onDispose` already.
3005
+ */
3006
+ const handOver = useCallback(function() {
3007
+ var _ref = _asyncToGenerator(function* (plugin, loaded, isCurrent) {
3008
+ const ready = onReadyRef.current;
3009
+ const { atomCount, chains: loadedChains, structure: parsed } = loaded;
3010
+ if (!ready || !parsed) return;
3011
+ handedOverRef.current = plugin;
3012
+ try {
3013
+ yield ready(plugin, {
3014
+ atomCount,
3015
+ chains: loadedChains,
3016
+ structure: parsed
3017
+ });
3018
+ } catch (error) {
3019
+ if (isCurrent()) reportError(error, "load");
3020
+ }
3021
+ });
3022
+ return function(_x2, _x3, _x4) {
3023
+ return _ref.apply(this, arguments);
3024
+ };
3025
+ }(), [reportError]);
3026
+ /**
3027
+ * Takes up whatever a load produced. A failed load leaves the viewer holding
3028
+ * nothing, as a structure with no chains would, and is reported rather than
3029
+ * handed to `onReady`.
3030
+ */
3031
+ const takeUpLoad = useCallback(function() {
3032
+ var _ref2 = _asyncToGenerator(function* (plugin, outcome, isCurrent) {
3033
+ var _onStructureLoadRef$c;
3034
+ if (!isCurrent()) return;
3035
+ if (!outcome.ok) {
3036
+ adoptLoadedStructure(NOTHING_LOADED);
3037
+ reportError(outcome.error, "load");
3038
+ return;
3039
+ }
3040
+ const { loaded } = outcome;
3041
+ adoptLoadedStructure(loaded);
3042
+ (_onStructureLoadRef$c = onStructureLoadRef.current) === null || _onStructureLoadRef$c === void 0 || _onStructureLoadRef$c.call(onStructureLoadRef, loadInfo(loaded));
3043
+ yield handOver(plugin, loaded, isCurrent);
3044
+ });
3045
+ return function(_x5, _x6, _x7) {
3046
+ return _ref2.apply(this, arguments);
3047
+ };
3048
+ }(), [
3049
+ adoptLoadedStructure,
3050
+ handOver,
3051
+ reportError
3052
+ ]);
3053
+ /**
3054
+ * Loads a structure and brings everything up on it: parses it, draws the
3055
+ * scene when the viewer is the one drawing it, places the camera, and hands
3056
+ * it over. Runs inside the scene's queue, so it never interleaves with an
3057
+ * update to the structure it is replacing.
3058
+ *
3059
+ * The camera is placed once something is drawn: Mol* settles a reset on the
3060
+ * next frame, and one settled against an empty scene is lost. So it follows
3061
+ * the viewer's own scene when there is one, and the consumer's `onReady`
3062
+ * when the consumer draws.
3063
+ */
3064
+ const loadAndDraw = useCallback(function() {
3065
+ var _ref3 = _asyncToGenerator(function* (plugin, scene, text, isCurrent) {
3066
+ const outcome = yield scene.load(text, scenePropsRef.current);
3067
+ if (!isCurrent()) return;
3068
+ const managed = sceneModeRef.current === "managed";
3069
+ if (outcome.ok) setAxes(plugin, initialPropsRef.current.showAxes);
3070
+ if (managed && outcome.ok) framedOrientationRef.current = scene.frame(scenePropsRef.current, true);
3071
+ yield takeUpLoad(plugin, outcome, isCurrent);
3072
+ if (!managed && outcome.ok && isCurrent()) framedOrientationRef.current = scene.frame(scenePropsRef.current, false);
3073
+ });
3074
+ return function(_x8, _x9, _x10, _x11) {
3075
+ return _ref3.apply(this, arguments);
3076
+ };
3077
+ }(), [takeUpLoad]);
3960
3078
  /**
3961
3079
  * The consumer's `canvas3d` overrides, keyed by content. Unlike the rest of
3962
3080
  * the spec these are re-applied when they change, and they commonly arrive
@@ -3977,7 +3095,7 @@ function useMolstarPlugin({ backgroundColor, chainColors, containerRef, download
3977
3095
  let cancelled = false;
3978
3096
  let clipSubscription;
3979
3097
  const init = function() {
3980
- var _ref2 = _asyncToGenerator(function* () {
3098
+ var _ref4 = _asyncToGenerator(function* () {
3981
3099
  yield nextFrame();
3982
3100
  if (cancelled) return;
3983
3101
  const rect = container.getBoundingClientRect();
@@ -3987,16 +3105,12 @@ function useMolstarPlugin({ backgroundColor, chainColors, containerRef, download
3987
3105
  }
3988
3106
  const initial = initialPropsRef.current;
3989
3107
  try {
3990
- var _plugin$canvas3d2;
3991
- const residueValueTheme = createResidueValueTheme(initial.mode);
3992
- const chainColorTheme = createChainColorTheme();
3108
+ var _plugin$canvas3d;
3993
3109
  const plugin = yield createViewer({
3994
3110
  backgroundColor: initial.backgroundColor,
3995
- chainColorTheme,
3996
3111
  edgeColor: initial.edgeColor,
3997
3112
  highlightColor: initial.highlightColor,
3998
3113
  molstarSpec: initial.molstarSpec,
3999
- residueValueTheme,
4000
3114
  root: container,
4001
3115
  showAxes: initial.showAxes,
4002
3116
  showSequenceViewer: initial.showSequenceViewer,
@@ -4007,12 +3121,18 @@ function useMolstarPlugin({ backgroundColor, chainColors, containerRef, download
4007
3121
  return;
4008
3122
  }
4009
3123
  pluginRef.current = plugin;
4010
- residueValueThemeRef.current = residueValueTheme;
4011
- chainColorThemeRef.current = chainColorTheme;
3124
+ const scene = new StructureScene(plugin, {
3125
+ draw: sceneModeRef.current === "managed",
3126
+ mode: initial.mode,
3127
+ onBusyChange: setSceneBusy,
3128
+ onRebuild: () => setSceneVersion((version) => version + 1),
3129
+ reportError
3130
+ });
3131
+ sceneRef.current = scene;
4012
3132
  const latest = initialPropsRef.current;
4013
- const loaded = yield loadStructure(plugin, latest.structure, latest.hasPlddt, latest.showAxes);
4014
3133
  currentStructureRef.current = latest.structure;
4015
- adoptLoadedStructure(plugin, loaded);
3134
+ yield scene.enqueue(() => loadAndDraw(plugin, scene, latest.structure, () => !cancelled));
3135
+ if (cancelled) return;
4016
3136
  /**
4017
3137
  * Mol*'s click behavior is a BehaviorSubject, so subscribing replays
4018
3138
  * whatever it is currently holding - which, before anything has been
@@ -4050,37 +3170,46 @@ function useMolstarPlugin({ backgroundColor, chainColors, containerRef, download
4050
3170
  lastHoverRef.current = residue;
4051
3171
  (_onResidueHoverRef$cu = onResidueHoverRef.current) === null || _onResidueHoverRef$cu === void 0 || _onResidueHoverRef$cu.call(onResidueHoverRef, residue);
4052
3172
  });
4053
- clipSubscription = (_plugin$canvas3d2 = plugin.canvas3d) === null || _plugin$canvas3d2 === void 0 ? void 0 : _plugin$canvas3d2.didDraw.subscribe(() => {
3173
+ clipSubscription = (_plugin$canvas3d = plugin.canvas3d) === null || _plugin$canvas3d === void 0 ? void 0 : _plugin$canvas3d.didDraw.subscribe(() => {
4054
3174
  syncClipToZoom(plugin, () => clipRatioRef.current);
4055
3175
  });
4056
3176
  if (!cancelled) setIsReady(true);
4057
3177
  } catch (error) {
4058
- console.error("Failed to initialize Mol* viewer:", error);
3178
+ if (!cancelled) reportError(error, "init");
4059
3179
  }
4060
3180
  });
4061
3181
  return function init() {
4062
- return _ref2.apply(this, arguments);
3182
+ return _ref4.apply(this, arguments);
4063
3183
  };
4064
3184
  }();
4065
3185
  init();
4066
3186
  return () => {
4067
- var _pluginRef$current;
3187
+ var _sceneRef$current;
4068
3188
  cancelled = true;
4069
3189
  clipSubscription === null || clipSubscription === void 0 || clipSubscription.unsubscribe();
4070
- (_pluginRef$current = pluginRef.current) === null || _pluginRef$current === void 0 || _pluginRef$current.dispose();
3190
+ (_sceneRef$current = sceneRef.current) === null || _sceneRef$current === void 0 || _sceneRef$current.dispose();
3191
+ sceneRef.current = null;
3192
+ const plugin = pluginRef.current;
3193
+ try {
3194
+ if (plugin && handedOverRef.current === plugin) {
3195
+ var _onDisposeRef$current;
3196
+ handedOverRef.current = null;
3197
+ (_onDisposeRef$current = onDisposeRef.current) === null || _onDisposeRef$current === void 0 || _onDisposeRef$current.call(onDisposeRef);
3198
+ }
3199
+ } finally {
3200
+ plugin === null || plugin === void 0 || plugin.dispose();
3201
+ }
4071
3202
  pluginRef.current = null;
4072
- residueValueThemeRef.current = null;
4073
- chainColorThemeRef.current = null;
4074
- componentRefsRef.current = /* @__PURE__ */ new Map();
4075
3203
  residuesByChainRef.current = /* @__PURE__ */ new Map();
3204
+ addressIndexRef.current = /* @__PURE__ */ new Map();
4076
3205
  currentStructureRef.current = null;
4077
3206
  clipRatioRef.current = null;
4078
3207
  setIsReady(false);
4079
3208
  };
4080
3209
  }, []);
4081
3210
  useEffect(() => {
4082
- var _pluginRef$current2, _initialPropsRef$curr;
4083
- const canvas3d = (_pluginRef$current2 = pluginRef.current) === null || _pluginRef$current2 === void 0 ? void 0 : _pluginRef$current2.canvas3d;
3211
+ var _pluginRef$current, _initialPropsRef$curr;
3212
+ const canvas3d = (_pluginRef$current = pluginRef.current) === null || _pluginRef$current === void 0 ? void 0 : _pluginRef$current.canvas3d;
4084
3213
  if (!canvas3d || !isReady) return;
4085
3214
  canvas3d.setProps({
4086
3215
  marking: {
@@ -4109,6 +3238,8 @@ function useMolstarPlugin({ backgroundColor, chainColors, containerRef, download
4109
3238
  onChainHover,
4110
3239
  onChainSelect,
4111
3240
  onChainToggle,
3241
+ onError: reportError,
3242
+ sceneBusy,
4112
3243
  selectedChains,
4113
3244
  sequenceViewerBackgroundColor,
4114
3245
  showAxes
@@ -4120,6 +3251,8 @@ function useMolstarPlugin({ backgroundColor, chainColors, containerRef, download
4120
3251
  onChainHover,
4121
3252
  onChainSelect,
4122
3253
  onChainToggle,
3254
+ reportError,
3255
+ sceneBusy,
4123
3256
  selectedChains,
4124
3257
  sequenceViewerBackgroundColor,
4125
3258
  showAxes,
@@ -4131,34 +3264,55 @@ function useMolstarPlugin({ backgroundColor, chainColors, containerRef, download
4131
3264
  }, [showAxes, isReady]);
4132
3265
  useEffect(() => {
4133
3266
  const plugin = pluginRef.current;
4134
- if (!plugin || !isReady) return;
3267
+ const scene = sceneRef.current;
3268
+ if (!plugin || !scene || !isReady) return;
4135
3269
  if (structure === currentStructureRef.current) return;
4136
3270
  currentStructureRef.current = structure;
4137
3271
  clipRatioRef.current = null;
4138
3272
  lastHoverRef.current = null;
4139
- loadStructure(plugin, structure, hasPlddt, showAxes).then((loaded) => {
4140
- if (pluginRef.current !== plugin) return;
4141
- adoptLoadedStructure(plugin, loaded);
3273
+ scene.enqueue(() => loadAndDraw(plugin, scene, structure, () => pluginRef.current === plugin)).catch((error) => {
3274
+ if (pluginRef.current === plugin) reportError(error, "load");
4142
3275
  });
4143
3276
  }, [
4144
3277
  structure,
4145
- hasPlddt,
4146
3278
  isReady,
4147
- adoptLoadedStructure
3279
+ loadAndDraw,
3280
+ reportError
4148
3281
  ]);
3282
+ /**
3283
+ * Keys for the scene props that arrive as fresh objects on every render, so
3284
+ * the update below turns on what they say rather than on their identity.
3285
+ * `plddt` and the overlay are compared by identity, as they are elsewhere.
3286
+ */
3287
+ const hiddenKey = chainSetKey(sceneProps.hiddenChains);
3288
+ const sceneHighlightsKey = highlightsKey(sceneProps.highlights);
3289
+ const chainColorsKey = JSON.stringify((_sceneProps$chainColo = sceneProps.chainColors) !== null && _sceneProps$chainColo !== void 0 ? _sceneProps$chainColo : null);
4149
3290
  useEffect(() => {
4150
- const plugin = pluginRef.current;
4151
- if (!plugin || !isReady) return;
4152
- applyChainVisibility(plugin, componentRefsRef.current, hiddenChains);
4153
- }, [hiddenChains, isReady]);
3291
+ const scene = sceneRef.current;
3292
+ if (!scene || !isReady || sceneModeRef.current !== "managed") return;
3293
+ scene.update(scenePropsRef.current);
3294
+ }, [
3295
+ isReady,
3296
+ sceneProps.representation,
3297
+ hiddenKey,
3298
+ sceneHighlightsKey,
3299
+ sceneProps.colorBy,
3300
+ chainColorsKey,
3301
+ sceneProps.plddt,
3302
+ sceneProps.overlay,
3303
+ sceneProps.mode
3304
+ ]);
4154
3305
  return {
4155
- chainColorThemeRef,
3306
+ addressIndexRef,
4156
3307
  chains,
3308
+ framedOrientationRef,
4157
3309
  isReady,
4158
3310
  loadCount,
4159
3311
  pluginRef,
4160
3312
  residuesByChainRef,
4161
- residueValueThemeRef,
3313
+ sceneMode: sceneModeRef.current,
3314
+ sceneRef,
3315
+ sceneVersion,
4162
3316
  setClipRatio
4163
3317
  };
4164
3318
  }
@@ -4466,52 +3620,6 @@ function useSelectionReadout({ hoveredResidue, isReady, plddt, pluginRef, residu
4466
3620
  };
4467
3621
  }
4468
3622
  //#endregion
4469
- //#region src/core/ProteinStructureViewer/hooks/useStructureColoring.ts
4470
- /**
4471
- * Decides what is coloring the structure and keeps that theme's data current.
4472
- *
4473
- * Three themes compete for the same representations, in a fixed order: a
4474
- * residue value overlay, then pLDDT scores, then per-chain colors. Only one
4475
- * can be applied, so the precedence lives here rather than being spread across
4476
- * the hooks that own each theme's data - and the legend resolves what to
4477
- * describe the same way, so the two cannot disagree about what is on screen.
4478
- *
4479
- * Recoloring happens in place: the camera is left where it is, so selecting or
4480
- * switching an overlay does not zoom back out to the default view.
4481
- */
4482
- function useStructureColoring({ chainColorThemeRef, chainColors, hasPlddt, isReady, mode, overlay, pluginRef, residueValueThemeRef }) {
4483
- useEffect(() => {
4484
- const plugin = pluginRef.current;
4485
- const residueTheme = residueValueThemeRef.current;
4486
- const chainTheme = chainColorThemeRef.current;
4487
- if (!plugin || !residueTheme || !chainTheme || !isReady) return;
4488
- chainTheme.setState(chainColors);
4489
- if (overlay) {
4490
- var _overlay$colorScale, _overlay$min;
4491
- residueTheme.setState({
4492
- colorScale: (_overlay$colorScale = overlay.colorScale) !== null && _overlay$colorScale !== void 0 ? _overlay$colorScale : PLASMA_COLOR_SCALE,
4493
- max: overlay.max,
4494
- min: (_overlay$min = overlay.min) !== null && _overlay$min !== void 0 ? _overlay$min : 0,
4495
- mode,
4496
- values: overlay.values
4497
- });
4498
- applyColorTheme(plugin, RESIDUE_VALUE_THEME_NAME);
4499
- return;
4500
- }
4501
- residueTheme.setState({
4502
- mode,
4503
- values: /* @__PURE__ */ new Map()
4504
- });
4505
- applyColorTheme(plugin, hasPlddt ? PLDDT_THEME_NAME : FALLBACK_THEME_NAME);
4506
- }, [
4507
- overlay,
4508
- hasPlddt,
4509
- isReady,
4510
- mode,
4511
- chainColors
4512
- ]);
4513
- }
4514
- //#endregion
4515
3623
  //#region src/core/ProteinStructureViewer/ProteinStructureViewer.types.ts
4516
3624
  /** Pixel dimensions each resolution produces, for documentation and labels. */
4517
3625
  const DOWNLOAD_RESOLUTIONS = {
@@ -4525,20 +3633,33 @@ const DOWNLOAD_RESOLUTIONS = {
4525
3633
  const _excluded = [
4526
3634
  "backgroundColor",
4527
3635
  "chainColors",
3636
+ "colorBy",
4528
3637
  "disableChainHighlightOnHover",
4529
3638
  "download",
4530
3639
  "hiddenChains",
3640
+ "highlights",
3641
+ "initialCamera",
3642
+ "onCameraChange",
4531
3643
  "onChainVisibilityChange",
4532
3644
  "onChainsChange",
4533
3645
  "molstarSpec",
3646
+ "onDispose",
3647
+ "onError",
3648
+ "onReady",
4534
3649
  "onResidueClick",
4535
3650
  "onResidueHover",
4536
3651
  "onSelectionChange",
3652
+ "onStructureLoad",
3653
+ "orientation",
3654
+ "projection",
3655
+ "representation",
4537
3656
  "structure",
4538
3657
  "plddt",
4539
3658
  "residueOverlay",
3659
+ "sceneMode",
4540
3660
  "selection",
4541
3661
  "sequenceViewerBackgroundColor",
3662
+ "sequenceViewerHeight",
4542
3663
  "showAxes",
4543
3664
  "showChainLegend",
4544
3665
  "showLegend",
@@ -4563,16 +3684,26 @@ const PLDDT_SCALE_LABEL = "pLDDT";
4563
3684
  const DEFAULT_OVERLAY_LABEL = "Value";
4564
3685
  /** Readout slot label used when an overlay does not name one. */
4565
3686
  const DEFAULT_READOUT_LABEL = "Value";
3687
+ const NO_SCALE = {
3688
+ scale: null,
3689
+ scaleLabel: void 0,
3690
+ scaleMax: null,
3691
+ scaleTooltip: void 0,
3692
+ scaleTooltipProps: void 0,
3693
+ valueLabel: void 0
3694
+ };
4566
3695
  /**
4567
3696
  * Chooses what the legend describes, following whatever is actually coloring
4568
- * the structure: the overlay when one is set, the pLDDT bands when scores were
4569
- * supplied, and nothing at all otherwise - Mol* falls back to chain coloring
4570
- * there, which no per-residue scale describes, so a key would be labelling
4571
- * colors that are not on screen.
4572
- */
4573
- function resolveScaleProps(overlay, hasPlddt) {
4574
- if (overlay) {
3697
+ * the structure: the overlay when it is painting, the pLDDT bands when they
3698
+ * are, and nothing at all for chain coloring - which no per-residue scale
3699
+ * describes, so a key would be labelling colors that are not on screen. An
3700
+ * overlay named by `colorBy` but not supplied paints everything neutral, and
3701
+ * has no key either.
3702
+ */
3703
+ function resolveScaleProps(colorBy, overlay) {
3704
+ if (colorBy === "overlay") {
4575
3705
  var _overlay$colorScale, _overlay$label, _overlay$min, _overlay$readoutLabel;
3706
+ if (!overlay) return NO_SCALE;
4576
3707
  return {
4577
3708
  scale: (_overlay$colorScale = overlay.colorScale) !== null && _overlay$colorScale !== void 0 ? _overlay$colorScale : PLASMA_COLOR_SCALE,
4578
3709
  scaleLabel: (_overlay$label = overlay.label) !== null && _overlay$label !== void 0 ? _overlay$label : DEFAULT_OVERLAY_LABEL,
@@ -4583,36 +3714,20 @@ function resolveScaleProps(overlay, hasPlddt) {
4583
3714
  valueLabel: (_overlay$readoutLabel = overlay.readoutLabel) !== null && _overlay$readoutLabel !== void 0 ? _overlay$readoutLabel : DEFAULT_READOUT_LABEL
4584
3715
  };
4585
3716
  }
4586
- if (hasPlddt) return {
3717
+ if (colorBy === "plddt") return _objectSpread2(_objectSpread2({}, NO_SCALE), {}, {
4587
3718
  scale: PLDDT_COLOR_SCALE,
4588
- scaleLabel: PLDDT_SCALE_LABEL,
4589
- scaleMax: null,
4590
- scaleTooltip: void 0,
4591
- scaleTooltipProps: void 0,
4592
- valueLabel: void 0
4593
- };
4594
- return {
4595
- scale: null,
4596
- scaleLabel: void 0,
4597
- scaleMax: null,
4598
- scaleTooltip: void 0,
4599
- scaleTooltipProps: void 0,
4600
- valueLabel: void 0
4601
- };
3719
+ scaleLabel: PLDDT_SCALE_LABEL
3720
+ });
3721
+ return NO_SCALE;
4602
3722
  }
4603
3723
  var ProteinStructureViewer_default = memo(forwardRef((props, ref) => {
4604
3724
  var _selection$chains;
4605
- const { backgroundColor, chainColors: chainColorOverrides, disableChainHighlightOnHover = false, download, hiddenChains: hiddenChainsProp, onChainVisibilityChange, onChainsChange, molstarSpec, onResidueClick, onResidueHover, onSelectionChange, structure, plddt, residueOverlay, selection: selectionProp, sequenceViewerBackgroundColor, showAxes = true, showChainLegend = true, showLegend = true, showSequenceViewer = true, stats } = props, rest = _objectWithoutProperties(props, _excluded);
3725
+ const { backgroundColor, chainColors: chainColorOverrides, colorBy: colorByProp, disableChainHighlightOnHover = false, download, hiddenChains: hiddenChainsProp, highlights, initialCamera, onCameraChange, onChainVisibilityChange, onChainsChange, molstarSpec, onDispose, onError, onReady, onResidueClick, onResidueHover, onSelectionChange, onStructureLoad, orientation, projection, representation = "cartoon", structure, plddt, residueOverlay, sceneMode = "managed", selection: selectionProp, sequenceViewerBackgroundColor, sequenceViewerHeight, showAxes = true, showChainLegend = true, showLegend = true, showSequenceViewer = true, stats } = props, rest = _objectWithoutProperties(props, _excluded);
4606
3726
  const theme = useTheme();
4607
3727
  const mode = getMode({ theme });
4608
3728
  const semanticColors = getSemanticColors({ theme });
4609
3729
  const pluginMountRef = useRef(null);
4610
- const hasPlddt = Boolean(plddt && plddt.length > 0);
4611
- const processedStructure = useMemo(() => hasPlddt ? injectPlddt(structure, plddt) : structure, [
4612
- structure,
4613
- plddt,
4614
- hasPlddt
4615
- ]);
3730
+ const colorBy = resolveColorBy(colorByProp, residueOverlay, plddt);
4616
3731
  const bgColor = useMemo(() => {
4617
3732
  var _semanticColors$base;
4618
3733
  return themeColor(backgroundColor !== null && backgroundColor !== void 0 ? backgroundColor : semanticColors === null || semanticColors === void 0 || (_semanticColors$base = semanticColors.base) === null || _semanticColors$base === void 0 ? void 0 : _semanticColors$base.backgroundPrimary, FALLBACK_BACKGROUND_COLOR[mode]);
@@ -4656,8 +3771,8 @@ var ProteinStructureViewer_default = memo(forwardRef((props, ref) => {
4656
3771
  * than undoing itself.
4657
3772
  */
4658
3773
  const handleChainSelect = useCallback((chainId) => {
4659
- var _selection$chains2, _selection$residues;
4660
- changeSelection((selection === null || selection === void 0 || (_selection$chains2 = selection.chains) === null || _selection$chains2 === void 0 ? void 0 : _selection$chains2.length) === 1 && selection.chains[0] === chainId && !((_selection$residues = selection.residues) === null || _selection$residues === void 0 ? void 0 : _selection$residues.length) ? null : { chains: [chainId] });
3774
+ var _selection$chains2, _selection$residues, _selection$addresses;
3775
+ changeSelection((selection === null || selection === void 0 || (_selection$chains2 = selection.chains) === null || _selection$chains2 === void 0 ? void 0 : _selection$chains2.length) === 1 && selection.chains[0] === chainId && !((_selection$residues = selection.residues) === null || _selection$residues === void 0 ? void 0 : _selection$residues.length) && !((_selection$addresses = selection.addresses) === null || _selection$addresses === void 0 ? void 0 : _selection$addresses.length) ? null : { chains: [chainId] });
4661
3776
  }, [changeSelection, selection]);
4662
3777
  /**
4663
3778
  * Dims every other chain while a chain's name is pointed at, in the legend
@@ -4681,13 +3796,11 @@ var ProteinStructureViewer_default = memo(forwardRef((props, ref) => {
4681
3796
  hiddenChains: hiddenChainsProp,
4682
3797
  onChainVisibilityChange
4683
3798
  });
4684
- const { chainColorThemeRef, chains: loadedChains, isReady, loadCount, pluginRef, residuesByChainRef, residueValueThemeRef, setClipRatio } = useMolstarPlugin({
3799
+ const { addressIndexRef, chains: loadedChains, framedOrientationRef, isReady, loadCount, pluginRef, residuesByChainRef, sceneRef, sceneVersion, setClipRatio } = useMolstarPlugin({
4685
3800
  backgroundColor: bgColor,
4686
- chainColors,
4687
3801
  containerRef: pluginMountRef,
4688
3802
  download,
4689
3803
  edgeColor,
4690
- hasPlddt,
4691
3804
  hiddenChains,
4692
3805
  highlightColor,
4693
3806
  mode,
@@ -4695,23 +3808,52 @@ var ProteinStructureViewer_default = memo(forwardRef((props, ref) => {
4695
3808
  onChainHover: highlightChain,
4696
3809
  onChainSelect: handleChainSelect,
4697
3810
  onChainToggle: toggleChain,
3811
+ onDispose,
3812
+ onError,
3813
+ onReady,
4698
3814
  onResidueClick,
4699
3815
  onResidueHover: handleResidueHover,
4700
3816
  onSelectionChange: changeSelection,
4701
3817
  onSelectionClear: handleSelectionClear,
3818
+ onStructureLoad,
3819
+ sceneProps: {
3820
+ chainColors: chainColorOverrides,
3821
+ colorBy: colorByProp,
3822
+ hiddenChains,
3823
+ highlights,
3824
+ initialCamera,
3825
+ mode,
3826
+ orientation,
3827
+ overlay: residueOverlay,
3828
+ plddt,
3829
+ projection,
3830
+ representation
3831
+ },
3832
+ sceneMode,
4702
3833
  selectedChains,
4703
3834
  sequenceViewerBackgroundColor,
4704
3835
  showAxes,
4705
3836
  showSequenceViewer,
4706
- structure: processedStructure
3837
+ structure
4707
3838
  });
4708
3839
  highlightChainRef.current = useChainHighlight({
4709
3840
  disabled: disableChainHighlightOnHover,
4710
3841
  hiddenChains,
4711
3842
  loadCount,
4712
3843
  pluginRef,
3844
+ sceneVersion,
4713
3845
  selectedChains
4714
3846
  });
3847
+ useCamera({
3848
+ framedOrientationRef,
3849
+ highlights,
3850
+ isReady,
3851
+ onCameraChange,
3852
+ orientation,
3853
+ pluginRef,
3854
+ projection,
3855
+ sceneRef
3856
+ });
4715
3857
  useEffect(() => {
4716
3858
  setChains(loadedChains);
4717
3859
  }, [loadedChains]);
@@ -4721,21 +3863,19 @@ var ProteinStructureViewer_default = memo(forwardRef((props, ref) => {
4721
3863
  var _onChainsChangeRef$cu;
4722
3864
  (_onChainsChangeRef$cu = onChainsChangeRef.current) === null || _onChainsChangeRef$cu === void 0 || _onChainsChangeRef$cu.call(onChainsChangeRef, loadedChains);
4723
3865
  }, [loadedChains]);
4724
- useStructureColoring({
4725
- chainColorThemeRef,
4726
- chainColors,
4727
- hasPlddt,
4728
- isReady,
4729
- mode,
4730
- overlay: residueOverlay,
4731
- pluginRef,
4732
- residueValueThemeRef
4733
- });
3866
+ const focusedSelection = useMemo(() => {
3867
+ var _addressIndexRef$curr;
3868
+ return resolveSelectionAddresses(selection, (_addressIndexRef$curr = addressIndexRef.current) !== null && _addressIndexRef$curr !== void 0 ? _addressIndexRef$curr : /* @__PURE__ */ new Map());
3869
+ }, [
3870
+ selection,
3871
+ residueAddressesKey(selection === null || selection === void 0 ? void 0 : selection.addresses),
3872
+ loadCount
3873
+ ]);
4734
3874
  useSelectionFocus({
4735
3875
  hiddenChains,
4736
3876
  isReady,
4737
3877
  pluginRef,
4738
- selection,
3878
+ selection: focusedSelection,
4739
3879
  setClipRatio
4740
3880
  });
4741
3881
  const { hoveredReadout, selectedReadout } = useSelectionReadout({
@@ -4745,9 +3885,9 @@ var ProteinStructureViewer_default = memo(forwardRef((props, ref) => {
4745
3885
  pluginRef,
4746
3886
  residueOverlay,
4747
3887
  residuesByChainRef,
4748
- selection
3888
+ selection: focusedSelection
4749
3889
  });
4750
- const scaleProps = useMemo(() => resolveScaleProps(residueOverlay, hasPlddt), [residueOverlay, hasPlddt]);
3890
+ const scaleProps = useMemo(() => resolveScaleProps(colorBy, residueOverlay), [colorBy, residueOverlay]);
4751
3891
  /**
4752
3892
  * What a chain's swatch can show follows whatever is painting the
4753
3893
  * structure, in the same order the color key resolves in.
@@ -4759,10 +3899,11 @@ var ProteinStructureViewer_default = memo(forwardRef((props, ref) => {
4759
3899
  * a swatch at all, so the rows keep their labels and toggles and the color
4760
3900
  * key beside them does the describing.
4761
3901
  */
4762
- const chainColoringActive = !residueOverlay && !hasPlddt;
4763
- const plddtColoringActive = !residueOverlay && hasPlddt;
3902
+ const chainColoringActive = colorBy === "chain";
3903
+ const plddtColoringActive = colorBy === "plddt";
4764
3904
  return /* @__PURE__ */ jsxs(ViewerRoot, _objectSpread2(_objectSpread2({
4765
3905
  ref,
3906
+ sequenceViewerHeight,
4766
3907
  showSequenceViewer
4767
3908
  }, rest), {}, { children: [/* @__PURE__ */ jsx(PluginMount, { ref: pluginMountRef }), showLegend && /* @__PURE__ */ jsx(StructureLegend, _objectSpread2({
4768
3909
  chainBandColors: plddtColoringActive ? PLDDT_BAND_COLORS : void 0,
@@ -4775,9 +3916,10 @@ var ProteinStructureViewer_default = memo(forwardRef((props, ref) => {
4775
3916
  onChainToggle: toggleChain,
4776
3917
  selectedChains,
4777
3918
  selectedResidue: selectedReadout,
3919
+ sequenceViewerHeight,
4778
3920
  showSequenceViewer,
4779
3921
  stats: stats !== null && stats !== void 0 ? stats : []
4780
3922
  }, scaleProps))] }));
4781
3923
  }));
4782
3924
  //#endregion
4783
- export { injectPlddtIntoMmcif as a, injectPlddt as i, DOWNLOAD_RESOLUTIONS as n, injectPlddtIntoPdb as o, PLDDT_COLOR_SCALE as r, detectStructureFormat as s, ProteinStructureViewer_default as t };
3925
+ export { DOWNLOAD_RESOLUTIONS as n, ProteinStructureViewer_default as t };