@czi-sds/data-viz 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs.js CHANGED
@@ -31,11 +31,14 @@ let react_jsx_runtime = require("react/jsx-runtime");
31
31
  let _czi_sds_components = require("@czi-sds/components");
32
32
  let _mui_material_styles = require("@mui/material/styles");
33
33
  let _phosphor_icons_react = require("@phosphor-icons/react");
34
+ let molstar_lib_mol_util_color = require("molstar/lib/mol-util/color");
35
+ let molstar_lib_mol_data_int = require("molstar/lib/mol-data/int");
34
36
  let molstar_lib_mol_model_structure = require("molstar/lib/mol-model/structure");
35
37
  let molstar_lib_mol_plugin_ui = require("molstar/lib/mol-plugin-ui");
36
38
  let molstar_lib_mol_plugin_ui_react18 = require("molstar/lib/mol-plugin-ui/react18");
37
39
  let molstar_lib_mol_plugin_ui_spec = require("molstar/lib/mol-plugin-ui/spec");
38
40
  let molstar_lib_mol_plugin_behavior = require("molstar/lib/mol-plugin/behavior");
41
+ let molstar_lib_mol_plugin_behavior_static_state = require("molstar/lib/mol-plugin/behavior/static/state");
39
42
  let molstar_lib_mol_plugin_config = require("molstar/lib/mol-plugin/config");
40
43
  let molstar_lib_mol_repr_representation = require("molstar/lib/mol-repr/representation");
41
44
  let rxjs = require("rxjs");
@@ -43,7 +46,6 @@ let molstar_lib_mol_model_loci = require("molstar/lib/mol-model/loci");
43
46
  let molstar_lib_mol_plugin_ui_base = require("molstar/lib/mol-plugin-ui/base");
44
47
  let molstar_lib_mol_util_input_input_observer = require("molstar/lib/mol-util/input/input-observer");
45
48
  let rxjs_operators = require("rxjs/operators");
46
- let molstar_lib_mol_data_int = require("molstar/lib/mol-data/int");
47
49
  let _mui_material = require("@mui/material");
48
50
  let molstar_lib_mol_plugin_state_objects = require("molstar/lib/mol-plugin-state/objects");
49
51
  let molstar_lib_mol_plugin_ui_sequence = require("molstar/lib/mol-plugin-ui/sequence");
@@ -54,8 +56,11 @@ let molstar_lib_mol_plugin_ui_toast = require("molstar/lib/mol-plugin-ui/toast")
54
56
  let molstar_lib_mol_plugin_ui_viewport = require("molstar/lib/mol-plugin-ui/viewport");
55
57
  let molstar_lib_mol_plugin_commands = require("molstar/lib/mol-plugin/commands");
56
58
  let molstar_lib_mol_math_linear_algebra = require("molstar/lib/mol-math/linear-algebra");
59
+ let molstar_lib_mol_model_structure_model_types = require("molstar/lib/mol-model/structure/model/types");
60
+ let molstar_lib_mol_theme_label = require("molstar/lib/mol-theme/label");
61
+ let molstar_lib_mol_script_language_builder = require("molstar/lib/mol-script/language/builder");
57
62
  let molstar_lib_mol_model_props_common_custom_element_property = require("molstar/lib/mol-model-props/common/custom-element-property");
58
- let molstar_lib_mol_util_color = require("molstar/lib/mol-util/color");
63
+ let molstar_lib_mol_plugin_behavior_dynamic_selection_structure_focus_representation = require("molstar/lib/mol-plugin/behavior/dynamic/selection/structure-focus-representation");
59
64
  //#region src/common/colorScales.ts
60
65
  /**
61
66
  * Perceptually uniform sequential scale (matplotlib's plasma), the default for
@@ -574,6 +579,191 @@ const HeatmapChart = (0, react.forwardRef)((props, ref) => {
574
579
  }
575
580
  });
576
581
  var HeatmapChart_default = (0, react.memo)(HeatmapChart);
582
+ //#endregion
583
+ //#region src/core/ProteinStructureViewer/components/ChainLegend/style.ts
584
+ /** Opacity applied to a hidden chain's row, so it reads as off but reachable. */
585
+ const HIDDEN_OPACITY = .4;
586
+ /** Side of the color swatch. */
587
+ const SWATCH_SIZE = 8;
588
+ /**
589
+ * The rows, stacked and right-aligned against the color key beside them.
590
+ *
591
+ * Pointer events are switched back on here: the legend overlay turns them off
592
+ * so drags meant for the structure are not intercepted, and this is one of the
593
+ * two places in it that has to be clickable.
594
+ */
595
+ const ChainRowList = (0, _emotion_styled.default)("div")`
596
+ display: flex;
597
+ flex-direction: column;
598
+ align-items: flex-end;
599
+ pointer-events: auto;
600
+ max-height: 80px;
601
+ overflow-y: auto;
602
+ `;
603
+ const ChainRow = (0, _emotion_styled.default)("div")`
604
+ display: flex;
605
+ align-items: center;
606
+
607
+ ${(props) => {
608
+ const spaces = (0, _czi_sds_components.getSpaces)(props);
609
+ return `
610
+ gap: ${spaces === null || spaces === void 0 ? void 0 : spaces.xs}px;
611
+ `;
612
+ }}
613
+ `;
614
+ const ChainSwatch = (0, _emotion_styled.default)("div")`
615
+ flex: none;
616
+ width: ${SWATCH_SIZE}px;
617
+ height: ${SWATCH_SIZE}px;
618
+ border-radius: 1px;
619
+
620
+ ${(props) => `
621
+ background-color: ${props.swatchColor};
622
+ opacity: ${props.isHidden ? HIDDEN_OPACITY : 1};
623
+ `}
624
+ `;
625
+ /**
626
+ * A swatch quartered into a grid, for a coloring no single color stands for.
627
+ *
628
+ * Occupies the same box as the solid swatch so the rows stay aligned whichever
629
+ * one a chain gets, with a hairline gap between the quadrants to read as four
630
+ * colors rather than one blurred square.
631
+ */
632
+ const ChainSwatchGrid = (0, _emotion_styled.default)("div")`
633
+ flex: none;
634
+ display: grid;
635
+ grid-template-columns: repeat(2, 1fr);
636
+ grid-template-rows: repeat(2, 1fr);
637
+ gap: 0px;
638
+ width: ${SWATCH_SIZE}px;
639
+ height: ${SWATCH_SIZE}px;
640
+ border-radius: 1px;
641
+ overflow: hidden;
642
+
643
+ ${(props) => `
644
+ opacity: ${props.isHidden ? HIDDEN_OPACITY : 1};
645
+ `}
646
+ `;
647
+ const ChainSwatchQuadrant = (0, _emotion_styled.default)("div")`
648
+ ${(props) => `
649
+ background-color: ${props.swatchColor};
650
+ `}
651
+ `;
652
+ /**
653
+ * Selects the whole chain, or clears it when it is already the selection. A
654
+ * button so it is reachable from the keyboard.
655
+ */
656
+ const ChainLabel$1 = (0, _emotion_styled.default)("button")`
657
+ ${_czi_sds_components.fontBodyXxxs}
658
+
659
+ padding: 0;
660
+ border: none;
661
+ background: none;
662
+
663
+ ${(props) => {
664
+ var _semanticColors$base, _semanticColors$base2, _semanticColors$base3;
665
+ const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
666
+ return `
667
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base = semanticColors.base) === null || _semanticColors$base === void 0 ? void 0 : _semanticColors$base.textSecondary};
668
+ cursor: ${props.isHidden ? "default" : "pointer"};
669
+ font-weight: ${props.isSelected ? 600 : "inherit"};
670
+ opacity: ${props.isHidden ? HIDDEN_OPACITY : 1};
671
+
672
+ &:hover {
673
+ color: ${props.isHidden ? semanticColors === null || semanticColors === void 0 || (_semanticColors$base2 = semanticColors.base) === null || _semanticColors$base2 === void 0 ? void 0 : _semanticColors$base2.textSecondary : semanticColors === null || semanticColors === void 0 || (_semanticColors$base3 = semanticColors.base) === null || _semanticColors$base3 === void 0 ? void 0 : _semanticColors$base3.textPrimary};
674
+ }
675
+ `;
676
+ }}
677
+ `;
678
+ const VisibilityToggle = (0, _emotion_styled.default)("button")`
679
+ display: flex;
680
+ align-items: center;
681
+ padding: 0;
682
+ border: none;
683
+ background: none;
684
+ cursor: pointer;
685
+ margin-top: 1px;
686
+
687
+ ${(props) => {
688
+ var _semanticColors$base4, _semanticColors$base5;
689
+ const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
690
+ return `
691
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base4 = semanticColors.base) === null || _semanticColors$base4 === void 0 ? void 0 : _semanticColors$base4.textTertiaryOnDark};
692
+
693
+ &:hover {
694
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base5 = semanticColors.base) === null || _semanticColors$base5 === void 0 ? void 0 : _semanticColors$base5.textPrimary};
695
+ }
696
+
697
+ svg {
698
+ display: block;
699
+ }
700
+ `;
701
+ }}
702
+ `;
703
+ //#endregion
704
+ //#region src/core/ProteinStructureViewer/components/ChainLegend/index.tsx
705
+ /**
706
+ * Lists the structure's chains, each with its color and a visibility toggle.
707
+ *
708
+ * Only drawn for a complex: one chain needs no key to tell it from anything,
709
+ * and hiding it would leave an empty canvas with no way back. Its home is the
710
+ * legend rather than the sequence panel so that it survives
711
+ * `showSequenceViewer={false}`, where the chains would otherwise have nowhere
712
+ * to be listed.
713
+ */
714
+ function ChainLegend({ bandColors, chainColors, chains, hiddenChains, onChainHover, onChainSelect, onChainToggle, selectedChains }) {
715
+ if (chains.length < 2) return null;
716
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChainRowList, { children: chains.map((chain) => {
717
+ var _selectedChains$has;
718
+ const hidden = hiddenChains.has(chain.chainId);
719
+ const selected = (_selectedChains$has = selectedChains === null || selectedChains === void 0 ? void 0 : selectedChains.has(chain.chainId)) !== null && _selectedChains$has !== void 0 ? _selectedChains$has : false;
720
+ const color = chainColors === null || chainColors === void 0 ? void 0 : chainColors.get(chain.chainId);
721
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ChainRow, {
722
+ onBlur: () => onChainHover === null || onChainHover === void 0 ? void 0 : onChainHover(null),
723
+ onFocus: () => onChainHover === null || onChainHover === void 0 ? void 0 : onChainHover(chain.chainId),
724
+ onMouseEnter: () => onChainHover === null || onChainHover === void 0 ? void 0 : onChainHover(chain.chainId),
725
+ onMouseLeave: () => onChainHover === null || onChainHover === void 0 ? void 0 : onChainHover(null),
726
+ children: [
727
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_czi_sds_components.Tooltip, {
728
+ arrow: true,
729
+ placement: "top",
730
+ disableInteractive: true,
731
+ title: hidden ? `Chain ${chain.label} is hidden` : `Click to ${selected ? "deselect" : "select"} Chain ${chain.label}`,
732
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ChainLabel$1, {
733
+ "aria-pressed": selected,
734
+ disabled: hidden,
735
+ isHidden: hidden,
736
+ isSelected: selected,
737
+ onClick: () => onChainSelect(chain.chainId),
738
+ type: "button",
739
+ children: ["Chain ", chain.label]
740
+ })
741
+ }),
742
+ color !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChainSwatch, {
743
+ isHidden: hidden,
744
+ swatchColor: color
745
+ }),
746
+ color === void 0 && bandColors !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChainSwatchGrid, {
747
+ isHidden: hidden,
748
+ children: bandColors.map((band) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChainSwatchQuadrant, { swatchColor: band }, band))
749
+ }),
750
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_czi_sds_components.Tooltip, {
751
+ arrow: true,
752
+ placement: "top",
753
+ disableInteractive: true,
754
+ title: hidden ? `Show Chain ${chain.label}` : `Hide Chain ${chain.label}`,
755
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VisibilityToggle, {
756
+ "aria-label": `${hidden ? "Show" : "Hide"} chain ${chain.label}`,
757
+ "aria-pressed": hidden,
758
+ onClick: () => onChainToggle(chain.chainId),
759
+ type: "button",
760
+ children: hidden ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_phosphor_icons_react.EyeSlashIcon, { size: 12 }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_phosphor_icons_react.EyeIcon, { size: 12 })
761
+ })
762
+ })
763
+ ]
764
+ }, chain.chainId);
765
+ }) });
766
+ }
577
767
  /** Height of the color bar itself. */
578
768
  const BAR_HEIGHT = 8;
579
769
  /**
@@ -698,14 +888,24 @@ const ViewerRoot = (0, _emotion_styled.default)("div")`
698
888
  height: 100% !important;
699
889
  }
700
890
 
701
- /* Mol* chrome that the custom viewport and sequence panel replace. */
702
- .msp-plugin .msp-viewport-controls-buttons,
703
- .msp-plugin .msp-layout-expanded,
704
- .msp-plugin .msp-log,
705
- .msp-plugin .msp-left-panel-controls,
706
- .msp-plugin .msp-layout-region.msp-layout-left,
707
- .msp-plugin .msp-layout-region.msp-layout-right,
708
- .msp-plugin .msp-layout-region.msp-layout-bottom {
891
+ /*
892
+ * The expanded layout is kept hidden because it is the one piece of Mol*
893
+ * chrome that would escape this container: it takes the plugin out of the
894
+ * box it was given and over the page.
895
+ *
896
+ * Its neighbours used to be here too - the viewport's icon column, and the
897
+ * left, right and bottom regions. They are not any more, because hiding them
898
+ * from here made them unreachable: Mol* already gates each of them behind a
899
+ * PluginConfig.Viewport flag and behind layout.initial.regionState, both of
900
+ * which a consumer can set through molstarSpec, and a display:none on top of
901
+ * that overrode the answer. None of them shows by default - the viewer's own
902
+ * flags and region state are what keep them away - so this only has to stay
903
+ * out of their way.
904
+ *
905
+ * No backticks in here: this is inside a template literal, and one would end
906
+ * it.
907
+ */
908
+ .msp-plugin .msp-layout-expanded {
709
909
  display: none !important;
710
910
  }
711
911
 
@@ -856,6 +1056,18 @@ const StatsGrid = (0, _emotion_styled.default)("div")`
856
1056
  `;
857
1057
  }}
858
1058
  `;
1059
+ const ChainScaleColumn = (0, _emotion_styled.default)("div")`
1060
+ display: flex;
1061
+ flex-direction: column;
1062
+ align-items: flex-end;
1063
+
1064
+ ${(props) => {
1065
+ const spaces = (0, _czi_sds_components.getSpaces)(props);
1066
+ return `
1067
+ gap: ${spaces === null || spaces === void 0 ? void 0 : spaces.l}px;
1068
+ `;
1069
+ }}
1070
+ `;
859
1071
  const StatColumnWrapper = (0, _emotion_styled.default)("div")`
860
1072
  display: flex;
861
1073
  flex-direction: column;
@@ -893,18 +1105,22 @@ const ScaleColumn = (0, _emotion_styled.default)("div")`
893
1105
  `;
894
1106
  }}
895
1107
  `;
1108
+ /**
1109
+ * A wrapping text line, not a flex row: the help icon has to sit at the end of
1110
+ * the last line of the caption rather than beside the whole wrapped block.
1111
+ */
896
1112
  const ScaleCaption = (0, _emotion_styled.default)("span")`
897
1113
  ${_czi_sds_components.fontBodyXxxs}
898
1114
 
1115
+ display: block;
899
1116
  text-align: left;
900
- display: flex;
901
- align-items: center;
902
1117
 
903
1118
  ${(props) => {
904
1119
  var _semanticColors$base3;
905
1120
  const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
906
1121
  return `
907
1122
  color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base3 = semanticColors.base) === null || _semanticColors$base3 === void 0 ? void 0 : _semanticColors$base3.textTertiaryOnDark};
1123
+ max-width: 142px;
908
1124
  `;
909
1125
  }}
910
1126
  `;
@@ -917,12 +1133,18 @@ const TooltipAnchor = (0, _emotion_styled.default)("span")`
917
1133
  const spaces = (0, _czi_sds_components.getSpaces)(props);
918
1134
  const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
919
1135
  return `
1136
+ display: inline-block;
920
1137
  padding-left: ${spaces === null || spaces === void 0 ? void 0 : spaces.xxxs}px;
921
- margin-bottom: -${spaces === null || spaces === void 0 ? void 0 : spaces.xxxs}px;
1138
+ margin-bottom: 1px;
1139
+ vertical-align: text-bottom;
922
1140
 
923
1141
  &:hover {
924
1142
  color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base4 = semanticColors.base) === null || _semanticColors$base4 === void 0 ? void 0 : _semanticColors$base4.ornamentSecondary};
925
1143
  }
1144
+
1145
+ svg {
1146
+ display: block;
1147
+ }
926
1148
  `;
927
1149
  }}
928
1150
  `;
@@ -938,6 +1160,19 @@ function StatColumn({ label, value }) {
938
1160
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(StatColumnWrapper, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatValue, { children: value }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatLabel, { children: label })] });
939
1161
  }
940
1162
  /**
1163
+ * Builds one readout slot.
1164
+ *
1165
+ * A readout covering several residues reports a mean, and the label says so:
1166
+ * calling it "pLDDT" while it averages ninety of them would read as a single
1167
+ * residue's score.
1168
+ */
1169
+ function readoutSlot(label, value, residueCount) {
1170
+ return {
1171
+ label: residueCount > 1 ? `Mean ${label}` : label,
1172
+ value: value !== null ? value.toFixed(VALUE_PRECISION) : MISSING_VALUE
1173
+ };
1174
+ }
1175
+ /**
941
1176
  * Stats and color key overlaid on the structure viewer.
942
1177
  *
943
1178
  * When a residue is selected or hovered, the whole-structure stats are replaced
@@ -947,24 +1182,19 @@ function StatColumn({ label, value }) {
947
1182
  * residue takes priority so the user can inspect others without losing their
948
1183
  * selection.
949
1184
  */
950
- function StructureLegend({ hoveredResidue = null, scale, scaleLabel, scaleMax = null, scaleMin = 0, scaleTooltip, selectedResidue = null, showSequenceViewer, stats, valueLabel }) {
951
- var _stats$, _stats$2, _stats$3;
1185
+ 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 }) {
1186
+ var _activeResidue$residu, _stats$, _stats$2, _stats$3;
952
1187
  const activeResidue = hoveredResidue !== null && hoveredResidue !== void 0 ? hoveredResidue : selectedResidue;
953
1188
  const showResidue = activeResidue !== null;
954
1189
  const showValue = showResidue && valueLabel !== void 0;
1190
+ const residueCount = (_activeResidue$residu = activeResidue === null || activeResidue === void 0 ? void 0 : activeResidue.residueCount) !== null && _activeResidue$residu !== void 0 ? _activeResidue$residu : 1;
955
1191
  const slots = [
956
1192
  showResidue ? {
957
- label: "Residue",
1193
+ label: residueCount > 1 ? "Selection" : "Residue",
958
1194
  value: activeResidue.label
959
1195
  } : (_stats$ = stats[0]) !== null && _stats$ !== void 0 ? _stats$ : null,
960
- showValue ? {
961
- label: valueLabel,
962
- value: activeResidue.value !== null ? activeResidue.value.toFixed(VALUE_PRECISION) : MISSING_VALUE
963
- } : (_stats$2 = stats[1]) !== null && _stats$2 !== void 0 ? _stats$2 : null,
964
- showResidue ? {
965
- label: "pLDDT",
966
- value: activeResidue.plddt !== null ? activeResidue.plddt.toFixed(VALUE_PRECISION) : MISSING_VALUE
967
- } : (_stats$3 = stats[2]) !== null && _stats$3 !== void 0 ? _stats$3 : null
1196
+ showValue ? readoutSlot(valueLabel, activeResidue.value, residueCount) : (_stats$2 = stats[1]) !== null && _stats$2 !== void 0 ? _stats$2 : null,
1197
+ showResidue ? readoutSlot("pLDDT", activeResidue.plddt, residueCount) : (_stats$3 = stats[2]) !== null && _stats$3 !== void 0 ? _stats$3 : null
968
1198
  ];
969
1199
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(LegendOverlay, {
970
1200
  showSequenceViewer,
@@ -975,20 +1205,369 @@ function StructureLegend({ hoveredResidue = null, scale, scaleLabel, scaleMax =
975
1205
  label: slot.label,
976
1206
  value: slot.value
977
1207
  }, i) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {}, i);
978
- }) }), scale && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ScaleColumn, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ColorScaleLegend, {
1208
+ }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ChainScaleColumn, { children: [onChainToggle && onChainSelect && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChainLegend, {
1209
+ bandColors: chainBandColors,
1210
+ chainColors,
1211
+ chains,
1212
+ hiddenChains: hiddenChains !== null && hiddenChains !== void 0 ? hiddenChains : /* @__PURE__ */ new Set(),
1213
+ onChainHover,
1214
+ onChainSelect,
1215
+ onChainToggle,
1216
+ selectedChains
1217
+ }), scale && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ScaleColumn, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ColorScaleLegend, {
979
1218
  max: scaleMax,
980
1219
  min: scaleMin,
981
1220
  scale
982
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ScaleCaption, { children: [scaleLabel, scaleTooltip !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_czi_sds_components.Tooltip, {
1221
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ScaleCaption, { children: [scaleLabel, (scaleTooltip !== void 0 || scaleTooltipProps !== void 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_czi_sds_components.Tooltip, _objectSpread2(_objectSpread2({
983
1222
  arrow: true,
984
- placement: "bottom",
985
- title: scaleTooltip,
1223
+ placement: "bottom"
1224
+ }, scaleTooltipProps), {}, {
1225
+ title: scaleTooltip !== null && scaleTooltip !== void 0 ? scaleTooltip : scaleTooltipProps === null || scaleTooltipProps === void 0 ? void 0 : scaleTooltipProps.title,
986
1226
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipAnchor, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_phosphor_icons_react.QuestionIcon, {
987
1227
  size: 10,
988
1228
  weight: "bold"
989
1229
  }) })
990
- })] })] })]
1230
+ }))] })] })] })]
1231
+ });
1232
+ }
1233
+ //#endregion
1234
+ //#region src/core/ProteinStructureViewer/utils/selection.ts
1235
+ /**
1236
+ * A stable string for a selection, for comparing one against another.
1237
+ *
1238
+ * A consumer holding the selection in state hands back a new object on every
1239
+ * render, so identity says nothing about whether the selection changed. Order
1240
+ * is normalized too: `{ residues: [2, 1] }` and `{ residues: [1, 2] }` select
1241
+ * the same residues and must not read as a change.
1242
+ */
1243
+ function selectionKey(selection) {
1244
+ var _selection$residues, _selection$chains;
1245
+ const residues = [...new Set((_selection$residues = selection.residues) !== null && _selection$residues !== void 0 ? _selection$residues : [])].sort((a, b) => a - b);
1246
+ return `${[...new Set((_selection$chains = selection.chains) !== null && _selection$chains !== void 0 ? _selection$chains : [])].sort().join(",")}|${residues.join(",")}`;
1247
+ }
1248
+ /** True when the selection names nothing at all. */
1249
+ function isEmptySelection(selection) {
1250
+ var _selection$residues2, _selection$chains2;
1251
+ if (!selection) return true;
1252
+ return !((_selection$residues2 = selection.residues) === null || _selection$residues2 === void 0 ? void 0 : _selection$residues2.length) && !((_selection$chains2 = selection.chains) === null || _selection$chains2 === void 0 ? void 0 : _selection$chains2.length);
1253
+ }
1254
+ /**
1255
+ * The residues a selection covers, ascending and without repeats.
1256
+ *
1257
+ * Chains are expanded through `residuesByChain`, which was read off the loaded
1258
+ * structure, so a chain contributes exactly the residues it holds rather than
1259
+ * the span between its ends. A residue named directly and also carried by a
1260
+ * selected chain is counted once.
1261
+ */
1262
+ function selectionResidues(selection, residuesByChain) {
1263
+ var _selection$residues3, _selection$chains3;
1264
+ const residues = new Set((_selection$residues3 = selection.residues) !== null && _selection$residues3 !== void 0 ? _selection$residues3 : []);
1265
+ for (const chainId of (_selection$chains3 = selection.chains) !== null && _selection$chains3 !== void 0 ? _selection$chains3 : []) {
1266
+ var _residuesByChain$get;
1267
+ for (const index of (_residuesByChain$get = residuesByChain.get(chainId)) !== null && _residuesByChain$get !== void 0 ? _residuesByChain$get : []) residues.add(index);
1268
+ }
1269
+ return [...residues].sort((a, b) => a - b);
1270
+ }
1271
+ /**
1272
+ * Names a selection for the legend's readout.
1273
+ *
1274
+ * A single chain is named as one, since "Chain A" says more than the number of
1275
+ * residues it happens to hold. Anything else is reported as a count, which is
1276
+ * the only thing true of every shape a selection can take. A lone residue is
1277
+ * named by the caller instead, which reads it back off the structure to get its
1278
+ * three-letter code and the number the file gives it.
1279
+ */
1280
+ function selectionLabel(selection, residueCount) {
1281
+ var _selection$chains4, _selection$residues4;
1282
+ const chains = (_selection$chains4 = selection.chains) !== null && _selection$chains4 !== void 0 ? _selection$chains4 : [];
1283
+ if (chains.length === 1 && !((_selection$residues4 = selection.residues) === null || _selection$residues4 === void 0 ? void 0 : _selection$residues4.length)) return `Chain ${chains[0]}`;
1284
+ return `${residueCount} residues`;
1285
+ }
1286
+ /** The mean of the values `read` returns, ignoring residues without one. */
1287
+ function meanOverResidues(residues, read) {
1288
+ let sum = 0;
1289
+ let count = 0;
1290
+ for (const index of residues) {
1291
+ const value = read(index);
1292
+ if (value === null || value === void 0) continue;
1293
+ sum += value;
1294
+ count++;
1295
+ }
1296
+ return count === 0 ? null : sum / count;
1297
+ }
1298
+ //#endregion
1299
+ //#region src/core/ProteinStructureViewer/utils/residueLoci.ts
1300
+ /**
1301
+ * Every atom the predicate accepts, as geometry Mol* can focus, frame and mark,
1302
+ * or undefined when it accepts none.
1303
+ *
1304
+ * One pass over the structure rather than a pass per residue: a whole-chain
1305
+ * selection names hundreds of residues, and resolving them one at a time would
1306
+ * walk the structure once for each.
1307
+ *
1308
+ * Atoms are bucketed by the unit they belong to because element indices are
1309
+ * unit-local. That bucketing is also what lets a result span a chain break: it
1310
+ * carries an entry per unit, which is what a range across two chains needs and
1311
+ * what a single unit-local entry cannot express.
1312
+ *
1313
+ * Whole residues are collected rather than one atom each, since the caller
1314
+ * frames the camera on the loci's bounding sphere and a lone atom would zoom
1315
+ * far closer than the residue warrants.
1316
+ */
1317
+ function lociMatching(structure, matches) {
1318
+ const location = molstar_lib_mol_model_structure.StructureElement.Location.create(structure);
1319
+ const elements = [];
1320
+ for (const unit of structure.units) {
1321
+ location.unit = unit;
1322
+ const indices = [];
1323
+ for (let i = 0; i < unit.elements.length; i++) {
1324
+ location.element = unit.elements[i];
1325
+ if (matches(location)) indices.push(i);
1326
+ }
1327
+ if (indices.length > 0) elements.push({
1328
+ indices,
1329
+ unit
1330
+ });
1331
+ }
1332
+ if (elements.length === 0) return void 0;
1333
+ return molstar_lib_mol_model_structure.StructureElement.Loci(structure, elements.map(({ indices, unit }) => ({
1334
+ indices: molstar_lib_mol_data_int.OrderedSet.ofSortedArray(indices),
1335
+ unit
1336
+ })));
1337
+ }
1338
+ /**
1339
+ * Every atom of the residues a selection covers, within one structure.
1340
+ *
1341
+ * `hiddenChains` is subtracted from the result. A hidden chain is not drawn, so
1342
+ * geometry built from it would have nothing to sit on: Mol*'s focus
1343
+ * representation draws its own ball-and-stick from whatever is focused, in a
1344
+ * part of the state tree the chain's own visibility does not reach, and a
1345
+ * hidden chain left in the loci would keep showing up there after its cartoon
1346
+ * had gone.
1347
+ */
1348
+ function lociForSelectionInStructure(structure, selection, hiddenChains) {
1349
+ var _selection$residues, _selection$chains;
1350
+ if (isEmptySelection(selection)) return void 0;
1351
+ const residues = new Set((_selection$residues = selection.residues) !== null && _selection$residues !== void 0 ? _selection$residues : []);
1352
+ const chains = new Set((_selection$chains = selection.chains) !== null && _selection$chains !== void 0 ? _selection$chains : []);
1353
+ return lociMatching(structure, (location) => {
1354
+ const chainId = molstar_lib_mol_model_structure.StructureProperties.chain.auth_asym_id(location);
1355
+ if (hiddenChains === null || hiddenChains === void 0 ? void 0 : hiddenChains.has(chainId)) return false;
1356
+ return residues.has(molstar_lib_mol_model_structure.StructureProperties.residue.key(location)) || chains.has(chainId);
1357
+ });
1358
+ }
1359
+ /**
1360
+ * Every atom of one chain's polymer, in the first structure that has one.
1361
+ *
1362
+ * The polymer alone, so pointing at a chain's name lights up the cartoon the
1363
+ * name is captioning. The ligands sitting on the chain belong to it but are
1364
+ * not what it is named for, and they are drawn by a representation of their
1365
+ * own.
1366
+ */
1367
+ function lociForChainPolymer(plugin, chainId) {
1368
+ for (const entry of plugin.managers.structure.hierarchy.current.structures) {
1369
+ var _entry$cell$obj;
1370
+ const structure = (_entry$cell$obj = entry.cell.obj) === null || _entry$cell$obj === void 0 ? void 0 : _entry$cell$obj.data;
1371
+ if (!structure) continue;
1372
+ const loci = lociMatching(structure, (location) => molstar_lib_mol_model_structure.StructureProperties.chain.auth_asym_id(location) === chainId && molstar_lib_mol_model_structure.StructureProperties.entity.type(location) === "polymer");
1373
+ if (loci) return loci;
1374
+ }
1375
+ }
1376
+ /**
1377
+ * Every atom of the residues between two 0-based indices, inclusive.
1378
+ *
1379
+ * A contiguous span is tested by comparison rather than by expanding it into a
1380
+ * set of indices: this runs on every residue a drag crosses, and a drag can
1381
+ * span the whole structure.
1382
+ */
1383
+ function lociForResidueRange(structure, min, max) {
1384
+ return lociMatching(structure, (location) => {
1385
+ const index = molstar_lib_mol_model_structure.StructureProperties.residue.key(location);
1386
+ return index >= min && index <= max;
1387
+ });
1388
+ }
1389
+ /**
1390
+ * The same, across every structure loaded into a plugin. Results are merged
1391
+ * into the first structure that matched: a loci belongs to one structure, and
1392
+ * the residue index space already spans them as though they were one.
1393
+ */
1394
+ function lociForSelection(plugin, selection, hiddenChains) {
1395
+ for (const entry of plugin.managers.structure.hierarchy.current.structures) {
1396
+ var _entry$cell$obj2;
1397
+ const structure = (_entry$cell$obj2 = entry.cell.obj) === null || _entry$cell$obj2 === void 0 ? void 0 : _entry$cell$obj2.data;
1398
+ if (!structure) continue;
1399
+ const loci = lociForSelectionInStructure(structure, selection, hiddenChains);
1400
+ if (loci) return loci;
1401
+ }
1402
+ }
1403
+ //#endregion
1404
+ //#region src/core/ProteinStructureViewer/hooks/useChainHighlight.ts
1405
+ /**
1406
+ * What a chain lights up in while its name is under the pointer.
1407
+ *
1408
+ * Its own color rather than the theme's hover tint, because it answers a
1409
+ * different question. Hovering a residue asks "what is this?" and a tint is
1410
+ * enough; pointing at a chain's name asks "where is this chain?" of a
1411
+ * structure where the answer may be half the screen, and has to carry across
1412
+ * a cartoon already painted in the chain palette, in pLDDT bands, or in an
1413
+ * overlay. A saturated pink is in none of those, so it cannot be mistaken for
1414
+ * the structure's own coloring.
1415
+ */
1416
+ const CHAIN_HIGHLIGHT_COLOR = molstar_lib_mol_util_color.Color.fromRgb(200, 45, 149);
1417
+ /**
1418
+ * Stronger than the residue hover's tint, for the same reason. A whole chain
1419
+ * washed in 20% pink reads as a rendering artifact; at this strength it reads
1420
+ * as deliberate.
1421
+ */
1422
+ const CHAIN_HIGHLIGHT_STRENGTH = .7;
1423
+ /**
1424
+ * Lights up a whole chain in the 3D view while its name is pointed at, in the
1425
+ * sequence panel's captions or the chain legend's rows.
1426
+ *
1427
+ * Mol*'s marking system is what draws it, so the chain is tinted and outlined
1428
+ * exactly as a hovered residue is, with no geometry added to the state tree
1429
+ * and nothing to tear down. Marking colors are a property of the renderer
1430
+ * rather than of the loci, though, so the pink is swapped in for the length of
1431
+ * the hover and the theme's own colors handed back on the way out.
1432
+ *
1433
+ * Those colors are read off the canvas at that moment rather than passed in.
1434
+ * They are already derived from the theme in two places, and a third copy kept
1435
+ * in step by hand would be the one to drift.
1436
+ */
1437
+ function useChainHighlight({ disabled, pluginRef }) {
1438
+ const borrowedRef = (0, react.useRef)(null);
1439
+ const highlightChain = (0, react.useCallback)((name) => {
1440
+ var _borrowedRef$current;
1441
+ const plugin = pluginRef.current;
1442
+ const canvas3d = plugin === null || plugin === void 0 ? void 0 : plugin.canvas3d;
1443
+ if (!plugin || !canvas3d) return;
1444
+ const chainId = disabled ? null : name;
1445
+ if (chainId === null) {
1446
+ plugin.managers.interactivity.lociHighlights.clearHighlights();
1447
+ const borrowed = borrowedRef.current;
1448
+ if (borrowed) {
1449
+ canvas3d.setProps({
1450
+ marking: { highlightEdgeColor: borrowed.highlightEdgeColor },
1451
+ renderer: {
1452
+ highlightColor: borrowed.highlightColor,
1453
+ highlightStrength: borrowed.highlightStrength
1454
+ }
1455
+ });
1456
+ borrowedRef.current = null;
1457
+ }
1458
+ return;
1459
+ }
1460
+ const loci = lociForChainPolymer(plugin, chainId);
1461
+ if (!loci) return;
1462
+ (_borrowedRef$current = borrowedRef.current) !== null && _borrowedRef$current !== void 0 || (borrowedRef.current = {
1463
+ highlightColor: canvas3d.props.renderer.highlightColor,
1464
+ highlightEdgeColor: canvas3d.props.marking.highlightEdgeColor,
1465
+ highlightStrength: canvas3d.props.renderer.highlightStrength
1466
+ });
1467
+ canvas3d.setProps({
1468
+ marking: { highlightEdgeColor: CHAIN_HIGHLIGHT_COLOR },
1469
+ renderer: {
1470
+ highlightColor: CHAIN_HIGHLIGHT_COLOR,
1471
+ highlightStrength: CHAIN_HIGHLIGHT_STRENGTH
1472
+ }
1473
+ });
1474
+ plugin.managers.interactivity.lociHighlights.highlightOnly({ loci });
1475
+ }, [disabled, pluginRef]);
1476
+ (0, react.useEffect)(() => () => void (borrowedRef.current = null), []);
1477
+ return highlightChain;
1478
+ }
1479
+ //#endregion
1480
+ //#region src/common/chainColors.ts
1481
+ /**
1482
+ * Default colors for telling chains apart, in assignment order.
1483
+ *
1484
+ * The Okabe-Ito qualitative palette, which is built to stay distinguishable
1485
+ * under every common form of color blindness - the reason to prefer it over a
1486
+ * hand-picked set, since a complex is read by which chain is which and a
1487
+ * viewer that collapses two of them into one color stops answering that.
1488
+ *
1489
+ * Its black is left out: the canvas inverts with the theme, so a chain painted
1490
+ * black would vanish against the dark background. The rest carry enough
1491
+ * lightness to hold up against both.
1492
+ *
1493
+ * Chains past the end of the palette wrap around to the start.
1494
+ */
1495
+ const CHAIN_COLOR_PALETTE = [
1496
+ "#009E73",
1497
+ "#0072B2",
1498
+ "#E69F00",
1499
+ "#CC79A7",
1500
+ "#56B4E9",
1501
+ "#D55E00",
1502
+ "#F0E442"
1503
+ ];
1504
+ /**
1505
+ * Assigns each chain a color, in the order given, with `overrides` taking
1506
+ * precedence by `chainId`.
1507
+ *
1508
+ * Palette positions are spent on every chain whether or not it is overridden,
1509
+ * so overriding one chain does not shift the colors of the others.
1510
+ */
1511
+ function chainColorMap(chainIds, overrides) {
1512
+ const colors = /* @__PURE__ */ new Map();
1513
+ chainIds.forEach((chainId, i) => {
1514
+ var _overrides$chainId;
1515
+ const fallback = CHAIN_COLOR_PALETTE[i % CHAIN_COLOR_PALETTE.length];
1516
+ colors.set(chainId, (_overrides$chainId = overrides === null || overrides === void 0 ? void 0 : overrides[chainId]) !== null && _overrides$chainId !== void 0 ? _overrides$chainId : fallback);
991
1517
  });
1518
+ return colors;
1519
+ }
1520
+ //#endregion
1521
+ //#region src/core/ProteinStructureViewer/hooks/useChains.ts
1522
+ /**
1523
+ * Reconciles the chains found in the structure with the visibility and coloring
1524
+ * the consumer asked for.
1525
+ *
1526
+ * Visibility is controlled when `hiddenChains` is passed and owned here when it
1527
+ * is not, so the legend's toggles work out of the box without taking the
1528
+ * decision away from a consumer that wants it. `onChainVisibilityChange` fires
1529
+ * either way: a consumer that only wants to follow along should not have to
1530
+ * take ownership to hear about it.
1531
+ */
1532
+ function useChains({ chainColors, chains, hiddenChains, onChainVisibilityChange }) {
1533
+ const isControlled = hiddenChains !== void 0;
1534
+ const [ownHidden, setOwnHidden] = (0, react.useState)([]);
1535
+ const onChainVisibilityChangeRef = (0, react.useRef)(onChainVisibilityChange);
1536
+ onChainVisibilityChangeRef.current = onChainVisibilityChange;
1537
+ const chainIds = (0, react.useMemo)(() => chains.map((c) => c.chainId), [chains]);
1538
+ /**
1539
+ * Chains the structure no longer has cannot be unhidden through the legend,
1540
+ * since it has no row for them, so a stale id would hide a chain that shares
1541
+ * its name in the next structure. Only the uncontrolled side is pruned; a
1542
+ * controlled consumer owns its own list.
1543
+ */
1544
+ (0, react.useEffect)(() => {
1545
+ if (isControlled) return;
1546
+ setOwnHidden((prev) => {
1547
+ const kept = prev.filter((id) => chainIds.includes(id));
1548
+ return kept.length === prev.length ? prev : kept;
1549
+ });
1550
+ }, [chainIds, isControlled]);
1551
+ const hiddenKey = [...isControlled ? hiddenChains : ownHidden].sort().join("\0");
1552
+ const hidden = (0, react.useMemo)(() => new Set(hiddenKey === "" ? [] : hiddenKey.split("\0")), [hiddenKey]);
1553
+ return {
1554
+ colors: (0, react.useMemo)(() => chainColorMap(chainIds, chainColors), [chainIds.map((id) => {
1555
+ var _chainColors$id;
1556
+ return `${id}:${(_chainColors$id = chainColors === null || chainColors === void 0 ? void 0 : chainColors[id]) !== null && _chainColors$id !== void 0 ? _chainColors$id : ""}`;
1557
+ }).join("\0")]),
1558
+ hidden,
1559
+ toggleChain: (0, react.useCallback)((chainId) => {
1560
+ var _onChainVisibilityCha;
1561
+ const next = hidden.has(chainId) ? [...hidden].filter((id) => id !== chainId) : [...hidden, chainId];
1562
+ const ordered = chainIds.filter((id) => next.includes(id));
1563
+ if (!isControlled) setOwnHidden(ordered);
1564
+ (_onChainVisibilityCha = onChainVisibilityChangeRef.current) === null || _onChainVisibilityCha === void 0 || _onChainVisibilityCha.call(onChainVisibilityChangeRef, ordered);
1565
+ }, [
1566
+ chainIds,
1567
+ hidden,
1568
+ isControlled
1569
+ ])
1570
+ };
992
1571
  }
993
1572
  //#endregion
994
1573
  //#region src/core/ProteinStructureViewer/utils/theme.ts
@@ -1050,7 +1629,7 @@ function useSubscribe(observable, handler) {
1050
1629
  */
1051
1630
  function useViewSetting(settings, select) {
1052
1631
  const [value, setValue] = (0, react.useState)(() => select(settings.value));
1053
- useSubscribe(settings, (next) => setValue(select(next)));
1632
+ useSubscribe(settings, (next) => setValue(() => select(next)));
1054
1633
  return value;
1055
1634
  }
1056
1635
  //#endregion
@@ -1079,57 +1658,412 @@ const HOVER_THROTTLE_MS = 3 * 16.666;
1079
1658
  /** How long the copy button shows its confirmation state. */
1080
1659
  const COPIED_FEEDBACK_MS = 2e3;
1081
1660
  //#endregion
1082
- //#region src/core/ProteinStructureViewer/hooks/usePlugin.ts
1083
- /**
1084
- * The plugin owning the React root this component renders in.
1085
- *
1086
- * Mol* renders its view components inside its own root and provides the plugin
1087
- * through this context, which is what `PluginUIComponent` reads to populate
1088
- * `this.plugin`.
1089
- */
1090
- function usePlugin() {
1091
- return (0, react.useContext)(molstar_lib_mol_plugin_ui_base.PluginReactContext);
1092
- }
1093
- //#endregion
1094
- //#region src/core/ProteinStructureViewer/components/SequenceView/utils/residue.ts
1095
- /** Reads the sequence index off a residue span, or undefined if it is not one. */
1096
- function readSeqIdx(el) {
1097
- if (!el || !el.getAttribute) return void 0;
1098
- return el.hasAttribute("data-seqid") ? +el.getAttribute(SEQ_ID_ATTR) : void 0;
1099
- }
1100
- /**
1101
- * Nearest ancestor that scrolls vertically, or null if none. Used to scope the
1102
- * residue tooltip's hide-on-scroll listener to the sequence's own overflow
1103
- * container instead of a global window listener.
1104
- */
1105
- function getScrollParent(node) {
1106
- var _node$parentElement;
1107
- let el = (_node$parentElement = node === null || node === void 0 ? void 0 : node.parentElement) !== null && _node$parentElement !== void 0 ? _node$parentElement : null;
1108
- while (el) {
1109
- const overflowY = getComputedStyle(el).overflowY;
1110
- if (overflowY === "auto" || overflowY === "scroll") return el;
1111
- el = el.parentElement;
1112
- }
1113
- return null;
1114
- }
1115
- function getResidueClass(sequenceWrapper, seqIdx) {
1116
- return sequenceWrapper.isHighlighted(seqIdx) || sequenceWrapper.isSelected(seqIdx) || sequenceWrapper.isFocused(seqIdx) ? "msp-sequence-present msp-sequence-residue-active" : "msp-sequence-present msp-sequence-residue";
1117
- }
1118
- /**
1119
- * The label shown above a residue: its author sequence id for atomic units,
1120
- * or a 1-based position for coarse ones.
1121
- *
1122
- * `location` is a scratch object the caller reuses across residues, since this
1123
- * runs once per numbered residue on every render.
1124
- */
1125
- function getSequenceNumber(sequenceWrapper, seqIdx, location) {
1126
- let seqNum = "";
1127
- const loci = sequenceWrapper.getLoci(seqIdx);
1128
- const l = molstar_lib_mol_model_structure.StructureElement.Loci.getFirstLocation(loci, location);
1129
- if (l) {
1130
- if (molstar_lib_mol_model_structure.Unit.isAtomic(l.unit)) {
1131
- const seqId = molstar_lib_mol_model_structure.StructureProperties.residue.auth_seq_id(l);
1132
- const insCode = molstar_lib_mol_model_structure.StructureProperties.residue.pdbx_PDB_ins_code(l);
1661
+ //#region src/core/ProteinStructureViewer/components/SequenceView/style.ts
1662
+ /** Height of the fade that masks residues scrolling under the panel header. */
1663
+ const SCROLL_FADE_HEIGHT = 8;
1664
+ const SequencePanel = (0, _emotion_styled.default)("div")`
1665
+ display: flex;
1666
+ flex-direction: column;
1667
+ height: 100%;
1668
+
1669
+ ${(props) => {
1670
+ var _semanticColors$base, _props$backgroundColo, _semanticColors$base2;
1671
+ const spaces = (0, _czi_sds_components.getSpaces)(props);
1672
+ const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
1673
+ return `
1674
+ padding: ${spaces === null || spaces === void 0 ? void 0 : spaces.m}px ${spaces === null || spaces === void 0 ? void 0 : spaces.m}px 0;
1675
+ border-top: 1px solid ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base = semanticColors.base) === null || _semanticColors$base === void 0 ? void 0 : _semanticColors$base.divider};
1676
+ background-color: ${(_props$backgroundColo = props.backgroundColor) !== null && _props$backgroundColo !== void 0 ? _props$backgroundColo : semanticColors === null || semanticColors === void 0 || (_semanticColors$base2 = semanticColors.base) === null || _semanticColors$base2 === void 0 ? void 0 : _semanticColors$base2.surfacePrimary};
1677
+ `;
1678
+ }}
1679
+ `;
1680
+ const EmptyState = (0, _emotion_styled.default)("div")`
1681
+ ${_czi_sds_components.fontBodyXs}
1682
+
1683
+ display: flex;
1684
+ align-items: center;
1685
+ justify-content: center;
1686
+ height: 100%;
1687
+
1688
+ ${(props) => {
1689
+ var _props$backgroundColo2, _semanticColors$base3, _semanticColors$base4;
1690
+ const spaces = (0, _czi_sds_components.getSpaces)(props);
1691
+ const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
1692
+ return `
1693
+ padding: ${spaces === null || spaces === void 0 ? void 0 : spaces.m}px;
1694
+ background-color: ${(_props$backgroundColo2 = props.backgroundColor) !== null && _props$backgroundColo2 !== void 0 ? _props$backgroundColo2 : semanticColors === null || semanticColors === void 0 || (_semanticColors$base3 = semanticColors.base) === null || _semanticColors$base3 === void 0 ? void 0 : _semanticColors$base3.surfaceSecondary};
1695
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base4 = semanticColors.base) === null || _semanticColors$base4 === void 0 ? void 0 : _semanticColors$base4.textTertiaryOnDark};
1696
+ `;
1697
+ }}
1698
+ `;
1699
+ const PanelHeader = (0, _emotion_styled.default)("div")`
1700
+ display: flex;
1701
+ align-items: center;
1702
+
1703
+ ${(props) => {
1704
+ const spaces = (0, _czi_sds_components.getSpaces)(props);
1705
+ return `
1706
+ gap: ${spaces === null || spaces === void 0 ? void 0 : spaces.xs}px;
1707
+ `;
1708
+ }}
1709
+ `;
1710
+ const PanelTitle = (0, _emotion_styled.default)("span")`
1711
+ ${_czi_sds_components.fontHeaderM}
1712
+
1713
+ ${(props) => {
1714
+ var _semanticColors$base5;
1715
+ const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
1716
+ return `
1717
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base5 = semanticColors.base) === null || _semanticColors$base5 === void 0 ? void 0 : _semanticColors$base5.textPrimary};
1718
+ `;
1719
+ }}
1720
+ `;
1721
+ const CopyButton = (0, _emotion_styled.default)("button")`
1722
+ display: flex;
1723
+ align-items: center;
1724
+ padding: 0;
1725
+ border: none;
1726
+ background: none;
1727
+ cursor: default;
1728
+
1729
+ ${(props) => {
1730
+ var _semanticColors$base6, _semanticColors$base7;
1731
+ const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
1732
+ return `
1733
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base6 = semanticColors.base) === null || _semanticColors$base6 === void 0 ? void 0 : _semanticColors$base6.ornamentSecondary};
1734
+
1735
+ &:hover {
1736
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base7 = semanticColors.base) === null || _semanticColors$base7 === void 0 ? void 0 : _semanticColors$base7.textPrimary};
1737
+ }
1738
+ `;
1739
+ }}
1740
+ `;
1741
+ const ResidueCount = (0, _emotion_styled.default)("div")`
1742
+ display: flex;
1743
+ align-items: center;
1744
+ margin-left: auto;
1745
+
1746
+ ${(props) => {
1747
+ const spaces = (0, _czi_sds_components.getSpaces)(props);
1748
+ return `
1749
+ gap: ${spaces === null || spaces === void 0 ? void 0 : spaces.xxs}px;
1750
+ `;
1751
+ }}
1752
+ `;
1753
+ const ResidueCountLabel = (0, _emotion_styled.default)("span")`
1754
+ ${_czi_sds_components.fontBodyXs}
1755
+
1756
+ ${(props) => {
1757
+ var _semanticColors$base8;
1758
+ const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
1759
+ return `
1760
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base8 = semanticColors.base) === null || _semanticColors$base8 === void 0 ? void 0 : _semanticColors$base8.textTertiary};
1761
+ `;
1762
+ }}
1763
+ `;
1764
+ const ResidueCountValue = (0, _emotion_styled.default)("span")`
1765
+ ${_czi_sds_components.fontBodyXs}
1766
+
1767
+ ${(props) => {
1768
+ var _semanticColors$base9;
1769
+ const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
1770
+ return `
1771
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base9 = semanticColors.base) === null || _semanticColors$base9 === void 0 ? void 0 : _semanticColors$base9.textPrimary};
1772
+ `;
1773
+ }}
1774
+ `;
1775
+ /**
1776
+ * Scroll region for the residue grid. The `::before` fade masks residues as
1777
+ * they scroll up under the header, so it has to start from whatever the panel
1778
+ * is painted with or it leaves a mismatched strip.
1779
+ */
1780
+ const SequenceScroller = (0, _emotion_styled.default)("div")`
1781
+ position: relative;
1782
+ flex: 1;
1783
+ min-height: 0;
1784
+
1785
+ ${(props) => {
1786
+ var _props$backgroundColo3, _semanticColors$base10;
1787
+ const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
1788
+ return `
1789
+ &::before {
1790
+ content: "";
1791
+ position: absolute;
1792
+ inset-inline: 0;
1793
+ top: 0;
1794
+ z-index: 10;
1795
+ height: ${SCROLL_FADE_HEIGHT}px;
1796
+ pointer-events: none;
1797
+ background: linear-gradient(to bottom, ${(_props$backgroundColo3 = props.backgroundColor) !== null && _props$backgroundColo3 !== void 0 ? _props$backgroundColo3 : semanticColors === null || semanticColors === void 0 || (_semanticColors$base10 = semanticColors.base) === null || _semanticColors$base10 === void 0 ? void 0 : _semanticColors$base10.surfacePrimary}, transparent);
1798
+ }
1799
+ `;
1800
+ }}
1801
+ `;
1802
+ /**
1803
+ * Residue spans are written to directly by `useResidueMarkers` rather than
1804
+ * re-rendered, so their hover and selection colors are set inline. Everything
1805
+ * here is the static styling underneath that: the monospace type, the pointer
1806
+ * affordance, and the superscript sequence numbers (which need their own color
1807
+ * rule, since they are nested inside residue spans and would otherwise inherit
1808
+ * the inline color).
1809
+ */
1810
+ const SequenceScrollArea = (0, _emotion_styled.default)("div")`
1811
+ height: 100%;
1812
+ overflow-y: auto;
1813
+
1814
+ .msp-sequence-wrapper {
1815
+ ${_czi_sds_components.fontCodeXs};
1816
+ font-size: 11px;
1817
+ line-height: 1.2;
1818
+ }
1819
+
1820
+ .msp-sequence-present {
1821
+ cursor: pointer;
1822
+ }
1823
+
1824
+ ${(props) => {
1825
+ var _semanticColors$base11;
1826
+ const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
1827
+ return `
1828
+ .msp-sequence-number {
1829
+ font-size: 8px;
1830
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base11 = semanticColors.base) === null || _semanticColors$base11 === void 0 ? void 0 : _semanticColors$base11.textTertiaryOnDark};
1831
+ }
1832
+ `;
1833
+ }}
1834
+ `;
1835
+ /**
1836
+ * Layout for one chain's residues.
1837
+ *
1838
+ * Residues are grouped into fixed-size sections, each one grid cell. The
1839
+ * equal-width auto-fill tracks and uniform gap keep the spacing between
1840
+ * sections identical on every row - including a partial last row, which keeps
1841
+ * the same column width (rather than stretching) and stacks from the leading
1842
+ * edge.
1843
+ */
1844
+ const ResidueGrid = (0, _emotion_styled.default)("div")`
1845
+ user-select: none;
1846
+ display: grid;
1847
+ grid-template-columns: repeat(
1848
+ auto-fill,
1849
+ minmax(${10}ch, 1fr)
1850
+ );
1851
+ gap: 8px;
1852
+ justify-items: start;
1853
+ padding-top: 6px;
1854
+
1855
+ /* Section wrappers, so a section's letters never split across lines. */
1856
+ & > span {
1857
+ white-space: nowrap;
1858
+ }
1859
+
1860
+ .msp-sequence-present {
1861
+ position: relative;
1862
+ display: inline-block;
1863
+ width: 1ch;
1864
+ /*
1865
+ * Reserves room above the letter for its superscript sequence number.
1866
+ * content-box clipping keeps the active fill off that gutter, so a
1867
+ * highlighted residue does not tint the number.
1868
+ */
1869
+ padding-top: 8px;
1870
+ background-clip: content-box;
1871
+ text-align: center;
1872
+ }
1873
+
1874
+ .msp-sequence-number {
1875
+ position: absolute;
1876
+ top: 0;
1877
+ line-height: 1.2;
1878
+ white-space: nowrap;
1879
+ pointer-events: none;
1880
+ }
1881
+ `;
1882
+ /**
1883
+ * The row above each chain's grid: a visibility toggle and the chain's name.
1884
+ *
1885
+ * A structure with more than one chain renders one grid per chain, and without
1886
+ * a caption between them the grids read as a single continuous protein. The
1887
+ * controls sit here rather than only in the legend because this is where a
1888
+ * reader is already looking at one chain at a time.
1889
+ */
1890
+ const ChainHeaderRow = (0, _emotion_styled.default)("div")`
1891
+ display: flex;
1892
+ align-items: center;
1893
+
1894
+ ${(props) => {
1895
+ const spaces = (0, _czi_sds_components.getSpaces)(props);
1896
+ return `
1897
+ gap: ${spaces === null || spaces === void 0 ? void 0 : spaces.xxs}px;
1898
+ margin-top: ${spaces === null || spaces === void 0 ? void 0 : spaces.xs}px;
1899
+ margin-bottom: ${spaces === null || spaces === void 0 ? void 0 : spaces.xxxs}px;
1900
+ `;
1901
+ }}
1902
+ `;
1903
+ /**
1904
+ * Shows or hides the chain's cartoon in the 3D view. Full strength even while
1905
+ * the chain is hidden, since it is the way back.
1906
+ */
1907
+ const ChainVisibilityToggle = (0, _emotion_styled.default)("button")`
1908
+ display: flex;
1909
+ align-items: center;
1910
+ padding: 0;
1911
+ border: none;
1912
+ background: none;
1913
+ cursor: pointer;
1914
+
1915
+ ${(props) => {
1916
+ var _semanticColors$base12, _semanticColors$base13;
1917
+ const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
1918
+ return `
1919
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base12 = semanticColors.base) === null || _semanticColors$base12 === void 0 ? void 0 : _semanticColors$base12.textTertiaryOnDark};
1920
+
1921
+ &:hover {
1922
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base13 = semanticColors.base) === null || _semanticColors$base13 === void 0 ? void 0 : _semanticColors$base13.textPrimary};
1923
+ }
1924
+
1925
+ svg {
1926
+ display: block;
1927
+ }
1928
+ `;
1929
+ }}
1930
+ `;
1931
+ /**
1932
+ * The chain's name, and a button because it also selects the chain: the caption
1933
+ * is already the one thing on screen that stands for a whole chain, so clicking
1934
+ * it is where a reader looks to select one. Clicking it again clears it.
1935
+ */
1936
+ const ChainLabel = (0, _emotion_styled.default)("button")`
1937
+ ${_czi_sds_components.fontBodyXs}
1938
+
1939
+ padding: 0;
1940
+ border: none;
1941
+ background: none;
1942
+ text-align: left;
1943
+
1944
+ ${(props) => {
1945
+ var _semanticColors$base14, _semanticColors$base15, _semanticColors$base16;
1946
+ const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
1947
+ const resting = props.isSelected ? semanticColors === null || semanticColors === void 0 || (_semanticColors$base14 = semanticColors.base) === null || _semanticColors$base14 === void 0 ? void 0 : _semanticColors$base14.textPrimary : semanticColors === null || semanticColors === void 0 || (_semanticColors$base15 = semanticColors.base) === null || _semanticColors$base15 === void 0 ? void 0 : _semanticColors$base15.textSecondary;
1948
+ return `
1949
+ color: ${resting};
1950
+ cursor: ${props.isHidden ? "default" : "pointer"};
1951
+ font-weight: ${props.isSelected ? 600 : 500};
1952
+ opacity: ${props.isHidden ? .4 : 1};
1953
+
1954
+ &:hover {
1955
+ color: ${props.isHidden ? resting : semanticColors === null || semanticColors === void 0 || (_semanticColors$base16 = semanticColors.base) === null || _semanticColors$base16 === void 0 ? void 0 : _semanticColors$base16.textPrimary};
1956
+ }
1957
+ `;
1958
+ }}
1959
+ `;
1960
+ /**
1961
+ * Wraps a chain whose 3D cartoon is hidden. The grid stays in place and stays
1962
+ * interactive - the sequence is still the sequence - but is dimmed to match the
1963
+ * chain being absent from the structure beside it. Dimmed rather than removed
1964
+ * so the panel does not reflow every time a chain is toggled.
1965
+ */
1966
+ const HiddenChainGroup = (0, _emotion_styled.default)("div")`
1967
+ opacity: 0.4;
1968
+ `;
1969
+ //#endregion
1970
+ //#region src/core/ProteinStructureViewer/components/SequenceView/components/ChainHeader/index.tsx
1971
+ /**
1972
+ * The row above one chain's residue grid: a visibility toggle and the chain's
1973
+ * name, which selects it.
1974
+ *
1975
+ * The name is a toggle rather than a one-way switch, so the tooltip says which
1976
+ * way the next click goes - offering to select a chain that is already selected
1977
+ * would be telling the reader the wrong thing. A hidden chain cannot be
1978
+ * selected at all, so the name stops being a button then: there is no geometry
1979
+ * to focus, and the toggle beside it is what the reader wants instead.
1980
+ */
1981
+ function ChainHeader({ chainId, isHidden, isSelected, label, onHover, onSelect, onToggle }) {
1982
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ChainHeaderRow, {
1983
+ onBlur: () => onHover === null || onHover === void 0 ? void 0 : onHover(null),
1984
+ onFocus: () => onHover === null || onHover === void 0 ? void 0 : onHover(chainId),
1985
+ onMouseEnter: () => onHover === null || onHover === void 0 ? void 0 : onHover(chainId),
1986
+ onMouseLeave: () => onHover === null || onHover === void 0 ? void 0 : onHover(null),
1987
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_czi_sds_components.Tooltip, {
1988
+ arrow: true,
1989
+ placement: "top",
1990
+ title: isHidden ? `Chain ${label} is hidden` : `Click to ${isSelected ? "deselect" : "select"} Chain ${label}`,
1991
+ textAlign: "left",
1992
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ChainLabel, {
1993
+ "aria-pressed": isSelected,
1994
+ disabled: isHidden,
1995
+ isHidden,
1996
+ isSelected,
1997
+ onClick: () => onSelect === null || onSelect === void 0 ? void 0 : onSelect(chainId),
1998
+ type: "button",
1999
+ children: ["Chain ", label]
2000
+ })
2001
+ }), onToggle && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_czi_sds_components.Tooltip, {
2002
+ arrow: true,
2003
+ placement: "top",
2004
+ title: `${isHidden ? "Show" : "Hide"} Chain ${label}`,
2005
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChainVisibilityToggle, {
2006
+ "aria-label": `${isHidden ? "Show" : "Hide"} chain ${label}`,
2007
+ "aria-pressed": isHidden,
2008
+ onClick: () => onToggle(chainId),
2009
+ type: "button",
2010
+ children: isHidden ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_phosphor_icons_react.EyeSlashIcon, { size: 12 }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_phosphor_icons_react.EyeIcon, { size: 12 })
2011
+ })
2012
+ })]
2013
+ });
2014
+ }
2015
+ //#endregion
2016
+ //#region src/core/ProteinStructureViewer/hooks/usePlugin.ts
2017
+ /**
2018
+ * The plugin owning the React root this component renders in.
2019
+ *
2020
+ * Mol* renders its view components inside its own root and provides the plugin
2021
+ * through this context, which is what `PluginUIComponent` reads to populate
2022
+ * `this.plugin`.
2023
+ */
2024
+ function usePlugin() {
2025
+ return (0, react.useContext)(molstar_lib_mol_plugin_ui_base.PluginReactContext);
2026
+ }
2027
+ //#endregion
2028
+ //#region src/core/ProteinStructureViewer/components/SequenceView/utils/residue.ts
2029
+ /** Reads the sequence index off a residue span, or undefined if it is not one. */
2030
+ function readSeqIdx(el) {
2031
+ if (!el || !el.getAttribute) return void 0;
2032
+ return el.hasAttribute("data-seqid") ? +el.getAttribute(SEQ_ID_ATTR) : void 0;
2033
+ }
2034
+ /**
2035
+ * Nearest ancestor that scrolls vertically, or null if none. Used to scope the
2036
+ * residue tooltip's hide-on-scroll listener to the sequence's own overflow
2037
+ * container instead of a global window listener.
2038
+ */
2039
+ function getScrollParent(node) {
2040
+ var _node$parentElement;
2041
+ let el = (_node$parentElement = node === null || node === void 0 ? void 0 : node.parentElement) !== null && _node$parentElement !== void 0 ? _node$parentElement : null;
2042
+ while (el) {
2043
+ const overflowY = getComputedStyle(el).overflowY;
2044
+ if (overflowY === "auto" || overflowY === "scroll") return el;
2045
+ el = el.parentElement;
2046
+ }
2047
+ return null;
2048
+ }
2049
+ function getResidueClass(sequenceWrapper, seqIdx) {
2050
+ return sequenceWrapper.isHighlighted(seqIdx) || sequenceWrapper.isSelected(seqIdx) || sequenceWrapper.isFocused(seqIdx) ? "msp-sequence-present msp-sequence-residue-active" : "msp-sequence-present msp-sequence-residue";
2051
+ }
2052
+ /**
2053
+ * The label shown above a residue: its author sequence id for atomic units,
2054
+ * or a 1-based position for coarse ones.
2055
+ *
2056
+ * `location` is a scratch object the caller reuses across residues, since this
2057
+ * runs once per numbered residue on every render.
2058
+ */
2059
+ function getSequenceNumber(sequenceWrapper, seqIdx, location) {
2060
+ let seqNum = "";
2061
+ const loci = sequenceWrapper.getLoci(seqIdx);
2062
+ const l = molstar_lib_mol_model_structure.StructureElement.Loci.getFirstLocation(loci, location);
2063
+ if (l) {
2064
+ if (molstar_lib_mol_model_structure.Unit.isAtomic(l.unit)) {
2065
+ const seqId = molstar_lib_mol_model_structure.StructureProperties.residue.auth_seq_id(l);
2066
+ const insCode = molstar_lib_mol_model_structure.StructureProperties.residue.pdbx_PDB_ins_code(l);
1133
2067
  seqNum = `${seqId}${insCode ? insCode : ""}`;
1134
2068
  } else if (molstar_lib_mol_model_structure.Unit.isCoarse(l.unit)) seqNum = `${seqIdx + 1}`;
1135
2069
  }
@@ -1240,6 +2174,63 @@ function useResidueMarkers({ containerRef, residueColors, sequenceWrapper }) {
1240
2174
  });
1241
2175
  }
1242
2176
  //#endregion
2177
+ //#region src/core/ProteinStructureViewer/utils/residueRef.ts
2178
+ /**
2179
+ * Reads the residue a loci points at, or null when it points at nothing the
2180
+ * viewer can address.
2181
+ *
2182
+ * `index` comes from Mol*'s residue key, which counts residues in file order
2183
+ * from zero and is the same counter `injectPlddtIntoPdb` walks. Deriving it
2184
+ * from the residue's *number* instead would tie it to how the file happens to
2185
+ * be numbered: a crop beginning at residue 200 would index `plddt` at 199, and
2186
+ * a file numbering each chain from 1 would give two residues the same index.
2187
+ */
2188
+ function residueRefFromLoci(loci) {
2189
+ var _StructureProperties$;
2190
+ const location = molstar_lib_mol_model_structure.StructureElement.Location.create(void 0);
2191
+ const firstLoc = molstar_lib_mol_model_structure.StructureElement.Loci.getFirstLocation(loci, location);
2192
+ if (!firstLoc) return null;
2193
+ return {
2194
+ chainId: molstar_lib_mol_model_structure.StructureProperties.chain.auth_asym_id(firstLoc),
2195
+ compId: molstar_lib_mol_model_structure.StructureProperties.atom.label_comp_id(firstLoc),
2196
+ insCode: (_StructureProperties$ = molstar_lib_mol_model_structure.StructureProperties.residue.pdbx_PDB_ins_code(firstLoc)) !== null && _StructureProperties$ !== void 0 ? _StructureProperties$ : "",
2197
+ index: molstar_lib_mol_model_structure.StructureProperties.residue.key(firstLoc),
2198
+ seqId: molstar_lib_mol_model_structure.StructureProperties.residue.auth_seq_id(firstLoc)
2199
+ };
2200
+ }
2201
+ /**
2202
+ * Every residue a loci covers, as a selection the consumer can echo back.
2203
+ *
2204
+ * The counterpart to `residueRefFromLoci`, which names only the first: a drag
2205
+ * across the sequence hands Mol* a range, and reporting its first residue
2206
+ * alone would turn a range the user drew into a single-residue selection.
2207
+ *
2208
+ * Reported as residues rather than as chains even when a whole chain is
2209
+ * covered. What the user drew is a span of residues, and calling it a chain
2210
+ * because it happens to end where one does would change the selection's meaning
2211
+ * on a structure where the two coincide.
2212
+ */
2213
+ function selectionFromLoci(loci) {
2214
+ const location = molstar_lib_mol_model_structure.StructureElement.Location.create(loci.structure);
2215
+ const residues = /* @__PURE__ */ new Set();
2216
+ for (const element of loci.elements) {
2217
+ location.unit = element.unit;
2218
+ molstar_lib_mol_data_int.OrderedSet.forEach(element.indices, (i) => {
2219
+ location.element = element.unit.elements[i];
2220
+ residues.add(molstar_lib_mol_model_structure.StructureProperties.residue.key(location));
2221
+ });
2222
+ }
2223
+ return { residues: [...residues].sort((a, b) => a - b) };
2224
+ }
2225
+ /**
2226
+ * How a residue is named in the readout, e.g. `"LYS 111"`. Takes the number and
2227
+ * the insertion code from the file so that the readout and the sequence panel
2228
+ * beneath it agree on what to call a residue.
2229
+ */
2230
+ function residueLabel(residue) {
2231
+ return `${residue.compId} ${residue.seqId}${residue.insCode}`;
2232
+ }
2233
+ //#endregion
1243
2234
  //#region src/core/ProteinStructureViewer/components/SequenceView/utils/loci.ts
1244
2235
  /** The loci for a residue, or undefined when there is no residue there. */
1245
2236
  function getLoci(sequenceWrapper, seqIdx) {
@@ -1252,25 +2243,28 @@ function getLoci(sequenceWrapper, seqIdx) {
1252
2243
  * Extends a loci to the range between the mouse-down anchor and the current
1253
2244
  * residue, so dragging across the sequence selects a contiguous span.
1254
2245
  *
1255
- * Element indices are only comparable within a single unit, and the range below
1256
- * is built on the anchor's unit alone. So a drag is only extended when both
1257
- * endpoints are a single element of the same unit; anything else -- different
1258
- * units, or an endpoint spanning several because the chain has more than one
1259
- * symmetry operator -- is left unextended rather than collapsed onto one unit
1260
- * and silently reinterpreted as a different range.
2246
+ * The span is measured in the viewer's own residue index - position in file
2247
+ * order across the whole structure - rather than in element indices, which are
2248
+ * unit-local and so cannot describe a range reaching from one chain into the
2249
+ * next. Measuring it the way every other residue-keyed API measures it is what
2250
+ * lets a drag cross a chain break, and it means a range covers exactly the
2251
+ * residues whose indices lie between its ends.
2252
+ *
2253
+ * An endpoint that names no residue leaves the drag unextended rather than
2254
+ * guessing at where it was. An endpoint spanning several units - a chain with
2255
+ * more than one symmetry operator - is fine: each copy of a residue carries the
2256
+ * same index, so the span is the same either way and resolves back across all
2257
+ * of them.
1261
2258
  */
1262
2259
  function extendToRange(loci, anchor) {
1263
- if (anchor.elements.length !== 1 || loci.elements.length !== 1) return loci;
1264
- const ref = anchor.elements[0];
1265
- const ext = loci.elements[0];
1266
- if (!ref || !ext) return loci;
1267
- if (ref.unit !== ext.unit) return loci;
1268
- const min = Math.min(molstar_lib_mol_data_int.OrderedSet.min(ref.indices), molstar_lib_mol_data_int.OrderedSet.min(ext.indices));
1269
- const max = Math.max(molstar_lib_mol_data_int.OrderedSet.max(ref.indices), molstar_lib_mol_data_int.OrderedSet.max(ext.indices));
1270
- return molstar_lib_mol_model_structure.StructureElement.Loci(loci.structure, [{
1271
- indices: molstar_lib_mol_data_int.OrderedSet.ofRange(min, max),
1272
- unit: ref.unit
1273
- }]);
2260
+ var _lociForResidueRange;
2261
+ const from = residueRefFromLoci(anchor);
2262
+ const to = residueRefFromLoci(loci);
2263
+ if (!from || !to) return loci;
2264
+ if (from.index === to.index) return loci;
2265
+ const min = Math.min(from.index, to.index);
2266
+ const max = Math.max(from.index, to.index);
2267
+ return (_lociForResidueRange = lociForResidueRange(loci.structure, min, max)) !== null && _lociForResidueRange !== void 0 ? _lociForResidueRange : loci;
1274
2268
  }
1275
2269
  //#endregion
1276
2270
  //#region src/core/ProteinStructureViewer/components/SequenceView/hooks/useHoveredResidue.ts
@@ -1280,430 +2274,236 @@ function extendToRange(loci, anchor) {
1280
2274
  * The tracked index also deduplicates pointer moves: the grid sees a move for
1281
2275
  * every pixel, but only the ones that cross into a different residue matter.
1282
2276
  */
1283
- function useHoveredResidue({ containerRef, sequenceWrapper }) {
1284
- const tooltipRef = (0, react.useRef)(null);
1285
- /** Index of the tracked residue, or -1 when the pointer is off the grid. */
1286
- const seqIdxRef = (0, react.useRef)(-1);
1287
- const hide = (0, react.useCallback)(() => {
1288
- var _tooltipRef$current;
1289
- return (_tooltipRef$current = tooltipRef.current) === null || _tooltipRef$current === void 0 ? void 0 : _tooltipRef$current.hide();
1290
- }, []);
1291
- const clear = (0, react.useCallback)(() => {
1292
- var _tooltipRef$current2;
1293
- if (seqIdxRef.current === -1) return false;
1294
- seqIdxRef.current = -1;
1295
- (_tooltipRef$current2 = tooltipRef.current) === null || _tooltipRef$current2 === void 0 || _tooltipRef$current2.hide();
1296
- return true;
1297
- }, []);
1298
- const moveTo = (0, react.useCallback)((seqIdx, el) => {
1299
- var _tooltipRef$current3, _tooltipRef$current4;
1300
- if (seqIdxRef.current === seqIdx) return false;
1301
- seqIdxRef.current = seqIdx;
1302
- if (seqIdx < 0) (_tooltipRef$current3 = tooltipRef.current) === null || _tooltipRef$current3 === void 0 || _tooltipRef$current3.hide();
1303
- else (_tooltipRef$current4 = tooltipRef.current) === null || _tooltipRef$current4 === void 0 || _tooltipRef$current4.show(el, sequenceWrapper.residueLabel(seqIdx));
1304
- return true;
1305
- }, [sequenceWrapper]);
1306
- (0, react.useEffect)(() => {
1307
- const scrollTarget = getScrollParent(containerRef.current);
1308
- if (!scrollTarget) return;
1309
- const handleScroll = () => clear();
1310
- scrollTarget.addEventListener("scroll", handleScroll, { passive: true });
1311
- return () => scrollTarget.removeEventListener("scroll", handleScroll);
1312
- }, [clear, containerRef]);
1313
- return (0, react.useMemo)(() => ({
1314
- clear,
1315
- hide,
1316
- moveTo,
1317
- tooltipRef
1318
- }), [
1319
- clear,
1320
- hide,
1321
- moveTo
1322
- ]);
1323
- }
1324
- //#endregion
1325
- //#region src/core/ProteinStructureViewer/components/SequenceView/hooks/useLociDispatch.ts
1326
- /**
1327
- * Wraps a residue loci and the mouse state into a Mol* interaction event. An
1328
- * `anchor` extends the loci to the range between it and `loci`, which is how a
1329
- * drag across the sequence comes out as a contiguous span.
1330
- */
1331
- function buildEvent(loci, buttons, button, modifiers, anchor) {
1332
- const ev = {
1333
- button,
1334
- buttons,
1335
- current: molstar_lib_mol_repr_representation.Representation.Loci.Empty,
1336
- modifiers
1337
- };
1338
- if (loci === void 0 || molstar_lib_mol_model_structure.StructureElement.Loci.isEmpty(loci)) return ev;
1339
- ev.current = { loci: anchor ? extendToRange(loci, anchor) : loci };
1340
- return ev;
1341
- }
1342
- /**
1343
- * Reports the grid's hover and click to Mol*, on the same behaviors the 3D
1344
- * view publishes to, so an interaction in either surface reaches the other.
1345
- */
1346
- function useLociDispatch() {
1347
- const plugin = usePlugin();
1348
- const hover = (0, react.useCallback)((loci, buttons, button, modifiers, anchor) => {
1349
- plugin.behaviors.interaction.hover.next(buildEvent(loci, buttons, button, modifiers, anchor));
1350
- }, [plugin]);
1351
- const click = (0, react.useCallback)((loci, buttons, button, modifiers, anchor) => {
1352
- plugin.behaviors.interaction.click.next(buildEvent(loci, buttons, button, modifiers, anchor));
1353
- }, [plugin]);
1354
- return (0, react.useMemo)(() => ({
1355
- click,
1356
- hover
1357
- }), [click, hover]);
1358
- }
1359
- //#endregion
1360
- //#region src/core/ProteinStructureViewer/components/SequenceView/hooks/useResidueSelection.ts
1361
- /**
1362
- * Turns a press and release on the grid into a selection: one residue when
1363
- * both land on the same one, otherwise the contiguous range dragged across.
1364
- */
1365
- function useResidueSelection({ sequenceWrapper }) {
1366
- const anchorRef = (0, react.useRef)(void 0);
1367
- const { click } = useLociDispatch();
1368
- const onMouseDown = (e) => {
1369
- e.stopPropagation();
1370
- anchorRef.current = getLoci(sequenceWrapper, readSeqIdx(e.target));
1371
- };
1372
- const onMouseUp = (e) => {
1373
- e.stopPropagation();
1374
- const anchor = anchorRef.current;
1375
- if (anchor === void 0) return;
1376
- const loci = getLoci(sequenceWrapper, readSeqIdx(e.target));
1377
- if (loci) {
1378
- const dragged = !molstar_lib_mol_model_structure.StructureElement.Loci.areEqual(anchor, loci);
1379
- click(loci, (0, molstar_lib_mol_util_input_input_observer.getButtons)(e.nativeEvent), (0, molstar_lib_mol_util_input_input_observer.getButton)(e.nativeEvent), (0, molstar_lib_mol_util_input_input_observer.getModifiers)(e.nativeEvent), dragged ? anchor : void 0);
1380
- }
1381
- anchorRef.current = void 0;
1382
- };
1383
- return {
1384
- clearAnchor: () => {
1385
- anchorRef.current = void 0;
1386
- },
1387
- getAnchor: () => anchorRef.current,
1388
- onMouseDown,
1389
- onMouseUp
1390
- };
1391
- }
1392
- //#endregion
1393
- //#region src/core/ProteinStructureViewer/components/SequenceView/hooks/useResiduePointer.ts
1394
- /**
1395
- * Everything the grid does with a pointer: hover on move, and the drag-to-range
1396
- * selection delegated to `useResidueSelection`.
1397
- *
1398
- * Hover reaches the 3D view through a throttled queue, so that sweeping the
1399
- * pointer across the grid does not ask Mol* to re-mark on every residue
1400
- * crossed. A drag is exempt: its hover carries the growing range, so it goes
1401
- * out immediately to keep the preview under the pointer.
1402
- */
1403
- function useResiduePointer({ containerRef, sequenceWrapper }) {
1404
- const { hover } = useLociDispatch();
1405
- const { clear, hide, moveTo, tooltipRef } = useHoveredResidue({
1406
- containerRef,
1407
- sequenceWrapper
1408
- });
1409
- const { clearAnchor, getAnchor, onMouseDown, onMouseUp } = useResidueSelection({ sequenceWrapper });
1410
- const queueRef = (0, react.useRef)(null);
1411
- if (queueRef.current === null) queueRef.current = new rxjs.Subject();
1412
- const queue = queueRef.current;
1413
- useSubscribe((0, react.useMemo)(() => queue.pipe((0, rxjs_operators.throttleTime)(HOVER_THROTTLE_MS, void 0, {
1414
- leading: true,
1415
- trailing: true
1416
- })), [queue]), (e) => {
1417
- hover(getLoci(sequenceWrapper, e.seqIdx < 0 ? void 0 : e.seqIdx), e.buttons, e.button, e.modifiers, getAnchor());
1418
- });
1419
- const queueHover = (e, seqIdx) => queue.next({
1420
- button: (0, molstar_lib_mol_util_input_input_observer.getButton)(e.nativeEvent),
1421
- buttons: (0, molstar_lib_mol_util_input_input_observer.getButtons)(e.nativeEvent),
1422
- modifiers: (0, molstar_lib_mol_util_input_input_observer.getModifiers)(e.nativeEvent),
1423
- seqIdx
1424
- });
1425
- /** Asks the 3D view to drop its hover, once, if the grid still holds one. */
1426
- const releaseHover = (e) => {
1427
- if (clear()) queueHover(e, -1);
1428
- };
1429
- const onContextMenu = (e) => {
1430
- e.preventDefault();
1431
- };
1432
- const onMouseMove = (e) => {
1433
- var _readSeqIdx;
1434
- e.stopPropagation();
1435
- const el = e.target;
1436
- if (!el || !el.getAttribute) {
1437
- releaseHover(e);
1438
- return;
1439
- }
1440
- const seqIdx = (_readSeqIdx = readSeqIdx(el)) !== null && _readSeqIdx !== void 0 ? _readSeqIdx : -1;
1441
- if (!moveTo(seqIdx, el)) return;
1442
- const anchor = getAnchor();
1443
- if (anchor === void 0) {
1444
- queueHover(e, seqIdx);
1445
- return;
1446
- }
1447
- hover(getLoci(sequenceWrapper, seqIdx), molstar_lib_mol_util_input_input_observer.ButtonsType.Flag.None, molstar_lib_mol_util_input_input_observer.ButtonsType.Flag.None, (0, molstar_lib_mol_util_input_input_observer.getModifiers)(e.nativeEvent), anchor);
1448
- };
1449
- const onMouseLeave = (e) => {
1450
- e.stopPropagation();
1451
- clearAnchor();
1452
- hide();
1453
- releaseHover(e);
1454
- };
1455
- return {
1456
- handlers: {
1457
- onContextMenu,
1458
- onMouseDown,
1459
- onMouseLeave,
1460
- onMouseMove,
1461
- onMouseUp
1462
- },
2277
+ function useHoveredResidue({ containerRef, sequenceWrapper }) {
2278
+ const tooltipRef = (0, react.useRef)(null);
2279
+ /** Index of the tracked residue, or -1 when the pointer is off the grid. */
2280
+ const seqIdxRef = (0, react.useRef)(-1);
2281
+ const hide = (0, react.useCallback)(() => {
2282
+ var _tooltipRef$current;
2283
+ return (_tooltipRef$current = tooltipRef.current) === null || _tooltipRef$current === void 0 ? void 0 : _tooltipRef$current.hide();
2284
+ }, []);
2285
+ const clear = (0, react.useCallback)(() => {
2286
+ var _tooltipRef$current2;
2287
+ if (seqIdxRef.current === -1) return false;
2288
+ seqIdxRef.current = -1;
2289
+ (_tooltipRef$current2 = tooltipRef.current) === null || _tooltipRef$current2 === void 0 || _tooltipRef$current2.hide();
2290
+ return true;
2291
+ }, []);
2292
+ const moveTo = (0, react.useCallback)((seqIdx, el) => {
2293
+ var _tooltipRef$current3, _tooltipRef$current4;
2294
+ if (seqIdxRef.current === seqIdx) return false;
2295
+ seqIdxRef.current = seqIdx;
2296
+ if (seqIdx < 0) (_tooltipRef$current3 = tooltipRef.current) === null || _tooltipRef$current3 === void 0 || _tooltipRef$current3.hide();
2297
+ else (_tooltipRef$current4 = tooltipRef.current) === null || _tooltipRef$current4 === void 0 || _tooltipRef$current4.show(el, sequenceWrapper.residueLabel(seqIdx));
2298
+ return true;
2299
+ }, [sequenceWrapper]);
2300
+ (0, react.useEffect)(() => {
2301
+ const scrollTarget = getScrollParent(containerRef.current);
2302
+ if (!scrollTarget) return;
2303
+ const handleScroll = () => clear();
2304
+ scrollTarget.addEventListener("scroll", handleScroll, { passive: true });
2305
+ return () => scrollTarget.removeEventListener("scroll", handleScroll);
2306
+ }, [clear, containerRef]);
2307
+ return (0, react.useMemo)(() => ({
2308
+ clear,
2309
+ hide,
2310
+ moveTo,
1463
2311
  tooltipRef
1464
- };
2312
+ }), [
2313
+ clear,
2314
+ hide,
2315
+ moveTo
2316
+ ]);
1465
2317
  }
1466
2318
  //#endregion
1467
- //#region src/core/ProteinStructureViewer/components/SequenceView/style.ts
1468
- /** Height of the fade that masks residues scrolling under the panel header. */
1469
- const SCROLL_FADE_HEIGHT = 8;
1470
- const SequencePanel = (0, _emotion_styled.default)("div")`
1471
- display: flex;
1472
- flex-direction: column;
1473
- height: 100%;
1474
-
1475
- ${(props) => {
1476
- var _semanticColors$base, _props$backgroundColo, _semanticColors$base2;
1477
- const spaces = (0, _czi_sds_components.getSpaces)(props);
1478
- const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
1479
- return `
1480
- padding: ${spaces === null || spaces === void 0 ? void 0 : spaces.m}px ${spaces === null || spaces === void 0 ? void 0 : spaces.m}px 0;
1481
- border-top: 1px solid ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base = semanticColors.base) === null || _semanticColors$base === void 0 ? void 0 : _semanticColors$base.divider};
1482
- background-color: ${(_props$backgroundColo = props.backgroundColor) !== null && _props$backgroundColo !== void 0 ? _props$backgroundColo : semanticColors === null || semanticColors === void 0 || (_semanticColors$base2 = semanticColors.base) === null || _semanticColors$base2 === void 0 ? void 0 : _semanticColors$base2.surfacePrimary};
1483
- `;
1484
- }}
1485
- `;
1486
- const EmptyState = (0, _emotion_styled.default)("div")`
1487
- ${_czi_sds_components.fontBodyXs}
1488
-
1489
- display: flex;
1490
- align-items: center;
1491
- justify-content: center;
1492
- height: 100%;
1493
-
1494
- ${(props) => {
1495
- var _props$backgroundColo2, _semanticColors$base3, _semanticColors$base4;
1496
- const spaces = (0, _czi_sds_components.getSpaces)(props);
1497
- const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
1498
- return `
1499
- padding: ${spaces === null || spaces === void 0 ? void 0 : spaces.m}px;
1500
- background-color: ${(_props$backgroundColo2 = props.backgroundColor) !== null && _props$backgroundColo2 !== void 0 ? _props$backgroundColo2 : semanticColors === null || semanticColors === void 0 || (_semanticColors$base3 = semanticColors.base) === null || _semanticColors$base3 === void 0 ? void 0 : _semanticColors$base3.surfaceSecondary};
1501
- color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base4 = semanticColors.base) === null || _semanticColors$base4 === void 0 ? void 0 : _semanticColors$base4.textTertiaryOnDark};
1502
- `;
1503
- }}
1504
- `;
1505
- const PanelHeader = (0, _emotion_styled.default)("div")`
1506
- display: flex;
1507
- align-items: center;
1508
-
1509
- ${(props) => {
1510
- const spaces = (0, _czi_sds_components.getSpaces)(props);
1511
- return `
1512
- gap: ${spaces === null || spaces === void 0 ? void 0 : spaces.xs}px;
1513
- `;
1514
- }}
1515
- `;
1516
- const PanelTitle = (0, _emotion_styled.default)("span")`
1517
- ${_czi_sds_components.fontHeaderM}
1518
-
1519
- ${(props) => {
1520
- var _semanticColors$base5;
1521
- const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
1522
- return `
1523
- color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base5 = semanticColors.base) === null || _semanticColors$base5 === void 0 ? void 0 : _semanticColors$base5.textPrimary};
1524
- `;
1525
- }}
1526
- `;
1527
- const CopyButton = (0, _emotion_styled.default)("button")`
1528
- display: flex;
1529
- align-items: center;
1530
- padding: 0;
1531
- border: none;
1532
- background: none;
1533
- cursor: default;
1534
-
1535
- ${(props) => {
1536
- var _semanticColors$base6, _semanticColors$base7;
1537
- const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
1538
- return `
1539
- color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base6 = semanticColors.base) === null || _semanticColors$base6 === void 0 ? void 0 : _semanticColors$base6.ornamentSecondary};
1540
-
1541
- &:hover {
1542
- color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base7 = semanticColors.base) === null || _semanticColors$base7 === void 0 ? void 0 : _semanticColors$base7.textPrimary};
1543
- }
1544
- `;
1545
- }}
1546
- `;
1547
- const ResidueCount = (0, _emotion_styled.default)("div")`
1548
- display: flex;
1549
- align-items: center;
1550
- margin-left: auto;
1551
-
1552
- ${(props) => {
1553
- const spaces = (0, _czi_sds_components.getSpaces)(props);
1554
- return `
1555
- gap: ${spaces === null || spaces === void 0 ? void 0 : spaces.xxs}px;
1556
- `;
1557
- }}
1558
- `;
1559
- const ResidueCountLabel = (0, _emotion_styled.default)("span")`
1560
- ${_czi_sds_components.fontBodyXs}
1561
-
1562
- ${(props) => {
1563
- var _semanticColors$base8;
1564
- const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
1565
- return `
1566
- color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base8 = semanticColors.base) === null || _semanticColors$base8 === void 0 ? void 0 : _semanticColors$base8.textTertiary};
1567
- `;
1568
- }}
1569
- `;
1570
- const ResidueCountValue = (0, _emotion_styled.default)("span")`
1571
- ${_czi_sds_components.fontBodyXs}
1572
-
1573
- ${(props) => {
1574
- var _semanticColors$base9;
1575
- const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
1576
- return `
1577
- color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base9 = semanticColors.base) === null || _semanticColors$base9 === void 0 ? void 0 : _semanticColors$base9.textPrimary};
1578
- `;
1579
- }}
1580
- `;
2319
+ //#region src/core/ProteinStructureViewer/components/SequenceView/hooks/useLociDispatch.ts
1581
2320
  /**
1582
- * Scroll region for the residue grid. The `::before` fade masks residues as
1583
- * they scroll up under the header, so it has to start from whatever the panel
1584
- * is painted with or it leaves a mismatched strip.
2321
+ * Wraps a residue loci and the mouse state into a Mol* interaction event. An
2322
+ * `anchor` extends the loci to the range between it and `loci`, which is how a
2323
+ * drag across the sequence comes out as a contiguous span.
1585
2324
  */
1586
- const SequenceScroller = (0, _emotion_styled.default)("div")`
1587
- position: relative;
1588
- flex: 1;
1589
- min-height: 0;
1590
-
1591
- ${(props) => {
1592
- var _props$backgroundColo3, _semanticColors$base10;
1593
- const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
1594
- return `
1595
- &::before {
1596
- content: "";
1597
- position: absolute;
1598
- inset-inline: 0;
1599
- top: 0;
1600
- z-index: 10;
1601
- height: ${SCROLL_FADE_HEIGHT}px;
1602
- pointer-events: none;
1603
- background: linear-gradient(to bottom, ${(_props$backgroundColo3 = props.backgroundColor) !== null && _props$backgroundColo3 !== void 0 ? _props$backgroundColo3 : semanticColors === null || semanticColors === void 0 || (_semanticColors$base10 = semanticColors.base) === null || _semanticColors$base10 === void 0 ? void 0 : _semanticColors$base10.surfacePrimary}, transparent);
1604
- }
1605
- `;
1606
- }}
1607
- `;
2325
+ function buildEvent(loci, buttons, button, modifiers, anchor) {
2326
+ const ev = {
2327
+ button,
2328
+ buttons,
2329
+ current: molstar_lib_mol_repr_representation.Representation.Loci.Empty,
2330
+ modifiers
2331
+ };
2332
+ if (loci === void 0 || molstar_lib_mol_model_structure.StructureElement.Loci.isEmpty(loci)) return ev;
2333
+ ev.current = { loci: anchor ? extendToRange(loci, anchor) : loci };
2334
+ return ev;
2335
+ }
1608
2336
  /**
1609
- * Residue spans are written to directly by `useResidueMarkers` rather than
1610
- * re-rendered, so their hover and selection colors are set inline. Everything
1611
- * here is the static styling underneath that: the monospace type, the pointer
1612
- * affordance, and the superscript sequence numbers (which need their own color
1613
- * rule, since they are nested inside residue spans and would otherwise inherit
1614
- * the inline color).
2337
+ * Reports the grid's hover and click to Mol*, on the same behaviors the 3D
2338
+ * view publishes to, so an interaction in either surface reaches the other.
1615
2339
  */
1616
- const SequenceScrollArea = (0, _emotion_styled.default)("div")`
1617
- height: 100%;
1618
- overflow-y: auto;
1619
-
1620
- .msp-sequence-wrapper {
1621
- ${_czi_sds_components.fontCodeXs};
1622
- font-size: 11px;
1623
- line-height: 1.2;
1624
- }
1625
-
1626
- .msp-sequence-present {
1627
- cursor: pointer;
1628
- }
1629
-
1630
- ${(props) => {
1631
- var _semanticColors$base11;
1632
- const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
1633
- return `
1634
- .msp-sequence-number {
1635
- font-size: 8px;
1636
- color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base11 = semanticColors.base) === null || _semanticColors$base11 === void 0 ? void 0 : _semanticColors$base11.textTertiaryOnDark};
1637
- }
1638
- `;
1639
- }}
1640
- `;
2340
+ function useLociDispatch() {
2341
+ const plugin = usePlugin();
2342
+ const hover = (0, react.useCallback)((loci, buttons, button, modifiers, anchor) => {
2343
+ plugin.behaviors.interaction.hover.next(buildEvent(loci, buttons, button, modifiers, anchor));
2344
+ }, [plugin]);
2345
+ const click = (0, react.useCallback)((loci, buttons, button, modifiers, anchor) => {
2346
+ plugin.behaviors.interaction.click.next(buildEvent(loci, buttons, button, modifiers, anchor));
2347
+ }, [plugin]);
2348
+ return (0, react.useMemo)(() => ({
2349
+ click,
2350
+ hover
2351
+ }), [click, hover]);
2352
+ }
2353
+ //#endregion
2354
+ //#region src/core/ProteinStructureViewer/components/SequenceView/hooks/useSequenceDrag.ts
2355
+ const SequenceDragContext = (0, react.createContext)({
2356
+ clearAnchor: () => void 0,
2357
+ getAnchor: () => void 0,
2358
+ setAnchor: () => void 0
2359
+ });
2360
+ const SequenceDragProvider = SequenceDragContext.Provider;
1641
2361
  /**
1642
- * Layout for one chain's residues.
2362
+ * The anchor a drag across the sequence started from, shared by every chain's
2363
+ * grid.
1643
2364
  *
1644
- * Residues are grouped into fixed-size sections, each one grid cell. The
1645
- * equal-width auto-fill tracks and uniform gap keep the spacing between
1646
- * sections identical on every row - including a partial last row, which keeps
1647
- * the same column width (rather than stretching) and stacks from the leading
1648
- * edge.
2365
+ * It has to live above the grids rather than inside one. A complex renders one
2366
+ * grid per chain, so a drag that begins on the target and ends on the binder
2367
+ * starts in one component and finishes in another; an anchor held by the grid
2368
+ * it started in is not there to be read by the grid it ends in, and the drag
2369
+ * would be reported as a plain click on whatever it landed on.
2370
+ *
2371
+ * Held in a ref rather than state: it changes on mouse-down and mouse-up, and
2372
+ * nothing renders from it, so re-rendering every residue in the panel twice per
2373
+ * drag would buy nothing.
2374
+ */
2375
+ function useCreateSequenceDrag() {
2376
+ const anchorRef = (0, react.useRef)(void 0);
2377
+ const getAnchor = (0, react.useCallback)(() => anchorRef.current, []);
2378
+ const setAnchor = (0, react.useCallback)((anchor) => {
2379
+ anchorRef.current = anchor;
2380
+ }, []);
2381
+ const clearAnchor = (0, react.useCallback)(() => {
2382
+ anchorRef.current = void 0;
2383
+ }, []);
2384
+ return (0, react.useMemo)(() => ({
2385
+ clearAnchor,
2386
+ getAnchor,
2387
+ setAnchor
2388
+ }), [
2389
+ clearAnchor,
2390
+ getAnchor,
2391
+ setAnchor
2392
+ ]);
2393
+ }
2394
+ function useSequenceDrag() {
2395
+ return (0, react.useContext)(SequenceDragContext);
2396
+ }
2397
+ //#endregion
2398
+ //#region src/core/ProteinStructureViewer/components/SequenceView/hooks/useResidueSelection.ts
2399
+ /**
2400
+ * Turns a press and release on the grid into a selection: one residue when
2401
+ * both land on the same one, otherwise the contiguous range dragged across -
2402
+ * which the panel-level anchor lets reach into another chain's grid.
1649
2403
  */
1650
- const ResidueGrid = (0, _emotion_styled.default)("div")`
1651
- user-select: none;
1652
- display: grid;
1653
- grid-template-columns: repeat(
1654
- auto-fill,
1655
- minmax(${10}ch, 1fr)
1656
- );
1657
- gap: 8px;
1658
- justify-items: start;
1659
- padding-top: 6px;
1660
-
1661
- /* Section wrappers, so a section's letters never split across lines. */
1662
- & > span {
1663
- white-space: nowrap;
1664
- }
1665
-
1666
- .msp-sequence-present {
1667
- position: relative;
1668
- display: inline-block;
1669
- width: 1ch;
1670
- /*
1671
- * Reserves room above the letter for its superscript sequence number.
1672
- * content-box clipping keeps the active fill off that gutter, so a
1673
- * highlighted residue does not tint the number.
1674
- */
1675
- padding-top: 8px;
1676
- background-clip: content-box;
1677
- text-align: center;
1678
- }
1679
-
1680
- .msp-sequence-number {
1681
- position: absolute;
1682
- top: 0;
1683
- line-height: 1.2;
1684
- white-space: nowrap;
1685
- pointer-events: none;
1686
- }
1687
- `;
2404
+ function useResidueSelection({ sequenceWrapper }) {
2405
+ const { clearAnchor, getAnchor, setAnchor } = useSequenceDrag();
2406
+ const { click } = useLociDispatch();
2407
+ const onMouseDown = (e) => {
2408
+ e.stopPropagation();
2409
+ setAnchor(getLoci(sequenceWrapper, readSeqIdx(e.target)));
2410
+ };
2411
+ const onMouseUp = (e) => {
2412
+ e.stopPropagation();
2413
+ const anchor = getAnchor();
2414
+ if (anchor === void 0) return;
2415
+ const loci = getLoci(sequenceWrapper, readSeqIdx(e.target));
2416
+ if (loci) {
2417
+ const dragged = !molstar_lib_mol_model_structure.StructureElement.Loci.areEqual(anchor, loci);
2418
+ click(loci, (0, molstar_lib_mol_util_input_input_observer.getButtons)(e.nativeEvent), (0, molstar_lib_mol_util_input_input_observer.getButton)(e.nativeEvent), (0, molstar_lib_mol_util_input_input_observer.getModifiers)(e.nativeEvent), dragged ? anchor : void 0);
2419
+ }
2420
+ clearAnchor();
2421
+ };
2422
+ return {
2423
+ onMouseDown,
2424
+ onMouseUp
2425
+ };
2426
+ }
2427
+ //#endregion
2428
+ //#region src/core/ProteinStructureViewer/components/SequenceView/hooks/useResiduePointer.ts
1688
2429
  /**
1689
- * Caption naming the chain a sequence grid belongs to. A structure with more
1690
- * than one chain renders one grid per chain, and without a caption between them
1691
- * the grids read as a single continuous protein.
2430
+ * Everything the grid does with a pointer: hover on move, and the drag-to-range
2431
+ * selection delegated to `useResidueSelection`.
2432
+ *
2433
+ * Hover reaches the 3D view through a throttled queue, so that sweeping the
2434
+ * pointer across the grid does not ask Mol* to re-mark on every residue
2435
+ * crossed. A drag is exempt: its hover carries the growing range, so it goes
2436
+ * out immediately to keep the preview under the pointer.
1692
2437
  */
1693
- const ChainLabel = (0, _emotion_styled.default)("div")`
1694
- ${_czi_sds_components.fontBodyXs}
1695
-
1696
- ${(props) => {
1697
- var _semanticColors$base12;
1698
- const spaces = (0, _czi_sds_components.getSpaces)(props);
1699
- const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
1700
- return `
1701
- margin-top: ${spaces === null || spaces === void 0 ? void 0 : spaces.xs}px;
1702
- margin-bottom: ${spaces === null || spaces === void 0 ? void 0 : spaces.xxxs}px;
1703
- color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base12 = semanticColors.base) === null || _semanticColors$base12 === void 0 ? void 0 : _semanticColors$base12.textSecondary};
1704
- `;
1705
- }}
1706
- `;
2438
+ function useResiduePointer({ containerRef, sequenceWrapper }) {
2439
+ const { hover } = useLociDispatch();
2440
+ const { clear, hide, moveTo, tooltipRef } = useHoveredResidue({
2441
+ containerRef,
2442
+ sequenceWrapper
2443
+ });
2444
+ const { getAnchor } = useSequenceDrag();
2445
+ const { onMouseDown, onMouseUp } = useResidueSelection({ sequenceWrapper });
2446
+ const queueRef = (0, react.useRef)(null);
2447
+ if (queueRef.current === null) queueRef.current = new rxjs.Subject();
2448
+ const queue = queueRef.current;
2449
+ useSubscribe((0, react.useMemo)(() => queue.pipe((0, rxjs_operators.throttleTime)(HOVER_THROTTLE_MS, void 0, {
2450
+ leading: true,
2451
+ trailing: true
2452
+ })), [queue]), (e) => {
2453
+ hover(getLoci(sequenceWrapper, e.seqIdx < 0 ? void 0 : e.seqIdx), e.buttons, e.button, e.modifiers, getAnchor());
2454
+ });
2455
+ const queueHover = (e, seqIdx) => queue.next({
2456
+ button: (0, molstar_lib_mol_util_input_input_observer.getButton)(e.nativeEvent),
2457
+ buttons: (0, molstar_lib_mol_util_input_input_observer.getButtons)(e.nativeEvent),
2458
+ modifiers: (0, molstar_lib_mol_util_input_input_observer.getModifiers)(e.nativeEvent),
2459
+ seqIdx
2460
+ });
2461
+ /** Asks the 3D view to drop its hover, once, if the grid still holds one. */
2462
+ const releaseHover = (e) => {
2463
+ if (clear()) queueHover(e, -1);
2464
+ };
2465
+ const onContextMenu = (e) => {
2466
+ e.preventDefault();
2467
+ };
2468
+ const onMouseMove = (e) => {
2469
+ var _readSeqIdx;
2470
+ e.stopPropagation();
2471
+ const el = e.target;
2472
+ if (!el || !el.getAttribute) {
2473
+ releaseHover(e);
2474
+ return;
2475
+ }
2476
+ const seqIdx = (_readSeqIdx = readSeqIdx(el)) !== null && _readSeqIdx !== void 0 ? _readSeqIdx : -1;
2477
+ if (!moveTo(seqIdx, el)) return;
2478
+ const anchor = getAnchor();
2479
+ if (anchor === void 0) {
2480
+ queueHover(e, seqIdx);
2481
+ return;
2482
+ }
2483
+ hover(getLoci(sequenceWrapper, seqIdx), molstar_lib_mol_util_input_input_observer.ButtonsType.Flag.None, molstar_lib_mol_util_input_input_observer.ButtonsType.Flag.None, (0, molstar_lib_mol_util_input_input_observer.getModifiers)(e.nativeEvent), anchor);
2484
+ };
2485
+ /**
2486
+ * Leaving one grid is not the end of a drag: on a complex the next chain's
2487
+ * grid is a separate element, so a drag from the target into the binder
2488
+ * leaves this one on its way. The anchor is therefore left alone here and
2489
+ * dropped by the release, or by the pointer leaving the panel altogether.
2490
+ */
2491
+ const onMouseLeave = (e) => {
2492
+ e.stopPropagation();
2493
+ hide();
2494
+ releaseHover(e);
2495
+ };
2496
+ return {
2497
+ handlers: {
2498
+ onContextMenu,
2499
+ onMouseDown,
2500
+ onMouseLeave,
2501
+ onMouseMove,
2502
+ onMouseUp
2503
+ },
2504
+ tooltipRef
2505
+ };
2506
+ }
1707
2507
  //#endregion
1708
2508
  //#region src/core/ProteinStructureViewer/components/SequenceView/components/ResidueTooltip/index.tsx
1709
2509
  var _getSpaces$m, _getSpaces;
@@ -1806,6 +2606,48 @@ const Sequence = ({ chainLabel, hideSequenceNumbers, residueColors, sequenceNumb
1806
2606
  }, handlers), {}, { children: groups })), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResidueTooltip, { ref: tooltipRef })] });
1807
2607
  };
1808
2608
  //#endregion
2609
+ //#region src/core/ProteinStructureViewer/components/SequenceView/utils/copyText.ts
2610
+ /**
2611
+ * Writes `text` to the clipboard.
2612
+ *
2613
+ * The async Clipboard API is preferred, but nested srcdoc sandboxes — MCP
2614
+ * inspectors, Claude Desktop, and similar hosts — often omit `clipboard-write`
2615
+ * from their Permissions Policy, so `navigator.clipboard.writeText` rejects
2616
+ * even on a user click. `document.execCommand("copy")` only needs that click, so
2617
+ * it covers those hosts without the consumer having to patch the bundle.
2618
+ */
2619
+ function copyText(text) {
2620
+ var _navigator$clipboard;
2621
+ if (!((_navigator$clipboard = navigator.clipboard) === null || _navigator$clipboard === void 0 ? void 0 : _navigator$clipboard.writeText)) return copyWithSelection(text);
2622
+ return navigator.clipboard.writeText(text).catch(() => copyWithSelection(text));
2623
+ }
2624
+ /**
2625
+ * Copies by selecting a transient textarea. Must run under user activation,
2626
+ * which a click still has after the Clipboard API has been refused.
2627
+ */
2628
+ function copyWithSelection(text) {
2629
+ return new Promise((resolve, reject) => {
2630
+ const area = document.createElement("textarea");
2631
+ area.value = text;
2632
+ area.setAttribute("readonly", "");
2633
+ area.setAttribute("aria-hidden", "true");
2634
+ area.style.position = "fixed";
2635
+ area.style.top = "-1000px";
2636
+ document.body.appendChild(area);
2637
+ area.select();
2638
+ area.setSelectionRange(0, text.length);
2639
+ let copied = false;
2640
+ try {
2641
+ copied = document.execCommand("copy");
2642
+ } catch (_unused) {
2643
+ copied = false;
2644
+ }
2645
+ area.remove();
2646
+ if (copied) resolve();
2647
+ else reject(/* @__PURE__ */ new Error("The host blocked clipboard access."));
2648
+ });
2649
+ }
2650
+ //#endregion
1809
2651
  //#region src/core/ProteinStructureViewer/components/SequenceView/utils/sequenceText.ts
1810
2652
  /**
1811
2653
  * The structure's residues as text, one section per chain separated by
@@ -1834,12 +2676,11 @@ function useCopySequence(entries) {
1834
2676
  return {
1835
2677
  copied,
1836
2678
  copySequence: (0, react.useCallback)(() => {
1837
- const sequence = sequenceTextFromEntries(entries);
1838
- navigator.clipboard.writeText(sequence).then(() => {
2679
+ copyText(sequenceTextFromEntries(entries)).then(() => {
1839
2680
  setCopied(true);
1840
2681
  if (timeoutRef.current) clearTimeout(timeoutRef.current);
1841
2682
  timeoutRef.current = setTimeout(() => setCopied(false), COPIED_FEEDBACK_MS);
1842
- });
2683
+ }).catch(() => {});
1843
2684
  }, [entries])
1844
2685
  };
1845
2686
  }
@@ -1885,10 +2726,37 @@ function buildParams(state) {
1885
2726
  structure: molstar_lib_mol_util_param_definition.ParamDefinition.Select(structureOptions.options[0][0], structureOptions.options, { shortLabel: true })
1886
2727
  };
1887
2728
  }
1888
- /** One entry per chain, in Mol*'s entity / chain / operator order. */
2729
+ /**
2730
+ * The file's own name for a chain group, or `""` when the structure holds no
2731
+ * unit for it. Mol* groups its sequence panel by `chainGroupId`, but every
2732
+ * chain-level prop on the viewer is keyed by `auth_asym_id`, so the two have
2733
+ * to be bridged through the structure itself.
2734
+ */
2735
+ function authAsymIdFor(structure, chainGroupId) {
2736
+ const location = molstar_lib_mol_model_structure.StructureElement.Location.create(structure);
2737
+ for (const unit of structure.units) {
2738
+ if (unit.chainGroupId !== chainGroupId) continue;
2739
+ const element = unit.elements[0];
2740
+ if (element === void 0) continue;
2741
+ molstar_lib_mol_model_structure.StructureElement.Location.set(location, structure, unit, element);
2742
+ return molstar_lib_mol_model_structure.StructureProperties.chain.auth_asym_id(location);
2743
+ }
2744
+ return "";
2745
+ }
2746
+ /**
2747
+ * One entry per polymer chain, in Mol*'s entity / chain / operator order.
2748
+ *
2749
+ * Polymer entities only. The heteroatoms on a chain are separate entities, and
2750
+ * Mol* would hand back a caption and a grid for each - a myoglobin listing
2751
+ * "Chain A" and then two more headers, both reading "Chain A_1 [auth A]", one
2752
+ * holding the heme and one the hydroxide. They are drawn in the 3D view and
2753
+ * they are not sequence, and a caption per ligand offers a chain toggle for
2754
+ * something that is not a chain.
2755
+ */
1889
2756
  function buildEntries(structure, selection, _params) {
1890
2757
  const entries = [];
1891
- for (const [modelEntityId] of (0, molstar_lib_mol_plugin_ui_sequence.getModelEntityOptions)(structure)) for (const [chainGroupId, cLabel] of (0, molstar_lib_mol_plugin_ui_sequence.getChainOptions)(structure, modelEntityId)) for (const [operatorKey] of (0, molstar_lib_mol_plugin_ui_sequence.getOperatorOptions)(structure, modelEntityId, chainGroupId)) entries.push({
2758
+ for (const [modelEntityId] of (0, molstar_lib_mol_plugin_ui_sequence.getModelEntityOptions)(structure, true)) for (const [chainGroupId, cLabel] of (0, molstar_lib_mol_plugin_ui_sequence.getChainOptions)(structure, modelEntityId)) for (const [operatorKey] of (0, molstar_lib_mol_plugin_ui_sequence.getOperatorOptions)(structure, modelEntityId, chainGroupId)) entries.push({
2759
+ chainId: authAsymIdFor(structure, chainGroupId),
1892
2760
  id: `${modelEntityId}:${chainGroupId}:${operatorKey}`,
1893
2761
  label: cLabel,
1894
2762
  wrapper: (0, molstar_lib_mol_plugin_ui_sequence.getSequenceWrapper)({
@@ -1953,74 +2821,147 @@ function useSequenceWrappers() {
1953
2821
  * for changes rather than read once. The theme is re-supplied through a local
1954
2822
  * `ThemeProvider` for the Emotion styles and the SDS `Tooltip` below.
1955
2823
  */
1956
- function createSequenceView(viewSettings) {
1957
- return function SequenceView() {
1958
- const { mode, theme } = useMolstarTheme(viewSettings);
1959
- const backgroundColor = useViewSetting(viewSettings, (s) => s.sequenceViewerBackgroundColor);
1960
- const { entries, isEmpty, residueCount } = useSequenceWrappers();
1961
- const { copied, copySequence } = useCopySequence(entries);
1962
- if (isEmpty) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_mui_material_styles.ThemeProvider, {
1963
- theme,
1964
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmptyState, {
1965
- backgroundColor,
1966
- className: "msp-sequence",
1967
- children: "No structure available"
1968
- })
1969
- });
1970
- const residueColors = residueColorsForMode(mode);
1971
- const showChainLabels = entries.length > 1;
1972
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_mui_material_styles.ThemeProvider, {
1973
- theme,
1974
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(SequencePanel, {
1975
- backgroundColor,
1976
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(PanelHeader, { children: [
1977
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PanelTitle, { children: "Sequence" }),
1978
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_czi_sds_components.Tooltip, {
1979
- arrow: true,
1980
- placement: "top",
1981
- title: copied ? "Copied!" : "Copy",
1982
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CopyButton, {
1983
- "aria-label": "Copy sequence to clipboard",
1984
- onClick: copySequence,
1985
- type: "button",
1986
- children: copied ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_phosphor_icons_react.CheckIcon, { size: 16 }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_phosphor_icons_react.CopyIcon, { size: 16 })
1987
- })
1988
- }),
1989
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ResidueCount, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResidueCountLabel, { children: "Residues" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResidueCountValue, { children: residueCount })] })
1990
- ] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SequenceScroller, {
1991
- backgroundColor,
1992
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SequenceScrollArea, {
1993
- className: "msp-sequence msp-sequence-wrapper-non-empty",
1994
- children: entries.map((s) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [showChainLabels && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChainLabel, { children: s.label }), typeof s.wrapper === "string" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1995
- className: "msp-sequence-wrapper",
1996
- children: s.wrapper
1997
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Sequence, {
1998
- chainLabel: showChainLabels ? s.label : void 0,
1999
- residueColors,
2000
- sequenceWrapper: s.wrapper
2001
- })] }, s.id))
2002
- })
2003
- })]
2004
- })
2005
- });
2006
- };
2824
+ function createSequenceView(viewSettings) {
2825
+ return function SequenceView() {
2826
+ const { mode, theme } = useMolstarTheme(viewSettings);
2827
+ const backgroundColor = useViewSetting(viewSettings, (s) => s.sequenceViewerBackgroundColor);
2828
+ const hiddenChains = useViewSetting(viewSettings, (s) => s.hiddenChains);
2829
+ const onChainHover = useViewSetting(viewSettings, (s) => s.onChainHover);
2830
+ const onChainSelect = useViewSetting(viewSettings, (s) => s.onChainSelect);
2831
+ const onChainToggle = useViewSetting(viewSettings, (s) => s.onChainToggle);
2832
+ const selectedChains = useViewSetting(viewSettings, (s) => s.selectedChains);
2833
+ const { entries, isEmpty, residueCount } = useSequenceWrappers();
2834
+ const { copied, copySequence } = useCopySequence(entries);
2835
+ const drag = useCreateSequenceDrag();
2836
+ if (isEmpty) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_mui_material_styles.ThemeProvider, {
2837
+ theme,
2838
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmptyState, {
2839
+ backgroundColor,
2840
+ className: "msp-sequence",
2841
+ children: "No structure available"
2842
+ })
2843
+ });
2844
+ const residueColors = residueColorsForMode(mode);
2845
+ const showChainLabels = entries.length > 1;
2846
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_mui_material_styles.ThemeProvider, {
2847
+ theme,
2848
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(SequencePanel, {
2849
+ backgroundColor,
2850
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(PanelHeader, { children: [
2851
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PanelTitle, { children: "Sequence" }),
2852
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_czi_sds_components.Tooltip, {
2853
+ arrow: true,
2854
+ placement: "top",
2855
+ title: copied ? "Copied!" : "Copy",
2856
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CopyButton, {
2857
+ "aria-label": "Copy sequence to clipboard",
2858
+ onClick: copySequence,
2859
+ type: "button",
2860
+ children: copied ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_phosphor_icons_react.CheckIcon, { size: 16 }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_phosphor_icons_react.CopyIcon, { size: 16 })
2861
+ })
2862
+ }),
2863
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ResidueCount, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResidueCountLabel, { children: "Residues" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResidueCountValue, { children: residueCount })] })
2864
+ ] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SequenceScroller, {
2865
+ backgroundColor,
2866
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SequenceDragProvider, {
2867
+ value: drag,
2868
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SequenceScrollArea, {
2869
+ className: "msp-sequence msp-sequence-wrapper-non-empty",
2870
+ onMouseLeave: drag.clearAnchor,
2871
+ children: entries.map((s) => {
2872
+ var _hiddenChains$has, _selectedChains$has;
2873
+ const hidden = (_hiddenChains$has = hiddenChains === null || hiddenChains === void 0 ? void 0 : hiddenChains.has(s.chainId)) !== null && _hiddenChains$has !== void 0 ? _hiddenChains$has : false;
2874
+ const grid = typeof s.wrapper === "string" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2875
+ className: "msp-sequence-wrapper",
2876
+ children: s.wrapper
2877
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Sequence, {
2878
+ chainLabel: showChainLabels ? s.label : void 0,
2879
+ residueColors,
2880
+ sequenceWrapper: s.wrapper
2881
+ });
2882
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [showChainLabels && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChainHeader, {
2883
+ chainId: s.chainId,
2884
+ isHidden: hidden,
2885
+ isSelected: (_selectedChains$has = selectedChains === null || selectedChains === void 0 ? void 0 : selectedChains.has(s.chainId)) !== null && _selectedChains$has !== void 0 ? _selectedChains$has : false,
2886
+ label: s.label,
2887
+ onHover: onChainHover,
2888
+ onSelect: onChainSelect,
2889
+ onToggle: onChainToggle
2890
+ }), hidden ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HiddenChainGroup, { children: grid }) : grid] }, s.id);
2891
+ })
2892
+ })
2893
+ })
2894
+ })]
2895
+ })
2896
+ });
2897
+ };
2898
+ }
2899
+ //#endregion
2900
+ //#region src/core/ProteinStructureViewer/utils/axes.ts
2901
+ /**
2902
+ * The orientation axes widget, as Mol* parameterizes it.
2903
+ *
2904
+ * Shared rather than declared where it is used, because two different parts of
2905
+ * Mol* take the same shape: the live canvas takes it as
2906
+ * `camera.helper.axes`, and the screenshot helper takes it as its own `axes`,
2907
+ * so an image can carry the widget or leave it out independently of the view
2908
+ * it was captured from.
2909
+ */
2910
+ const AXES_ON = {
2911
+ name: "on",
2912
+ params: {
2913
+ alpha: .51,
2914
+ colorX: 16711680,
2915
+ colorY: 32768,
2916
+ colorZ: 255,
2917
+ labelColorX: 8421504,
2918
+ labelColorY: 8421504,
2919
+ labelColorZ: 8421504,
2920
+ labelOpacity: 1,
2921
+ labelScale: .25,
2922
+ labelX: "X",
2923
+ labelY: "Y",
2924
+ labelZ: "Z",
2925
+ location: "bottom-left",
2926
+ locationOffsetX: 0,
2927
+ locationOffsetY: 0,
2928
+ originColor: 8421504,
2929
+ planeColorXY: 8421504,
2930
+ planeColorXZ: 8421504,
2931
+ planeColorYZ: 8421504,
2932
+ radiusScale: .075,
2933
+ scale: .15,
2934
+ showLabels: false,
2935
+ showPlanes: true
2936
+ }
2937
+ };
2938
+ const AXES_OFF = {
2939
+ name: "off",
2940
+ params: {}
2941
+ };
2942
+ //#endregion
2943
+ //#region src/core/ProteinStructureViewer/utils/color.ts
2944
+ /** Parses `#RRGGBB` (or `RRGGBB`) into a Mol* `Color`. */
2945
+ function parseHexColor(hex) {
2946
+ const h = hex.replace("#", "");
2947
+ return molstar_lib_mol_util_color.Color.fromRgb(parseInt(h.substring(0, 2), 16), parseInt(h.substring(2, 4), 16), parseInt(h.substring(4, 6), 16));
2948
+ }
2949
+ /**
2950
+ * Parses a theme color into a Mol* `Color`, falling back when the token is
2951
+ * missing or is a format Mol* cannot consume (theme values are hex in practice,
2952
+ * but the palette types allow any CSS color string).
2953
+ */
2954
+ function themeColor(value, fallback) {
2955
+ const candidate = value === null || value === void 0 ? void 0 : value.trim();
2956
+ return parseHexColor(candidate !== void 0 && /^#?[0-9a-f]{6}$/i.test(candidate) ? candidate : fallback);
2007
2957
  }
2008
- //#endregion
2009
- //#region src/core/ProteinStructureViewer/components/Viewport/constants.ts
2010
2958
  /**
2011
- * Fraction of the bounding sphere radius within which the camera still counts
2012
- * as sitting at the default view, absorbing easing and float error.
2959
+ * Neutral fill for residues with no overlay value. Sits between the light and
2960
+ * dark backgrounds so unhighlighted geometry stays legible in both modes.
2013
2961
  */
2014
- const DEFAULT_VIEW_TOLERANCE = .02;
2015
- //#endregion
2016
- //#region src/core/ProteinStructureViewer/components/Viewport/utils/camera.ts
2017
- /** True when the camera's zoom/center differs from the default fit. */
2018
- function isZoomedOffDefault(canvas3d) {
2019
- const { center, radius } = canvas3d.boundingSphereVisible;
2020
- if (radius <= 0) return false;
2021
- const { state } = canvas3d.camera;
2022
- const tolerance = radius * DEFAULT_VIEW_TOLERANCE;
2023
- return Math.abs(state.radius - radius) > tolerance || molstar_lib_mol_math_linear_algebra.Vec3.distance(state.target, center) > tolerance;
2962
+ function neutralResidueColor(mode) {
2963
+ const g = mode === "dark" ? 100 : 180;
2964
+ return molstar_lib_mol_util_color.Color.fromRgb(g, g, g);
2024
2965
  }
2025
2966
  //#endregion
2026
2967
  //#region \0@oxc-project+runtime@0.134.0/helpers/esm/asyncToGenerator.js
@@ -2049,6 +2990,88 @@ function _asyncToGenerator(n) {
2049
2990
  };
2050
2991
  }
2051
2992
  //#endregion
2993
+ //#region src/core/ProteinStructureViewer/utils/screenshot.ts
2994
+ /**
2995
+ * Each resolution as the Mol* preset that produces it: 1280x720, 1920x1080,
2996
+ * 3840x2160, 7680x4320. Named here rather than given as dimensions so the
2997
+ * aspect stays Mol*'s to choose.
2998
+ *
2999
+ * Mol*'s own `viewport` preset - the canvas at its current size - is left out
3000
+ * deliberately: it makes the image depend on how big the viewer happens to be
3001
+ * on screen, which is not something a caller asking for "high" is asking for.
3002
+ */
3003
+ const RESOLUTION_PRESETS = {
3004
+ high: "ultra-hd",
3005
+ low: "hd",
3006
+ maximum: "8k-ultra-hd",
3007
+ medium: "full-hd"
3008
+ };
3009
+ /** Painted behind the structure when a background is asked for but unreadable. */
3010
+ const FALLBACK_BACKGROUND = "#ffffff";
3011
+ /**
3012
+ * Downloads the structure as a PNG.
3013
+ *
3014
+ * Mol*'s screenshot helper renders its own pass rather than reading the canvas
3015
+ * back, so the image is drawn fresh at the requested size and carries its own
3016
+ * transparency and axes settings - none of which disturb the view on screen.
3017
+ *
3018
+ * A named background is the one exception. The helper only chooses between
3019
+ * transparent and whatever the renderer is already painting, so a color has to
3020
+ * be put on the canvas for the length of the capture and taken off afterwards.
3021
+ * `finally` is what guarantees the second half: leaving a figure's background
3022
+ * behind on the viewer would be a worse bug than a failed download.
3023
+ */
3024
+ function downloadStructureImage(_x) {
3025
+ return _downloadStructureImage.apply(this, arguments);
3026
+ }
3027
+ function _downloadStructureImage() {
3028
+ _downloadStructureImage = _asyncToGenerator(function* (plugin, options = {}) {
3029
+ const helper = plugin.helpers.viewportScreenshot;
3030
+ if (!helper) return;
3031
+ const { backgroundColor, filename, resolution = "medium", showAxes } = options;
3032
+ const transparent = backgroundColor === void 0;
3033
+ helper.behaviors.values.next(_objectSpread2(_objectSpread2({}, helper.behaviors.values.value), {}, {
3034
+ axes: showAxes ? AXES_ON : AXES_OFF,
3035
+ format: {
3036
+ name: "png",
3037
+ params: {}
3038
+ },
3039
+ resolution: {
3040
+ name: RESOLUTION_PRESETS[resolution],
3041
+ params: {}
3042
+ },
3043
+ transparent
3044
+ }));
3045
+ const canvas3d = plugin.canvas3d;
3046
+ const painted = canvas3d === null || canvas3d === void 0 ? void 0 : canvas3d.props.renderer.backgroundColor;
3047
+ const overrideBackground = !transparent && canvas3d !== void 0;
3048
+ if (overrideBackground) canvas3d === null || canvas3d === void 0 || canvas3d.setProps({ renderer: { backgroundColor: themeColor(backgroundColor, FALLBACK_BACKGROUND) } });
3049
+ try {
3050
+ yield helper.download(filename);
3051
+ } finally {
3052
+ if (overrideBackground && painted !== void 0) canvas3d === null || canvas3d === void 0 || canvas3d.setProps({ renderer: { backgroundColor: painted } });
3053
+ }
3054
+ });
3055
+ return _downloadStructureImage.apply(this, arguments);
3056
+ }
3057
+ //#endregion
3058
+ //#region src/core/ProteinStructureViewer/components/Viewport/constants.ts
3059
+ /**
3060
+ * Fraction of the bounding sphere radius within which the camera still counts
3061
+ * as sitting at the default view, absorbing easing and float error.
3062
+ */
3063
+ const DEFAULT_VIEW_TOLERANCE = .02;
3064
+ //#endregion
3065
+ //#region src/core/ProteinStructureViewer/components/Viewport/utils/camera.ts
3066
+ /** True when the camera's zoom/center differs from the default fit. */
3067
+ function isZoomedOffDefault(canvas3d) {
3068
+ const { center, radius } = canvas3d.boundingSphereVisible;
3069
+ if (radius <= 0) return false;
3070
+ const { state } = canvas3d.camera;
3071
+ const tolerance = radius * DEFAULT_VIEW_TOLERANCE;
3072
+ return Math.abs(state.radius - radius) > tolerance || molstar_lib_mol_math_linear_algebra.Vec3.distance(state.target, center) > tolerance;
3073
+ }
3074
+ //#endregion
2052
3075
  //#region src/core/ProteinStructureViewer/components/Viewport/hooks/useCameraReset.ts
2053
3076
  /**
2054
3077
  * Tracks whether the camera has moved off the default view, and puts it back.
@@ -2149,16 +3172,24 @@ function useCameraReset() {
2149
3172
  /**
2150
3173
  * Pinned to the lower-left of the viewport, clear of the sequence panel below
2151
3174
  * and the axes widget in the corner.
3175
+ *
3176
+ * Anchored by its bottom edge, so the buttons stack upwards: the last one
3177
+ * stays put where a reader last saw it, and an earlier one appearing or going
3178
+ * does not move the rest. Reset comes first in the markup and so sits above
3179
+ * capture.
2152
3180
  */
2153
- const ResetCameraSlot = (0, _emotion_styled.default)("div")`
3181
+ const ViewportButtonStack = (0, _emotion_styled.default)("div")`
2154
3182
  position: absolute;
2155
3183
  bottom: 48px;
2156
3184
  z-index: 10;
3185
+ display: flex;
3186
+ flex-direction: column;
2157
3187
 
2158
3188
  ${(props) => {
2159
3189
  const spaces = (0, _czi_sds_components.getSpaces)(props);
2160
3190
  return `
2161
3191
  left: ${spaces === null || spaces === void 0 ? void 0 : spaces.m}px;
3192
+ gap: ${spaces === null || spaces === void 0 ? void 0 : spaces.xxs}px;
2162
3193
  `;
2163
3194
  }}
2164
3195
  `;
@@ -2176,8 +3207,30 @@ const ResetCameraSlot = (0, _emotion_styled.default)("div")`
2176
3207
  */
2177
3208
  function createViewportView(viewSettings) {
2178
3209
  return function ViewportWithReset() {
3210
+ const plugin = usePlugin();
2179
3211
  const { theme } = useMolstarTheme(viewSettings);
2180
3212
  const { cameraChanged, resetCamera } = useCameraReset();
3213
+ const showAxes = useViewSetting(viewSettings, (s) => {
3214
+ var _s$showAxes;
3215
+ return (_s$showAxes = s.showAxes) !== null && _s$showAxes !== void 0 ? _s$showAxes : true;
3216
+ });
3217
+ const download = useViewSetting(viewSettings, (s) => s.download);
3218
+ /**
3219
+ * Disabled while a capture is in flight. A high resolution takes long
3220
+ * enough to click twice through, and each click renders its own pass.
3221
+ */
3222
+ const [downloading, setDownloading] = (0, react.useState)(false);
3223
+ const captureImage = (0, react.useCallback)(_asyncToGenerator(function* () {
3224
+ if (!download) return;
3225
+ setDownloading(true);
3226
+ try {
3227
+ yield downloadStructureImage(plugin, download);
3228
+ } catch (error) {
3229
+ console.error("Failed to download the structure image:", error);
3230
+ } finally {
3231
+ setDownloading(false);
3232
+ }
3233
+ }), [download, plugin]);
2181
3234
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2182
3235
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(molstar_lib_mol_plugin_ui_viewport.Viewport, {}),
2183
3236
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -2186,18 +3239,37 @@ function createViewportView(viewSettings) {
2186
3239
  }),
2187
3240
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_mui_material_styles.ThemeProvider, {
2188
3241
  theme,
2189
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResetCameraSlot, {
2190
- hidden: !cameraChanged,
2191
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_czi_sds_components.Button, {
2192
- "aria-label": "Reset camera",
2193
- onClick: resetCamera,
3242
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ViewportButtonStack, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3243
+ hidden: !showAxes || !cameraChanged,
3244
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_czi_sds_components.Tooltip, {
3245
+ arrow: true,
3246
+ placement: "right",
2194
3247
  title: "Reset camera",
3248
+ textAlign: "left",
3249
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_czi_sds_components.Button, {
3250
+ "aria-label": "Reset camera",
3251
+ onClick: resetCamera,
3252
+ sdsStyle: "minimal",
3253
+ sdsType: "secondary",
3254
+ size: "medium",
3255
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_phosphor_icons_react.ArrowCounterClockwiseIcon, {})
3256
+ })
3257
+ })
3258
+ }), download && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_czi_sds_components.Tooltip, {
3259
+ arrow: true,
3260
+ placement: "right",
3261
+ title: "Download image",
3262
+ textAlign: "left",
3263
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_czi_sds_components.Button, {
3264
+ "aria-label": "Download image of the structure",
3265
+ disabled: downloading,
3266
+ onClick: captureImage,
2195
3267
  sdsStyle: "minimal",
2196
3268
  sdsType: "secondary",
2197
3269
  size: "medium",
2198
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_phosphor_icons_react.ArrowCounterClockwiseIcon, {})
3270
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_phosphor_icons_react.ApertureIcon, {})
2199
3271
  })
2200
- })
3272
+ })] })
2201
3273
  })
2202
3274
  ] });
2203
3275
  };
@@ -2277,37 +3349,274 @@ function syncClipToZoom(plugin, getClipRatio) {
2277
3349
  }
2278
3350
  }
2279
3351
  //#endregion
2280
- //#region src/core/ProteinStructureViewer/utils/residueRef.ts
3352
+ //#region src/core/ProteinStructureViewer/utils/chains.ts
2281
3353
  /**
2282
- * Reads the residue a loci points at, or null when it points at nothing the
2283
- * viewer can address.
3354
+ * The polymer of one chain, as named in the file. What each chain's cartoon is
3355
+ * built from, and so what makes a chain a thing Mol* can hide on its own.
2284
3356
  *
2285
- * `index` comes from Mol*'s residue key, which counts residues in file order
2286
- * from zero and is the same counter `injectPlddtIntoPdb` walks. Deriving it
2287
- * from the residue's *number* instead would tie it to how the file happens to
2288
- * be numbered: a crop beginning at residue 200 would index `plddt` at 199, and
2289
- * a file numbering each chain from 1 would give two residues the same index.
3357
+ * Paired with `chainLigandExpression`: between them they cover everything on a
3358
+ * chain the viewer draws, split by what it draws it as. The polymer
3359
+ * restriction is the one Mol*'s `polymer-cartoon` preset applied, and is what
3360
+ * keeps a heme from being handed to a representation that can only trace a
3361
+ * backbone.
2290
3362
  */
2291
- function residueRefFromLoci(loci) {
2292
- var _StructureProperties$;
2293
- const location = molstar_lib_mol_model_structure.StructureElement.Location.create(void 0);
2294
- const firstLoc = molstar_lib_mol_model_structure.StructureElement.Loci.getFirstLocation(loci, location);
2295
- if (!firstLoc) return null;
3363
+ function chainPolymerExpression(chainId) {
3364
+ return molstar_lib_mol_script_language_builder.MolScriptBuilder.struct.generator.atomGroups({
3365
+ "chain-test": molstar_lib_mol_script_language_builder.MolScriptBuilder.core.rel.eq([molstar_lib_mol_script_language_builder.MolScriptBuilder.ammp("auth_asym_id"), chainId]),
3366
+ "entity-test": molstar_lib_mol_script_language_builder.MolScriptBuilder.core.rel.eq([molstar_lib_mol_script_language_builder.MolScriptBuilder.ammp("entityType"), "polymer"])
3367
+ });
3368
+ }
3369
+ /**
3370
+ * Bonds worth following out of a ligand: the ones holding it to the protein.
3371
+ *
3372
+ * Mol*'s own pair, and metallic coordination is the half that matters most -
3373
+ * a heme's iron is held to its histidine by a coordination bond and not a
3374
+ * covalent one, so covalent alone would leave every metalloprotein's cofactor
3375
+ * looking dropped in rather than bound.
3376
+ */
3377
+ const CONNECTING_BOND = molstar_lib_mol_script_language_builder.MolScriptBuilder.core.flags.hasAny([molstar_lib_mol_script_language_builder.MolScriptBuilder.struct.bondProperty.flags(), molstar_lib_mol_script_language_builder.MolScriptBuilder.core.type.bitflags([molstar_lib_mol_model_structure_model_types.BondType.fromNames(["covalent", "metal-coordination"])])]);
3378
+ /**
3379
+ * The heteroatoms of one chain - ligands, ions, glycans, lipids - along with
3380
+ * the residues holding them, which is what its ball-and-stick is built from.
3381
+ *
3382
+ * `non-polymer` is the one entity type a PDB's HETATM records land under,
3383
+ * ligands and ions alike, so there is nothing finer to test here: the
3384
+ * distinction Mol*'s presets draw between their `ligand`, `ion` and `lipid`
3385
+ * components is a residue-name lookup made to give each a slightly different
3386
+ * style, and all three are sticks. `branched` is the separate type glycans
3387
+ * arrive as.
3388
+ *
3389
+ * Water is the third type and is left out of the seed. Every solvent molecule
3390
+ * drawn as sticks buries the structure they surround, which is why Mol* draws
3391
+ * them at 60% alpha rather than plain, and none of them is what "show the
3392
+ * ligands" asks for.
3393
+ *
3394
+ * One layer of bonds out from there, taken as whole residues, is what brings
3395
+ * the binding residue along - myoglobin's His93, say. Without it the heme
3396
+ * floats unattached in the middle of a cartoon it is visibly bonded to, and
3397
+ * the bond drawn to its iron ends in mid-air. One layer and no more: the
3398
+ * residue that holds the ligand, not that residue's own neighbours.
3399
+ *
3400
+ * Intersected back with the chain, so everything a chain is drawn with belongs
3401
+ * to that chain. A ligand bonded across an interface would otherwise leave the
3402
+ * partner chain's residue in this chain's component, somewhere hiding the
3403
+ * partner chain could not reach.
3404
+ */
3405
+ function chainLigandExpression(chainId) {
3406
+ const onThisChain = molstar_lib_mol_script_language_builder.MolScriptBuilder.core.rel.eq([molstar_lib_mol_script_language_builder.MolScriptBuilder.ammp("auth_asym_id"), chainId]);
3407
+ return molstar_lib_mol_script_language_builder.MolScriptBuilder.struct.modifier.union([molstar_lib_mol_script_language_builder.MolScriptBuilder.struct.modifier.intersectBy({
3408
+ 0: molstar_lib_mol_script_language_builder.MolScriptBuilder.struct.modifier.includeConnected({
3409
+ 0: molstar_lib_mol_script_language_builder.MolScriptBuilder.struct.generator.atomGroups({
3410
+ "chain-test": onThisChain,
3411
+ "entity-test": molstar_lib_mol_script_language_builder.MolScriptBuilder.core.set.has([molstar_lib_mol_script_language_builder.MolScriptBuilder.set("non-polymer", "branched"), molstar_lib_mol_script_language_builder.MolScriptBuilder.ammp("entityType")])
3412
+ }),
3413
+ "as-whole-residues": true,
3414
+ "bond-test": CONNECTING_BOND,
3415
+ "layer-count": 1
3416
+ }),
3417
+ by: molstar_lib_mol_script_language_builder.MolScriptBuilder.struct.generator.atomGroups({ "chain-test": onThisChain })
3418
+ })]);
3419
+ }
3420
+ /**
3421
+ * Matches how the sequence panel captions its chains, which reaches the same
3422
+ * label through Mol*'s own `getChainOptions`. Deriving it the same way is what
3423
+ * keeps a chain from going by one name in the legend and another in the panel.
3424
+ */
3425
+ const CHAIN_LABEL_OPTIONS = {
3426
+ granularity: "chain",
3427
+ hidePrefix: true,
3428
+ htmlStyling: false
3429
+ };
3430
+ /**
3431
+ * Reads the chains of a structure and the polymer residues on each.
3432
+ *
3433
+ * Residue indices are Mol*'s residue keys, counting residues in file order
3434
+ * across the whole structure, so what is reported here addresses the same
3435
+ * residues as `plddt`, `residueOverlay` and `selection`.
3436
+ *
3437
+ * Heteroatoms are drawn (see `chainLigandExpression`) but are not counted
3438
+ * here. A heme is a residue to the file and not to a reader: it is no part of
3439
+ * the sequence the panel lists, and averaging a confidence score over it would
3440
+ * divide by a residue that has none.
3441
+ *
3442
+ * Residues are gathered into a set rather than inferred from the ends: a chain
3443
+ * carrying several symmetry operators repeats each of its residues once per
3444
+ * operator, and a structure whose chains interleave leaves a chain's indices
3445
+ * non-contiguous. Neither is what a predicted complex looks like, but a range
3446
+ * would misread both.
3447
+ */
3448
+ function scanChains(structure) {
3449
+ const accumulators = /* @__PURE__ */ new Map();
3450
+ const location = molstar_lib_mol_model_structure.StructureElement.Location.create(structure);
3451
+ for (const unit of structure.units) {
3452
+ location.unit = unit;
3453
+ for (let i = 0; i < unit.elements.length; i++) {
3454
+ location.element = unit.elements[i];
3455
+ const chainId = molstar_lib_mol_model_structure.StructureProperties.chain.auth_asym_id(location);
3456
+ let chain = accumulators.get(chainId);
3457
+ if (!chain) {
3458
+ chain = {
3459
+ chainId,
3460
+ label: (0, molstar_lib_mol_theme_label.elementLabel)(location, CHAIN_LABEL_OPTIONS),
3461
+ residues: /* @__PURE__ */ new Set()
3462
+ };
3463
+ accumulators.set(chainId, chain);
3464
+ }
3465
+ if (molstar_lib_mol_model_structure.StructureProperties.entity.type(location) !== "polymer") continue;
3466
+ chain.residues.add(molstar_lib_mol_model_structure.StructureProperties.residue.key(location));
3467
+ }
3468
+ }
3469
+ 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) => {
3470
+ var _a$residues$, _b$residues$;
3471
+ 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);
3472
+ });
2296
3473
  return {
2297
- chainId: molstar_lib_mol_model_structure.StructureProperties.chain.auth_asym_id(firstLoc),
2298
- compId: molstar_lib_mol_model_structure.StructureProperties.atom.label_comp_id(firstLoc),
2299
- insCode: (_StructureProperties$ = molstar_lib_mol_model_structure.StructureProperties.residue.pdbx_PDB_ins_code(firstLoc)) !== null && _StructureProperties$ !== void 0 ? _StructureProperties$ : "",
2300
- index: molstar_lib_mol_model_structure.StructureProperties.residue.key(firstLoc),
2301
- seqId: molstar_lib_mol_model_structure.StructureProperties.residue.auth_seq_id(firstLoc)
3474
+ chainLabels: new Map([...accumulators.values()].map(({ chainId, label }) => [chainId, label])),
3475
+ chains: ordered.map(({ chainId, label, residues }) => {
3476
+ var _residues, _residues$;
3477
+ return {
3478
+ chainId,
3479
+ endIndex: (_residues = residues[residues.length - 1]) !== null && _residues !== void 0 ? _residues : 0,
3480
+ label,
3481
+ residueCount: residues.length,
3482
+ startIndex: (_residues$ = residues[0]) !== null && _residues$ !== void 0 ? _residues$ : 0
3483
+ };
3484
+ }),
3485
+ residuesByChain: new Map(ordered.map(({ chainId, residues }) => [chainId, residues]))
2302
3486
  };
2303
3487
  }
3488
+ /** True when the two lists name the same chains, in the same order. */
3489
+ function chainsEqual(a, b) {
3490
+ return a.length === b.length && a.every((chain, i) => {
3491
+ const other = b[i];
3492
+ return other !== void 0 && chain.chainId === other.chainId && chain.startIndex === other.startIndex && chain.endIndex === other.endIndex && chain.residueCount === other.residueCount;
3493
+ });
3494
+ }
3495
+ //#endregion
3496
+ //#region src/core/ProteinStructureViewer/utils/molstarSpec.ts
2304
3497
  /**
2305
- * How a residue is named in the readout, e.g. `"LYS 111"`. Takes the number and
2306
- * the insertion code from the file so that the readout and the sequence panel
2307
- * beneath it agree on what to call a residue.
3498
+ * The spec's list-valued keys. These are appended rather than merged: Mol*
3499
+ * reads them as collections, so a consumer's entries join the viewer's instead
3500
+ * of replacing them.
3501
+ *
3502
+ * `config` is the one where order carries meaning - Mol* folds the list into a
3503
+ * Map in order, so a later entry overwrites an earlier one, and appending is
3504
+ * what lets a consumer override a setting the viewer chose.
2308
3505
  */
2309
- function residueLabel(residue) {
2310
- return `${residue.compId} ${residue.seqId}${residue.insCode}`;
3506
+ const LIST_KEYS = [
3507
+ "actions",
3508
+ "animations",
3509
+ "behaviors",
3510
+ "config",
3511
+ "customFormats",
3512
+ "customParamEditors"
3513
+ ];
3514
+ /**
3515
+ * Whether a value should be merged into rather than replaced.
3516
+ *
3517
+ * Only object literals qualify. Arrays are collections, and the spec is full
3518
+ * of React components and Mol* class instances, all of which are values in
3519
+ * their own right - merging into one would take it apart.
3520
+ */
3521
+ function isPlainObject(value) {
3522
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
3523
+ const proto = Object.getPrototypeOf(value);
3524
+ return proto === Object.prototype || proto === null;
3525
+ }
3526
+ function deepMerge(base, override) {
3527
+ if (!isPlainObject(base) || !isPlainObject(override)) return override;
3528
+ const merged = _objectSpread2({}, base);
3529
+ for (const [key, value] of Object.entries(override)) merged[key] = key in base ? deepMerge(base[key], value) : value;
3530
+ return merged;
3531
+ }
3532
+ function withoutLists(spec) {
3533
+ const rest = _objectSpread2({}, spec);
3534
+ for (const key of LIST_KEYS) delete rest[key];
3535
+ return rest;
3536
+ }
3537
+ /**
3538
+ * Lays a consumer's Mol* spec over the viewer's own.
3539
+ *
3540
+ * The viewer builds a spec of its own - the sequence panel and viewport it
3541
+ * renders, the controls it hides, the behaviors it drops - and this is how a
3542
+ * consumer reaches the rest of Mol* without the viewer having to grow a prop
3543
+ * per setting. Anything the consumer names wins, so `canvas3d.renderer` or a
3544
+ * `PluginConfig` item can be set to something other than the default the
3545
+ * viewer picked.
3546
+ *
3547
+ * The one thing it cannot do is take something away: `behaviors` is appended
3548
+ * to, so a behavior the viewer removed - Mol*'s click-to-zoom camera - stays
3549
+ * removed, and one the viewer kept cannot be dropped.
3550
+ */
3551
+ function mergeMolstarSpec(base, override) {
3552
+ if (!override) return base;
3553
+ const merged = deepMerge(withoutLists(base), withoutLists(override));
3554
+ for (const key of LIST_KEYS) {
3555
+ var _base, _override;
3556
+ const ours = (_base = base[key]) !== null && _base !== void 0 ? _base : [];
3557
+ const theirs = (_override = override[key]) !== null && _override !== void 0 ? _override : [];
3558
+ if (ours.length > 0 || theirs.length > 0) merged[key] = [...ours, ...theirs];
3559
+ }
3560
+ return merged;
3561
+ }
3562
+ //#endregion
3563
+ //#region src/core/ProteinStructureViewer/utils/chainColorTheme.ts
3564
+ /** Mol* color theme name under which per-chain coloring is registered. */
3565
+ const CHAIN_COLOR_THEME_NAME = "chain-color";
3566
+ /** Painted for a chain with no color assigned, or one assigned a bad value. */
3567
+ const DEFAULT_CHAIN_COLOR_HEX = "#808080";
3568
+ const DEFAULT_CHAIN_COLOR = molstar_lib_mol_util_color.Color.fromRgb(128, 128, 128);
3569
+ /**
3570
+ * Builds a Mol* color theme that paints each chain its own color.
3571
+ *
3572
+ * This stands in for Mol*'s built-in `chain-id` theme, which picks its own
3573
+ * colors from a palette we cannot read back. Owning the assignment is what
3574
+ * lets the chain legend show a swatch that matches the structure, rather than
3575
+ * a key for colors it has to guess at.
3576
+ *
3577
+ * Created per plugin instance, and its state read live inside `color()`, for
3578
+ * the same reasons as the residue value theme beside it: several viewers can
3579
+ * color chains differently on one page, and recoloring does not rebuild the
3580
+ * structure.
3581
+ */
3582
+ function createChainColorTheme() {
3583
+ const state = {
3584
+ colors: /* @__PURE__ */ new Map(),
3585
+ version: 0
3586
+ };
3587
+ const provider = {
3588
+ category: "chain property",
3589
+ defaultValues: {},
3590
+ factory() {
3591
+ const { colors } = state;
3592
+ const parsed = /* @__PURE__ */ new Map();
3593
+ for (const [chainId, hex] of colors) parsed.set(chainId, themeColor(hex, DEFAULT_CHAIN_COLOR_HEX));
3594
+ return {
3595
+ color(location) {
3596
+ var _parsed$get;
3597
+ if (!molstar_lib_mol_model_structure.StructureElement.Location.is(location)) return DEFAULT_CHAIN_COLOR;
3598
+ const chainId = molstar_lib_mol_model_structure.StructureProperties.chain.auth_asym_id(location);
3599
+ return (_parsed$get = parsed.get(chainId)) !== null && _parsed$get !== void 0 ? _parsed$get : DEFAULT_CHAIN_COLOR;
3600
+ },
3601
+ contextHash: state.version,
3602
+ description: "Color residues by the chain they sit on",
3603
+ factory: provider.factory,
3604
+ granularity: "group",
3605
+ props: {}
3606
+ };
3607
+ },
3608
+ getParams: () => ({}),
3609
+ isApplicable: () => true,
3610
+ label: "Chain Color",
3611
+ name: CHAIN_COLOR_THEME_NAME
3612
+ };
3613
+ return {
3614
+ provider,
3615
+ setState(colors) {
3616
+ state.colors = colors;
3617
+ state.version++;
3618
+ }
3619
+ };
2311
3620
  }
2312
3621
  //#endregion
2313
3622
  //#region src/core/ProteinStructureViewer/utils/plddt.ts
@@ -2343,6 +3652,14 @@ const PLDDT_COLOR_SCALE = {
2343
3652
  ]
2344
3653
  };
2345
3654
  const PLDDT_BANDS = PLDDT_COLOR_SCALE.kind === "stepped" ? PLDDT_COLOR_SCALE.stops : [];
3655
+ /**
3656
+ * The band colors alone, ascending from least to most confident.
3657
+ *
3658
+ * What the chain legend shows in place of a single color while pLDDT is
3659
+ * painting the structure: no one color stands for a chain then, so the swatch
3660
+ * carries the whole key instead.
3661
+ */
3662
+ const PLDDT_BAND_COLORS = PLDDT_BANDS.map((stop) => stop.color);
2346
3663
  const DEFAULT_PLDDT_COLOR = molstar_lib_mol_util_color.Color.fromRgb(128, 128, 128);
2347
3664
  /**
2348
3665
  * Maps a pLDDT confidence score on the 0-100 B-factor scale to its band color.
@@ -2383,48 +3700,24 @@ function injectPlddtIntoPdb(pdbData, plddtValues) {
2383
3700
  }
2384
3701
  /**
2385
3702
  * Mol* color theme that reads pLDDT scores out of the B-factor column, paired
2386
- * with `injectPlddtIntoPdb`. Registered per plugin instance.
2387
- */
2388
- const PlddtColoring = molstar_lib_mol_model_props_common_custom_element_property.CustomElementProperty.create({
2389
- coloring: {
2390
- defaultColor: DEFAULT_PLDDT_COLOR,
2391
- getColor(e) {
2392
- return plddtToColor(e);
2393
- }
2394
- },
2395
- getData(model) {
2396
- const map = /* @__PURE__ */ new Map();
2397
- const atomCount = model.atomicHierarchy.atoms._rowCount;
2398
- for (let i = 0; i < atomCount; i++) map.set(i, model.atomicConformation.B_iso_or_equiv.value(i));
2399
- return { value: map };
2400
- },
2401
- label: "pLDDT",
2402
- name: PLDDT_THEME_NAME
2403
- });
2404
- //#endregion
2405
- //#region src/core/ProteinStructureViewer/utils/color.ts
2406
- /** Parses `#RRGGBB` (or `RRGGBB`) into a Mol* `Color`. */
2407
- function parseHexColor(hex) {
2408
- const h = hex.replace("#", "");
2409
- return molstar_lib_mol_util_color.Color.fromRgb(parseInt(h.substring(0, 2), 16), parseInt(h.substring(2, 4), 16), parseInt(h.substring(4, 6), 16));
2410
- }
2411
- /**
2412
- * Parses a theme color into a Mol* `Color`, falling back when the token is
2413
- * missing or is a format Mol* cannot consume (theme values are hex in practice,
2414
- * but the palette types allow any CSS color string).
2415
- */
2416
- function themeColor(value, fallback) {
2417
- const candidate = value === null || value === void 0 ? void 0 : value.trim();
2418
- return parseHexColor(candidate !== void 0 && /^#?[0-9a-f]{6}$/i.test(candidate) ? candidate : fallback);
2419
- }
2420
- /**
2421
- * Neutral fill for residues with no overlay value. Sits between the light and
2422
- * dark backgrounds so unhighlighted geometry stays legible in both modes.
2423
- */
2424
- function neutralResidueColor(mode) {
2425
- const g = mode === "dark" ? 100 : 180;
2426
- return molstar_lib_mol_util_color.Color.fromRgb(g, g, g);
2427
- }
3703
+ * with `injectPlddtIntoPdb`. Registered per plugin instance.
3704
+ */
3705
+ const PlddtColoring = molstar_lib_mol_model_props_common_custom_element_property.CustomElementProperty.create({
3706
+ coloring: {
3707
+ defaultColor: DEFAULT_PLDDT_COLOR,
3708
+ getColor(e) {
3709
+ return plddtToColor(e);
3710
+ }
3711
+ },
3712
+ getData(model) {
3713
+ const map = /* @__PURE__ */ new Map();
3714
+ const atomCount = model.atomicHierarchy.atoms._rowCount;
3715
+ for (let i = 0; i < atomCount; i++) map.set(i, model.atomicConformation.B_iso_or_equiv.value(i));
3716
+ return { value: map };
3717
+ },
3718
+ label: "pLDDT",
3719
+ name: PLDDT_THEME_NAME
3720
+ });
2428
3721
  //#endregion
2429
3722
  //#region src/core/ProteinStructureViewer/utils/residueValueTheme.ts
2430
3723
  /** Mol* color theme name under which residue value overlays are registered. */
@@ -2483,10 +3776,13 @@ function createResidueValueTheme(mode) {
2483
3776
  }
2484
3777
  //#endregion
2485
3778
  //#region src/core/ProteinStructureViewer/hooks/useMolstarPlugin.ts
2486
- /** Structure representation Mol* builds for the loaded model. */
2487
- const REPRESENTATION_PRESET = "polymer-cartoon";
2488
- /** Theme used when no pLDDT scores are supplied. */
2489
- const FALLBACK_THEME_NAME = "chain-id";
3779
+ /**
3780
+ * Theme used when neither pLDDT scores nor an overlay are supplied. Our own
3781
+ * per-chain theme rather than Mol*'s built-in `chain-id`, so the chain legend
3782
+ * can show swatches that match the structure instead of guessing at the colors
3783
+ * Mol* picked.
3784
+ */
3785
+ const FALLBACK_THEME_NAME = CHAIN_COLOR_THEME_NAME;
2490
3786
  /** Delay before retrying initialization while the container has no size. */
2491
3787
  const LAYOUT_RETRY_MS = 100;
2492
3788
  /** How long to wait for a frame before starting without one. */
@@ -2523,38 +3819,6 @@ function nextFrame() {
2523
3819
  */
2524
3820
  const HIGHLIGHT_STRENGTH = .2;
2525
3821
  const SELECT_STRENGTH = 0;
2526
- const AXES_ON = {
2527
- name: "on",
2528
- params: {
2529
- alpha: .51,
2530
- colorX: 16711680,
2531
- colorY: 32768,
2532
- colorZ: 255,
2533
- labelColorX: 8421504,
2534
- labelColorY: 8421504,
2535
- labelColorZ: 8421504,
2536
- labelOpacity: 1,
2537
- labelScale: .25,
2538
- labelX: "X",
2539
- labelY: "Y",
2540
- labelZ: "Z",
2541
- location: "bottom-left",
2542
- locationOffsetX: 0,
2543
- locationOffsetY: 0,
2544
- originColor: 8421504,
2545
- planeColorXY: 8421504,
2546
- planeColorXZ: 8421504,
2547
- planeColorYZ: 8421504,
2548
- radiusScale: .075,
2549
- scale: .15,
2550
- showLabels: false,
2551
- showPlanes: true
2552
- }
2553
- };
2554
- const AXES_OFF = {
2555
- name: "off",
2556
- params: {}
2557
- };
2558
3822
  /**
2559
3823
  * Turns the screenshot controls off, on the versions of Mol* that have them.
2560
3824
  *
@@ -2580,13 +3844,13 @@ function createViewer(_x) {
2580
3844
  return _createViewer.apply(this, arguments);
2581
3845
  }
2582
3846
  function _createViewer() {
2583
- _createViewer = _asyncToGenerator(function* ({ backgroundColor, edgeColor, highlightColor, residueValueTheme, root, showAxes, showSequenceViewer, viewSettings }) {
3847
+ _createViewer = _asyncToGenerator(function* ({ backgroundColor, chainColorTheme, edgeColor, highlightColor, molstarSpec, residueValueTheme, root, showAxes, showSequenceViewer, viewSettings }) {
2584
3848
  var _spec$config;
2585
3849
  const spec = (0, molstar_lib_mol_plugin_ui_spec.DefaultPluginUISpec)();
2586
3850
  const behaviors = spec.behaviors.filter((b) => b.transformer !== molstar_lib_mol_plugin_behavior.PluginBehaviors.Camera.FocusLoci);
2587
3851
  const plugin = yield (0, molstar_lib_mol_plugin_ui.createPluginUI)({
2588
3852
  render: molstar_lib_mol_plugin_ui_react18.renderReact18,
2589
- spec: _objectSpread2(_objectSpread2({}, spec), {}, {
3853
+ spec: mergeMolstarSpec(_objectSpread2(_objectSpread2({}, spec), {}, {
2590
3854
  behaviors,
2591
3855
  canvas3d: {
2592
3856
  camera: { helper: { axes: showAxes ? AXES_ON : AXES_OFF } },
@@ -2602,10 +3866,16 @@ function _createViewer() {
2602
3866
  selectStrength: SELECT_STRENGTH
2603
3867
  }
2604
3868
  },
2605
- components: _objectSpread2({
3869
+ components: {
3870
+ controls: {
3871
+ bottom: "none",
3872
+ left: "none",
3873
+ right: "none"
3874
+ },
2606
3875
  remoteState: "none",
2607
- sequenceViewer: { view: createSequenceView(viewSettings) }
2608
- }, showAxes && { viewport: { view: createViewportView(viewSettings) } }),
3876
+ sequenceViewer: { view: createSequenceView(viewSettings) },
3877
+ viewport: { view: createViewportView(viewSettings) }
3878
+ },
2609
3879
  config: [
2610
3880
  ...(_spec$config = spec.config) !== null && _spec$config !== void 0 ? _spec$config : [],
2611
3881
  [molstar_lib_mol_plugin_config.PluginConfig.Viewport.ShowExpand, false],
@@ -2626,12 +3896,13 @@ function _createViewer() {
2626
3896
  top: showSequenceViewer ? "full" : "hidden"
2627
3897
  }
2628
3898
  } }
2629
- }),
3899
+ }), molstarSpec),
2630
3900
  target: root
2631
3901
  });
2632
3902
  const registry = plugin.representation.structure.themes.colorThemeRegistry;
2633
3903
  if (PlddtColoring.colorThemeProvider) registry.add(PlddtColoring.colorThemeProvider);
2634
3904
  registry.add(residueValueTheme.provider);
3905
+ registry.add(chainColorTheme.provider);
2635
3906
  for (const p of [...plugin.managers.lociLabels.providers]) plugin.managers.lociLabels.removeProvider(p);
2636
3907
  plugin.managers.structure.focus.behaviors.current.subscribe((entry) => {
2637
3908
  const select = plugin.managers.interactivity.lociSelects;
@@ -2642,7 +3913,112 @@ function _createViewer() {
2642
3913
  });
2643
3914
  return _createViewer.apply(this, arguments);
2644
3915
  }
2645
- function loadStructure(_x2, _x3, _x4, _x5) {
3916
+ /**
3917
+ * The representation parameters Mol*'s own presets compute, read from the same
3918
+ * manager state they read. Ported rather than hardcoded so a chain drawn here
3919
+ * looks exactly like the one the preset used to draw. Mol* hands the same set
3920
+ * to its cartoons and its ball-and-sticks, and so does this.
3921
+ */
3922
+ function representationTypeParams(plugin) {
3923
+ const { hydrogens, ignoreLight, visualQuality } = plugin.managers.structure.component.state.options;
3924
+ return {
3925
+ ignoreHydrogens: hydrogens !== "all",
3926
+ ignoreHydrogensVariant: hydrogens === "only-polar" ? "non-polar" : "all",
3927
+ ignoreLight,
3928
+ quality: visualQuality
3929
+ };
3930
+ }
3931
+ /**
3932
+ * The two halves of a chain the viewer draws. Each names the component its
3933
+ * half is built under and tags the representation drawn over it, and it is
3934
+ * that tag `applyColorTheme` reads back to tell the two apart.
3935
+ */
3936
+ const POLYMER_PART = "polymer";
3937
+ const LIGAND_PART = "ligand";
3938
+ /**
3939
+ * True for the representation drawing a chain's heteroatoms.
3940
+ *
3941
+ * Read off the tag put on the representation when it was built, rather than
3942
+ * off the component's key. Mol* does not store the key as it was given: it
3943
+ * files components under `structure-component-<key>`, so a test against the
3944
+ * key as written here silently matches nothing, and every ligand quietly takes
3945
+ * the structure-wide theme instead. The tag survives untouched.
3946
+ */
3947
+ function isLigandRepresentation(representation) {
3948
+ var _representation$cell$;
3949
+ return ((_representation$cell$ = representation.cell.transform.tags) === null || _representation$cell$ === void 0 ? void 0 : _representation$cell$.includes(LIGAND_PART)) === true;
3950
+ }
3951
+ /**
3952
+ * How heteroatoms are colored: the theme Mol* puts on a ball-and-stick when
3953
+ * left to itself, with the parameters it chooses too.
3954
+ *
3955
+ * Named rather than left blank only so that it survives `applyColorTheme`,
3956
+ * which would otherwise repaint the ligands along with everything else. The
3957
+ * structure-wide theme is the wrong answer for them twice over: a heme reads
3958
+ * as a heme because its iron is orange and its nitrogens are blue, which one
3959
+ * flat color throws away, and pLDDT and residue overlays have no value for a
3960
+ * HETATM to be colored by at all.
3961
+ */
3962
+ const ELEMENT_THEME = "element-symbol";
3963
+ /**
3964
+ * Draws one chain: its polymer as a cartoon, its ligands and ions as
3965
+ * ball-and-stick.
3966
+ *
3967
+ * Two components rather than one, because a component is what a representation
3968
+ * attaches to and neither representation suits both halves - a cartoon has no
3969
+ * backbone to trace through a heme, and a ball-and-stick over the whole chain
3970
+ * would draw every protein atom as a sphere.
3971
+ *
3972
+ * Hands back the refs of whichever components the chain turned out to have,
3973
+ * which is what `setSubtreeVisibility` is later pointed at. Grouping them
3974
+ * under the chain is what makes hiding a chain take its ligands with it.
3975
+ */
3976
+ function buildChainComponents(_x5, _x6, _x7, _x8) {
3977
+ return _buildChainComponents.apply(this, arguments);
3978
+ }
3979
+ function _buildChainComponents() {
3980
+ _buildChainComponents = _asyncToGenerator(function* (plugin, structure, chain, typeParams) {
3981
+ const builders = plugin.builders.structure;
3982
+ const refs = [];
3983
+ const addPart = function() {
3984
+ var _ref = _asyncToGenerator(function* (part, expression, props) {
3985
+ const component = yield builders.tryCreateComponentFromExpression(structure, expression, `${part}-${chain.chainId}`, { label: chain.label });
3986
+ if (!component) return;
3987
+ yield builders.representation.addRepresentation(component, props, { tag: part });
3988
+ refs.push(component.ref);
3989
+ });
3990
+ return function addPart(_x2, _x3, _x4) {
3991
+ return _ref.apply(this, arguments);
3992
+ };
3993
+ }();
3994
+ yield addPart(POLYMER_PART, chainPolymerExpression(chain.chainId), {
3995
+ type: "cartoon",
3996
+ typeParams
3997
+ });
3998
+ yield addPart(LIGAND_PART, chainLigandExpression(chain.chainId), {
3999
+ color: ELEMENT_THEME,
4000
+ type: "ball-and-stick",
4001
+ typeParams
4002
+ });
4003
+ return refs;
4004
+ });
4005
+ return _buildChainComponents.apply(this, arguments);
4006
+ }
4007
+ const NOTHING_LOADED = {
4008
+ chains: [],
4009
+ componentRefs: /* @__PURE__ */ new Map(),
4010
+ residuesByChain: /* @__PURE__ */ new Map()
4011
+ };
4012
+ /**
4013
+ * Parses the PDB and draws each chain: a cartoon over its polymer, and
4014
+ * ball-and-stick over its ligands and ions.
4015
+ *
4016
+ * Mol*'s `default` hierarchy preset would be shorter, but it groups every
4017
+ * polymer chain into a single component, and a component is the unit Mol* can
4018
+ * hide. Building them per chain is what makes a chain individually hideable;
4019
+ * the refs handed back are what `setSubtreeVisibility` is later pointed at.
4020
+ */
4021
+ function loadStructure(_x9, _x10, _x11, _x12) {
2646
4022
  return _loadStructure.apply(this, arguments);
2647
4023
  }
2648
4024
  function _loadStructure() {
@@ -2655,31 +4031,69 @@ function _loadStructure() {
2655
4031
  label: "Structure"
2656
4032
  });
2657
4033
  const trajectory = yield plugin.builders.structure.parseTrajectory(data, "pdb");
2658
- yield plugin.builders.structure.hierarchy.applyPreset(trajectory, "default", {
2659
- representationPreset: REPRESENTATION_PRESET,
2660
- showUnitcell: false,
2661
- structure: {
2662
- name: "model",
2663
- params: {}
2664
- }
4034
+ const model = yield plugin.builders.structure.createModel(trajectory);
4035
+ const structure = yield plugin.builders.structure.createStructure(model, {
4036
+ name: "model",
4037
+ params: {}
2665
4038
  });
4039
+ const data3d = structure.data;
4040
+ if (!data3d) return NOTHING_LOADED;
4041
+ const { chainLabels, chains, residuesByChain } = scanChains(data3d);
4042
+ const typeParams = representationTypeParams(plugin);
4043
+ const componentRefs = /* @__PURE__ */ new Map();
4044
+ for (const [chainId, label] of chainLabels) {
4045
+ const refs = yield buildChainComponents(plugin, structure, {
4046
+ chainId,
4047
+ label
4048
+ }, typeParams);
4049
+ if (refs.length > 0) componentRefs.set(chainId, refs);
4050
+ }
2666
4051
  if (usePlddtColoring) yield applyColorTheme(plugin, PLDDT_THEME_NAME);
2667
4052
  (_plugin$canvas3d = plugin.canvas3d) === null || _plugin$canvas3d === void 0 || _plugin$canvas3d.requestCameraReset();
2668
4053
  setAxes(plugin, showAxes);
4054
+ return {
4055
+ chains,
4056
+ componentRefs,
4057
+ residuesByChain
4058
+ };
2669
4059
  } catch (error) {
2670
4060
  console.error("Failed to load structure:", error);
4061
+ return NOTHING_LOADED;
2671
4062
  }
2672
4063
  });
2673
4064
  return _loadStructure.apply(this, arguments);
2674
4065
  }
2675
- /** Recolors every loaded structure's representations with the named theme. */
2676
- function applyColorTheme(_x6, _x7) {
4066
+ /**
4067
+ * Shows or hides everything each chain is drawn with, cartoon and sticks
4068
+ * alike. Every component under the chain, so hiding it takes its ligands with
4069
+ * it rather than leaving a heme floating where its protein used to be.
4070
+ *
4071
+ * The whole subtree of each, not just the component cell: visibility is
4072
+ * per-cell, and it is the representation underneath that actually draws.
4073
+ *
4074
+ * Set outright rather than through the component manager's `toggleVisibility`,
4075
+ * which flips whatever it finds: the hidden set is derived from props on every
4076
+ * pass, so it has to be applied as a statement of what should be true and not
4077
+ * as a change to what already is.
4078
+ */
4079
+ function applyChainVisibility(plugin, componentRefs, hidden) {
4080
+ for (const [chainId, refs] of componentRefs) for (const ref of refs) (0, molstar_lib_mol_plugin_behavior_static_state.setSubtreeVisibility)(plugin.state.data, ref, hidden.has(chainId));
4081
+ }
4082
+ /**
4083
+ * Recolors every loaded structure's representations with the named theme,
4084
+ * leaving the heteroatoms on `ELEMENT_THEME`.
4085
+ *
4086
+ * The per-representation form of `updateRepresentationsTheme`, so the two can
4087
+ * be told apart in one pass; handing it a filtered list instead would leave
4088
+ * the ligands on whichever theme happened to be current when they were built.
4089
+ */
4090
+ function applyColorTheme(_x13, _x14) {
2677
4091
  return _applyColorTheme.apply(this, arguments);
2678
4092
  }
2679
4093
  function _applyColorTheme() {
2680
4094
  _applyColorTheme = _asyncToGenerator(function* (plugin, colorTheme) {
2681
4095
  yield plugin.dataTransaction(_asyncToGenerator(function* () {
2682
- for (const s of plugin.managers.structure.hierarchy.current.structures) yield plugin.managers.structure.component.updateRepresentationsTheme(s.components, { color: colorTheme });
4096
+ 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 }));
2683
4097
  }));
2684
4098
  });
2685
4099
  return _applyColorTheme.apply(this, arguments);
@@ -2693,11 +4107,35 @@ function _applyColorTheme() {
2693
4107
  * axes, coloring, new PDB) are pushed in through the effects below and in the
2694
4108
  * sibling hooks rather than by rebuilding.
2695
4109
  */
2696
- function useMolstarPlugin({ backgroundColor, containerRef, edgeColor, hasPlddt, highlightColor, mode, onResidueClick, onResidueHover, onSelectionClear, pdb, sequenceViewerBackgroundColor, showAxes, showSequenceViewer }) {
4110
+ function useMolstarPlugin({ backgroundColor, chainColors, containerRef, download, edgeColor, hasPlddt, hiddenChains, highlightColor, mode, molstarSpec, onChainHover, onChainSelect, onChainToggle, onResidueClick, onResidueHover, onSelectionChange, onSelectionClear, pdb, selectedChains, sequenceViewerBackgroundColor, showAxes, showSequenceViewer }) {
2697
4111
  const pluginRef = (0, react.useRef)(null);
2698
4112
  const residueValueThemeRef = (0, react.useRef)(null);
4113
+ const chainColorThemeRef = (0, react.useRef)(null);
2699
4114
  const currentPdbRef = (0, react.useRef)(null);
2700
4115
  const [isReady, setIsReady] = (0, react.useState)(false);
4116
+ const [chains, setChains] = (0, react.useState)([]);
4117
+ /**
4118
+ * Where each chain's components live in the state tree, which is what the
4119
+ * visibility effect points `setSubtreeVisibility` at. Rewritten by every
4120
+ * load, since the refs do not survive `plugin.clear()`.
4121
+ */
4122
+ const componentRefsRef = (0, react.useRef)(/* @__PURE__ */ new Map());
4123
+ /**
4124
+ * Which residues sit on each chain, for the readout to average a score over
4125
+ * a whole-chain selection. A ref rather than state: nothing renders from it
4126
+ * directly, and it is rewritten in step with `chains`, which does.
4127
+ */
4128
+ const residuesByChainRef = (0, react.useRef)(/* @__PURE__ */ new Map());
4129
+ /**
4130
+ * The visibility and coloring to apply as soon as a structure exists. Read
4131
+ * through refs because both arrive as fresh objects on every render, and a
4132
+ * load has to be able to consult them without the load effect depending on
4133
+ * them - a new Set each render would reload the structure on every pass.
4134
+ */
4135
+ const hiddenChainsRef = (0, react.useRef)(hiddenChains);
4136
+ hiddenChainsRef.current = hiddenChains;
4137
+ const chainColorsRef = (0, react.useRef)(chainColors);
4138
+ chainColorsRef.current = chainColors;
2701
4139
  /**
2702
4140
  * Carries the theme-dependent props into the views Mol* renders in its own
2703
4141
  * React root. They cannot read them from context, and rebuilding the plugin
@@ -2706,7 +4144,8 @@ function useMolstarPlugin({ backgroundColor, containerRef, edgeColor, hasPlddt,
2706
4144
  const viewSettingsRef = (0, react.useRef)(null);
2707
4145
  if (viewSettingsRef.current === null) viewSettingsRef.current = new rxjs.BehaviorSubject({
2708
4146
  mode,
2709
- sequenceViewerBackgroundColor
4147
+ sequenceViewerBackgroundColor,
4148
+ showAxes
2710
4149
  });
2711
4150
  const viewSettings = viewSettingsRef.current;
2712
4151
  /**
@@ -2717,10 +4156,27 @@ function useMolstarPlugin({ backgroundColor, containerRef, edgeColor, hasPlddt,
2717
4156
  const setClipRatio = (0, react.useCallback)((ratio) => {
2718
4157
  clipRatioRef.current = ratio;
2719
4158
  }, []);
4159
+ /**
4160
+ * Takes up what a load produced: the component refs the chain-keyed props
4161
+ * address, the visibility they were already asking for, and the chain list
4162
+ * the consumer is told about.
4163
+ *
4164
+ * Chains are published only when they actually differ, so reloading the same
4165
+ * structure - a theme switch that happens to reload, a parent handing back an
4166
+ * equal PDB string - does not hand every consumer a new array to react to.
4167
+ */
4168
+ const adoptLoadedStructure = (0, react.useCallback)((plugin, loaded) => {
4169
+ componentRefsRef.current = loaded.componentRefs;
4170
+ residuesByChainRef.current = loaded.residuesByChain;
4171
+ applyChainVisibility(plugin, loaded.componentRefs, hiddenChainsRef.current);
4172
+ setChains((prev) => chainsEqual(prev, loaded.chains) ? prev : loaded.chains);
4173
+ }, []);
2720
4174
  const onResidueClickRef = (0, react.useRef)(onResidueClick);
2721
4175
  onResidueClickRef.current = onResidueClick;
2722
4176
  const onSelectionClearRef = (0, react.useRef)(onSelectionClear);
2723
4177
  onSelectionClearRef.current = onSelectionClear;
4178
+ const onSelectionChangeRef = (0, react.useRef)(onSelectionChange);
4179
+ onSelectionChangeRef.current = onSelectionChange;
2724
4180
  const onResidueHoverRef = (0, react.useRef)(onResidueHover);
2725
4181
  onResidueHoverRef.current = onResidueHover;
2726
4182
  /** Last residue reported to `onResidueHover`, to suppress repeats. */
@@ -2731,6 +4187,7 @@ function useMolstarPlugin({ backgroundColor, containerRef, edgeColor, hasPlddt,
2731
4187
  hasPlddt,
2732
4188
  highlightColor,
2733
4189
  mode,
4190
+ molstarSpec,
2734
4191
  pdb,
2735
4192
  showAxes,
2736
4193
  showSequenceViewer
@@ -2741,17 +4198,32 @@ function useMolstarPlugin({ backgroundColor, containerRef, edgeColor, hasPlddt,
2741
4198
  hasPlddt,
2742
4199
  highlightColor,
2743
4200
  mode,
4201
+ molstarSpec,
2744
4202
  pdb,
2745
4203
  showAxes,
2746
4204
  showSequenceViewer
2747
4205
  };
4206
+ /**
4207
+ * The consumer's `canvas3d` overrides, keyed by content. Unlike the rest of
4208
+ * the spec these are re-applied when they change, and they commonly arrive
4209
+ * as an object written inline - so the effect below turns on what they say
4210
+ * rather than on the identity of the object saying it.
4211
+ */
4212
+ const canvas3dOverrideKey = (() => {
4213
+ try {
4214
+ var _molstarSpec$canvas3d;
4215
+ return JSON.stringify((_molstarSpec$canvas3d = molstarSpec === null || molstarSpec === void 0 ? void 0 : molstarSpec.canvas3d) !== null && _molstarSpec$canvas3d !== void 0 ? _molstarSpec$canvas3d : null);
4216
+ } catch (_unused) {
4217
+ return String(Date.now());
4218
+ }
4219
+ })();
2748
4220
  (0, react.useEffect)(() => {
2749
4221
  const container = containerRef.current;
2750
4222
  if (!container) return;
2751
4223
  let cancelled = false;
2752
4224
  let clipSubscription;
2753
4225
  const init = function() {
2754
- var _ref = _asyncToGenerator(function* () {
4226
+ var _ref2 = _asyncToGenerator(function* () {
2755
4227
  yield nextFrame();
2756
4228
  if (cancelled) return;
2757
4229
  const rect = container.getBoundingClientRect();
@@ -2763,10 +4235,13 @@ function useMolstarPlugin({ backgroundColor, containerRef, edgeColor, hasPlddt,
2763
4235
  try {
2764
4236
  var _plugin$canvas3d2;
2765
4237
  const residueValueTheme = createResidueValueTheme(initial.mode);
4238
+ const chainColorTheme = createChainColorTheme();
2766
4239
  const plugin = yield createViewer({
2767
4240
  backgroundColor: initial.backgroundColor,
4241
+ chainColorTheme,
2768
4242
  edgeColor: initial.edgeColor,
2769
4243
  highlightColor: initial.highlightColor,
4244
+ molstarSpec: initial.molstarSpec,
2770
4245
  residueValueTheme,
2771
4246
  root: container,
2772
4247
  showAxes: initial.showAxes,
@@ -2779,11 +4254,28 @@ function useMolstarPlugin({ backgroundColor, containerRef, edgeColor, hasPlddt,
2779
4254
  }
2780
4255
  pluginRef.current = plugin;
2781
4256
  residueValueThemeRef.current = residueValueTheme;
4257
+ chainColorThemeRef.current = chainColorTheme;
2782
4258
  const latest = initialPropsRef.current;
2783
- yield loadStructure(plugin, latest.pdb, latest.hasPlddt, latest.showAxes);
4259
+ const loaded = yield loadStructure(plugin, latest.pdb, latest.hasPlddt, latest.showAxes);
2784
4260
  currentPdbRef.current = latest.pdb;
4261
+ adoptLoadedStructure(plugin, loaded);
4262
+ /**
4263
+ * Mol*'s click behavior is a BehaviorSubject, so subscribing replays
4264
+ * whatever it is currently holding - which, before anything has been
4265
+ * clicked, is an empty click. Acting on that would read as the user
4266
+ * clicking empty space and clear a selection the consumer had set
4267
+ * before the viewer was even interactive.
4268
+ *
4269
+ * Only the replay is skipped, not every empty click: clicking away
4270
+ * from the structure is how a selection is meant to be cleared.
4271
+ */
4272
+ let clickReplayConsumed = false;
2785
4273
  plugin.behaviors.interaction.click.subscribe((e) => {
2786
- var _onResidueClickRef$cu;
4274
+ var _onResidueClickRef$cu, _onSelectionChangeRef;
4275
+ if (!clickReplayConsumed) {
4276
+ clickReplayConsumed = true;
4277
+ if (molstar_lib_mol_repr_representation.Representation.Loci.isEmpty(e.current)) return;
4278
+ }
2787
4279
  if (molstar_lib_mol_repr_representation.Representation.Loci.isEmpty(e.current)) {
2788
4280
  var _onSelectionClearRef$;
2789
4281
  (_onSelectionClearRef$ = onSelectionClearRef.current) === null || _onSelectionClearRef$ === void 0 || _onSelectionClearRef$.call(onSelectionClearRef);
@@ -2794,6 +4286,7 @@ function useMolstarPlugin({ backgroundColor, containerRef, edgeColor, hasPlddt,
2794
4286
  const residue = residueRefFromLoci(loci);
2795
4287
  if (!residue) return;
2796
4288
  (_onResidueClickRef$cu = onResidueClickRef.current) === null || _onResidueClickRef$cu === void 0 || _onResidueClickRef$cu.call(onResidueClickRef, residue);
4289
+ (_onSelectionChangeRef = onSelectionChangeRef.current) === null || _onSelectionChangeRef === void 0 || _onSelectionChangeRef.call(onSelectionChangeRef, selectionFromLoci(loci));
2797
4290
  });
2798
4291
  plugin.behaviors.interaction.hover.subscribe((e) => {
2799
4292
  var _lastHoverRef$current, _onResidueHoverRef$cu;
@@ -2812,7 +4305,7 @@ function useMolstarPlugin({ backgroundColor, containerRef, edgeColor, hasPlddt,
2812
4305
  }
2813
4306
  });
2814
4307
  return function init() {
2815
- return _ref.apply(this, arguments);
4308
+ return _ref2.apply(this, arguments);
2816
4309
  };
2817
4310
  }();
2818
4311
  init();
@@ -2823,13 +4316,16 @@ function useMolstarPlugin({ backgroundColor, containerRef, edgeColor, hasPlddt,
2823
4316
  (_pluginRef$current = pluginRef.current) === null || _pluginRef$current === void 0 || _pluginRef$current.dispose();
2824
4317
  pluginRef.current = null;
2825
4318
  residueValueThemeRef.current = null;
4319
+ chainColorThemeRef.current = null;
4320
+ componentRefsRef.current = /* @__PURE__ */ new Map();
4321
+ residuesByChainRef.current = /* @__PURE__ */ new Map();
2826
4322
  currentPdbRef.current = null;
2827
4323
  clipRatioRef.current = null;
2828
4324
  setIsReady(false);
2829
4325
  };
2830
4326
  }, []);
2831
4327
  (0, react.useEffect)(() => {
2832
- var _pluginRef$current2;
4328
+ var _pluginRef$current2, _initialPropsRef$curr;
2833
4329
  const canvas3d = (_pluginRef$current2 = pluginRef.current) === null || _pluginRef$current2 === void 0 ? void 0 : _pluginRef$current2.canvas3d;
2834
4330
  if (!canvas3d || !isReady) return;
2835
4331
  canvas3d.setProps({
@@ -2842,20 +4338,37 @@ function useMolstarPlugin({ backgroundColor, containerRef, edgeColor, hasPlddt,
2842
4338
  highlightColor
2843
4339
  }
2844
4340
  });
4341
+ const override = (_initialPropsRef$curr = initialPropsRef.current.molstarSpec) === null || _initialPropsRef$curr === void 0 ? void 0 : _initialPropsRef$curr.canvas3d;
4342
+ if (override) canvas3d.setProps(override);
2845
4343
  }, [
2846
4344
  backgroundColor,
4345
+ canvas3dOverrideKey,
2847
4346
  edgeColor,
2848
4347
  highlightColor,
2849
4348
  isReady
2850
4349
  ]);
2851
4350
  (0, react.useEffect)(() => {
2852
4351
  viewSettings.next({
4352
+ download,
4353
+ hiddenChains,
2853
4354
  mode,
2854
- sequenceViewerBackgroundColor
4355
+ onChainHover,
4356
+ onChainSelect,
4357
+ onChainToggle,
4358
+ selectedChains,
4359
+ sequenceViewerBackgroundColor,
4360
+ showAxes
2855
4361
  });
2856
4362
  }, [
4363
+ download,
4364
+ hiddenChains,
2857
4365
  mode,
4366
+ onChainHover,
4367
+ onChainSelect,
4368
+ onChainToggle,
4369
+ selectedChains,
2858
4370
  sequenceViewerBackgroundColor,
4371
+ showAxes,
2859
4372
  viewSettings
2860
4373
  ]);
2861
4374
  (0, react.useEffect)(() => {
@@ -2869,120 +4382,312 @@ function useMolstarPlugin({ backgroundColor, containerRef, edgeColor, hasPlddt,
2869
4382
  currentPdbRef.current = pdb;
2870
4383
  clipRatioRef.current = null;
2871
4384
  lastHoverRef.current = null;
2872
- loadStructure(plugin, pdb, hasPlddt, showAxes);
4385
+ loadStructure(plugin, pdb, hasPlddt, showAxes).then((loaded) => {
4386
+ if (pluginRef.current !== plugin) return;
4387
+ adoptLoadedStructure(plugin, loaded);
4388
+ });
2873
4389
  }, [
2874
4390
  pdb,
2875
4391
  hasPlddt,
2876
- isReady
4392
+ isReady,
4393
+ adoptLoadedStructure
2877
4394
  ]);
4395
+ (0, react.useEffect)(() => {
4396
+ const plugin = pluginRef.current;
4397
+ if (!plugin || !isReady) return;
4398
+ applyChainVisibility(plugin, componentRefsRef.current, hiddenChains);
4399
+ }, [hiddenChains, isReady]);
2878
4400
  return {
4401
+ chainColorThemeRef,
4402
+ chains,
2879
4403
  isReady,
2880
4404
  pluginRef,
4405
+ residuesByChainRef,
2881
4406
  residueValueThemeRef,
2882
4407
  setClipRatio
2883
4408
  };
2884
4409
  }
2885
4410
  //#endregion
2886
- //#region src/core/ProteinStructureViewer/utils/residueLoci.ts
4411
+ //#region src/core/ProteinStructureViewer/utils/focusRepresentation.ts
4412
+ /**
4413
+ * What Mol* draws around a focused selection, as ball-and-stick over the
4414
+ * cartoon: the selection itself, the residues near it, and the non-covalent
4415
+ * contacts between the two. All three stay on; what changes is how far "near"
4416
+ * reaches.
4417
+ */
4418
+ const FOCUS_COMPONENTS = [
4419
+ "target",
4420
+ "surroundings",
4421
+ "interactions"
4422
+ ];
4423
+ /** Mol*'s own default, in angstroms. */
4424
+ const NEIGHBOUR_RADIUS = 5;
4425
+ /**
4426
+ * Mol* builds the surroundings as `includeSurroundings(target, radius)` with
4427
+ * whole residues, so a radius of zero resolves to the residues of the target
4428
+ * and nothing else.
4429
+ */
4430
+ const OWN_RESIDUES_ONLY = 0;
2887
4431
  /**
2888
- * Every atom of the residue at a 0-based index, as geometry Mol* can focus and
2889
- * frame, or undefined when the structure has no such residue.
4432
+ * Chooses how far the focus shell reaches around a selection.
2890
4433
  *
2891
- * The inverse of `residueRefFromLoci`, and keyed on the same residue ordinal,
2892
- * so a `selectedResidue` echoed back from a click resolves to the residue that
2893
- * was clicked - on a complex as much as on a single chain.
4434
+ * A residue, or a short range, is a site: the 5A shell around it is the point
4435
+ * of looking at it, and Mol*'s default earns its keep. A whole chain is a
4436
+ * structural unit instead, and its neighbours are the chain bound to it - so
4437
+ * on a complex the shell reaches straight across the interface and draws the
4438
+ * partner's contact face in atoms, which reads as the partner being selected
4439
+ * too.
2894
4440
  *
2895
- * The whole residue is collected rather than one atom, since the caller frames
2896
- * the camera on the loci's bounding sphere and a single atom would zoom far
2897
- * closer than the residue warrants.
4441
+ * Taking the radius to zero confines the shell to the selection rather than
4442
+ * turning it off: the surroundings and the interactions are still drawn, over
4443
+ * the selected chain's own residues, so a chain shows its own atoms and
4444
+ * contacts and no others'.
2898
4445
  */
2899
- function lociForResidueIndex(plugin, residueIndex) {
2900
- for (const entry of plugin.managers.structure.hierarchy.current.structures) {
2901
- var _entry$cell$obj;
2902
- const structure = (_entry$cell$obj = entry.cell.obj) === null || _entry$cell$obj === void 0 ? void 0 : _entry$cell$obj.data;
2903
- if (!structure) continue;
2904
- const location = molstar_lib_mol_model_structure.StructureElement.Location.create(structure);
2905
- for (const unit of structure.units) {
2906
- location.unit = unit;
2907
- const indices = [];
2908
- for (let i = 0; i < unit.elements.length; i++) {
2909
- location.element = unit.elements[i];
2910
- if (molstar_lib_mol_model_structure.StructureProperties.residue.key(location) === residueIndex) indices.push(i);
2911
- }
2912
- if (indices.length > 0) return molstar_lib_mol_model_structure.StructureElement.Loci(structure, [{
2913
- indices: molstar_lib_mol_data_int.OrderedSet.ofSortedArray(indices),
2914
- unit
2915
- }]);
2916
- }
2917
- }
4446
+ function setFocusNeighbourhood(_x, _x2) {
4447
+ return _setFocusNeighbourhood.apply(this, arguments);
4448
+ }
4449
+ function _setFocusNeighbourhood() {
4450
+ _setFocusNeighbourhood = _asyncToGenerator(function* (plugin, includeNeighbours) {
4451
+ const expandRadius = includeNeighbours ? NEIGHBOUR_RADIUS : OWN_RESIDUES_ONLY;
4452
+ yield plugin.state.updateBehavior(molstar_lib_mol_plugin_behavior_dynamic_selection_structure_focus_representation.StructureFocusRepresentation, (params) => {
4453
+ params.components = FOCUS_COMPONENTS;
4454
+ params.expandRadius = expandRadius;
4455
+ });
4456
+ });
4457
+ return _setFocusNeighbourhood.apply(this, arguments);
2918
4458
  }
2919
4459
  //#endregion
2920
- //#region src/core/ProteinStructureViewer/hooks/useResidueFocus.ts
4460
+ //#region src/core/ProteinStructureViewer/hooks/useSelectionFocus.ts
4461
+ /**
4462
+ * Points the camera at a loci and marks it.
4463
+ *
4464
+ * A click has already focused what the consumer echoes back by the time it
4465
+ * arrives, so the focus is only set when it is not already what is marked;
4466
+ * setting it again would restate the selection and repaint the sequence panel
4467
+ * for nothing.
4468
+ */
4469
+ function focusLoci(plugin, loci, setClipRatio) {
4470
+ const focused = plugin.managers.structure.focus.behaviors.current.value;
4471
+ if (!focused || !molstar_lib_mol_model_structure.StructureElement.Loci.areEqual(focused.loci, loci)) plugin.managers.structure.focus.setFromLoci(loci);
4472
+ setClipRatio(focusResidue(plugin, loci));
4473
+ }
4474
+ /**
4475
+ * Drops the focus, and the depth clipping anchored to it.
4476
+ *
4477
+ * `resetCamera` separates the two ways this happens. The consumer clearing the
4478
+ * selection means there is nothing to look at, so the view goes back to the
4479
+ * default. A selection that merely resolved to nothing visible - the chain it
4480
+ * covers having been hidden - still stands, so throwing the camera back would
4481
+ * be answering a question nobody asked.
4482
+ */
4483
+ function clearFocus(plugin, setClipRatio, resetCamera) {
4484
+ var _plugin$canvas3d;
4485
+ setClipRatio(null);
4486
+ plugin.managers.structure.focus.clear();
4487
+ if (resetCamera) (_plugin$canvas3d = plugin.canvas3d) === null || _plugin$canvas3d === void 0 || _plugin$canvas3d.requestCameraReset();
4488
+ }
4489
+ /**
4490
+ * Focuses a loci, resizing the focus shell first when this selection wants a
4491
+ * different reach than the last one did.
4492
+ *
4493
+ * The resize is asynchronous and the focus has to follow it - the shell is
4494
+ * sized before focusing so it is not drawn at the old reach and rebuilt - so
4495
+ * this hands back a teardown that abandons the focus if the effect is torn
4496
+ * down while the resize is still in flight.
4497
+ *
4498
+ * `currentShell` is the reach already configured, carried in a ref so that two
4499
+ * selections of the same kind do not each restate it.
4500
+ */
4501
+ function focusWithShell(plugin, loci, includeNeighbours, currentShell, pluginRef, setClipRatio) {
4502
+ if (currentShell.current === includeNeighbours) {
4503
+ focusLoci(plugin, loci, setClipRatio);
4504
+ return;
4505
+ }
4506
+ let cancelled = false;
4507
+ currentShell.current = includeNeighbours;
4508
+ setFocusNeighbourhood(plugin, includeNeighbours).then(() => {
4509
+ if (cancelled || pluginRef.current !== plugin) return;
4510
+ focusLoci(plugin, loci, setClipRatio);
4511
+ });
4512
+ return () => {
4513
+ cancelled = true;
4514
+ };
4515
+ }
2921
4516
  /**
2922
- * Makes `selectedResidue` the one thing that decides where the camera sits.
4517
+ * Makes `selection` the one thing that decides where the camera sits.
2923
4518
  *
2924
- * Setting the prop zooms in on that residue and marks it; clearing it drops the
2925
- * focus and zooms back out. The click subscription only reports the residue it
2926
- * was given, so a click moves the camera only once the consumer accepts it and
2927
- * echoes it back - which is what the prop being controlled is supposed to mean.
4519
+ * Setting the prop frames whatever it covers and marks it; clearing it drops
4520
+ * the focus and zooms back out. The camera frames the selection's bounding
4521
+ * sphere, so a range or a whole chain is fitted rather than approached as a
4522
+ * point - one residue is just the smallest case of that.
2928
4523
  *
2929
- * Clicking still marks the residue immediately, through Mol*'s own focus
2930
- * behavior, so the structure responds to a click whether or not the consumer
2931
- * wires the prop up.
4524
+ * The click subscription only reports the selection it was given, so a click
4525
+ * moves the camera only once the consumer accepts it and echoes it back -
4526
+ * which is what the prop being controlled is supposed to mean. Clicking still
4527
+ * marks immediately, through Mol*'s own focus behavior, so the structure
4528
+ * responds to a click whether or not the consumer wires the prop up.
2932
4529
  */
2933
- function useResidueFocus({ isReady, pluginRef, selectedResidue, setClipRatio }) {
4530
+ function useSelectionFocus({ hiddenChains, isReady, pluginRef, selection, setClipRatio }) {
2934
4531
  /**
2935
- * The residue this hook last pointed the camera at. Tracked rather than
2936
- * compared against the previous prop value so that a selection made before
2937
- * the plugin was ready is still applied once it is, and so that a plugin
2938
- * rebuilt underneath us - which comes up focused on nothing - is not assumed
2939
- * to still be holding the old one.
4532
+ * What this hook last pointed the camera at, as a key rather than the object
4533
+ * itself: a consumer echoing a selection back builds a new object each time,
4534
+ * and comparing identity would refocus on every render.
4535
+ *
4536
+ * Tracked here rather than compared against the previous prop so that a
4537
+ * selection made before the plugin was ready is still applied once it is,
4538
+ * and so that a plugin rebuilt underneath us - which comes up focused on
4539
+ * nothing - is not assumed to still be holding the old one.
2940
4540
  */
2941
4541
  const focusedRef = (0, react.useRef)(null);
4542
+ /**
4543
+ * Whether the focus shell currently reaches beyond the selection, so the
4544
+ * behavior is only updated when the answer actually changes rather than on
4545
+ * every selection.
4546
+ */
4547
+ const neighbourhoodRef = (0, react.useRef)(null);
4548
+ /**
4549
+ * What the camera should be pointed at, as a string. Hiding a chain changes
4550
+ * what there is to focus, so it belongs in here alongside the selection
4551
+ * itself - and comparing strings rather than objects is what keeps a consumer
4552
+ * echoing an equal selection back from refocusing on every render.
4553
+ */
4554
+ const focusKey = selection ? `${selectionKey(selection)}#${[...hiddenChains].sort().join(",")}` : null;
2942
4555
  (0, react.useEffect)(() => {
4556
+ var _chosen$chains;
2943
4557
  const plugin = pluginRef.current;
2944
4558
  if (!plugin || !isReady) {
2945
4559
  focusedRef.current = null;
4560
+ neighbourhoodRef.current = null;
2946
4561
  return;
2947
4562
  }
2948
- const residue = selectedResidue !== null && selectedResidue !== void 0 ? selectedResidue : null;
2949
- if (residue === focusedRef.current) return;
2950
- focusedRef.current = residue;
2951
- if (residue === null) {
2952
- var _plugin$canvas3d;
2953
- setClipRatio(null);
2954
- plugin.managers.structure.focus.clear();
2955
- (_plugin$canvas3d = plugin.canvas3d) === null || _plugin$canvas3d === void 0 || _plugin$canvas3d.requestCameraReset();
4563
+ if (focusKey === focusedRef.current) return;
4564
+ focusedRef.current = focusKey;
4565
+ if (focusKey === null) {
4566
+ clearFocus(plugin, setClipRatio, true);
2956
4567
  return;
2957
4568
  }
2958
- const loci = lociForResidueIndex(plugin, residue);
2959
- if (!loci) return;
2960
- const focused = plugin.managers.structure.focus.behaviors.current.value;
2961
- if (!focused || !molstar_lib_mol_model_structure.StructureElement.Loci.areEqual(focused.loci, loci)) plugin.managers.structure.focus.setFromLoci(loci);
2962
- setClipRatio(focusResidue(plugin, loci));
4569
+ const chosen = selection;
4570
+ const loci = lociForSelection(plugin, chosen, hiddenChains);
4571
+ if (!loci) {
4572
+ clearFocus(plugin, setClipRatio, false);
4573
+ return;
4574
+ }
4575
+ return focusWithShell(plugin, loci, !((_chosen$chains = chosen.chains) === null || _chosen$chains === void 0 ? void 0 : _chosen$chains.length), neighbourhoodRef, pluginRef, setClipRatio);
2963
4576
  }, [
2964
- selectedResidue,
4577
+ focusKey,
2965
4578
  isReady,
2966
4579
  setClipRatio
2967
4580
  ]);
2968
4581
  }
2969
4582
  //#endregion
2970
- //#region src/core/ProteinStructureViewer/hooks/useResidueOverlay.ts
4583
+ //#region src/core/ProteinStructureViewer/hooks/useSelectionReadout.ts
4584
+ /**
4585
+ * Tracks the residue under the pointer, deduplicated, and passes the event on
4586
+ * to the consumer.
4587
+ *
4588
+ * Mol* emits hover events continuously, so a residue the pointer is resting on
4589
+ * would otherwise cost a state update per event. The index alone does not
4590
+ * identify one: loading a new structure into the same plugin renumbers from
4591
+ * zero, so the residue now at an index is not the one that was there before.
4592
+ * Comparing the label as well is enough, because the readout this drives is
4593
+ * built from the label and the index and from nothing else.
4594
+ */
4595
+ function useResidueHoverState(onResidueHover) {
4596
+ const [hoveredResidue, setHoveredResidue] = (0, react.useState)(null);
4597
+ return {
4598
+ handleResidueHover: (0, react.useCallback)((residue) => {
4599
+ setHoveredResidue((prev) => {
4600
+ if (residue === null) return prev === null ? prev : null;
4601
+ const label = residueLabel(residue);
4602
+ return prev !== null && prev.index === residue.index && prev.label === label ? prev : {
4603
+ index: residue.index,
4604
+ label
4605
+ };
4606
+ });
4607
+ onResidueHover === null || onResidueHover === void 0 || onResidueHover(residue);
4608
+ }, [onResidueHover]),
4609
+ hoveredResidue
4610
+ };
4611
+ }
4612
+ /**
4613
+ * Derives the legend's readouts from what is hovered and what is selected.
4614
+ *
4615
+ * The viewer already knows everything they need - the label, the pLDDT, the
4616
+ * overlay value - so they are worked out here rather than asked of the
4617
+ * consumer. A readout covering several residues reports the mean of each,
4618
+ * which is the only reading that survives a range or a whole chain.
4619
+ */
4620
+ function useSelectionReadout({ hoveredResidue, isReady, plddt, pluginRef, residueOverlay, residuesByChainRef, selection }) {
4621
+ const [resolved, setResolved] = (0, react.useState)(null);
4622
+ /**
4623
+ * What the selection covers has to be read off the structure, since a
4624
+ * selection can be made without a click ever naming anything: a lone residue
4625
+ * is named the way the click path names it, so the two readouts cannot
4626
+ * disagree, and a chain is expanded to the residues it actually holds rather
4627
+ * than to the span between its ends.
4628
+ */
4629
+ (0, react.useEffect)(() => {
4630
+ var _residuesByChainRef$c;
4631
+ const plugin = pluginRef.current;
4632
+ if (!plugin || !isReady || isEmptySelection(selection)) {
4633
+ setResolved(null);
4634
+ return;
4635
+ }
4636
+ const chosen = selection;
4637
+ const residues = selectionResidues(chosen, (_residuesByChainRef$c = residuesByChainRef.current) !== null && _residuesByChainRef$c !== void 0 ? _residuesByChainRef$c : /* @__PURE__ */ new Map());
4638
+ if (residues.length === 0) {
4639
+ setResolved(null);
4640
+ return;
4641
+ }
4642
+ if (residues.length > 1) {
4643
+ setResolved({
4644
+ label: selectionLabel(chosen, residues.length),
4645
+ residues
4646
+ });
4647
+ return;
4648
+ }
4649
+ const loci = lociForSelection(plugin, chosen);
4650
+ const residue = loci && residueRefFromLoci(loci);
4651
+ setResolved(residue ? {
4652
+ label: residueLabel(residue),
4653
+ residues
4654
+ } : null);
4655
+ }, [isReady, selection && selectionKey(selection)]);
4656
+ const buildReadout = (0, react.useCallback)((label, residues) => ({
4657
+ label,
4658
+ plddt: meanOverResidues(residues, (i) => plddt === null || plddt === void 0 ? void 0 : plddt[i]),
4659
+ value: meanOverResidues(residues, (i) => residueOverlay === null || residueOverlay === void 0 ? void 0 : residueOverlay.values.get(i)),
4660
+ residueCount: residues.length
4661
+ }), [plddt, residueOverlay]);
4662
+ return {
4663
+ hoveredReadout: (0, react.useMemo)(() => hoveredResidue === null ? null : buildReadout(hoveredResidue.label, [hoveredResidue.index]), [hoveredResidue, buildReadout]),
4664
+ selectedReadout: (0, react.useMemo)(() => resolved === null ? null : buildReadout(resolved.label, resolved.residues), [resolved, buildReadout])
4665
+ };
4666
+ }
4667
+ //#endregion
4668
+ //#region src/core/ProteinStructureViewer/hooks/useStructureColoring.ts
2971
4669
  /**
2972
- * Switches the structure's coloring between the residue value overlay and the
2973
- * default (pLDDT, or chain-id when no scores were supplied).
4670
+ * Decides what is coloring the structure and keeps that theme's data current.
4671
+ *
4672
+ * Three themes compete for the same representations, in a fixed order: a
4673
+ * residue value overlay, then pLDDT scores, then per-chain colors. Only one
4674
+ * can be applied, so the precedence lives here rather than being spread across
4675
+ * the hooks that own each theme's data - and the legend resolves what to
4676
+ * describe the same way, so the two cannot disagree about what is on screen.
2974
4677
  *
2975
4678
  * Recoloring happens in place: the camera is left where it is, so selecting or
2976
4679
  * switching an overlay does not zoom back out to the default view.
2977
4680
  */
2978
- function useResidueOverlay({ hasPlddt, isReady, mode, overlay, pluginRef, residueValueThemeRef }) {
4681
+ function useStructureColoring({ chainColorThemeRef, chainColors, hasPlddt, isReady, mode, overlay, pluginRef, residueValueThemeRef }) {
2979
4682
  (0, react.useEffect)(() => {
2980
4683
  const plugin = pluginRef.current;
2981
- const theme = residueValueThemeRef.current;
2982
- if (!plugin || !theme || !isReady) return;
4684
+ const residueTheme = residueValueThemeRef.current;
4685
+ const chainTheme = chainColorThemeRef.current;
4686
+ if (!plugin || !residueTheme || !chainTheme || !isReady) return;
4687
+ chainTheme.setState(chainColors);
2983
4688
  if (overlay) {
2984
4689
  var _overlay$colorScale, _overlay$min;
2985
- theme.setState({
4690
+ residueTheme.setState({
2986
4691
  colorScale: (_overlay$colorScale = overlay.colorScale) !== null && _overlay$colorScale !== void 0 ? _overlay$colorScale : PLASMA_COLOR_SCALE,
2987
4692
  max: overlay.max,
2988
4693
  min: (_overlay$min = overlay.min) !== null && _overlay$min !== void 0 ? _overlay$min : 0,
@@ -2990,33 +4695,51 @@ function useResidueOverlay({ hasPlddt, isReady, mode, overlay, pluginRef, residu
2990
4695
  values: overlay.values
2991
4696
  });
2992
4697
  applyColorTheme(plugin, RESIDUE_VALUE_THEME_NAME);
2993
- } else {
2994
- theme.setState({
2995
- mode,
2996
- values: /* @__PURE__ */ new Map()
2997
- });
2998
- applyColorTheme(plugin, hasPlddt ? PLDDT_THEME_NAME : FALLBACK_THEME_NAME);
4698
+ return;
2999
4699
  }
4700
+ residueTheme.setState({
4701
+ mode,
4702
+ values: /* @__PURE__ */ new Map()
4703
+ });
4704
+ applyColorTheme(plugin, hasPlddt ? PLDDT_THEME_NAME : FALLBACK_THEME_NAME);
3000
4705
  }, [
3001
4706
  overlay,
3002
4707
  hasPlddt,
3003
4708
  isReady,
3004
- mode
4709
+ mode,
4710
+ chainColors
3005
4711
  ]);
3006
4712
  }
3007
4713
  //#endregion
4714
+ //#region src/core/ProteinStructureViewer/ProteinStructureViewer.types.ts
4715
+ /** Pixel dimensions each resolution produces, for documentation and labels. */
4716
+ const DOWNLOAD_RESOLUTIONS = {
4717
+ high: "3840x2160",
4718
+ low: "1280x720",
4719
+ maximum: "7680x4320",
4720
+ medium: "1920x1080"
4721
+ };
4722
+ //#endregion
3008
4723
  //#region src/core/ProteinStructureViewer/index.tsx
3009
4724
  const _excluded$1 = [
3010
4725
  "backgroundColor",
4726
+ "chainColors",
4727
+ "disableChainHighlightOnHover",
4728
+ "download",
4729
+ "hiddenChains",
4730
+ "onChainVisibilityChange",
4731
+ "onChainsChange",
4732
+ "molstarSpec",
3011
4733
  "onResidueClick",
3012
4734
  "onResidueHover",
3013
- "onSelectionClear",
4735
+ "onSelectionChange",
3014
4736
  "pdb",
3015
4737
  "plddt",
3016
4738
  "residueOverlay",
3017
- "selectedResidue",
4739
+ "selection",
3018
4740
  "sequenceViewerBackgroundColor",
3019
4741
  "showAxes",
4742
+ "showChainLegend",
3020
4743
  "showLegend",
3021
4744
  "showSequenceViewer",
3022
4745
  "stats"
@@ -3055,6 +4778,7 @@ function resolveScaleProps(overlay, hasPlddt) {
3055
4778
  scaleMax: overlay.max,
3056
4779
  scaleMin: (_overlay$min = overlay.min) !== null && _overlay$min !== void 0 ? _overlay$min : 0,
3057
4780
  scaleTooltip: overlay.tooltip,
4781
+ scaleTooltipProps: overlay.tooltipProps,
3058
4782
  valueLabel: (_overlay$readoutLabel = overlay.readoutLabel) !== null && _overlay$readoutLabel !== void 0 ? _overlay$readoutLabel : DEFAULT_READOUT_LABEL
3059
4783
  };
3060
4784
  }
@@ -3063,6 +4787,7 @@ function resolveScaleProps(overlay, hasPlddt) {
3063
4787
  scaleLabel: PLDDT_SCALE_LABEL,
3064
4788
  scaleMax: null,
3065
4789
  scaleTooltip: void 0,
4790
+ scaleTooltipProps: void 0,
3066
4791
  valueLabel: void 0
3067
4792
  };
3068
4793
  return {
@@ -3070,6 +4795,7 @@ function resolveScaleProps(overlay, hasPlddt) {
3070
4795
  scaleLabel: void 0,
3071
4796
  scaleMax: null,
3072
4797
  scaleTooltip: void 0,
4798
+ scaleTooltipProps: void 0,
3073
4799
  valueLabel: void 0
3074
4800
  };
3075
4801
  }
@@ -3083,64 +4809,121 @@ function resolveScaleProps(overlay, hasPlddt) {
3083
4809
  * without the consumer round-tripping every pointer move.
3084
4810
  */
3085
4811
  const ProteinStructureViewer = (0, react.forwardRef)((props, ref) => {
3086
- const { backgroundColor, onResidueClick, onResidueHover, onSelectionClear, pdb, plddt, residueOverlay, selectedResidue = null, sequenceViewerBackgroundColor, showAxes = true, showLegend = true, showSequenceViewer = true, stats } = props, rest = _objectWithoutProperties(props, _excluded$1);
4812
+ var _selection$chains;
4813
+ const { backgroundColor, chainColors: chainColorOverrides, disableChainHighlightOnHover = false, download, hiddenChains: hiddenChainsProp, onChainVisibilityChange, onChainsChange, molstarSpec, onResidueClick, onResidueHover, onSelectionChange, pdb, plddt, residueOverlay, selection = null, sequenceViewerBackgroundColor, showAxes = true, showChainLegend = true, showLegend = true, showSequenceViewer = true, stats } = props, rest = _objectWithoutProperties(props, _excluded$1);
3087
4814
  const theme = (0, _mui_material_styles.useTheme)();
3088
4815
  const mode = (0, _czi_sds_components.getMode)({ theme });
3089
4816
  const semanticColors = (0, _czi_sds_components.getSemanticColors)({ theme });
3090
4817
  const pluginMountRef = (0, react.useRef)(null);
3091
- const [hoveredResidue, setHoveredResidue] = (0, react.useState)(null);
3092
- /**
3093
- * Label for the selected residue, read back off the structure rather than
3094
- * remembered from the click that selected it, so the readout follows the
3095
- * prop however the selection was made.
3096
- */
3097
- const [selectedLabel, setSelectedLabel] = (0, react.useState)(null);
3098
4818
  const hasPlddt = Boolean(plddt && plddt.length > 0);
3099
4819
  const processedPdb = (0, react.useMemo)(() => hasPlddt ? injectPlddtIntoPdb(pdb, plddt) : pdb, [
3100
4820
  pdb,
3101
4821
  plddt,
3102
4822
  hasPlddt
3103
4823
  ]);
3104
- const { isReady, pluginRef, residueValueThemeRef, setClipRatio } = useMolstarPlugin({
3105
- backgroundColor: (0, react.useMemo)(() => {
3106
- var _semanticColors$base;
3107
- 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]);
3108
- }, [
3109
- backgroundColor,
3110
- mode,
3111
- semanticColors
3112
- ]),
4824
+ const bgColor = (0, react.useMemo)(() => {
4825
+ var _semanticColors$base;
4826
+ 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]);
4827
+ }, [
4828
+ backgroundColor,
4829
+ mode,
4830
+ semanticColors
4831
+ ]);
4832
+ const edgeColor = (0, react.useMemo)(() => {
4833
+ var _semanticColors$base2;
4834
+ return themeColor(semanticColors === null || semanticColors === void 0 || (_semanticColors$base2 = semanticColors.base) === null || _semanticColors$base2 === void 0 ? void 0 : _semanticColors$base2.borderPrimary, FALLBACK_EDGE_COLOR);
4835
+ }, [semanticColors]);
4836
+ const highlightColor = (0, react.useMemo)(() => {
4837
+ var _semanticColors$base3;
4838
+ return themeColor(semanticColors === null || semanticColors === void 0 || (_semanticColors$base3 = semanticColors.base) === null || _semanticColors$base3 === void 0 ? void 0 : _semanticColors$base3.fillSelected, FALLBACK_HIGHLIGHT_COLOR);
4839
+ }, [semanticColors]);
4840
+ const { handleResidueHover, hoveredResidue } = useResidueHoverState(onResidueHover);
4841
+ const handleSelectionClear = (0, react.useCallback)(() => {
4842
+ onSelectionChange === null || onSelectionChange === void 0 || onSelectionChange(null);
4843
+ }, [onSelectionChange]);
4844
+ /**
4845
+ * Chains the selection covers whole, which is what makes a chain caption a
4846
+ * toggle rather than a one-way switch. Keyed on its contents because the
4847
+ * set is pushed into the views Mol* renders, where a fresh object every
4848
+ * render would be a fresh push every render.
4849
+ */
4850
+ const selectedChainsKey = [...(_selection$chains = selection === null || selection === void 0 ? void 0 : selection.chains) !== null && _selection$chains !== void 0 ? _selection$chains : []].sort().join("\0");
4851
+ const selectedChains = (0, react.useMemo)(() => new Set(selectedChainsKey === "" ? [] : selectedChainsKey.split("\0")), [selectedChainsKey]);
4852
+ /**
4853
+ * Clicking a chain selects it; clicking the selected chain again clears it.
4854
+ *
4855
+ * A whole chain is reported as the chain it is, not as the hundreds of
4856
+ * residue indices it stands for, so echoing it back costs nothing. Only a
4857
+ * selection that is exactly this one chain toggles off - with a residue or
4858
+ * another chain also selected, the click is narrowing to this chain rather
4859
+ * than undoing itself.
4860
+ */
4861
+ const handleChainSelect = (0, react.useCallback)((chainId) => {
4862
+ var _selection$chains2, _selection$residues;
4863
+ const isOnlyThisChain = (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);
4864
+ onSelectionChange === null || onSelectionChange === void 0 || onSelectionChange(isOnlyThisChain ? null : { chains: [chainId] });
4865
+ }, [onSelectionChange, selection]);
4866
+ /**
4867
+ * Lights a chain up while its name is pointed at, in the legend or in the
4868
+ * sequence panel's captions.
4869
+ *
4870
+ * Reached through a ref because the two ends need each other: lighting a
4871
+ * chain up takes the plugin, which the hook below creates, and that hook
4872
+ * has to be handed the callback to give the captions it renders. This one
4873
+ * is stable and forwards to whatever the highlight hook installs once
4874
+ * there is a plugin for it to talk to.
4875
+ */
4876
+ const highlightChainRef = (0, react.useRef)(() => void 0);
4877
+ const highlightChain = (0, react.useCallback)((chainId) => {
4878
+ highlightChainRef.current(chainId);
4879
+ }, []);
4880
+ const [chains, setChains] = (0, react.useState)([]);
4881
+ const { colors: chainColors, hidden: hiddenChains, toggleChain } = useChains({
4882
+ chainColors: chainColorOverrides,
4883
+ chains,
4884
+ hiddenChains: hiddenChainsProp,
4885
+ onChainVisibilityChange
4886
+ });
4887
+ const { chainColorThemeRef, chains: loadedChains, isReady, pluginRef, residuesByChainRef, residueValueThemeRef, setClipRatio } = useMolstarPlugin({
4888
+ backgroundColor: bgColor,
4889
+ chainColors,
3113
4890
  containerRef: pluginMountRef,
3114
- edgeColor: (0, react.useMemo)(() => {
3115
- var _semanticColors$base2;
3116
- return themeColor(semanticColors === null || semanticColors === void 0 || (_semanticColors$base2 = semanticColors.base) === null || _semanticColors$base2 === void 0 ? void 0 : _semanticColors$base2.borderPrimary, FALLBACK_EDGE_COLOR);
3117
- }, [semanticColors]),
4891
+ download,
4892
+ edgeColor,
3118
4893
  hasPlddt,
3119
- highlightColor: (0, react.useMemo)(() => {
3120
- var _semanticColors$base3;
3121
- return themeColor(semanticColors === null || semanticColors === void 0 || (_semanticColors$base3 = semanticColors.base) === null || _semanticColors$base3 === void 0 ? void 0 : _semanticColors$base3.fillSelected, FALLBACK_HIGHLIGHT_COLOR);
3122
- }, [semanticColors]),
4894
+ hiddenChains,
4895
+ highlightColor,
3123
4896
  mode,
4897
+ molstarSpec,
4898
+ onChainHover: highlightChain,
4899
+ onChainSelect: handleChainSelect,
4900
+ onChainToggle: toggleChain,
3124
4901
  onResidueClick,
3125
- onResidueHover: (0, react.useCallback)((residue) => {
3126
- setHoveredResidue((prev) => {
3127
- if (residue === null) return prev === null ? prev : null;
3128
- const label = residueLabel(residue);
3129
- if (prev !== null && prev.index === residue.index && prev.label === label) return prev;
3130
- return {
3131
- index: residue.index,
3132
- label
3133
- };
3134
- });
3135
- onResidueHover === null || onResidueHover === void 0 || onResidueHover(residue);
3136
- }, [onResidueHover]),
3137
- onSelectionClear,
4902
+ onResidueHover: handleResidueHover,
4903
+ onSelectionChange,
4904
+ onSelectionClear: handleSelectionClear,
3138
4905
  pdb: processedPdb,
4906
+ selectedChains,
3139
4907
  sequenceViewerBackgroundColor,
3140
4908
  showAxes,
3141
4909
  showSequenceViewer
3142
4910
  });
3143
- useResidueOverlay({
4911
+ highlightChainRef.current = useChainHighlight({
4912
+ disabled: disableChainHighlightOnHover,
4913
+ pluginRef
4914
+ });
4915
+ (0, react.useEffect)(() => {
4916
+ setChains(loadedChains);
4917
+ }, [loadedChains]);
4918
+ const onChainsChangeRef = (0, react.useRef)(onChainsChange);
4919
+ onChainsChangeRef.current = onChainsChange;
4920
+ (0, react.useEffect)(() => {
4921
+ var _onChainsChangeRef$cu;
4922
+ (_onChainsChangeRef$cu = onChainsChangeRef.current) === null || _onChainsChangeRef$cu === void 0 || _onChainsChangeRef$cu.call(onChainsChangeRef, loadedChains);
4923
+ }, [loadedChains]);
4924
+ useStructureColoring({
4925
+ chainColorThemeRef,
4926
+ chainColors,
3144
4927
  hasPlddt,
3145
4928
  isReady,
3146
4929
  mode,
@@ -3148,46 +4931,49 @@ const ProteinStructureViewer = (0, react.forwardRef)((props, ref) => {
3148
4931
  pluginRef,
3149
4932
  residueValueThemeRef
3150
4933
  });
3151
- useResidueFocus({
4934
+ useSelectionFocus({
4935
+ hiddenChains,
3152
4936
  isReady,
3153
4937
  pluginRef,
3154
- selectedResidue,
4938
+ selection,
3155
4939
  setClipRatio
3156
4940
  });
3157
- (0, react.useEffect)(() => {
3158
- const plugin = pluginRef.current;
3159
- if (!plugin || !isReady || selectedResidue === null) {
3160
- setSelectedLabel(null);
3161
- return;
3162
- }
3163
- const loci = lociForResidueIndex(plugin, selectedResidue);
3164
- const residue = loci && residueRefFromLoci(loci);
3165
- setSelectedLabel(residue ? residueLabel(residue) : null);
3166
- }, [
4941
+ const { hoveredReadout, selectedReadout } = useSelectionReadout({
4942
+ hoveredResidue,
3167
4943
  isReady,
4944
+ plddt,
3168
4945
  pluginRef,
3169
- selectedResidue
3170
- ]);
3171
- const buildReadout = (0, react.useCallback)((label, index) => {
3172
- var _plddt$index, _residueOverlay$value;
3173
- return {
3174
- label,
3175
- plddt: (_plddt$index = plddt === null || plddt === void 0 ? void 0 : plddt[index]) !== null && _plddt$index !== void 0 ? _plddt$index : null,
3176
- value: (_residueOverlay$value = residueOverlay === null || residueOverlay === void 0 ? void 0 : residueOverlay.values.get(index)) !== null && _residueOverlay$value !== void 0 ? _residueOverlay$value : null
3177
- };
3178
- }, [plddt, residueOverlay]);
3179
- const hoveredReadout = (0, react.useMemo)(() => hoveredResidue === null ? null : buildReadout(hoveredResidue.label, hoveredResidue.index), [hoveredResidue, buildReadout]);
3180
- const selectedReadout = (0, react.useMemo)(() => selectedResidue === null || selectedLabel === null ? null : buildReadout(selectedLabel, selectedResidue), [
3181
- selectedResidue,
3182
- selectedLabel,
3183
- buildReadout
3184
- ]);
4946
+ residueOverlay,
4947
+ residuesByChainRef,
4948
+ selection
4949
+ });
3185
4950
  const scaleProps = (0, react.useMemo)(() => resolveScaleProps(residueOverlay, hasPlddt), [residueOverlay, hasPlddt]);
4951
+ /**
4952
+ * What a chain's swatch can show follows whatever is painting the
4953
+ * structure, in the same order the color key resolves in.
4954
+ *
4955
+ * Chain coloring gives each chain one color of its own. pLDDT gives every
4956
+ * chain the same four bands, which says they are colored by confidence
4957
+ * rather than telling them apart - so the swatch carries the whole key,
4958
+ * quartered. An overlay is continuous and has no discrete colors to put in
4959
+ * a swatch at all, so the rows keep their labels and toggles and the color
4960
+ * key beside them does the describing.
4961
+ */
4962
+ const chainColoringActive = !residueOverlay && !hasPlddt;
4963
+ const plddtColoringActive = !residueOverlay && hasPlddt;
3186
4964
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ViewerRoot, _objectSpread2(_objectSpread2({
3187
4965
  ref,
3188
4966
  showSequenceViewer
3189
4967
  }, rest), {}, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PluginMount, { ref: pluginMountRef }), showLegend && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StructureLegend, _objectSpread2({
4968
+ chainBandColors: plddtColoringActive ? PLDDT_BAND_COLORS : void 0,
4969
+ chainColors: chainColoringActive ? chainColors : void 0,
4970
+ chains: showChainLegend ? chains : [],
4971
+ hiddenChains,
3190
4972
  hoveredResidue: hoveredReadout,
4973
+ onChainHover: highlightChain,
4974
+ onChainSelect: handleChainSelect,
4975
+ onChainToggle: toggleChain,
4976
+ selectedChains,
3191
4977
  selectedResidue: selectedReadout,
3192
4978
  showSequenceViewer,
3193
4979
  stats: stats !== null && stats !== void 0 ? stats : []
@@ -3670,6 +5456,7 @@ const StackedBarChart = (props) => {
3670
5456
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ChartWrapper, _objectSpread2(_objectSpread2({ width }, rest), {}, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(TitleContainer, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChartTitle, { children: title }), !hideBadge && displayBadge && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StyledBadge, { children: displayBadge })] }), chartContent] }));
3671
5457
  };
3672
5458
  //#endregion
5459
+ exports.DOWNLOAD_RESOLUTIONS = DOWNLOAD_RESOLUTIONS;
3673
5460
  exports.HeatmapChart = HeatmapChart_default;
3674
5461
  exports.PLASMA_COLOR_SCALE = PLASMA_COLOR_SCALE;
3675
5462
  exports.PLDDT_COLOR_SCALE = PLDDT_COLOR_SCALE;