@czi-sds/data-viz 2.1.1 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs.js CHANGED
@@ -32,10 +32,12 @@ 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
34
  let molstar_lib_mol_model_structure = require("molstar/lib/mol-model/structure");
35
+ let molstar_lib_mol_plugin_state_helpers_structure_transparency = require("molstar/lib/mol-plugin-state/helpers/structure-transparency");
35
36
  let molstar_lib_mol_plugin_ui = require("molstar/lib/mol-plugin-ui");
36
37
  let molstar_lib_mol_plugin_ui_react18 = require("molstar/lib/mol-plugin-ui/react18");
37
38
  let molstar_lib_mol_plugin_ui_spec = require("molstar/lib/mol-plugin-ui/spec");
38
39
  let molstar_lib_mol_plugin_behavior = require("molstar/lib/mol-plugin/behavior");
40
+ let molstar_lib_mol_plugin_behavior_static_state = require("molstar/lib/mol-plugin/behavior/static/state");
39
41
  let molstar_lib_mol_plugin_config = require("molstar/lib/mol-plugin/config");
40
42
  let molstar_lib_mol_repr_representation = require("molstar/lib/mol-repr/representation");
41
43
  let rxjs = require("rxjs");
@@ -52,10 +54,14 @@ let molstar_lib_mol_util = require("molstar/lib/mol-util");
52
54
  let molstar_lib_mol_util_param_definition = require("molstar/lib/mol-util/param-definition");
53
55
  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");
57
+ let molstar_lib_mol_util_color = require("molstar/lib/mol-util/color");
55
58
  let molstar_lib_mol_plugin_commands = require("molstar/lib/mol-plugin/commands");
56
59
  let molstar_lib_mol_math_linear_algebra = require("molstar/lib/mol-math/linear-algebra");
60
+ let molstar_lib_mol_model_structure_model_types = require("molstar/lib/mol-model/structure/model/types");
61
+ let molstar_lib_mol_theme_label = require("molstar/lib/mol-theme/label");
62
+ let molstar_lib_mol_script_language_builder = require("molstar/lib/mol-script/language/builder");
57
63
  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");
64
+ let molstar_lib_mol_plugin_behavior_dynamic_selection_structure_focus_representation = require("molstar/lib/mol-plugin/behavior/dynamic/selection/structure-focus-representation");
59
65
  //#region src/common/colorScales.ts
60
66
  /**
61
67
  * Perceptually uniform sequential scale (matplotlib's plasma), the default for
@@ -574,6 +580,191 @@ const HeatmapChart = (0, react.forwardRef)((props, ref) => {
574
580
  }
575
581
  });
576
582
  var HeatmapChart_default = (0, react.memo)(HeatmapChart);
583
+ //#endregion
584
+ //#region src/core/ProteinStructureViewer/components/ChainLegend/style.ts
585
+ /** Opacity applied to a hidden chain's row, so it reads as off but reachable. */
586
+ const HIDDEN_OPACITY = .4;
587
+ /** Side of the color swatch. */
588
+ const SWATCH_SIZE = 8;
589
+ /**
590
+ * The rows, stacked and right-aligned against the color key beside them.
591
+ *
592
+ * Pointer events are switched back on here: the legend overlay turns them off
593
+ * so drags meant for the structure are not intercepted, and this is one of the
594
+ * two places in it that has to be clickable.
595
+ */
596
+ const ChainRowList = (0, _emotion_styled.default)("div")`
597
+ display: flex;
598
+ flex-direction: column;
599
+ align-items: flex-end;
600
+ pointer-events: auto;
601
+ max-height: 80px;
602
+ overflow-y: auto;
603
+ `;
604
+ const ChainRow = (0, _emotion_styled.default)("div")`
605
+ display: flex;
606
+ align-items: center;
607
+
608
+ ${(props) => {
609
+ const spaces = (0, _czi_sds_components.getSpaces)(props);
610
+ return `
611
+ gap: ${spaces === null || spaces === void 0 ? void 0 : spaces.xs}px;
612
+ `;
613
+ }}
614
+ `;
615
+ const ChainSwatch = (0, _emotion_styled.default)("div")`
616
+ flex: none;
617
+ width: ${SWATCH_SIZE}px;
618
+ height: ${SWATCH_SIZE}px;
619
+ border-radius: 1px;
620
+
621
+ ${(props) => `
622
+ background-color: ${props.swatchColor};
623
+ opacity: ${props.isHidden ? HIDDEN_OPACITY : 1};
624
+ `}
625
+ `;
626
+ /**
627
+ * A swatch quartered into a grid, for a coloring no single color stands for.
628
+ *
629
+ * Occupies the same box as the solid swatch so the rows stay aligned whichever
630
+ * one a chain gets, with a hairline gap between the quadrants to read as four
631
+ * colors rather than one blurred square.
632
+ */
633
+ const ChainSwatchGrid = (0, _emotion_styled.default)("div")`
634
+ flex: none;
635
+ display: grid;
636
+ grid-template-columns: repeat(2, 1fr);
637
+ grid-template-rows: repeat(2, 1fr);
638
+ gap: 0px;
639
+ width: ${SWATCH_SIZE}px;
640
+ height: ${SWATCH_SIZE}px;
641
+ border-radius: 1px;
642
+ overflow: hidden;
643
+
644
+ ${(props) => `
645
+ opacity: ${props.isHidden ? HIDDEN_OPACITY : 1};
646
+ `}
647
+ `;
648
+ const ChainSwatchQuadrant = (0, _emotion_styled.default)("div")`
649
+ ${(props) => `
650
+ background-color: ${props.swatchColor};
651
+ `}
652
+ `;
653
+ /**
654
+ * Selects the whole chain, or clears it when it is already the selection. A
655
+ * button so it is reachable from the keyboard.
656
+ */
657
+ const ChainLabel$1 = (0, _emotion_styled.default)("button")`
658
+ ${_czi_sds_components.fontBodyXxxs}
659
+
660
+ padding: 0;
661
+ border: none;
662
+ background: none;
663
+
664
+ ${(props) => {
665
+ var _semanticColors$base, _semanticColors$base2, _semanticColors$base3;
666
+ const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
667
+ return `
668
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base = semanticColors.base) === null || _semanticColors$base === void 0 ? void 0 : _semanticColors$base.textSecondary};
669
+ cursor: ${props.isHidden ? "default" : "pointer"};
670
+ font-weight: ${props.isSelected ? 600 : "inherit"};
671
+ opacity: ${props.isHidden ? HIDDEN_OPACITY : 1};
672
+
673
+ &:hover {
674
+ 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};
675
+ }
676
+ `;
677
+ }}
678
+ `;
679
+ const VisibilityToggle = (0, _emotion_styled.default)("button")`
680
+ display: flex;
681
+ align-items: center;
682
+ padding: 0;
683
+ border: none;
684
+ background: none;
685
+ cursor: pointer;
686
+ margin-top: 1px;
687
+
688
+ ${(props) => {
689
+ var _semanticColors$base4, _semanticColors$base5;
690
+ const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
691
+ return `
692
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base4 = semanticColors.base) === null || _semanticColors$base4 === void 0 ? void 0 : _semanticColors$base4.textTertiaryOnDark};
693
+
694
+ &:hover {
695
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base5 = semanticColors.base) === null || _semanticColors$base5 === void 0 ? void 0 : _semanticColors$base5.textPrimary};
696
+ }
697
+
698
+ svg {
699
+ display: block;
700
+ }
701
+ `;
702
+ }}
703
+ `;
704
+ //#endregion
705
+ //#region src/core/ProteinStructureViewer/components/ChainLegend/index.tsx
706
+ /**
707
+ * Lists the structure's chains, each with its color and a visibility toggle.
708
+ *
709
+ * Only drawn for a complex: one chain needs no key to tell it from anything,
710
+ * and hiding it would leave an empty canvas with no way back. Its home is the
711
+ * legend rather than the sequence panel so that it survives
712
+ * `showSequenceViewer={false}`, where the chains would otherwise have nowhere
713
+ * to be listed.
714
+ */
715
+ function ChainLegend({ bandColors, chainColors, chains, hiddenChains, onChainHover, onChainSelect, onChainToggle, selectedChains }) {
716
+ if (chains.length < 2) return null;
717
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChainRowList, { children: chains.map((chain) => {
718
+ var _selectedChains$has;
719
+ const hidden = hiddenChains.has(chain.chainId);
720
+ const selected = (_selectedChains$has = selectedChains === null || selectedChains === void 0 ? void 0 : selectedChains.has(chain.chainId)) !== null && _selectedChains$has !== void 0 ? _selectedChains$has : false;
721
+ const color = chainColors === null || chainColors === void 0 ? void 0 : chainColors.get(chain.chainId);
722
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ChainRow, {
723
+ onBlur: () => onChainHover === null || onChainHover === void 0 ? void 0 : onChainHover(null),
724
+ onFocus: () => onChainHover === null || onChainHover === void 0 ? void 0 : onChainHover(chain.chainId),
725
+ onMouseEnter: () => onChainHover === null || onChainHover === void 0 ? void 0 : onChainHover(chain.chainId),
726
+ onMouseLeave: () => onChainHover === null || onChainHover === void 0 ? void 0 : onChainHover(null),
727
+ children: [
728
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_czi_sds_components.Tooltip, {
729
+ arrow: true,
730
+ placement: "top",
731
+ disableInteractive: true,
732
+ title: hidden ? `Chain ${chain.label} is hidden` : `Click to ${selected ? "deselect" : "select"} Chain ${chain.label}`,
733
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ChainLabel$1, {
734
+ "aria-pressed": selected,
735
+ disabled: hidden,
736
+ isHidden: hidden,
737
+ isSelected: selected,
738
+ onClick: () => onChainSelect(chain.chainId),
739
+ type: "button",
740
+ children: ["Chain ", chain.label]
741
+ })
742
+ }),
743
+ color !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChainSwatch, {
744
+ isHidden: hidden,
745
+ swatchColor: color
746
+ }),
747
+ color === void 0 && bandColors !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChainSwatchGrid, {
748
+ isHidden: hidden,
749
+ children: bandColors.map((band) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChainSwatchQuadrant, { swatchColor: band }, band))
750
+ }),
751
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_czi_sds_components.Tooltip, {
752
+ arrow: true,
753
+ placement: "top",
754
+ disableInteractive: true,
755
+ title: hidden ? `Show Chain ${chain.label}` : `Hide Chain ${chain.label}`,
756
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VisibilityToggle, {
757
+ "aria-label": `${hidden ? "Show" : "Hide"} chain ${chain.label}`,
758
+ "aria-pressed": hidden,
759
+ onClick: () => onChainToggle(chain.chainId),
760
+ type: "button",
761
+ 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 })
762
+ })
763
+ })
764
+ ]
765
+ }, chain.chainId);
766
+ }) });
767
+ }
577
768
  /** Height of the color bar itself. */
578
769
  const BAR_HEIGHT = 8;
579
770
  /**
@@ -698,14 +889,24 @@ const ViewerRoot = (0, _emotion_styled.default)("div")`
698
889
  height: 100% !important;
699
890
  }
700
891
 
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 {
892
+ /*
893
+ * The expanded layout is kept hidden because it is the one piece of Mol*
894
+ * chrome that would escape this container: it takes the plugin out of the
895
+ * box it was given and over the page.
896
+ *
897
+ * Its neighbours used to be here too - the viewport's icon column, and the
898
+ * left, right and bottom regions. They are not any more, because hiding them
899
+ * from here made them unreachable: Mol* already gates each of them behind a
900
+ * PluginConfig.Viewport flag and behind layout.initial.regionState, both of
901
+ * which a consumer can set through molstarSpec, and a display:none on top of
902
+ * that overrode the answer. None of them shows by default - the viewer's own
903
+ * flags and region state are what keep them away - so this only has to stay
904
+ * out of their way.
905
+ *
906
+ * No backticks in here: this is inside a template literal, and one would end
907
+ * it.
908
+ */
909
+ .msp-plugin .msp-layout-expanded {
709
910
  display: none !important;
710
911
  }
711
912
 
@@ -856,6 +1057,18 @@ const StatsGrid = (0, _emotion_styled.default)("div")`
856
1057
  `;
857
1058
  }}
858
1059
  `;
1060
+ const ChainScaleColumn = (0, _emotion_styled.default)("div")`
1061
+ display: flex;
1062
+ flex-direction: column;
1063
+ align-items: flex-end;
1064
+
1065
+ ${(props) => {
1066
+ const spaces = (0, _czi_sds_components.getSpaces)(props);
1067
+ return `
1068
+ gap: ${spaces === null || spaces === void 0 ? void 0 : spaces.l}px;
1069
+ `;
1070
+ }}
1071
+ `;
859
1072
  const StatColumnWrapper = (0, _emotion_styled.default)("div")`
860
1073
  display: flex;
861
1074
  flex-direction: column;
@@ -948,6 +1161,19 @@ function StatColumn({ label, value }) {
948
1161
  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 })] });
949
1162
  }
950
1163
  /**
1164
+ * Builds one readout slot.
1165
+ *
1166
+ * A readout covering several residues reports a mean, and the label says so:
1167
+ * calling it "pLDDT" while it averages ninety of them would read as a single
1168
+ * residue's score.
1169
+ */
1170
+ function readoutSlot(label, value, residueCount) {
1171
+ return {
1172
+ label: residueCount > 1 ? `Mean ${label}` : label,
1173
+ value: value !== null ? value.toFixed(VALUE_PRECISION) : MISSING_VALUE
1174
+ };
1175
+ }
1176
+ /**
951
1177
  * Stats and color key overlaid on the structure viewer.
952
1178
  *
953
1179
  * When a residue is selected or hovered, the whole-structure stats are replaced
@@ -957,24 +1183,19 @@ function StatColumn({ label, value }) {
957
1183
  * residue takes priority so the user can inspect others without losing their
958
1184
  * selection.
959
1185
  */
960
- function StructureLegend({ hoveredResidue = null, scale, scaleLabel, scaleMax = null, scaleMin = 0, scaleTooltip, scaleTooltipProps, selectedResidue = null, showSequenceViewer, stats, valueLabel }) {
961
- var _stats$, _stats$2, _stats$3;
1186
+ 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 }) {
1187
+ var _activeResidue$residu, _stats$, _stats$2, _stats$3;
962
1188
  const activeResidue = hoveredResidue !== null && hoveredResidue !== void 0 ? hoveredResidue : selectedResidue;
963
1189
  const showResidue = activeResidue !== null;
964
1190
  const showValue = showResidue && valueLabel !== void 0;
1191
+ const residueCount = (_activeResidue$residu = activeResidue === null || activeResidue === void 0 ? void 0 : activeResidue.residueCount) !== null && _activeResidue$residu !== void 0 ? _activeResidue$residu : 1;
965
1192
  const slots = [
966
1193
  showResidue ? {
967
- label: "Residue",
1194
+ label: residueCount > 1 ? "Selection" : "Residue",
968
1195
  value: activeResidue.label
969
1196
  } : (_stats$ = stats[0]) !== null && _stats$ !== void 0 ? _stats$ : null,
970
- showValue ? {
971
- label: valueLabel,
972
- value: activeResidue.value !== null ? activeResidue.value.toFixed(VALUE_PRECISION) : MISSING_VALUE
973
- } : (_stats$2 = stats[1]) !== null && _stats$2 !== void 0 ? _stats$2 : null,
974
- showResidue ? {
975
- label: "pLDDT",
976
- value: activeResidue.plddt !== null ? activeResidue.plddt.toFixed(VALUE_PRECISION) : MISSING_VALUE
977
- } : (_stats$3 = stats[2]) !== null && _stats$3 !== void 0 ? _stats$3 : null
1197
+ showValue ? readoutSlot(valueLabel, activeResidue.value, residueCount) : (_stats$2 = stats[1]) !== null && _stats$2 !== void 0 ? _stats$2 : null,
1198
+ showResidue ? readoutSlot("pLDDT", activeResidue.plddt, residueCount) : (_stats$3 = stats[2]) !== null && _stats$3 !== void 0 ? _stats$3 : null
978
1199
  ];
979
1200
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(LegendOverlay, {
980
1201
  showSequenceViewer,
@@ -985,7 +1206,16 @@ function StructureLegend({ hoveredResidue = null, scale, scaleLabel, scaleMax =
985
1206
  label: slot.label,
986
1207
  value: slot.value
987
1208
  }, i) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {}, i);
988
- }) }), scale && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ScaleColumn, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ColorScaleLegend, {
1209
+ }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ChainScaleColumn, { children: [onChainToggle && onChainSelect && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChainLegend, {
1210
+ bandColors: chainBandColors,
1211
+ chainColors,
1212
+ chains,
1213
+ hiddenChains: hiddenChains !== null && hiddenChains !== void 0 ? hiddenChains : /* @__PURE__ */ new Set(),
1214
+ onChainHover,
1215
+ onChainSelect,
1216
+ onChainToggle,
1217
+ selectedChains
1218
+ }), scale && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ScaleColumn, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ColorScaleLegend, {
989
1219
  max: scaleMax,
990
1220
  min: scaleMin,
991
1221
  scale
@@ -998,8 +1228,281 @@ function StructureLegend({ hoveredResidue = null, scale, scaleLabel, scaleMax =
998
1228
  size: 10,
999
1229
  weight: "bold"
1000
1230
  }) })
1001
- }))] })] })]
1231
+ }))] })] })] })]
1232
+ });
1233
+ }
1234
+ //#endregion
1235
+ //#region \0@oxc-project+runtime@0.134.0/helpers/esm/asyncToGenerator.js
1236
+ function asyncGeneratorStep(n, t, e, r, o, a, c) {
1237
+ try {
1238
+ var i = n[a](c), u = i.value;
1239
+ } catch (n) {
1240
+ e(n);
1241
+ return;
1242
+ }
1243
+ i.done ? t(u) : Promise.resolve(u).then(r, o);
1244
+ }
1245
+ function _asyncToGenerator(n) {
1246
+ return function() {
1247
+ var t = this, e = arguments;
1248
+ return new Promise(function(r, o) {
1249
+ var a = n.apply(t, e);
1250
+ function _next(n) {
1251
+ asyncGeneratorStep(a, r, o, _next, _throw, "next", n);
1252
+ }
1253
+ function _throw(n) {
1254
+ asyncGeneratorStep(a, r, o, _next, _throw, "throw", n);
1255
+ }
1256
+ _next(void 0);
1257
+ });
1258
+ };
1259
+ }
1260
+ //#endregion
1261
+ //#region src/core/ProteinStructureViewer/hooks/useChainHighlight.ts
1262
+ /**
1263
+ * How transparent the other chains go while one is pointed at.
1264
+ *
1265
+ * Enough that the chain under the pointer is the one being read, not enough
1266
+ * that the rest of the complex stops being context for it. 0 is opaque, 1 is
1267
+ * invisible.
1268
+ */
1269
+ const CHAIN_DIM_TRANSPARENCY = .65;
1270
+ /**
1271
+ * The chain id in a component key.
1272
+ *
1273
+ * Chain components are tagged `structure-component-<part>-<chainId>`, and a
1274
+ * key is the component's tags sorted and joined with commas. Anchoring on the
1275
+ * part name and stopping at the comma is what keeps `A` from also matching the
1276
+ * chain named `AA`.
1277
+ */
1278
+ const COMPONENT_CHAIN_ID = /structure-component-(?:polymer|ligand)-([^,]+)/;
1279
+ /** The chain a component was built for, or undefined if it is not a chain's. */
1280
+ function componentChainId(component) {
1281
+ var _component$key, _COMPONENT_CHAIN_ID$e;
1282
+ const key = (_component$key = component.key) !== null && _component$key !== void 0 ? _component$key : component.cell.transform.ref;
1283
+ return (_COMPONENT_CHAIN_ID$e = COMPONENT_CHAIN_ID.exec(key)) === null || _COMPONENT_CHAIN_ID$e === void 0 ? void 0 : _COMPONENT_CHAIN_ID$e[1];
1284
+ }
1285
+ /** Every chain component of every loaded structure, polymers and ligands. */
1286
+ function structureComponents(plugin) {
1287
+ return plugin.managers.structure.hierarchy.current.structures.flatMap((entry) => entry.components);
1288
+ }
1289
+ /** A set of chain ids as one comparable string. */
1290
+ function chainsKey(chains) {
1291
+ return [...chains].sort().join("\0");
1292
+ }
1293
+ /**
1294
+ * Makes every chain outside `lit` transparent, or all of them opaque again when
1295
+ * nothing is lit.
1296
+ *
1297
+ * Transparency layers accumulate, so what is already there is cleared before
1298
+ * the next chain is dimmed rather than added to - otherwise moving along the
1299
+ * list would leave every chain it passed dimmed behind it.
1300
+ *
1301
+ * False when there is no structure to write to yet, which is what a selection
1302
+ * arriving before the first load looks like. It is applied again after the load
1303
+ * rather than being taken as done.
1304
+ */
1305
+ function dimChainsOutside(_x2, _x3) {
1306
+ return _dimChainsOutside.apply(this, arguments);
1307
+ }
1308
+ function _dimChainsOutside() {
1309
+ _dimChainsOutside = _asyncToGenerator(function* (plugin, lit) {
1310
+ const components = structureComponents(plugin);
1311
+ if (components.length === 0) return false;
1312
+ yield (0, molstar_lib_mol_plugin_state_helpers_structure_transparency.clearStructureTransparency)(plugin, components);
1313
+ if (lit.size === 0) return true;
1314
+ const dimmed = components.filter((component) => {
1315
+ const chainId = componentChainId(component);
1316
+ return chainId !== void 0 && !lit.has(chainId);
1317
+ });
1318
+ if (dimmed.length === 0) return true;
1319
+ yield (0, molstar_lib_mol_plugin_state_helpers_structure_transparency.setStructureTransparency)(plugin, dimmed, CHAIN_DIM_TRANSPARENCY, function() {
1320
+ var _ref = _asyncToGenerator(function* (structure) {
1321
+ return molstar_lib_mol_model_structure.Structure.toStructureElementLoci(structure);
1322
+ });
1323
+ return function(_x) {
1324
+ return _ref.apply(this, arguments);
1325
+ };
1326
+ }());
1327
+ return true;
1328
+ });
1329
+ return _dimChainsOutside.apply(this, arguments);
1330
+ }
1331
+ /**
1332
+ * Leaves one chain lit and dims the rest: the chain whose name is pointed at,
1333
+ * in the sequence panel's captions or the chain legend's rows, or the chain the
1334
+ * selection covers once the pointer has gone.
1335
+ *
1336
+ * The chain left lit is exactly as it was - in the chain palette, in pLDDT
1337
+ * bands, or in an overlay - and the others recede around it. Nothing is painted
1338
+ * over the top, so what the reader is being pointed at is still the color the
1339
+ * legend beside it is describing, and a selected chain reads the same as a
1340
+ * hovered one rather than sprouting a highlight of its own.
1341
+ *
1342
+ * A hover outranks the selection while it lasts, since pointing at a chain is a
1343
+ * question about that chain, and hands it back on the way out.
1344
+ *
1345
+ * Dimming is a state-tree commit rather than a renderer setting, so it is
1346
+ * applied one at a time and coalesced: a pointer crossing the list faster than
1347
+ * a commit finishes skips the chains it passed over and settles on the one it
1348
+ * came to rest on, instead of working through a queue of hovers that have
1349
+ * already been left.
1350
+ */
1351
+ function useChainHighlight({ disabled, hiddenChains, loadCount, pluginRef, selectedChains }) {
1352
+ const hoveredRef = (0, react.useRef)(null);
1353
+ /**
1354
+ * The chains left lit on the canvas as it stands. A freshly loaded structure
1355
+ * carries no dimming at all, which is what the empty key means.
1356
+ */
1357
+ const shownRef = (0, react.useRef)(chainsKey(/* @__PURE__ */ new Set()));
1358
+ const committingRef = (0, react.useRef)(false);
1359
+ /**
1360
+ * Read when a commit comes round rather than closed over, so one already in
1361
+ * flight settles on the current props instead of the ones it started under.
1362
+ */
1363
+ const propsRef = (0, react.useRef)({
1364
+ disabled,
1365
+ hiddenChains,
1366
+ selectedChains
1367
+ });
1368
+ propsRef.current = {
1369
+ disabled,
1370
+ hiddenChains,
1371
+ selectedChains
1372
+ };
1373
+ const litChains = (0, react.useCallback)(() => {
1374
+ const { disabled: hoverOff, hiddenChains: hidden, selectedChains: selected } = propsRef.current;
1375
+ const hovered = hoveredRef.current;
1376
+ if (!hoverOff && hovered !== null && !hidden.has(hovered)) return new Set([hovered]);
1377
+ return new Set([...selected].filter((chainId) => !hidden.has(chainId)));
1378
+ }, []);
1379
+ const settle = (0, react.useCallback)(_asyncToGenerator(function* () {
1380
+ if (committingRef.current) return;
1381
+ committingRef.current = true;
1382
+ try {
1383
+ while (pluginRef.current) {
1384
+ const lit = litChains();
1385
+ const key = chainsKey(lit);
1386
+ if (key === shownRef.current) break;
1387
+ if (!(yield dimChainsOutside(pluginRef.current, lit))) break;
1388
+ shownRef.current = key;
1389
+ }
1390
+ } catch (error) {
1391
+ console.error("Failed to dim chains:", error);
1392
+ } finally {
1393
+ committingRef.current = false;
1394
+ }
1395
+ }), [litChains, pluginRef]);
1396
+ const highlightChain = (0, react.useCallback)((chainId) => {
1397
+ hoveredRef.current = chainId;
1398
+ settle();
1399
+ }, [settle]);
1400
+ (0, react.useEffect)(() => {
1401
+ settle();
1402
+ }, [
1403
+ disabled,
1404
+ hiddenChains,
1405
+ selectedChains,
1406
+ settle
1407
+ ]);
1408
+ (0, react.useEffect)(() => {
1409
+ shownRef.current = chainsKey(/* @__PURE__ */ new Set());
1410
+ settle();
1411
+ }, [loadCount, settle]);
1412
+ return highlightChain;
1413
+ }
1414
+ //#endregion
1415
+ //#region src/common/chainColors.ts
1416
+ /**
1417
+ * Default colors for telling chains apart, in assignment order.
1418
+ *
1419
+ * The Okabe-Ito qualitative palette, which is built to stay distinguishable
1420
+ * under every common form of color blindness - the reason to prefer it over a
1421
+ * hand-picked set, since a complex is read by which chain is which and a
1422
+ * viewer that collapses two of them into one color stops answering that.
1423
+ *
1424
+ * Its black is left out: the canvas inverts with the theme, so a chain painted
1425
+ * black would vanish against the dark background. The rest carry enough
1426
+ * lightness to hold up against both.
1427
+ *
1428
+ * Chains past the end of the palette wrap around to the start.
1429
+ */
1430
+ const CHAIN_COLOR_PALETTE = [
1431
+ "#009E73",
1432
+ "#0072B2",
1433
+ "#E69F00",
1434
+ "#CC79A7",
1435
+ "#56B4E9",
1436
+ "#D55E00",
1437
+ "#F0E442"
1438
+ ];
1439
+ /**
1440
+ * Assigns each chain a color, in the order given, with `overrides` taking
1441
+ * precedence by `chainId`.
1442
+ *
1443
+ * Palette positions are spent on every chain whether or not it is overridden,
1444
+ * so overriding one chain does not shift the colors of the others.
1445
+ */
1446
+ function chainColorMap(chainIds, overrides) {
1447
+ const colors = /* @__PURE__ */ new Map();
1448
+ chainIds.forEach((chainId, i) => {
1449
+ var _overrides$chainId;
1450
+ const fallback = CHAIN_COLOR_PALETTE[i % CHAIN_COLOR_PALETTE.length];
1451
+ colors.set(chainId, (_overrides$chainId = overrides === null || overrides === void 0 ? void 0 : overrides[chainId]) !== null && _overrides$chainId !== void 0 ? _overrides$chainId : fallback);
1002
1452
  });
1453
+ return colors;
1454
+ }
1455
+ //#endregion
1456
+ //#region src/core/ProteinStructureViewer/hooks/useChains.ts
1457
+ /**
1458
+ * Reconciles the chains found in the structure with the visibility and coloring
1459
+ * the consumer asked for.
1460
+ *
1461
+ * Visibility is controlled when `hiddenChains` is passed and owned here when it
1462
+ * is not, so the legend's toggles work out of the box without taking the
1463
+ * decision away from a consumer that wants it. `onChainVisibilityChange` fires
1464
+ * either way: a consumer that only wants to follow along should not have to
1465
+ * take ownership to hear about it.
1466
+ */
1467
+ function useChains({ chainColors, chains, hiddenChains, onChainVisibilityChange }) {
1468
+ const isControlled = hiddenChains !== void 0;
1469
+ const [ownHidden, setOwnHidden] = (0, react.useState)([]);
1470
+ const onChainVisibilityChangeRef = (0, react.useRef)(onChainVisibilityChange);
1471
+ onChainVisibilityChangeRef.current = onChainVisibilityChange;
1472
+ const chainIds = (0, react.useMemo)(() => chains.map((c) => c.chainId), [chains]);
1473
+ /**
1474
+ * Chains the structure no longer has cannot be unhidden through the legend,
1475
+ * since it has no row for them, so a stale id would hide a chain that shares
1476
+ * its name in the next structure. Only the uncontrolled side is pruned; a
1477
+ * controlled consumer owns its own list.
1478
+ */
1479
+ (0, react.useEffect)(() => {
1480
+ if (isControlled) return;
1481
+ setOwnHidden((prev) => {
1482
+ const kept = prev.filter((id) => chainIds.includes(id));
1483
+ return kept.length === prev.length ? prev : kept;
1484
+ });
1485
+ }, [chainIds, isControlled]);
1486
+ const hiddenKey = [...isControlled ? hiddenChains : ownHidden].sort().join("\0");
1487
+ const hidden = (0, react.useMemo)(() => new Set(hiddenKey === "" ? [] : hiddenKey.split("\0")), [hiddenKey]);
1488
+ return {
1489
+ colors: (0, react.useMemo)(() => chainColorMap(chainIds, chainColors), [chainIds.map((id) => {
1490
+ var _chainColors$id;
1491
+ return `${id}:${(_chainColors$id = chainColors === null || chainColors === void 0 ? void 0 : chainColors[id]) !== null && _chainColors$id !== void 0 ? _chainColors$id : ""}`;
1492
+ }).join("\0")]),
1493
+ hidden,
1494
+ toggleChain: (0, react.useCallback)((chainId) => {
1495
+ var _onChainVisibilityCha;
1496
+ const next = hidden.has(chainId) ? [...hidden].filter((id) => id !== chainId) : [...hidden, chainId];
1497
+ const ordered = chainIds.filter((id) => next.includes(id));
1498
+ if (!isControlled) setOwnHidden(ordered);
1499
+ (_onChainVisibilityCha = onChainVisibilityChangeRef.current) === null || _onChainVisibilityCha === void 0 || _onChainVisibilityCha.call(onChainVisibilityChangeRef, ordered);
1500
+ }, [
1501
+ chainIds,
1502
+ hidden,
1503
+ isControlled
1504
+ ])
1505
+ };
1003
1506
  }
1004
1507
  //#endregion
1005
1508
  //#region src/core/ProteinStructureViewer/utils/theme.ts
@@ -1061,7 +1564,7 @@ function useSubscribe(observable, handler) {
1061
1564
  */
1062
1565
  function useViewSetting(settings, select) {
1063
1566
  const [value, setValue] = (0, react.useState)(() => select(settings.value));
1064
- useSubscribe(settings, (next) => setValue(select(next)));
1567
+ useSubscribe(settings, (next) => setValue(() => select(next)));
1065
1568
  return value;
1066
1569
  }
1067
1570
  //#endregion
@@ -1090,72 +1593,427 @@ const HOVER_THROTTLE_MS = 3 * 16.666;
1090
1593
  /** How long the copy button shows its confirmation state. */
1091
1594
  const COPIED_FEEDBACK_MS = 2e3;
1092
1595
  //#endregion
1093
- //#region src/core/ProteinStructureViewer/hooks/usePlugin.ts
1094
- /**
1095
- * The plugin owning the React root this component renders in.
1096
- *
1097
- * Mol* renders its view components inside its own root and provides the plugin
1098
- * through this context, which is what `PluginUIComponent` reads to populate
1099
- * `this.plugin`.
1100
- */
1101
- function usePlugin() {
1102
- return (0, react.useContext)(molstar_lib_mol_plugin_ui_base.PluginReactContext);
1103
- }
1104
- //#endregion
1105
- //#region src/core/ProteinStructureViewer/components/SequenceView/utils/residue.ts
1106
- /** Reads the sequence index off a residue span, or undefined if it is not one. */
1107
- function readSeqIdx(el) {
1108
- if (!el || !el.getAttribute) return void 0;
1109
- return el.hasAttribute("data-seqid") ? +el.getAttribute(SEQ_ID_ATTR) : void 0;
1110
- }
1111
- /**
1112
- * Nearest ancestor that scrolls vertically, or null if none. Used to scope the
1113
- * residue tooltip's hide-on-scroll listener to the sequence's own overflow
1114
- * container instead of a global window listener.
1115
- */
1116
- function getScrollParent(node) {
1117
- var _node$parentElement;
1118
- let el = (_node$parentElement = node === null || node === void 0 ? void 0 : node.parentElement) !== null && _node$parentElement !== void 0 ? _node$parentElement : null;
1119
- while (el) {
1120
- const overflowY = getComputedStyle(el).overflowY;
1121
- if (overflowY === "auto" || overflowY === "scroll") return el;
1122
- el = el.parentElement;
1123
- }
1124
- return null;
1125
- }
1126
- function getResidueClass(sequenceWrapper, seqIdx) {
1127
- return sequenceWrapper.isHighlighted(seqIdx) || sequenceWrapper.isSelected(seqIdx) || sequenceWrapper.isFocused(seqIdx) ? "msp-sequence-present msp-sequence-residue-active" : "msp-sequence-present msp-sequence-residue";
1128
- }
1129
- /**
1130
- * The label shown above a residue: its author sequence id for atomic units,
1131
- * or a 1-based position for coarse ones.
1132
- *
1133
- * `location` is a scratch object the caller reuses across residues, since this
1134
- * runs once per numbered residue on every render.
1135
- */
1136
- function getSequenceNumber(sequenceWrapper, seqIdx, location) {
1137
- let seqNum = "";
1138
- const loci = sequenceWrapper.getLoci(seqIdx);
1139
- const l = molstar_lib_mol_model_structure.StructureElement.Loci.getFirstLocation(loci, location);
1140
- if (l) {
1141
- if (molstar_lib_mol_model_structure.Unit.isAtomic(l.unit)) {
1142
- const seqId = molstar_lib_mol_model_structure.StructureProperties.residue.auth_seq_id(l);
1143
- const insCode = molstar_lib_mol_model_structure.StructureProperties.residue.pdbx_PDB_ins_code(l);
1144
- seqNum = `${seqId}${insCode ? insCode : ""}`;
1145
- } else if (molstar_lib_mol_model_structure.Unit.isCoarse(l.unit)) seqNum = `${seqIdx + 1}`;
1146
- }
1147
- return seqNum;
1148
- }
1149
- /**
1150
- * How often a sequence number is drawn above the residues.
1151
- *
1152
- * Long sequences get one every ten residues. Short ones number every residue,
1153
- * unless their numbers run to more than one digit, which would crowd them.
1154
- */
1155
- function resolveSequenceNumberPeriod(sequenceWrapper, location, sequenceNumberPeriod) {
1156
- if (sequenceNumberPeriod !== void 0) return sequenceNumberPeriod;
1157
- if (sequenceWrapper.length > 10) return 10;
1158
- if (getSequenceNumber(sequenceWrapper, sequenceWrapper.length - 1, location).length > 1) return 5;
1596
+ //#region src/core/ProteinStructureViewer/components/SequenceView/style.ts
1597
+ /** Height of the fade that masks residues scrolling under the panel header. */
1598
+ const SCROLL_FADE_HEIGHT = 8;
1599
+ const SequencePanel = (0, _emotion_styled.default)("div")`
1600
+ display: flex;
1601
+ flex-direction: column;
1602
+ height: 100%;
1603
+
1604
+ ${(props) => {
1605
+ var _semanticColors$base, _props$backgroundColo, _semanticColors$base2;
1606
+ const spaces = (0, _czi_sds_components.getSpaces)(props);
1607
+ const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
1608
+ return `
1609
+ padding: ${spaces === null || spaces === void 0 ? void 0 : spaces.m}px ${spaces === null || spaces === void 0 ? void 0 : spaces.m}px 0;
1610
+ border-top: 1px solid ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base = semanticColors.base) === null || _semanticColors$base === void 0 ? void 0 : _semanticColors$base.divider};
1611
+ 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};
1612
+ `;
1613
+ }}
1614
+ `;
1615
+ const EmptyState = (0, _emotion_styled.default)("div")`
1616
+ ${_czi_sds_components.fontBodyXs}
1617
+
1618
+ display: flex;
1619
+ align-items: center;
1620
+ justify-content: center;
1621
+ height: 100%;
1622
+
1623
+ ${(props) => {
1624
+ var _props$backgroundColo2, _semanticColors$base3, _semanticColors$base4;
1625
+ const spaces = (0, _czi_sds_components.getSpaces)(props);
1626
+ const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
1627
+ return `
1628
+ padding: ${spaces === null || spaces === void 0 ? void 0 : spaces.m}px;
1629
+ 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};
1630
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base4 = semanticColors.base) === null || _semanticColors$base4 === void 0 ? void 0 : _semanticColors$base4.textTertiaryOnDark};
1631
+ `;
1632
+ }}
1633
+ `;
1634
+ const PanelHeader = (0, _emotion_styled.default)("div")`
1635
+ display: flex;
1636
+ align-items: center;
1637
+
1638
+ ${(props) => {
1639
+ const spaces = (0, _czi_sds_components.getSpaces)(props);
1640
+ return `
1641
+ gap: ${spaces === null || spaces === void 0 ? void 0 : spaces.xs}px;
1642
+ `;
1643
+ }}
1644
+ `;
1645
+ const PanelTitle = (0, _emotion_styled.default)("span")`
1646
+ ${_czi_sds_components.fontHeaderM}
1647
+
1648
+ ${(props) => {
1649
+ var _semanticColors$base5;
1650
+ const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
1651
+ return `
1652
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base5 = semanticColors.base) === null || _semanticColors$base5 === void 0 ? void 0 : _semanticColors$base5.textPrimary};
1653
+ `;
1654
+ }}
1655
+ `;
1656
+ const CopyButton = (0, _emotion_styled.default)("button")`
1657
+ display: flex;
1658
+ align-items: center;
1659
+ padding: 0;
1660
+ border: none;
1661
+ background: none;
1662
+ cursor: default;
1663
+
1664
+ ${(props) => {
1665
+ var _semanticColors$base6, _semanticColors$base7;
1666
+ const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
1667
+ return `
1668
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base6 = semanticColors.base) === null || _semanticColors$base6 === void 0 ? void 0 : _semanticColors$base6.ornamentSecondary};
1669
+
1670
+ &:hover {
1671
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base7 = semanticColors.base) === null || _semanticColors$base7 === void 0 ? void 0 : _semanticColors$base7.textPrimary};
1672
+ }
1673
+ `;
1674
+ }}
1675
+ `;
1676
+ const ResidueCount = (0, _emotion_styled.default)("div")`
1677
+ display: flex;
1678
+ align-items: center;
1679
+ margin-left: auto;
1680
+
1681
+ ${(props) => {
1682
+ const spaces = (0, _czi_sds_components.getSpaces)(props);
1683
+ return `
1684
+ gap: ${spaces === null || spaces === void 0 ? void 0 : spaces.xxs}px;
1685
+ `;
1686
+ }}
1687
+ `;
1688
+ const ResidueCountLabel = (0, _emotion_styled.default)("span")`
1689
+ ${_czi_sds_components.fontBodyXs}
1690
+
1691
+ ${(props) => {
1692
+ var _semanticColors$base8;
1693
+ const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
1694
+ return `
1695
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base8 = semanticColors.base) === null || _semanticColors$base8 === void 0 ? void 0 : _semanticColors$base8.textTertiary};
1696
+ `;
1697
+ }}
1698
+ `;
1699
+ const ResidueCountValue = (0, _emotion_styled.default)("span")`
1700
+ ${_czi_sds_components.fontBodyXs}
1701
+
1702
+ ${(props) => {
1703
+ var _semanticColors$base9;
1704
+ const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
1705
+ return `
1706
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base9 = semanticColors.base) === null || _semanticColors$base9 === void 0 ? void 0 : _semanticColors$base9.textPrimary};
1707
+ `;
1708
+ }}
1709
+ `;
1710
+ /**
1711
+ * Scroll region for the residue grid. The `::before` fade masks residues as
1712
+ * they scroll up under the header, so it has to start from whatever the panel
1713
+ * is painted with or it leaves a mismatched strip.
1714
+ */
1715
+ const SequenceScroller = (0, _emotion_styled.default)("div")`
1716
+ position: relative;
1717
+ flex: 1;
1718
+ min-height: 0;
1719
+
1720
+ ${(props) => {
1721
+ var _props$backgroundColo3, _semanticColors$base10;
1722
+ const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
1723
+ return `
1724
+ &::before {
1725
+ content: "";
1726
+ position: absolute;
1727
+ inset-inline: 0;
1728
+ top: 0;
1729
+ z-index: 10;
1730
+ height: ${SCROLL_FADE_HEIGHT}px;
1731
+ pointer-events: none;
1732
+ 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);
1733
+ }
1734
+ `;
1735
+ }}
1736
+ `;
1737
+ /**
1738
+ * Residue spans are written to directly by `useResidueMarkers` rather than
1739
+ * re-rendered, so their hover and selection colors are set inline. Everything
1740
+ * here is the static styling underneath that: the monospace type, the pointer
1741
+ * affordance, and the superscript sequence numbers (which need their own color
1742
+ * rule, since they are nested inside residue spans and would otherwise inherit
1743
+ * the inline color).
1744
+ */
1745
+ const SequenceScrollArea = (0, _emotion_styled.default)("div")`
1746
+ height: 100%;
1747
+ overflow-y: auto;
1748
+
1749
+ .msp-sequence-wrapper {
1750
+ ${_czi_sds_components.fontCodeXs};
1751
+ font-size: 11px;
1752
+ line-height: 1.2;
1753
+ }
1754
+
1755
+ .msp-sequence-present {
1756
+ cursor: pointer;
1757
+ }
1758
+
1759
+ ${(props) => {
1760
+ var _semanticColors$base11;
1761
+ const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
1762
+ return `
1763
+ .msp-sequence-number {
1764
+ font-size: 8px;
1765
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base11 = semanticColors.base) === null || _semanticColors$base11 === void 0 ? void 0 : _semanticColors$base11.textTertiaryOnDark};
1766
+ }
1767
+ `;
1768
+ }}
1769
+ `;
1770
+ /**
1771
+ * Layout for one chain's residues.
1772
+ *
1773
+ * Residues are grouped into fixed-size sections, each one grid cell. The
1774
+ * equal-width auto-fill tracks and uniform gap keep the spacing between
1775
+ * sections identical on every row - including a partial last row, which keeps
1776
+ * the same column width (rather than stretching) and stacks from the leading
1777
+ * edge.
1778
+ */
1779
+ const ResidueGrid = (0, _emotion_styled.default)("div")`
1780
+ user-select: none;
1781
+ display: grid;
1782
+ grid-template-columns: repeat(
1783
+ auto-fill,
1784
+ minmax(${10}ch, 1fr)
1785
+ );
1786
+ gap: 8px;
1787
+ justify-items: start;
1788
+ padding-top: 6px;
1789
+
1790
+ /* Section wrappers, so a section's letters never split across lines. */
1791
+ & > span {
1792
+ white-space: nowrap;
1793
+ }
1794
+
1795
+ .msp-sequence-present {
1796
+ position: relative;
1797
+ display: inline-block;
1798
+ width: 1ch;
1799
+ /*
1800
+ * Reserves room above the letter for its superscript sequence number.
1801
+ * content-box clipping keeps the active fill off that gutter, so a
1802
+ * highlighted residue does not tint the number.
1803
+ */
1804
+ padding-top: 8px;
1805
+ background-clip: content-box;
1806
+ text-align: center;
1807
+ }
1808
+
1809
+ .msp-sequence-number {
1810
+ position: absolute;
1811
+ top: 0;
1812
+ line-height: 1.2;
1813
+ white-space: nowrap;
1814
+ pointer-events: none;
1815
+ }
1816
+ `;
1817
+ /**
1818
+ * The row above each chain's grid: a visibility toggle and the chain's name.
1819
+ *
1820
+ * A structure with more than one chain renders one grid per chain, and without
1821
+ * a caption between them the grids read as a single continuous protein. The
1822
+ * controls sit here rather than only in the legend because this is where a
1823
+ * reader is already looking at one chain at a time.
1824
+ */
1825
+ const ChainHeaderRow = (0, _emotion_styled.default)("div")`
1826
+ display: flex;
1827
+ align-items: center;
1828
+
1829
+ ${(props) => {
1830
+ const spaces = (0, _czi_sds_components.getSpaces)(props);
1831
+ return `
1832
+ gap: ${spaces === null || spaces === void 0 ? void 0 : spaces.xxs}px;
1833
+ margin-top: ${spaces === null || spaces === void 0 ? void 0 : spaces.xs}px;
1834
+ margin-bottom: ${spaces === null || spaces === void 0 ? void 0 : spaces.xxxs}px;
1835
+ `;
1836
+ }}
1837
+ `;
1838
+ /**
1839
+ * Shows or hides the chain's cartoon in the 3D view. Full strength even while
1840
+ * the chain is hidden, since it is the way back.
1841
+ */
1842
+ const ChainVisibilityToggle = (0, _emotion_styled.default)("button")`
1843
+ display: flex;
1844
+ align-items: center;
1845
+ padding: 0;
1846
+ border: none;
1847
+ background: none;
1848
+ cursor: pointer;
1849
+
1850
+ ${(props) => {
1851
+ var _semanticColors$base12, _semanticColors$base13;
1852
+ const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
1853
+ return `
1854
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base12 = semanticColors.base) === null || _semanticColors$base12 === void 0 ? void 0 : _semanticColors$base12.textTertiaryOnDark};
1855
+
1856
+ &:hover {
1857
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base13 = semanticColors.base) === null || _semanticColors$base13 === void 0 ? void 0 : _semanticColors$base13.textPrimary};
1858
+ }
1859
+
1860
+ svg {
1861
+ display: block;
1862
+ }
1863
+ `;
1864
+ }}
1865
+ `;
1866
+ /**
1867
+ * The chain's name, and a button because it also selects the chain: the caption
1868
+ * is already the one thing on screen that stands for a whole chain, so clicking
1869
+ * it is where a reader looks to select one. Clicking it again clears it.
1870
+ */
1871
+ const ChainLabel = (0, _emotion_styled.default)("button")`
1872
+ ${_czi_sds_components.fontBodyXs}
1873
+
1874
+ padding: 0;
1875
+ border: none;
1876
+ background: none;
1877
+ text-align: left;
1878
+
1879
+ ${(props) => {
1880
+ var _semanticColors$base14, _semanticColors$base15, _semanticColors$base16;
1881
+ const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
1882
+ 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;
1883
+ return `
1884
+ color: ${resting};
1885
+ cursor: ${props.isHidden ? "default" : "pointer"};
1886
+ font-weight: ${props.isSelected ? 600 : 500};
1887
+ opacity: ${props.isHidden ? .4 : 1};
1888
+
1889
+ &:hover {
1890
+ color: ${props.isHidden ? resting : semanticColors === null || semanticColors === void 0 || (_semanticColors$base16 = semanticColors.base) === null || _semanticColors$base16 === void 0 ? void 0 : _semanticColors$base16.textPrimary};
1891
+ }
1892
+ `;
1893
+ }}
1894
+ `;
1895
+ /**
1896
+ * Wraps a chain whose 3D cartoon is hidden. The grid stays in place and stays
1897
+ * interactive - the sequence is still the sequence - but is dimmed to match the
1898
+ * chain being absent from the structure beside it. Dimmed rather than removed
1899
+ * so the panel does not reflow every time a chain is toggled.
1900
+ */
1901
+ const HiddenChainGroup = (0, _emotion_styled.default)("div")`
1902
+ opacity: 0.4;
1903
+ `;
1904
+ //#endregion
1905
+ //#region src/core/ProteinStructureViewer/components/SequenceView/components/ChainHeader/index.tsx
1906
+ /**
1907
+ * The row above one chain's residue grid: a visibility toggle and the chain's
1908
+ * name, which selects it.
1909
+ *
1910
+ * The name is a toggle rather than a one-way switch, so the tooltip says which
1911
+ * way the next click goes - offering to select a chain that is already selected
1912
+ * would be telling the reader the wrong thing. A hidden chain cannot be
1913
+ * selected at all, so the name stops being a button then: there is no geometry
1914
+ * to focus, and the toggle beside it is what the reader wants instead.
1915
+ */
1916
+ function ChainHeader({ chainId, isHidden, isSelected, label, onHover, onSelect, onToggle }) {
1917
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ChainHeaderRow, {
1918
+ onBlur: () => onHover === null || onHover === void 0 ? void 0 : onHover(null),
1919
+ onFocus: () => onHover === null || onHover === void 0 ? void 0 : onHover(chainId),
1920
+ onMouseEnter: () => onHover === null || onHover === void 0 ? void 0 : onHover(chainId),
1921
+ onMouseLeave: () => onHover === null || onHover === void 0 ? void 0 : onHover(null),
1922
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_czi_sds_components.Tooltip, {
1923
+ arrow: true,
1924
+ placement: "top",
1925
+ title: isHidden ? `Chain ${label} is hidden` : `Click to ${isSelected ? "deselect" : "select"} Chain ${label}`,
1926
+ textAlign: "left",
1927
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ChainLabel, {
1928
+ "aria-pressed": isSelected,
1929
+ disabled: isHidden,
1930
+ isHidden,
1931
+ isSelected,
1932
+ onClick: () => onSelect === null || onSelect === void 0 ? void 0 : onSelect(chainId),
1933
+ type: "button",
1934
+ children: ["Chain ", label]
1935
+ })
1936
+ }), onToggle && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_czi_sds_components.Tooltip, {
1937
+ arrow: true,
1938
+ placement: "top",
1939
+ title: `${isHidden ? "Show" : "Hide"} Chain ${label}`,
1940
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChainVisibilityToggle, {
1941
+ "aria-label": `${isHidden ? "Show" : "Hide"} chain ${label}`,
1942
+ "aria-pressed": isHidden,
1943
+ onClick: () => onToggle(chainId),
1944
+ type: "button",
1945
+ 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 })
1946
+ })
1947
+ })]
1948
+ });
1949
+ }
1950
+ //#endregion
1951
+ //#region src/core/ProteinStructureViewer/hooks/usePlugin.ts
1952
+ /**
1953
+ * The plugin owning the React root this component renders in.
1954
+ *
1955
+ * Mol* renders its view components inside its own root and provides the plugin
1956
+ * through this context, which is what `PluginUIComponent` reads to populate
1957
+ * `this.plugin`.
1958
+ */
1959
+ function usePlugin() {
1960
+ return (0, react.useContext)(molstar_lib_mol_plugin_ui_base.PluginReactContext);
1961
+ }
1962
+ //#endregion
1963
+ //#region src/core/ProteinStructureViewer/components/SequenceView/utils/residue.ts
1964
+ /** Reads the sequence index off a residue span, or undefined if it is not one. */
1965
+ function readSeqIdx(el) {
1966
+ if (!el || !el.getAttribute) return void 0;
1967
+ return el.hasAttribute("data-seqid") ? +el.getAttribute(SEQ_ID_ATTR) : void 0;
1968
+ }
1969
+ /**
1970
+ * Nearest ancestor that scrolls vertically, or null if none. Used to scope the
1971
+ * residue tooltip's hide-on-scroll listener to the sequence's own overflow
1972
+ * container instead of a global window listener.
1973
+ */
1974
+ function getScrollParent(node) {
1975
+ var _node$parentElement;
1976
+ let el = (_node$parentElement = node === null || node === void 0 ? void 0 : node.parentElement) !== null && _node$parentElement !== void 0 ? _node$parentElement : null;
1977
+ while (el) {
1978
+ const overflowY = getComputedStyle(el).overflowY;
1979
+ if (overflowY === "auto" || overflowY === "scroll") return el;
1980
+ el = el.parentElement;
1981
+ }
1982
+ return null;
1983
+ }
1984
+ function getResidueClass(sequenceWrapper, seqIdx) {
1985
+ return sequenceWrapper.isHighlighted(seqIdx) || sequenceWrapper.isSelected(seqIdx) || sequenceWrapper.isFocused(seqIdx) ? "msp-sequence-present msp-sequence-residue-active" : "msp-sequence-present msp-sequence-residue";
1986
+ }
1987
+ /**
1988
+ * The label shown above a residue: its author sequence id for atomic units,
1989
+ * or a 1-based position for coarse ones.
1990
+ *
1991
+ * `location` is a scratch object the caller reuses across residues, since this
1992
+ * runs once per numbered residue on every render.
1993
+ */
1994
+ function getSequenceNumber(sequenceWrapper, seqIdx, location) {
1995
+ let seqNum = "";
1996
+ const loci = sequenceWrapper.getLoci(seqIdx);
1997
+ const l = molstar_lib_mol_model_structure.StructureElement.Loci.getFirstLocation(loci, location);
1998
+ if (l) {
1999
+ if (molstar_lib_mol_model_structure.Unit.isAtomic(l.unit)) {
2000
+ const seqId = molstar_lib_mol_model_structure.StructureProperties.residue.auth_seq_id(l);
2001
+ const insCode = molstar_lib_mol_model_structure.StructureProperties.residue.pdbx_PDB_ins_code(l);
2002
+ seqNum = `${seqId}${insCode ? insCode : ""}`;
2003
+ } else if (molstar_lib_mol_model_structure.Unit.isCoarse(l.unit)) seqNum = `${seqIdx + 1}`;
2004
+ }
2005
+ return seqNum;
2006
+ }
2007
+ /**
2008
+ * How often a sequence number is drawn above the residues.
2009
+ *
2010
+ * Long sequences get one every ten residues. Short ones number every residue,
2011
+ * unless their numbers run to more than one digit, which would crowd them.
2012
+ */
2013
+ function resolveSequenceNumberPeriod(sequenceWrapper, location, sequenceNumberPeriod) {
2014
+ if (sequenceNumberPeriod !== void 0) return sequenceNumberPeriod;
2015
+ if (sequenceWrapper.length > 10) return 10;
2016
+ if (getSequenceNumber(sequenceWrapper, sequenceWrapper.length - 1, location).length > 1) return 5;
1159
2017
  return 1;
1160
2018
  }
1161
2019
  /**
@@ -1251,470 +2109,489 @@ function useResidueMarkers({ containerRef, residueColors, sequenceWrapper }) {
1251
2109
  });
1252
2110
  }
1253
2111
  //#endregion
1254
- //#region src/core/ProteinStructureViewer/components/SequenceView/utils/loci.ts
1255
- /** The loci for a residue, or undefined when there is no residue there. */
1256
- function getLoci(sequenceWrapper, seqIdx) {
1257
- if (seqIdx !== void 0) {
1258
- const loci = sequenceWrapper.getLoci(seqIdx);
1259
- if (!molstar_lib_mol_model_structure.StructureElement.Loci.isEmpty(loci)) return loci;
2112
+ //#region src/core/ProteinStructureViewer/utils/selection.ts
2113
+ /**
2114
+ * A stable string for a selection, for comparing one against another.
2115
+ *
2116
+ * A consumer holding the selection in state hands back a new object on every
2117
+ * render, so identity says nothing about whether the selection changed. Order
2118
+ * is normalized too: `{ residues: [2, 1] }` and `{ residues: [1, 2] }` select
2119
+ * the same residues and must not read as a change.
2120
+ */
2121
+ function selectionKey(selection) {
2122
+ var _selection$residues, _selection$chains;
2123
+ const residues = [...new Set((_selection$residues = selection.residues) !== null && _selection$residues !== void 0 ? _selection$residues : [])].sort((a, b) => a - b);
2124
+ return `${[...new Set((_selection$chains = selection.chains) !== null && _selection$chains !== void 0 ? _selection$chains : [])].sort().join(",")}|${residues.join(",")}`;
2125
+ }
2126
+ /** True when the selection names nothing at all. */
2127
+ function isEmptySelection(selection) {
2128
+ var _selection$residues2, _selection$chains2;
2129
+ if (!selection) return true;
2130
+ return !((_selection$residues2 = selection.residues) === null || _selection$residues2 === void 0 ? void 0 : _selection$residues2.length) && !((_selection$chains2 = selection.chains) === null || _selection$chains2 === void 0 ? void 0 : _selection$chains2.length);
2131
+ }
2132
+ /**
2133
+ * The residues a selection covers, ascending and without repeats.
2134
+ *
2135
+ * Chains are expanded through `residuesByChain`, which was read off the loaded
2136
+ * structure, so a chain contributes exactly the residues it holds rather than
2137
+ * the span between its ends. A residue named directly and also carried by a
2138
+ * selected chain is counted once.
2139
+ */
2140
+ function selectionResidues(selection, residuesByChain) {
2141
+ var _selection$residues3, _selection$chains3;
2142
+ const residues = new Set((_selection$residues3 = selection.residues) !== null && _selection$residues3 !== void 0 ? _selection$residues3 : []);
2143
+ for (const chainId of (_selection$chains3 = selection.chains) !== null && _selection$chains3 !== void 0 ? _selection$chains3 : []) {
2144
+ var _residuesByChain$get;
2145
+ for (const index of (_residuesByChain$get = residuesByChain.get(chainId)) !== null && _residuesByChain$get !== void 0 ? _residuesByChain$get : []) residues.add(index);
1260
2146
  }
2147
+ return [...residues].sort((a, b) => a - b);
1261
2148
  }
1262
2149
  /**
1263
- * Extends a loci to the range between the mouse-down anchor and the current
1264
- * residue, so dragging across the sequence selects a contiguous span.
2150
+ * Names a selection for the legend's readout.
1265
2151
  *
1266
- * Element indices are only comparable within a single unit, and the range below
1267
- * is built on the anchor's unit alone. So a drag is only extended when both
1268
- * endpoints are a single element of the same unit; anything else -- different
1269
- * units, or an endpoint spanning several because the chain has more than one
1270
- * symmetry operator -- is left unextended rather than collapsed onto one unit
1271
- * and silently reinterpreted as a different range.
2152
+ * A single chain is named as one, since "Chain A" says more than the number of
2153
+ * residues it happens to hold. Anything else is reported as a count, which is
2154
+ * the only thing true of every shape a selection can take. A lone residue is
2155
+ * named by the caller instead, which reads it back off the structure to get its
2156
+ * three-letter code and the number the file gives it.
1272
2157
  */
1273
- function extendToRange(loci, anchor) {
1274
- if (anchor.elements.length !== 1 || loci.elements.length !== 1) return loci;
1275
- const ref = anchor.elements[0];
1276
- const ext = loci.elements[0];
1277
- if (!ref || !ext) return loci;
1278
- if (ref.unit !== ext.unit) return loci;
1279
- const min = Math.min(molstar_lib_mol_data_int.OrderedSet.min(ref.indices), molstar_lib_mol_data_int.OrderedSet.min(ext.indices));
1280
- const max = Math.max(molstar_lib_mol_data_int.OrderedSet.max(ref.indices), molstar_lib_mol_data_int.OrderedSet.max(ext.indices));
1281
- return molstar_lib_mol_model_structure.StructureElement.Loci(loci.structure, [{
1282
- indices: molstar_lib_mol_data_int.OrderedSet.ofRange(min, max),
1283
- unit: ref.unit
1284
- }]);
2158
+ function selectionLabel(selection, residueCount) {
2159
+ var _selection$chains4, _selection$residues4;
2160
+ const chains = (_selection$chains4 = selection.chains) !== null && _selection$chains4 !== void 0 ? _selection$chains4 : [];
2161
+ if (chains.length === 1 && !((_selection$residues4 = selection.residues) === null || _selection$residues4 === void 0 ? void 0 : _selection$residues4.length)) return `Chain ${chains[0]}`;
2162
+ return `${residueCount} residues`;
2163
+ }
2164
+ /** The mean of the values `read` returns, ignoring residues without one. */
2165
+ function meanOverResidues(residues, read) {
2166
+ let sum = 0;
2167
+ let count = 0;
2168
+ for (const index of residues) {
2169
+ const value = read(index);
2170
+ if (value === null || value === void 0) continue;
2171
+ sum += value;
2172
+ count++;
2173
+ }
2174
+ return count === 0 ? null : sum / count;
1285
2175
  }
1286
2176
  //#endregion
1287
- //#region src/core/ProteinStructureViewer/components/SequenceView/hooks/useHoveredResidue.ts
2177
+ //#region src/core/ProteinStructureViewer/utils/residueLoci.ts
1288
2178
  /**
1289
- * Tracks which residue the pointer is over and keeps the tooltip pinned to it.
2179
+ * Every atom the predicate accepts, as geometry Mol* can focus, frame and mark,
2180
+ * or undefined when it accepts none.
1290
2181
  *
1291
- * The tracked index also deduplicates pointer moves: the grid sees a move for
1292
- * every pixel, but only the ones that cross into a different residue matter.
2182
+ * One pass over the structure rather than a pass per residue: a whole-chain
2183
+ * selection names hundreds of residues, and resolving them one at a time would
2184
+ * walk the structure once for each.
2185
+ *
2186
+ * Atoms are bucketed by the unit they belong to because element indices are
2187
+ * unit-local. That bucketing is also what lets a result span a chain break: it
2188
+ * carries an entry per unit, which is what a range across two chains needs and
2189
+ * what a single unit-local entry cannot express.
2190
+ *
2191
+ * Whole residues are collected rather than one atom each, since the caller
2192
+ * frames the camera on the loci's bounding sphere and a lone atom would zoom
2193
+ * far closer than the residue warrants.
1293
2194
  */
1294
- function useHoveredResidue({ containerRef, sequenceWrapper }) {
1295
- const tooltipRef = (0, react.useRef)(null);
1296
- /** Index of the tracked residue, or -1 when the pointer is off the grid. */
1297
- const seqIdxRef = (0, react.useRef)(-1);
1298
- const hide = (0, react.useCallback)(() => {
1299
- var _tooltipRef$current;
1300
- return (_tooltipRef$current = tooltipRef.current) === null || _tooltipRef$current === void 0 ? void 0 : _tooltipRef$current.hide();
1301
- }, []);
1302
- const clear = (0, react.useCallback)(() => {
1303
- var _tooltipRef$current2;
1304
- if (seqIdxRef.current === -1) return false;
1305
- seqIdxRef.current = -1;
1306
- (_tooltipRef$current2 = tooltipRef.current) === null || _tooltipRef$current2 === void 0 || _tooltipRef$current2.hide();
1307
- return true;
1308
- }, []);
1309
- const moveTo = (0, react.useCallback)((seqIdx, el) => {
1310
- var _tooltipRef$current3, _tooltipRef$current4;
1311
- if (seqIdxRef.current === seqIdx) return false;
1312
- seqIdxRef.current = seqIdx;
1313
- if (seqIdx < 0) (_tooltipRef$current3 = tooltipRef.current) === null || _tooltipRef$current3 === void 0 || _tooltipRef$current3.hide();
1314
- else (_tooltipRef$current4 = tooltipRef.current) === null || _tooltipRef$current4 === void 0 || _tooltipRef$current4.show(el, sequenceWrapper.residueLabel(seqIdx));
1315
- return true;
1316
- }, [sequenceWrapper]);
1317
- (0, react.useEffect)(() => {
1318
- const scrollTarget = getScrollParent(containerRef.current);
1319
- if (!scrollTarget) return;
1320
- const handleScroll = () => clear();
1321
- scrollTarget.addEventListener("scroll", handleScroll, { passive: true });
1322
- return () => scrollTarget.removeEventListener("scroll", handleScroll);
1323
- }, [clear, containerRef]);
1324
- return (0, react.useMemo)(() => ({
1325
- clear,
1326
- hide,
1327
- moveTo,
1328
- tooltipRef
1329
- }), [
1330
- clear,
1331
- hide,
1332
- moveTo
1333
- ]);
2195
+ function lociMatching(structure, matches) {
2196
+ const location = molstar_lib_mol_model_structure.StructureElement.Location.create(structure);
2197
+ const elements = [];
2198
+ for (const unit of structure.units) {
2199
+ location.unit = unit;
2200
+ const indices = [];
2201
+ for (let i = 0; i < unit.elements.length; i++) {
2202
+ location.element = unit.elements[i];
2203
+ if (matches(location)) indices.push(i);
2204
+ }
2205
+ if (indices.length > 0) elements.push({
2206
+ indices,
2207
+ unit
2208
+ });
2209
+ }
2210
+ if (elements.length === 0) return void 0;
2211
+ return molstar_lib_mol_model_structure.StructureElement.Loci(structure, elements.map(({ indices, unit }) => ({
2212
+ indices: molstar_lib_mol_data_int.OrderedSet.ofSortedArray(indices),
2213
+ unit
2214
+ })));
1334
2215
  }
1335
- //#endregion
1336
- //#region src/core/ProteinStructureViewer/components/SequenceView/hooks/useLociDispatch.ts
1337
2216
  /**
1338
- * Wraps a residue loci and the mouse state into a Mol* interaction event. An
1339
- * `anchor` extends the loci to the range between it and `loci`, which is how a
1340
- * drag across the sequence comes out as a contiguous span.
2217
+ * Every atom of the residues a selection covers, within one structure.
2218
+ *
2219
+ * `hiddenChains` is subtracted from the result. A hidden chain is not drawn, so
2220
+ * geometry built from it would have nothing to sit on: Mol*'s focus
2221
+ * representation draws its own ball-and-stick from whatever is focused, in a
2222
+ * part of the state tree the chain's own visibility does not reach, and a
2223
+ * hidden chain left in the loci would keep showing up there after its cartoon
2224
+ * had gone.
1341
2225
  */
1342
- function buildEvent(loci, buttons, button, modifiers, anchor) {
1343
- const ev = {
1344
- button,
1345
- buttons,
1346
- current: molstar_lib_mol_repr_representation.Representation.Loci.Empty,
1347
- modifiers
1348
- };
1349
- if (loci === void 0 || molstar_lib_mol_model_structure.StructureElement.Loci.isEmpty(loci)) return ev;
1350
- ev.current = { loci: anchor ? extendToRange(loci, anchor) : loci };
1351
- return ev;
2226
+ function lociForSelectionInStructure(structure, selection, hiddenChains) {
2227
+ var _selection$residues, _selection$chains;
2228
+ if (isEmptySelection(selection)) return void 0;
2229
+ const residues = new Set((_selection$residues = selection.residues) !== null && _selection$residues !== void 0 ? _selection$residues : []);
2230
+ const chains = new Set((_selection$chains = selection.chains) !== null && _selection$chains !== void 0 ? _selection$chains : []);
2231
+ return lociMatching(structure, (location) => {
2232
+ const chainId = molstar_lib_mol_model_structure.StructureProperties.chain.auth_asym_id(location);
2233
+ if (hiddenChains === null || hiddenChains === void 0 ? void 0 : hiddenChains.has(chainId)) return false;
2234
+ return residues.has(molstar_lib_mol_model_structure.StructureProperties.residue.key(location)) || chains.has(chainId);
2235
+ });
1352
2236
  }
1353
2237
  /**
1354
- * Reports the grid's hover and click to Mol*, on the same behaviors the 3D
1355
- * view publishes to, so an interaction in either surface reaches the other.
2238
+ * Every atom of the residues between two 0-based indices, inclusive.
2239
+ *
2240
+ * A contiguous span is tested by comparison rather than by expanding it into a
2241
+ * set of indices: this runs on every residue a drag crosses, and a drag can
2242
+ * span the whole structure.
1356
2243
  */
1357
- function useLociDispatch() {
1358
- const plugin = usePlugin();
1359
- const hover = (0, react.useCallback)((loci, buttons, button, modifiers, anchor) => {
1360
- plugin.behaviors.interaction.hover.next(buildEvent(loci, buttons, button, modifiers, anchor));
1361
- }, [plugin]);
1362
- const click = (0, react.useCallback)((loci, buttons, button, modifiers, anchor) => {
1363
- plugin.behaviors.interaction.click.next(buildEvent(loci, buttons, button, modifiers, anchor));
1364
- }, [plugin]);
1365
- return (0, react.useMemo)(() => ({
1366
- click,
1367
- hover
1368
- }), [click, hover]);
2244
+ function lociForResidueRange(structure, min, max) {
2245
+ return lociMatching(structure, (location) => {
2246
+ const index = molstar_lib_mol_model_structure.StructureProperties.residue.key(location);
2247
+ return index >= min && index <= max;
2248
+ });
1369
2249
  }
1370
- //#endregion
1371
- //#region src/core/ProteinStructureViewer/components/SequenceView/hooks/useResidueSelection.ts
1372
2250
  /**
1373
- * Turns a press and release on the grid into a selection: one residue when
1374
- * both land on the same one, otherwise the contiguous range dragged across.
2251
+ * The same, across every structure loaded into a plugin. Results are merged
2252
+ * into the first structure that matched: a loci belongs to one structure, and
2253
+ * the residue index space already spans them as though they were one.
1375
2254
  */
1376
- function useResidueSelection({ sequenceWrapper }) {
1377
- const anchorRef = (0, react.useRef)(void 0);
1378
- const { click } = useLociDispatch();
1379
- const onMouseDown = (e) => {
1380
- e.stopPropagation();
1381
- anchorRef.current = getLoci(sequenceWrapper, readSeqIdx(e.target));
1382
- };
1383
- const onMouseUp = (e) => {
1384
- e.stopPropagation();
1385
- const anchor = anchorRef.current;
1386
- if (anchor === void 0) return;
1387
- const loci = getLoci(sequenceWrapper, readSeqIdx(e.target));
1388
- if (loci) {
1389
- const dragged = !molstar_lib_mol_model_structure.StructureElement.Loci.areEqual(anchor, loci);
1390
- 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);
1391
- }
1392
- anchorRef.current = void 0;
1393
- };
1394
- return {
1395
- clearAnchor: () => {
1396
- anchorRef.current = void 0;
1397
- },
1398
- getAnchor: () => anchorRef.current,
1399
- onMouseDown,
1400
- onMouseUp
1401
- };
2255
+ function lociForSelection(plugin, selection, hiddenChains) {
2256
+ for (const entry of plugin.managers.structure.hierarchy.current.structures) {
2257
+ var _entry$cell$obj;
2258
+ const structure = (_entry$cell$obj = entry.cell.obj) === null || _entry$cell$obj === void 0 ? void 0 : _entry$cell$obj.data;
2259
+ if (!structure) continue;
2260
+ const loci = lociForSelectionInStructure(structure, selection, hiddenChains);
2261
+ if (loci) return loci;
2262
+ }
1402
2263
  }
1403
2264
  //#endregion
1404
- //#region src/core/ProteinStructureViewer/components/SequenceView/hooks/useResiduePointer.ts
2265
+ //#region src/core/ProteinStructureViewer/utils/residueRef.ts
1405
2266
  /**
1406
- * Everything the grid does with a pointer: hover on move, and the drag-to-range
1407
- * selection delegated to `useResidueSelection`.
2267
+ * Reads the residue a loci points at, or null when it points at nothing the
2268
+ * viewer can address.
1408
2269
  *
1409
- * Hover reaches the 3D view through a throttled queue, so that sweeping the
1410
- * pointer across the grid does not ask Mol* to re-mark on every residue
1411
- * crossed. A drag is exempt: its hover carries the growing range, so it goes
1412
- * out immediately to keep the preview under the pointer.
2270
+ * `index` comes from Mol*'s residue key, which counts residues in file order
2271
+ * from zero and is the same counter `injectPlddt` walks. Deriving it
2272
+ * from the residue's *number* instead would tie it to how the file happens to
2273
+ * be numbered: a crop beginning at residue 200 would index `plddt` at 199, and
2274
+ * a file numbering each chain from 1 would give two residues the same index.
1413
2275
  */
1414
- function useResiduePointer({ containerRef, sequenceWrapper }) {
1415
- const { hover } = useLociDispatch();
1416
- const { clear, hide, moveTo, tooltipRef } = useHoveredResidue({
1417
- containerRef,
1418
- sequenceWrapper
1419
- });
1420
- const { clearAnchor, getAnchor, onMouseDown, onMouseUp } = useResidueSelection({ sequenceWrapper });
1421
- const queueRef = (0, react.useRef)(null);
1422
- if (queueRef.current === null) queueRef.current = new rxjs.Subject();
1423
- const queue = queueRef.current;
1424
- useSubscribe((0, react.useMemo)(() => queue.pipe((0, rxjs_operators.throttleTime)(HOVER_THROTTLE_MS, void 0, {
1425
- leading: true,
1426
- trailing: true
1427
- })), [queue]), (e) => {
1428
- hover(getLoci(sequenceWrapper, e.seqIdx < 0 ? void 0 : e.seqIdx), e.buttons, e.button, e.modifiers, getAnchor());
1429
- });
1430
- const queueHover = (e, seqIdx) => queue.next({
1431
- button: (0, molstar_lib_mol_util_input_input_observer.getButton)(e.nativeEvent),
1432
- buttons: (0, molstar_lib_mol_util_input_input_observer.getButtons)(e.nativeEvent),
1433
- modifiers: (0, molstar_lib_mol_util_input_input_observer.getModifiers)(e.nativeEvent),
1434
- seqIdx
1435
- });
1436
- /** Asks the 3D view to drop its hover, once, if the grid still holds one. */
1437
- const releaseHover = (e) => {
1438
- if (clear()) queueHover(e, -1);
1439
- };
1440
- const onContextMenu = (e) => {
1441
- e.preventDefault();
1442
- };
1443
- const onMouseMove = (e) => {
1444
- var _readSeqIdx;
1445
- e.stopPropagation();
1446
- const el = e.target;
1447
- if (!el || !el.getAttribute) {
1448
- releaseHover(e);
1449
- return;
1450
- }
1451
- const seqIdx = (_readSeqIdx = readSeqIdx(el)) !== null && _readSeqIdx !== void 0 ? _readSeqIdx : -1;
1452
- if (!moveTo(seqIdx, el)) return;
1453
- const anchor = getAnchor();
1454
- if (anchor === void 0) {
1455
- queueHover(e, seqIdx);
1456
- return;
1457
- }
1458
- 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);
1459
- };
1460
- const onMouseLeave = (e) => {
1461
- e.stopPropagation();
1462
- clearAnchor();
1463
- hide();
1464
- releaseHover(e);
1465
- };
2276
+ function residueRefFromLoci(loci) {
2277
+ var _StructureProperties$;
2278
+ const location = molstar_lib_mol_model_structure.StructureElement.Location.create(void 0);
2279
+ const firstLoc = molstar_lib_mol_model_structure.StructureElement.Loci.getFirstLocation(loci, location);
2280
+ if (!firstLoc) return null;
1466
2281
  return {
1467
- handlers: {
1468
- onContextMenu,
1469
- onMouseDown,
1470
- onMouseLeave,
1471
- onMouseMove,
1472
- onMouseUp
1473
- },
1474
- tooltipRef
2282
+ chainId: molstar_lib_mol_model_structure.StructureProperties.chain.auth_asym_id(firstLoc),
2283
+ compId: molstar_lib_mol_model_structure.StructureProperties.atom.label_comp_id(firstLoc),
2284
+ insCode: (_StructureProperties$ = molstar_lib_mol_model_structure.StructureProperties.residue.pdbx_PDB_ins_code(firstLoc)) !== null && _StructureProperties$ !== void 0 ? _StructureProperties$ : "",
2285
+ index: molstar_lib_mol_model_structure.StructureProperties.residue.key(firstLoc),
2286
+ seqId: molstar_lib_mol_model_structure.StructureProperties.residue.auth_seq_id(firstLoc)
1475
2287
  };
1476
2288
  }
2289
+ /**
2290
+ * Every residue a loci covers, as a selection the consumer can echo back.
2291
+ *
2292
+ * The counterpart to `residueRefFromLoci`, which names only the first: a drag
2293
+ * across the sequence hands Mol* a range, and reporting its first residue
2294
+ * alone would turn a range the user drew into a single-residue selection.
2295
+ *
2296
+ * Reported as residues rather than as chains even when a whole chain is
2297
+ * covered. What the user drew is a span of residues, and calling it a chain
2298
+ * because it happens to end where one does would change the selection's meaning
2299
+ * on a structure where the two coincide.
2300
+ */
2301
+ function selectionFromLoci(loci) {
2302
+ const location = molstar_lib_mol_model_structure.StructureElement.Location.create(loci.structure);
2303
+ const residues = /* @__PURE__ */ new Set();
2304
+ for (const element of loci.elements) {
2305
+ location.unit = element.unit;
2306
+ molstar_lib_mol_data_int.OrderedSet.forEach(element.indices, (i) => {
2307
+ location.element = element.unit.elements[i];
2308
+ residues.add(molstar_lib_mol_model_structure.StructureProperties.residue.key(location));
2309
+ });
2310
+ }
2311
+ return { residues: [...residues].sort((a, b) => a - b) };
2312
+ }
2313
+ /**
2314
+ * How a residue is named in the readout, e.g. `"LYS 111"`. Takes the number and
2315
+ * the insertion code from the file so that the readout and the sequence panel
2316
+ * beneath it agree on what to call a residue.
2317
+ */
2318
+ function residueLabel(residue) {
2319
+ return `${residue.compId} ${residue.seqId}${residue.insCode}`;
2320
+ }
1477
2321
  //#endregion
1478
- //#region src/core/ProteinStructureViewer/components/SequenceView/style.ts
1479
- /** Height of the fade that masks residues scrolling under the panel header. */
1480
- const SCROLL_FADE_HEIGHT = 8;
1481
- const SequencePanel = (0, _emotion_styled.default)("div")`
1482
- display: flex;
1483
- flex-direction: column;
1484
- height: 100%;
1485
-
1486
- ${(props) => {
1487
- var _semanticColors$base, _props$backgroundColo, _semanticColors$base2;
1488
- const spaces = (0, _czi_sds_components.getSpaces)(props);
1489
- const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
1490
- return `
1491
- padding: ${spaces === null || spaces === void 0 ? void 0 : spaces.m}px ${spaces === null || spaces === void 0 ? void 0 : spaces.m}px 0;
1492
- border-top: 1px solid ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base = semanticColors.base) === null || _semanticColors$base === void 0 ? void 0 : _semanticColors$base.divider};
1493
- 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};
1494
- `;
1495
- }}
1496
- `;
1497
- const EmptyState = (0, _emotion_styled.default)("div")`
1498
- ${_czi_sds_components.fontBodyXs}
1499
-
1500
- display: flex;
1501
- align-items: center;
1502
- justify-content: center;
1503
- height: 100%;
1504
-
1505
- ${(props) => {
1506
- var _props$backgroundColo2, _semanticColors$base3, _semanticColors$base4;
1507
- const spaces = (0, _czi_sds_components.getSpaces)(props);
1508
- const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
1509
- return `
1510
- padding: ${spaces === null || spaces === void 0 ? void 0 : spaces.m}px;
1511
- 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};
1512
- color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base4 = semanticColors.base) === null || _semanticColors$base4 === void 0 ? void 0 : _semanticColors$base4.textTertiaryOnDark};
1513
- `;
1514
- }}
1515
- `;
1516
- const PanelHeader = (0, _emotion_styled.default)("div")`
1517
- display: flex;
1518
- align-items: center;
1519
-
1520
- ${(props) => {
1521
- const spaces = (0, _czi_sds_components.getSpaces)(props);
1522
- return `
1523
- gap: ${spaces === null || spaces === void 0 ? void 0 : spaces.xs}px;
1524
- `;
1525
- }}
1526
- `;
1527
- const PanelTitle = (0, _emotion_styled.default)("span")`
1528
- ${_czi_sds_components.fontHeaderM}
1529
-
1530
- ${(props) => {
1531
- var _semanticColors$base5;
1532
- const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
1533
- return `
1534
- color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base5 = semanticColors.base) === null || _semanticColors$base5 === void 0 ? void 0 : _semanticColors$base5.textPrimary};
1535
- `;
1536
- }}
1537
- `;
1538
- const CopyButton = (0, _emotion_styled.default)("button")`
1539
- display: flex;
1540
- align-items: center;
1541
- padding: 0;
1542
- border: none;
1543
- background: none;
1544
- cursor: default;
1545
-
1546
- ${(props) => {
1547
- var _semanticColors$base6, _semanticColors$base7;
1548
- const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
1549
- return `
1550
- color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base6 = semanticColors.base) === null || _semanticColors$base6 === void 0 ? void 0 : _semanticColors$base6.ornamentSecondary};
1551
-
1552
- &:hover {
1553
- color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base7 = semanticColors.base) === null || _semanticColors$base7 === void 0 ? void 0 : _semanticColors$base7.textPrimary};
1554
- }
1555
- `;
1556
- }}
1557
- `;
1558
- const ResidueCount = (0, _emotion_styled.default)("div")`
1559
- display: flex;
1560
- align-items: center;
1561
- margin-left: auto;
1562
-
1563
- ${(props) => {
1564
- const spaces = (0, _czi_sds_components.getSpaces)(props);
1565
- return `
1566
- gap: ${spaces === null || spaces === void 0 ? void 0 : spaces.xxs}px;
1567
- `;
1568
- }}
1569
- `;
1570
- const ResidueCountLabel = (0, _emotion_styled.default)("span")`
1571
- ${_czi_sds_components.fontBodyXs}
1572
-
1573
- ${(props) => {
1574
- var _semanticColors$base8;
1575
- const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
1576
- return `
1577
- color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base8 = semanticColors.base) === null || _semanticColors$base8 === void 0 ? void 0 : _semanticColors$base8.textTertiary};
1578
- `;
1579
- }}
1580
- `;
1581
- const ResidueCountValue = (0, _emotion_styled.default)("span")`
1582
- ${_czi_sds_components.fontBodyXs}
1583
-
1584
- ${(props) => {
1585
- var _semanticColors$base9;
1586
- const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
1587
- return `
1588
- color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base9 = semanticColors.base) === null || _semanticColors$base9 === void 0 ? void 0 : _semanticColors$base9.textPrimary};
1589
- `;
1590
- }}
1591
- `;
2322
+ //#region src/core/ProteinStructureViewer/components/SequenceView/utils/loci.ts
2323
+ /** The loci for a residue, or undefined when there is no residue there. */
2324
+ function getLoci(sequenceWrapper, seqIdx) {
2325
+ if (seqIdx !== void 0) {
2326
+ const loci = sequenceWrapper.getLoci(seqIdx);
2327
+ if (!molstar_lib_mol_model_structure.StructureElement.Loci.isEmpty(loci)) return loci;
2328
+ }
2329
+ }
1592
2330
  /**
1593
- * Scroll region for the residue grid. The `::before` fade masks residues as
1594
- * they scroll up under the header, so it has to start from whatever the panel
1595
- * is painted with or it leaves a mismatched strip.
2331
+ * Extends a loci to the range between the mouse-down anchor and the current
2332
+ * residue, so dragging across the sequence selects a contiguous span.
2333
+ *
2334
+ * The span is measured in the viewer's own residue index - position in file
2335
+ * order across the whole structure - rather than in element indices, which are
2336
+ * unit-local and so cannot describe a range reaching from one chain into the
2337
+ * next. Measuring it the way every other residue-keyed API measures it is what
2338
+ * lets a drag cross a chain break, and it means a range covers exactly the
2339
+ * residues whose indices lie between its ends.
2340
+ *
2341
+ * An endpoint that names no residue leaves the drag unextended rather than
2342
+ * guessing at where it was. An endpoint spanning several units - a chain with
2343
+ * more than one symmetry operator - is fine: each copy of a residue carries the
2344
+ * same index, so the span is the same either way and resolves back across all
2345
+ * of them.
1596
2346
  */
1597
- const SequenceScroller = (0, _emotion_styled.default)("div")`
1598
- position: relative;
1599
- flex: 1;
1600
- min-height: 0;
1601
-
1602
- ${(props) => {
1603
- var _props$backgroundColo3, _semanticColors$base10;
1604
- const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
1605
- return `
1606
- &::before {
1607
- content: "";
1608
- position: absolute;
1609
- inset-inline: 0;
1610
- top: 0;
1611
- z-index: 10;
1612
- height: ${SCROLL_FADE_HEIGHT}px;
1613
- pointer-events: none;
1614
- 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);
1615
- }
1616
- `;
1617
- }}
1618
- `;
2347
+ function extendToRange(loci, anchor) {
2348
+ var _lociForResidueRange;
2349
+ const from = residueRefFromLoci(anchor);
2350
+ const to = residueRefFromLoci(loci);
2351
+ if (!from || !to) return loci;
2352
+ if (from.index === to.index) return loci;
2353
+ const min = Math.min(from.index, to.index);
2354
+ const max = Math.max(from.index, to.index);
2355
+ return (_lociForResidueRange = lociForResidueRange(loci.structure, min, max)) !== null && _lociForResidueRange !== void 0 ? _lociForResidueRange : loci;
2356
+ }
2357
+ //#endregion
2358
+ //#region src/core/ProteinStructureViewer/components/SequenceView/hooks/useHoveredResidue.ts
1619
2359
  /**
1620
- * Residue spans are written to directly by `useResidueMarkers` rather than
1621
- * re-rendered, so their hover and selection colors are set inline. Everything
1622
- * here is the static styling underneath that: the monospace type, the pointer
1623
- * affordance, and the superscript sequence numbers (which need their own color
1624
- * rule, since they are nested inside residue spans and would otherwise inherit
1625
- * the inline color).
2360
+ * Tracks which residue the pointer is over and keeps the tooltip pinned to it.
2361
+ *
2362
+ * The tracked index also deduplicates pointer moves: the grid sees a move for
2363
+ * every pixel, but only the ones that cross into a different residue matter.
2364
+ */
2365
+ function useHoveredResidue({ containerRef, sequenceWrapper }) {
2366
+ const tooltipRef = (0, react.useRef)(null);
2367
+ /** Index of the tracked residue, or -1 when the pointer is off the grid. */
2368
+ const seqIdxRef = (0, react.useRef)(-1);
2369
+ const hide = (0, react.useCallback)(() => {
2370
+ var _tooltipRef$current;
2371
+ return (_tooltipRef$current = tooltipRef.current) === null || _tooltipRef$current === void 0 ? void 0 : _tooltipRef$current.hide();
2372
+ }, []);
2373
+ const clear = (0, react.useCallback)(() => {
2374
+ var _tooltipRef$current2;
2375
+ if (seqIdxRef.current === -1) return false;
2376
+ seqIdxRef.current = -1;
2377
+ (_tooltipRef$current2 = tooltipRef.current) === null || _tooltipRef$current2 === void 0 || _tooltipRef$current2.hide();
2378
+ return true;
2379
+ }, []);
2380
+ const moveTo = (0, react.useCallback)((seqIdx, el) => {
2381
+ var _tooltipRef$current3, _tooltipRef$current4;
2382
+ if (seqIdxRef.current === seqIdx) return false;
2383
+ seqIdxRef.current = seqIdx;
2384
+ if (seqIdx < 0) (_tooltipRef$current3 = tooltipRef.current) === null || _tooltipRef$current3 === void 0 || _tooltipRef$current3.hide();
2385
+ else (_tooltipRef$current4 = tooltipRef.current) === null || _tooltipRef$current4 === void 0 || _tooltipRef$current4.show(el, sequenceWrapper.residueLabel(seqIdx));
2386
+ return true;
2387
+ }, [sequenceWrapper]);
2388
+ (0, react.useEffect)(() => {
2389
+ const scrollTarget = getScrollParent(containerRef.current);
2390
+ if (!scrollTarget) return;
2391
+ const handleScroll = () => clear();
2392
+ scrollTarget.addEventListener("scroll", handleScroll, { passive: true });
2393
+ return () => scrollTarget.removeEventListener("scroll", handleScroll);
2394
+ }, [clear, containerRef]);
2395
+ return (0, react.useMemo)(() => ({
2396
+ clear,
2397
+ hide,
2398
+ moveTo,
2399
+ tooltipRef
2400
+ }), [
2401
+ clear,
2402
+ hide,
2403
+ moveTo
2404
+ ]);
2405
+ }
2406
+ //#endregion
2407
+ //#region src/core/ProteinStructureViewer/components/SequenceView/hooks/useLociDispatch.ts
2408
+ /**
2409
+ * Wraps a residue loci and the mouse state into a Mol* interaction event. An
2410
+ * `anchor` extends the loci to the range between it and `loci`, which is how a
2411
+ * drag across the sequence comes out as a contiguous span.
2412
+ */
2413
+ function buildEvent(loci, buttons, button, modifiers, anchor) {
2414
+ const ev = {
2415
+ button,
2416
+ buttons,
2417
+ current: molstar_lib_mol_repr_representation.Representation.Loci.Empty,
2418
+ modifiers
2419
+ };
2420
+ if (loci === void 0 || molstar_lib_mol_model_structure.StructureElement.Loci.isEmpty(loci)) return ev;
2421
+ ev.current = { loci: anchor ? extendToRange(loci, anchor) : loci };
2422
+ return ev;
2423
+ }
2424
+ /**
2425
+ * Reports the grid's hover and click to Mol*, on the same behaviors the 3D
2426
+ * view publishes to, so an interaction in either surface reaches the other.
1626
2427
  */
1627
- const SequenceScrollArea = (0, _emotion_styled.default)("div")`
1628
- height: 100%;
1629
- overflow-y: auto;
1630
-
1631
- .msp-sequence-wrapper {
1632
- ${_czi_sds_components.fontCodeXs};
1633
- font-size: 11px;
1634
- line-height: 1.2;
1635
- }
1636
-
1637
- .msp-sequence-present {
1638
- cursor: pointer;
1639
- }
1640
-
1641
- ${(props) => {
1642
- var _semanticColors$base11;
1643
- const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
1644
- return `
1645
- .msp-sequence-number {
1646
- font-size: 8px;
1647
- color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base11 = semanticColors.base) === null || _semanticColors$base11 === void 0 ? void 0 : _semanticColors$base11.textTertiaryOnDark};
1648
- }
1649
- `;
1650
- }}
1651
- `;
2428
+ function useLociDispatch() {
2429
+ const plugin = usePlugin();
2430
+ const hover = (0, react.useCallback)((loci, buttons, button, modifiers, anchor) => {
2431
+ plugin.behaviors.interaction.hover.next(buildEvent(loci, buttons, button, modifiers, anchor));
2432
+ }, [plugin]);
2433
+ const click = (0, react.useCallback)((loci, buttons, button, modifiers, anchor) => {
2434
+ plugin.behaviors.interaction.click.next(buildEvent(loci, buttons, button, modifiers, anchor));
2435
+ }, [plugin]);
2436
+ return (0, react.useMemo)(() => ({
2437
+ click,
2438
+ hover
2439
+ }), [click, hover]);
2440
+ }
2441
+ //#endregion
2442
+ //#region src/core/ProteinStructureViewer/components/SequenceView/hooks/useSequenceDrag.ts
2443
+ const SequenceDragContext = (0, react.createContext)({
2444
+ clearAnchor: () => void 0,
2445
+ getAnchor: () => void 0,
2446
+ setAnchor: () => void 0
2447
+ });
2448
+ const SequenceDragProvider = SequenceDragContext.Provider;
1652
2449
  /**
1653
- * Layout for one chain's residues.
2450
+ * The anchor a drag across the sequence started from, shared by every chain's
2451
+ * grid.
1654
2452
  *
1655
- * Residues are grouped into fixed-size sections, each one grid cell. The
1656
- * equal-width auto-fill tracks and uniform gap keep the spacing between
1657
- * sections identical on every row - including a partial last row, which keeps
1658
- * the same column width (rather than stretching) and stacks from the leading
1659
- * edge.
2453
+ * It has to live above the grids rather than inside one. A complex renders one
2454
+ * grid per chain, so a drag that begins on the target and ends on the binder
2455
+ * starts in one component and finishes in another; an anchor held by the grid
2456
+ * it started in is not there to be read by the grid it ends in, and the drag
2457
+ * would be reported as a plain click on whatever it landed on.
2458
+ *
2459
+ * Held in a ref rather than state: it changes on mouse-down and mouse-up, and
2460
+ * nothing renders from it, so re-rendering every residue in the panel twice per
2461
+ * drag would buy nothing.
1660
2462
  */
1661
- const ResidueGrid = (0, _emotion_styled.default)("div")`
1662
- user-select: none;
1663
- display: grid;
1664
- grid-template-columns: repeat(
1665
- auto-fill,
1666
- minmax(${10}ch, 1fr)
1667
- );
1668
- gap: 8px;
1669
- justify-items: start;
1670
- padding-top: 6px;
1671
-
1672
- /* Section wrappers, so a section's letters never split across lines. */
1673
- & > span {
1674
- white-space: nowrap;
1675
- }
1676
-
1677
- .msp-sequence-present {
1678
- position: relative;
1679
- display: inline-block;
1680
- width: 1ch;
1681
- /*
1682
- * Reserves room above the letter for its superscript sequence number.
1683
- * content-box clipping keeps the active fill off that gutter, so a
1684
- * highlighted residue does not tint the number.
1685
- */
1686
- padding-top: 8px;
1687
- background-clip: content-box;
1688
- text-align: center;
1689
- }
1690
-
1691
- .msp-sequence-number {
1692
- position: absolute;
1693
- top: 0;
1694
- line-height: 1.2;
1695
- white-space: nowrap;
1696
- pointer-events: none;
1697
- }
1698
- `;
2463
+ function useCreateSequenceDrag() {
2464
+ const anchorRef = (0, react.useRef)(void 0);
2465
+ const getAnchor = (0, react.useCallback)(() => anchorRef.current, []);
2466
+ const setAnchor = (0, react.useCallback)((anchor) => {
2467
+ anchorRef.current = anchor;
2468
+ }, []);
2469
+ const clearAnchor = (0, react.useCallback)(() => {
2470
+ anchorRef.current = void 0;
2471
+ }, []);
2472
+ return (0, react.useMemo)(() => ({
2473
+ clearAnchor,
2474
+ getAnchor,
2475
+ setAnchor
2476
+ }), [
2477
+ clearAnchor,
2478
+ getAnchor,
2479
+ setAnchor
2480
+ ]);
2481
+ }
2482
+ function useSequenceDrag() {
2483
+ return (0, react.useContext)(SequenceDragContext);
2484
+ }
2485
+ //#endregion
2486
+ //#region src/core/ProteinStructureViewer/components/SequenceView/hooks/useResidueSelection.ts
1699
2487
  /**
1700
- * Caption naming the chain a sequence grid belongs to. A structure with more
1701
- * than one chain renders one grid per chain, and without a caption between them
1702
- * the grids read as a single continuous protein.
2488
+ * Turns a press and release on the grid into a selection: one residue when
2489
+ * both land on the same one, otherwise the contiguous range dragged across -
2490
+ * which the panel-level anchor lets reach into another chain's grid.
1703
2491
  */
1704
- const ChainLabel = (0, _emotion_styled.default)("div")`
1705
- ${_czi_sds_components.fontBodyXs}
1706
-
1707
- ${(props) => {
1708
- var _semanticColors$base12;
1709
- const spaces = (0, _czi_sds_components.getSpaces)(props);
1710
- const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
1711
- return `
1712
- margin-top: ${spaces === null || spaces === void 0 ? void 0 : spaces.xs}px;
1713
- margin-bottom: ${spaces === null || spaces === void 0 ? void 0 : spaces.xxxs}px;
1714
- color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base12 = semanticColors.base) === null || _semanticColors$base12 === void 0 ? void 0 : _semanticColors$base12.textSecondary};
1715
- `;
1716
- }}
1717
- `;
2492
+ function useResidueSelection({ sequenceWrapper }) {
2493
+ const { clearAnchor, getAnchor, setAnchor } = useSequenceDrag();
2494
+ const { click } = useLociDispatch();
2495
+ const onMouseDown = (e) => {
2496
+ e.stopPropagation();
2497
+ setAnchor(getLoci(sequenceWrapper, readSeqIdx(e.target)));
2498
+ };
2499
+ const onMouseUp = (e) => {
2500
+ e.stopPropagation();
2501
+ const anchor = getAnchor();
2502
+ if (anchor === void 0) return;
2503
+ const loci = getLoci(sequenceWrapper, readSeqIdx(e.target));
2504
+ if (loci) {
2505
+ const dragged = !molstar_lib_mol_model_structure.StructureElement.Loci.areEqual(anchor, loci);
2506
+ 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);
2507
+ }
2508
+ clearAnchor();
2509
+ };
2510
+ return {
2511
+ onMouseDown,
2512
+ onMouseUp
2513
+ };
2514
+ }
2515
+ //#endregion
2516
+ //#region src/core/ProteinStructureViewer/components/SequenceView/hooks/useResiduePointer.ts
2517
+ /**
2518
+ * Everything the grid does with a pointer: hover on move, and the drag-to-range
2519
+ * selection delegated to `useResidueSelection`.
2520
+ *
2521
+ * Hover reaches the 3D view through a throttled queue, so that sweeping the
2522
+ * pointer across the grid does not ask Mol* to re-mark on every residue
2523
+ * crossed. A drag is exempt: its hover carries the growing range, so it goes
2524
+ * out immediately to keep the preview under the pointer.
2525
+ */
2526
+ function useResiduePointer({ containerRef, sequenceWrapper }) {
2527
+ const { hover } = useLociDispatch();
2528
+ const { clear, hide, moveTo, tooltipRef } = useHoveredResidue({
2529
+ containerRef,
2530
+ sequenceWrapper
2531
+ });
2532
+ const { getAnchor } = useSequenceDrag();
2533
+ const { onMouseDown, onMouseUp } = useResidueSelection({ sequenceWrapper });
2534
+ const queueRef = (0, react.useRef)(null);
2535
+ if (queueRef.current === null) queueRef.current = new rxjs.Subject();
2536
+ const queue = queueRef.current;
2537
+ useSubscribe((0, react.useMemo)(() => queue.pipe((0, rxjs_operators.throttleTime)(HOVER_THROTTLE_MS, void 0, {
2538
+ leading: true,
2539
+ trailing: true
2540
+ })), [queue]), (e) => {
2541
+ hover(getLoci(sequenceWrapper, e.seqIdx < 0 ? void 0 : e.seqIdx), e.buttons, e.button, e.modifiers, getAnchor());
2542
+ });
2543
+ const queueHover = (e, seqIdx) => queue.next({
2544
+ button: (0, molstar_lib_mol_util_input_input_observer.getButton)(e.nativeEvent),
2545
+ buttons: (0, molstar_lib_mol_util_input_input_observer.getButtons)(e.nativeEvent),
2546
+ modifiers: (0, molstar_lib_mol_util_input_input_observer.getModifiers)(e.nativeEvent),
2547
+ seqIdx
2548
+ });
2549
+ /** Asks the 3D view to drop its hover, once, if the grid still holds one. */
2550
+ const releaseHover = (e) => {
2551
+ if (clear()) queueHover(e, -1);
2552
+ };
2553
+ const onContextMenu = (e) => {
2554
+ e.preventDefault();
2555
+ };
2556
+ const onMouseMove = (e) => {
2557
+ var _readSeqIdx;
2558
+ e.stopPropagation();
2559
+ const el = e.target;
2560
+ if (!el || !el.getAttribute) {
2561
+ releaseHover(e);
2562
+ return;
2563
+ }
2564
+ const seqIdx = (_readSeqIdx = readSeqIdx(el)) !== null && _readSeqIdx !== void 0 ? _readSeqIdx : -1;
2565
+ if (!moveTo(seqIdx, el)) return;
2566
+ const anchor = getAnchor();
2567
+ if (anchor === void 0) {
2568
+ queueHover(e, seqIdx);
2569
+ return;
2570
+ }
2571
+ 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);
2572
+ };
2573
+ /**
2574
+ * Leaving one grid is not the end of a drag: on a complex the next chain's
2575
+ * grid is a separate element, so a drag from the target into the binder
2576
+ * leaves this one on its way. The anchor is therefore left alone here and
2577
+ * dropped by the release, or by the pointer leaving the panel altogether.
2578
+ */
2579
+ const onMouseLeave = (e) => {
2580
+ e.stopPropagation();
2581
+ hide();
2582
+ releaseHover(e);
2583
+ };
2584
+ return {
2585
+ handlers: {
2586
+ onContextMenu,
2587
+ onMouseDown,
2588
+ onMouseLeave,
2589
+ onMouseMove,
2590
+ onMouseUp
2591
+ },
2592
+ tooltipRef
2593
+ };
2594
+ }
1718
2595
  //#endregion
1719
2596
  //#region src/core/ProteinStructureViewer/components/SequenceView/components/ResidueTooltip/index.tsx
1720
2597
  var _getSpaces$m, _getSpaces;
@@ -1937,10 +2814,37 @@ function buildParams(state) {
1937
2814
  structure: molstar_lib_mol_util_param_definition.ParamDefinition.Select(structureOptions.options[0][0], structureOptions.options, { shortLabel: true })
1938
2815
  };
1939
2816
  }
1940
- /** One entry per chain, in Mol*'s entity / chain / operator order. */
2817
+ /**
2818
+ * The file's own name for a chain group, or `""` when the structure holds no
2819
+ * unit for it. Mol* groups its sequence panel by `chainGroupId`, but every
2820
+ * chain-level prop on the viewer is keyed by `auth_asym_id`, so the two have
2821
+ * to be bridged through the structure itself.
2822
+ */
2823
+ function authAsymIdFor(structure, chainGroupId) {
2824
+ const location = molstar_lib_mol_model_structure.StructureElement.Location.create(structure);
2825
+ for (const unit of structure.units) {
2826
+ if (unit.chainGroupId !== chainGroupId) continue;
2827
+ const element = unit.elements[0];
2828
+ if (element === void 0) continue;
2829
+ molstar_lib_mol_model_structure.StructureElement.Location.set(location, structure, unit, element);
2830
+ return molstar_lib_mol_model_structure.StructureProperties.chain.auth_asym_id(location);
2831
+ }
2832
+ return "";
2833
+ }
2834
+ /**
2835
+ * One entry per polymer chain, in Mol*'s entity / chain / operator order.
2836
+ *
2837
+ * Polymer entities only. The heteroatoms on a chain are separate entities, and
2838
+ * Mol* would hand back a caption and a grid for each - a myoglobin listing
2839
+ * "Chain A" and then two more headers, both reading "Chain A_1 [auth A]", one
2840
+ * holding the heme and one the hydroxide. They are drawn in the 3D view and
2841
+ * they are not sequence, and a caption per ligand offers a chain toggle for
2842
+ * something that is not a chain.
2843
+ */
1941
2844
  function buildEntries(structure, selection, _params) {
1942
2845
  const entries = [];
1943
- 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({
2846
+ 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({
2847
+ chainId: authAsymIdFor(structure, chainGroupId),
1944
2848
  id: `${modelEntityId}:${chainGroupId}:${operatorKey}`,
1945
2849
  label: cLabel,
1946
2850
  wrapper: (0, molstar_lib_mol_plugin_ui_sequence.getSequenceWrapper)({
@@ -2009,8 +2913,14 @@ function createSequenceView(viewSettings) {
2009
2913
  return function SequenceView() {
2010
2914
  const { mode, theme } = useMolstarTheme(viewSettings);
2011
2915
  const backgroundColor = useViewSetting(viewSettings, (s) => s.sequenceViewerBackgroundColor);
2916
+ const hiddenChains = useViewSetting(viewSettings, (s) => s.hiddenChains);
2917
+ const onChainHover = useViewSetting(viewSettings, (s) => s.onChainHover);
2918
+ const onChainSelect = useViewSetting(viewSettings, (s) => s.onChainSelect);
2919
+ const onChainToggle = useViewSetting(viewSettings, (s) => s.onChainToggle);
2920
+ const selectedChains = useViewSetting(viewSettings, (s) => s.selectedChains);
2012
2921
  const { entries, isEmpty, residueCount } = useSequenceWrappers();
2013
2922
  const { copied, copySequence } = useCopySequence(entries);
2923
+ const drag = useCreateSequenceDrag();
2014
2924
  if (isEmpty) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_mui_material_styles.ThemeProvider, {
2015
2925
  theme,
2016
2926
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmptyState, {
@@ -2041,16 +2951,33 @@ function createSequenceView(viewSettings) {
2041
2951
  /* @__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 })] })
2042
2952
  ] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SequenceScroller, {
2043
2953
  backgroundColor,
2044
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SequenceScrollArea, {
2045
- className: "msp-sequence msp-sequence-wrapper-non-empty",
2046
- 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", {
2047
- className: "msp-sequence-wrapper",
2048
- children: s.wrapper
2049
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Sequence, {
2050
- chainLabel: showChainLabels ? s.label : void 0,
2051
- residueColors,
2052
- sequenceWrapper: s.wrapper
2053
- })] }, s.id))
2954
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SequenceDragProvider, {
2955
+ value: drag,
2956
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SequenceScrollArea, {
2957
+ className: "msp-sequence msp-sequence-wrapper-non-empty",
2958
+ onMouseLeave: drag.clearAnchor,
2959
+ children: entries.map((s) => {
2960
+ var _hiddenChains$has, _selectedChains$has;
2961
+ const hidden = (_hiddenChains$has = hiddenChains === null || hiddenChains === void 0 ? void 0 : hiddenChains.has(s.chainId)) !== null && _hiddenChains$has !== void 0 ? _hiddenChains$has : false;
2962
+ const grid = typeof s.wrapper === "string" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2963
+ className: "msp-sequence-wrapper",
2964
+ children: s.wrapper
2965
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Sequence, {
2966
+ chainLabel: showChainLabels ? s.label : void 0,
2967
+ residueColors,
2968
+ sequenceWrapper: s.wrapper
2969
+ });
2970
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [showChainLabels && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChainHeader, {
2971
+ chainId: s.chainId,
2972
+ isHidden: hidden,
2973
+ isSelected: (_selectedChains$has = selectedChains === null || selectedChains === void 0 ? void 0 : selectedChains.has(s.chainId)) !== null && _selectedChains$has !== void 0 ? _selectedChains$has : false,
2974
+ label: s.label,
2975
+ onHover: onChainHover,
2976
+ onSelect: onChainSelect,
2977
+ onToggle: onChainToggle
2978
+ }), hidden ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HiddenChainGroup, { children: grid }) : grid] }, s.id);
2979
+ })
2980
+ })
2054
2981
  })
2055
2982
  })]
2056
2983
  })
@@ -2058,6 +2985,138 @@ function createSequenceView(viewSettings) {
2058
2985
  };
2059
2986
  }
2060
2987
  //#endregion
2988
+ //#region src/core/ProteinStructureViewer/utils/axes.ts
2989
+ /**
2990
+ * The orientation axes widget, as Mol* parameterizes it.
2991
+ *
2992
+ * Shared rather than declared where it is used, because two different parts of
2993
+ * Mol* take the same shape: the live canvas takes it as
2994
+ * `camera.helper.axes`, and the screenshot helper takes it as its own `axes`,
2995
+ * so an image can carry the widget or leave it out independently of the view
2996
+ * it was captured from.
2997
+ */
2998
+ const AXES_ON = {
2999
+ name: "on",
3000
+ params: {
3001
+ alpha: .51,
3002
+ colorX: 16711680,
3003
+ colorY: 32768,
3004
+ colorZ: 255,
3005
+ labelColorX: 8421504,
3006
+ labelColorY: 8421504,
3007
+ labelColorZ: 8421504,
3008
+ labelOpacity: 1,
3009
+ labelScale: .25,
3010
+ labelX: "X",
3011
+ labelY: "Y",
3012
+ labelZ: "Z",
3013
+ location: "bottom-left",
3014
+ locationOffsetX: 0,
3015
+ locationOffsetY: 0,
3016
+ originColor: 8421504,
3017
+ planeColorXY: 8421504,
3018
+ planeColorXZ: 8421504,
3019
+ planeColorYZ: 8421504,
3020
+ radiusScale: .075,
3021
+ scale: .15,
3022
+ showLabels: false,
3023
+ showPlanes: true
3024
+ }
3025
+ };
3026
+ const AXES_OFF = {
3027
+ name: "off",
3028
+ params: {}
3029
+ };
3030
+ //#endregion
3031
+ //#region src/core/ProteinStructureViewer/utils/color.ts
3032
+ /** Parses `#RRGGBB` (or `RRGGBB`) into a Mol* `Color`. */
3033
+ function parseHexColor(hex) {
3034
+ const h = hex.replace("#", "");
3035
+ 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));
3036
+ }
3037
+ /**
3038
+ * Parses a theme color into a Mol* `Color`, falling back when the token is
3039
+ * missing or is a format Mol* cannot consume (theme values are hex in practice,
3040
+ * but the palette types allow any CSS color string).
3041
+ */
3042
+ function themeColor(value, fallback) {
3043
+ const candidate = value === null || value === void 0 ? void 0 : value.trim();
3044
+ return parseHexColor(candidate !== void 0 && /^#?[0-9a-f]{6}$/i.test(candidate) ? candidate : fallback);
3045
+ }
3046
+ /**
3047
+ * Neutral fill for residues with no overlay value. Sits between the light and
3048
+ * dark backgrounds so unhighlighted geometry stays legible in both modes.
3049
+ */
3050
+ function neutralResidueColor(mode) {
3051
+ const g = mode === "dark" ? 100 : 180;
3052
+ return molstar_lib_mol_util_color.Color.fromRgb(g, g, g);
3053
+ }
3054
+ //#endregion
3055
+ //#region src/core/ProteinStructureViewer/utils/screenshot.ts
3056
+ /**
3057
+ * Each resolution as the Mol* preset that produces it: 1280x720, 1920x1080,
3058
+ * 3840x2160, 7680x4320. Named here rather than given as dimensions so the
3059
+ * aspect stays Mol*'s to choose.
3060
+ *
3061
+ * Mol*'s own `viewport` preset - the canvas at its current size - is left out
3062
+ * deliberately: it makes the image depend on how big the viewer happens to be
3063
+ * on screen, which is not something a caller asking for "high" is asking for.
3064
+ */
3065
+ const RESOLUTION_PRESETS = {
3066
+ high: "ultra-hd",
3067
+ low: "hd",
3068
+ maximum: "8k-ultra-hd",
3069
+ medium: "full-hd"
3070
+ };
3071
+ /** Painted behind the structure when a background is asked for but unreadable. */
3072
+ const FALLBACK_BACKGROUND = "#ffffff";
3073
+ /**
3074
+ * Downloads the structure as a PNG.
3075
+ *
3076
+ * Mol*'s screenshot helper renders its own pass rather than reading the canvas
3077
+ * back, so the image is drawn fresh at the requested size and carries its own
3078
+ * transparency and axes settings - none of which disturb the view on screen.
3079
+ *
3080
+ * A named background is the one exception. The helper only chooses between
3081
+ * transparent and whatever the renderer is already painting, so a color has to
3082
+ * be put on the canvas for the length of the capture and taken off afterwards.
3083
+ * `finally` is what guarantees the second half: leaving a figure's background
3084
+ * behind on the viewer would be a worse bug than a failed download.
3085
+ */
3086
+ function downloadStructureImage(_x) {
3087
+ return _downloadStructureImage.apply(this, arguments);
3088
+ }
3089
+ function _downloadStructureImage() {
3090
+ _downloadStructureImage = _asyncToGenerator(function* (plugin, options = {}) {
3091
+ const helper = plugin.helpers.viewportScreenshot;
3092
+ if (!helper) return;
3093
+ const { backgroundColor, filename, resolution = "medium", showAxes } = options;
3094
+ const transparent = backgroundColor === void 0;
3095
+ helper.behaviors.values.next(_objectSpread2(_objectSpread2({}, helper.behaviors.values.value), {}, {
3096
+ axes: showAxes ? AXES_ON : AXES_OFF,
3097
+ format: {
3098
+ name: "png",
3099
+ params: {}
3100
+ },
3101
+ resolution: {
3102
+ name: RESOLUTION_PRESETS[resolution],
3103
+ params: {}
3104
+ },
3105
+ transparent
3106
+ }));
3107
+ const canvas3d = plugin.canvas3d;
3108
+ const painted = canvas3d === null || canvas3d === void 0 ? void 0 : canvas3d.props.renderer.backgroundColor;
3109
+ const overrideBackground = !transparent && canvas3d !== void 0;
3110
+ if (overrideBackground) canvas3d === null || canvas3d === void 0 || canvas3d.setProps({ renderer: { backgroundColor: themeColor(backgroundColor, FALLBACK_BACKGROUND) } });
3111
+ try {
3112
+ yield helper.download(filename);
3113
+ } finally {
3114
+ if (overrideBackground && painted !== void 0) canvas3d === null || canvas3d === void 0 || canvas3d.setProps({ renderer: { backgroundColor: painted } });
3115
+ }
3116
+ });
3117
+ return _downloadStructureImage.apply(this, arguments);
3118
+ }
3119
+ //#endregion
2061
3120
  //#region src/core/ProteinStructureViewer/components/Viewport/constants.ts
2062
3121
  /**
2063
3122
  * Fraction of the bounding sphere radius within which the camera still counts
@@ -2075,32 +3134,6 @@ function isZoomedOffDefault(canvas3d) {
2075
3134
  return Math.abs(state.radius - radius) > tolerance || molstar_lib_mol_math_linear_algebra.Vec3.distance(state.target, center) > tolerance;
2076
3135
  }
2077
3136
  //#endregion
2078
- //#region \0@oxc-project+runtime@0.134.0/helpers/esm/asyncToGenerator.js
2079
- function asyncGeneratorStep(n, t, e, r, o, a, c) {
2080
- try {
2081
- var i = n[a](c), u = i.value;
2082
- } catch (n) {
2083
- e(n);
2084
- return;
2085
- }
2086
- i.done ? t(u) : Promise.resolve(u).then(r, o);
2087
- }
2088
- function _asyncToGenerator(n) {
2089
- return function() {
2090
- var t = this, e = arguments;
2091
- return new Promise(function(r, o) {
2092
- var a = n.apply(t, e);
2093
- function _next(n) {
2094
- asyncGeneratorStep(a, r, o, _next, _throw, "next", n);
2095
- }
2096
- function _throw(n) {
2097
- asyncGeneratorStep(a, r, o, _next, _throw, "throw", n);
2098
- }
2099
- _next(void 0);
2100
- });
2101
- };
2102
- }
2103
- //#endregion
2104
3137
  //#region src/core/ProteinStructureViewer/components/Viewport/hooks/useCameraReset.ts
2105
3138
  /**
2106
3139
  * Tracks whether the camera has moved off the default view, and puts it back.
@@ -2201,16 +3234,24 @@ function useCameraReset() {
2201
3234
  /**
2202
3235
  * Pinned to the lower-left of the viewport, clear of the sequence panel below
2203
3236
  * and the axes widget in the corner.
3237
+ *
3238
+ * Anchored by its bottom edge, so the buttons stack upwards: the last one
3239
+ * stays put where a reader last saw it, and an earlier one appearing or going
3240
+ * does not move the rest. Reset comes first in the markup and so sits above
3241
+ * capture.
2204
3242
  */
2205
- const ResetCameraSlot = (0, _emotion_styled.default)("div")`
3243
+ const ViewportButtonStack = (0, _emotion_styled.default)("div")`
2206
3244
  position: absolute;
2207
3245
  bottom: 48px;
2208
3246
  z-index: 10;
3247
+ display: flex;
3248
+ flex-direction: column;
2209
3249
 
2210
3250
  ${(props) => {
2211
3251
  const spaces = (0, _czi_sds_components.getSpaces)(props);
2212
3252
  return `
2213
3253
  left: ${spaces === null || spaces === void 0 ? void 0 : spaces.m}px;
3254
+ gap: ${spaces === null || spaces === void 0 ? void 0 : spaces.xxs}px;
2214
3255
  `;
2215
3256
  }}
2216
3257
  `;
@@ -2228,8 +3269,30 @@ const ResetCameraSlot = (0, _emotion_styled.default)("div")`
2228
3269
  */
2229
3270
  function createViewportView(viewSettings) {
2230
3271
  return function ViewportWithReset() {
3272
+ const plugin = usePlugin();
2231
3273
  const { theme } = useMolstarTheme(viewSettings);
2232
3274
  const { cameraChanged, resetCamera } = useCameraReset();
3275
+ const showAxes = useViewSetting(viewSettings, (s) => {
3276
+ var _s$showAxes;
3277
+ return (_s$showAxes = s.showAxes) !== null && _s$showAxes !== void 0 ? _s$showAxes : true;
3278
+ });
3279
+ const download = useViewSetting(viewSettings, (s) => s.download);
3280
+ /**
3281
+ * Disabled while a capture is in flight. A high resolution takes long
3282
+ * enough to click twice through, and each click renders its own pass.
3283
+ */
3284
+ const [downloading, setDownloading] = (0, react.useState)(false);
3285
+ const captureImage = (0, react.useCallback)(_asyncToGenerator(function* () {
3286
+ if (!download) return;
3287
+ setDownloading(true);
3288
+ try {
3289
+ yield downloadStructureImage(plugin, download);
3290
+ } catch (error) {
3291
+ console.error("Failed to download the structure image:", error);
3292
+ } finally {
3293
+ setDownloading(false);
3294
+ }
3295
+ }), [download, plugin]);
2233
3296
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2234
3297
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(molstar_lib_mol_plugin_ui_viewport.Viewport, {}),
2235
3298
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -2238,18 +3301,37 @@ function createViewportView(viewSettings) {
2238
3301
  }),
2239
3302
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_mui_material_styles.ThemeProvider, {
2240
3303
  theme,
2241
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResetCameraSlot, {
2242
- hidden: !cameraChanged,
2243
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_czi_sds_components.Button, {
2244
- "aria-label": "Reset camera",
2245
- onClick: resetCamera,
3304
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ViewportButtonStack, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3305
+ hidden: !showAxes || !cameraChanged,
3306
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_czi_sds_components.Tooltip, {
3307
+ arrow: true,
3308
+ placement: "right",
2246
3309
  title: "Reset camera",
3310
+ textAlign: "left",
3311
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_czi_sds_components.Button, {
3312
+ "aria-label": "Reset camera",
3313
+ onClick: resetCamera,
3314
+ sdsStyle: "minimal",
3315
+ sdsType: "secondary",
3316
+ size: "medium",
3317
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_phosphor_icons_react.ArrowCounterClockwiseIcon, {})
3318
+ })
3319
+ })
3320
+ }), download && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_czi_sds_components.Tooltip, {
3321
+ arrow: true,
3322
+ placement: "right",
3323
+ title: "Download image",
3324
+ textAlign: "left",
3325
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_czi_sds_components.Button, {
3326
+ "aria-label": "Download image of the structure",
3327
+ disabled: downloading,
3328
+ onClick: captureImage,
2247
3329
  sdsStyle: "minimal",
2248
3330
  sdsType: "secondary",
2249
3331
  size: "medium",
2250
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_phosphor_icons_react.ArrowCounterClockwiseIcon, {})
3332
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_phosphor_icons_react.ApertureIcon, {})
2251
3333
  })
2252
- })
3334
+ })] })
2253
3335
  })
2254
3336
  ] });
2255
3337
  };
@@ -2308,6 +3390,25 @@ function focusResidue(plugin, loci) {
2308
3390
  return clipRatio;
2309
3391
  }
2310
3392
  /**
3393
+ * Frames the camera on a loci without cropping anything around it.
3394
+ *
3395
+ * The counterpart to `focusResidue` for something meant to be read in context
3396
+ * rather than peeled down to: the camera fits what was selected, and the depth
3397
+ * clip is left open to the whole scene, so the rest of the structure stays on
3398
+ * screen. A selected chain is shown by dimming the chains around it, and they
3399
+ * have to be there to be dimmed.
3400
+ */
3401
+ function frameLoci(plugin, loci) {
3402
+ var _plugin$canvas3d2;
3403
+ const camera = (_plugin$canvas3d2 = plugin.canvas3d) === null || _plugin$canvas3d2 === void 0 ? void 0 : _plugin$canvas3d2.camera;
3404
+ if (!camera) return;
3405
+ const sphere = molstar_lib_mol_model_loci.Loci.getBoundingSphere(loci);
3406
+ if (!sphere) return;
3407
+ const framingRadius = Math.max(sphere.radius + FOCUS_EXTRA_RADIUS, FOCUS_MIN_RADIUS);
3408
+ const snapshot = camera.getFocus(sphere.center, framingRadius);
3409
+ camera.setState(_objectSpread2(_objectSpread2({}, snapshot), {}, { radius: camera.state.radiusMax }), FOCUS_DURATION_MS);
3410
+ }
3411
+ /**
2311
3412
  * Keeps the depth clip in sync with the zoom while a residue is focused.
2312
3413
  *
2313
3414
  * Mol*'s trackball zoom moves the camera without touching the clip radius, so
@@ -2317,49 +3418,302 @@ function focusResidue(plugin, loci) {
2317
3418
  * camera is mid-transition, so it does not fight the focus animation.
2318
3419
  */
2319
3420
  function syncClipToZoom(plugin, getClipRatio) {
2320
- var _plugin$canvas3d2;
2321
- const camera = (_plugin$canvas3d2 = plugin.canvas3d) === null || _plugin$canvas3d2 === void 0 ? void 0 : _plugin$canvas3d2.camera;
3421
+ var _plugin$canvas3d3;
3422
+ const camera = (_plugin$canvas3d3 = plugin.canvas3d) === null || _plugin$canvas3d3 === void 0 ? void 0 : _plugin$canvas3d3.camera;
2322
3423
  const clipRatio = getClipRatio();
2323
3424
  if (!camera || clipRatio === null || camera.transition.inTransition) return;
2324
3425
  const radius = clipRadiusForDistance(molstar_lib_mol_math_linear_algebra.Vec3.distance(camera.state.position, camera.state.target), clipRatio, camera.state.radiusMax);
2325
3426
  if (Math.abs(camera.state.radius - radius) > CLIP_EPSILON) {
2326
- var _plugin$canvas3d3;
3427
+ var _plugin$canvas3d4;
2327
3428
  camera.setState({ radius });
2328
- (_plugin$canvas3d3 = plugin.canvas3d) === null || _plugin$canvas3d3 === void 0 || _plugin$canvas3d3.requestDraw();
3429
+ (_plugin$canvas3d4 = plugin.canvas3d) === null || _plugin$canvas3d4 === void 0 || _plugin$canvas3d4.requestDraw();
2329
3430
  }
2330
3431
  }
2331
3432
  //#endregion
2332
- //#region src/core/ProteinStructureViewer/utils/residueRef.ts
3433
+ //#region src/core/ProteinStructureViewer/utils/chains.ts
2333
3434
  /**
2334
- * Reads the residue a loci points at, or null when it points at nothing the
2335
- * viewer can address.
3435
+ * The polymer of one chain, as named in the file. What each chain's cartoon is
3436
+ * built from, and so what makes a chain a thing Mol* can hide on its own.
2336
3437
  *
2337
- * `index` comes from Mol*'s residue key, which counts residues in file order
2338
- * from zero and is the same counter `injectPlddtIntoPdb` walks. Deriving it
2339
- * from the residue's *number* instead would tie it to how the file happens to
2340
- * be numbered: a crop beginning at residue 200 would index `plddt` at 199, and
2341
- * a file numbering each chain from 1 would give two residues the same index.
3438
+ * Paired with `chainLigandExpression`: between them they cover everything on a
3439
+ * chain the viewer draws, split by what it draws it as. The polymer
3440
+ * restriction is the one Mol*'s `polymer-cartoon` preset applied, and is what
3441
+ * keeps a heme from being handed to a representation that can only trace a
3442
+ * backbone.
2342
3443
  */
2343
- function residueRefFromLoci(loci) {
2344
- var _StructureProperties$;
2345
- const location = molstar_lib_mol_model_structure.StructureElement.Location.create(void 0);
2346
- const firstLoc = molstar_lib_mol_model_structure.StructureElement.Loci.getFirstLocation(loci, location);
2347
- if (!firstLoc) return null;
3444
+ function chainPolymerExpression(chainId) {
3445
+ return molstar_lib_mol_script_language_builder.MolScriptBuilder.struct.generator.atomGroups({
3446
+ "chain-test": molstar_lib_mol_script_language_builder.MolScriptBuilder.core.rel.eq([molstar_lib_mol_script_language_builder.MolScriptBuilder.ammp("auth_asym_id"), chainId]),
3447
+ "entity-test": molstar_lib_mol_script_language_builder.MolScriptBuilder.core.rel.eq([molstar_lib_mol_script_language_builder.MolScriptBuilder.ammp("entityType"), "polymer"])
3448
+ });
3449
+ }
3450
+ /**
3451
+ * Bonds worth following out of a ligand: the ones holding it to the protein.
3452
+ *
3453
+ * Mol*'s own pair, and metallic coordination is the half that matters most -
3454
+ * a heme's iron is held to its histidine by a coordination bond and not a
3455
+ * covalent one, so covalent alone would leave every metalloprotein's cofactor
3456
+ * looking dropped in rather than bound.
3457
+ */
3458
+ 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"])])]);
3459
+ /**
3460
+ * The heteroatoms of one chain - ligands, ions, glycans, lipids - along with
3461
+ * the residues holding them, which is what its ball-and-stick is built from.
3462
+ *
3463
+ * `non-polymer` is the one entity type a PDB's HETATM records land under,
3464
+ * ligands and ions alike, so there is nothing finer to test here: the
3465
+ * distinction Mol*'s presets draw between their `ligand`, `ion` and `lipid`
3466
+ * components is a residue-name lookup made to give each a slightly different
3467
+ * style, and all three are sticks. `branched` is the separate type glycans
3468
+ * arrive as.
3469
+ *
3470
+ * Water is the third type and is left out of the seed. Every solvent molecule
3471
+ * drawn as sticks buries the structure they surround, which is why Mol* draws
3472
+ * them at 60% alpha rather than plain, and none of them is what "show the
3473
+ * ligands" asks for.
3474
+ *
3475
+ * One layer of bonds out from there, taken as whole residues, is what brings
3476
+ * the binding residue along - myoglobin's His93, say. Without it the heme
3477
+ * floats unattached in the middle of a cartoon it is visibly bonded to, and
3478
+ * the bond drawn to its iron ends in mid-air. One layer and no more: the
3479
+ * residue that holds the ligand, not that residue's own neighbours.
3480
+ *
3481
+ * Intersected back with the chain, so everything a chain is drawn with belongs
3482
+ * to that chain. A ligand bonded across an interface would otherwise leave the
3483
+ * partner chain's residue in this chain's component, somewhere hiding the
3484
+ * partner chain could not reach.
3485
+ */
3486
+ function chainLigandExpression(chainId) {
3487
+ const onThisChain = molstar_lib_mol_script_language_builder.MolScriptBuilder.core.rel.eq([molstar_lib_mol_script_language_builder.MolScriptBuilder.ammp("auth_asym_id"), chainId]);
3488
+ return molstar_lib_mol_script_language_builder.MolScriptBuilder.struct.modifier.union([molstar_lib_mol_script_language_builder.MolScriptBuilder.struct.modifier.intersectBy({
3489
+ 0: molstar_lib_mol_script_language_builder.MolScriptBuilder.struct.modifier.includeConnected({
3490
+ 0: molstar_lib_mol_script_language_builder.MolScriptBuilder.struct.generator.atomGroups({
3491
+ "chain-test": onThisChain,
3492
+ "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")])
3493
+ }),
3494
+ "as-whole-residues": true,
3495
+ "bond-test": CONNECTING_BOND,
3496
+ "layer-count": 1
3497
+ }),
3498
+ by: molstar_lib_mol_script_language_builder.MolScriptBuilder.struct.generator.atomGroups({ "chain-test": onThisChain })
3499
+ })]);
3500
+ }
3501
+ /**
3502
+ * Matches how the sequence panel captions its chains, which reaches the same
3503
+ * label through Mol*'s own `getChainOptions`. Deriving it the same way is what
3504
+ * keeps a chain from going by one name in the legend and another in the panel.
3505
+ */
3506
+ const CHAIN_LABEL_OPTIONS = {
3507
+ granularity: "chain",
3508
+ hidePrefix: true,
3509
+ htmlStyling: false
3510
+ };
3511
+ /**
3512
+ * Reads the chains of a structure and the polymer residues on each.
3513
+ *
3514
+ * Residue indices are Mol*'s residue keys, counting residues in file order
3515
+ * across the whole structure, so what is reported here addresses the same
3516
+ * residues as `plddt`, `residueOverlay` and `selection`.
3517
+ *
3518
+ * Heteroatoms are drawn (see `chainLigandExpression`) but are not counted
3519
+ * here. A heme is a residue to the file and not to a reader: it is no part of
3520
+ * the sequence the panel lists, and averaging a confidence score over it would
3521
+ * divide by a residue that has none.
3522
+ *
3523
+ * Residues are gathered into a set rather than inferred from the ends: a chain
3524
+ * carrying several symmetry operators repeats each of its residues once per
3525
+ * operator, and a structure whose chains interleave leaves a chain's indices
3526
+ * non-contiguous. Neither is what a predicted complex looks like, but a range
3527
+ * would misread both.
3528
+ */
3529
+ function scanChains(structure) {
3530
+ const accumulators = /* @__PURE__ */ new Map();
3531
+ const location = molstar_lib_mol_model_structure.StructureElement.Location.create(structure);
3532
+ for (const unit of structure.units) {
3533
+ location.unit = unit;
3534
+ for (let i = 0; i < unit.elements.length; i++) {
3535
+ location.element = unit.elements[i];
3536
+ const chainId = molstar_lib_mol_model_structure.StructureProperties.chain.auth_asym_id(location);
3537
+ let chain = accumulators.get(chainId);
3538
+ if (!chain) {
3539
+ chain = {
3540
+ chainId,
3541
+ label: (0, molstar_lib_mol_theme_label.elementLabel)(location, CHAIN_LABEL_OPTIONS),
3542
+ residues: /* @__PURE__ */ new Set()
3543
+ };
3544
+ accumulators.set(chainId, chain);
3545
+ }
3546
+ if (molstar_lib_mol_model_structure.StructureProperties.entity.type(location) !== "polymer") continue;
3547
+ chain.residues.add(molstar_lib_mol_model_structure.StructureProperties.residue.key(location));
3548
+ }
3549
+ }
3550
+ 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) => {
3551
+ var _a$residues$, _b$residues$;
3552
+ 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);
3553
+ });
2348
3554
  return {
2349
- chainId: molstar_lib_mol_model_structure.StructureProperties.chain.auth_asym_id(firstLoc),
2350
- compId: molstar_lib_mol_model_structure.StructureProperties.atom.label_comp_id(firstLoc),
2351
- insCode: (_StructureProperties$ = molstar_lib_mol_model_structure.StructureProperties.residue.pdbx_PDB_ins_code(firstLoc)) !== null && _StructureProperties$ !== void 0 ? _StructureProperties$ : "",
2352
- index: molstar_lib_mol_model_structure.StructureProperties.residue.key(firstLoc),
2353
- seqId: molstar_lib_mol_model_structure.StructureProperties.residue.auth_seq_id(firstLoc)
3555
+ chainLabels: new Map([...accumulators.values()].map(({ chainId, label }) => [chainId, label])),
3556
+ chains: ordered.map(({ chainId, label, residues }) => {
3557
+ var _residues, _residues$;
3558
+ return {
3559
+ chainId,
3560
+ endIndex: (_residues = residues[residues.length - 1]) !== null && _residues !== void 0 ? _residues : 0,
3561
+ label,
3562
+ residueCount: residues.length,
3563
+ startIndex: (_residues$ = residues[0]) !== null && _residues$ !== void 0 ? _residues$ : 0
3564
+ };
3565
+ }),
3566
+ residuesByChain: new Map(ordered.map(({ chainId, residues }) => [chainId, residues]))
2354
3567
  };
2355
3568
  }
3569
+ /** True when the two lists name the same chains, in the same order. */
3570
+ function chainsEqual(a, b) {
3571
+ return a.length === b.length && a.every((chain, i) => {
3572
+ const other = b[i];
3573
+ return other !== void 0 && chain.chainId === other.chainId && chain.startIndex === other.startIndex && chain.endIndex === other.endIndex && chain.residueCount === other.residueCount;
3574
+ });
3575
+ }
3576
+ //#endregion
3577
+ //#region src/core/ProteinStructureViewer/utils/molstarSpec.ts
2356
3578
  /**
2357
- * How a residue is named in the readout, e.g. `"LYS 111"`. Takes the number and
2358
- * the insertion code from the file so that the readout and the sequence panel
2359
- * beneath it agree on what to call a residue.
3579
+ * The spec's list-valued keys. These are appended rather than merged: Mol*
3580
+ * reads them as collections, so a consumer's entries join the viewer's instead
3581
+ * of replacing them.
3582
+ *
3583
+ * `config` is the one where order carries meaning - Mol* folds the list into a
3584
+ * Map in order, so a later entry overwrites an earlier one, and appending is
3585
+ * what lets a consumer override a setting the viewer chose.
2360
3586
  */
2361
- function residueLabel(residue) {
2362
- return `${residue.compId} ${residue.seqId}${residue.insCode}`;
3587
+ const LIST_KEYS = [
3588
+ "actions",
3589
+ "animations",
3590
+ "behaviors",
3591
+ "config",
3592
+ "customFormats",
3593
+ "customParamEditors"
3594
+ ];
3595
+ /**
3596
+ * Whether a value should be merged into rather than replaced.
3597
+ *
3598
+ * Only object literals qualify. Arrays are collections, and the spec is full
3599
+ * of React components and Mol* class instances, all of which are values in
3600
+ * their own right - merging into one would take it apart.
3601
+ */
3602
+ function isPlainObject(value) {
3603
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
3604
+ const proto = Object.getPrototypeOf(value);
3605
+ return proto === Object.prototype || proto === null;
3606
+ }
3607
+ function deepMerge(base, override) {
3608
+ if (!isPlainObject(base) || !isPlainObject(override)) return override;
3609
+ const merged = _objectSpread2({}, base);
3610
+ for (const [key, value] of Object.entries(override)) merged[key] = key in base ? deepMerge(base[key], value) : value;
3611
+ return merged;
3612
+ }
3613
+ function withoutLists(spec) {
3614
+ const rest = _objectSpread2({}, spec);
3615
+ for (const key of LIST_KEYS) delete rest[key];
3616
+ return rest;
3617
+ }
3618
+ /**
3619
+ * Lays a consumer's Mol* spec over the viewer's own.
3620
+ *
3621
+ * The viewer builds a spec of its own - the sequence panel and viewport it
3622
+ * renders, the controls it hides, the behaviors it drops - and this is how a
3623
+ * consumer reaches the rest of Mol* without the viewer having to grow a prop
3624
+ * per setting. Anything the consumer names wins, so `canvas3d.renderer` or a
3625
+ * `PluginConfig` item can be set to something other than the default the
3626
+ * viewer picked.
3627
+ *
3628
+ * The one thing it cannot do is take something away: `behaviors` is appended
3629
+ * to, so a behavior the viewer removed - Mol*'s click-to-zoom camera - stays
3630
+ * removed, and one the viewer kept cannot be dropped.
3631
+ */
3632
+ function mergeMolstarSpec(base, override) {
3633
+ if (!override) return base;
3634
+ const merged = deepMerge(withoutLists(base), withoutLists(override));
3635
+ for (const key of LIST_KEYS) {
3636
+ var _base, _override;
3637
+ const ours = (_base = base[key]) !== null && _base !== void 0 ? _base : [];
3638
+ const theirs = (_override = override[key]) !== null && _override !== void 0 ? _override : [];
3639
+ if (ours.length > 0 || theirs.length > 0) merged[key] = [...ours, ...theirs];
3640
+ }
3641
+ return merged;
3642
+ }
3643
+ //#endregion
3644
+ //#region src/core/ProteinStructureViewer/utils/structureFormat.ts
3645
+ /** How much of the file to inspect when guessing the format. */
3646
+ const DETECT_SAMPLE = 16384;
3647
+ /**
3648
+ * Infers whether `text` is PDB or mmCIF (PDBx).
3649
+ *
3650
+ * mmCIF documents start with a `data_` block and name their coordinates
3651
+ * `_atom_site.`. PDB never does either, so either marker is enough. A leading
3652
+ * comment (`# ...`) before `data_` is still mmCIF.
3653
+ */
3654
+ function detectStructureFormat(text) {
3655
+ const sample = text.slice(0, DETECT_SAMPLE);
3656
+ if (/(?:^|\n)\s*data_/.test(sample) || /_atom_site\./.test(sample)) return "mmcif";
3657
+ return "pdb";
3658
+ }
3659
+ //#endregion
3660
+ //#region src/core/ProteinStructureViewer/utils/chainColorTheme.ts
3661
+ /** Mol* color theme name under which per-chain coloring is registered. */
3662
+ const CHAIN_COLOR_THEME_NAME = "chain-color";
3663
+ /** Painted for a chain with no color assigned, or one assigned a bad value. */
3664
+ const DEFAULT_CHAIN_COLOR_HEX = "#808080";
3665
+ const DEFAULT_CHAIN_COLOR = molstar_lib_mol_util_color.Color.fromRgb(128, 128, 128);
3666
+ /**
3667
+ * Builds a Mol* color theme that paints each chain its own color.
3668
+ *
3669
+ * This stands in for Mol*'s built-in `chain-id` theme, which picks its own
3670
+ * colors from a palette we cannot read back. Owning the assignment is what
3671
+ * lets the chain legend show a swatch that matches the structure, rather than
3672
+ * a key for colors it has to guess at.
3673
+ *
3674
+ * Created per plugin instance, and its state read live inside `color()`, for
3675
+ * the same reasons as the residue value theme beside it: several viewers can
3676
+ * color chains differently on one page, and recoloring does not rebuild the
3677
+ * structure.
3678
+ */
3679
+ function createChainColorTheme() {
3680
+ const state = {
3681
+ colors: /* @__PURE__ */ new Map(),
3682
+ version: 0
3683
+ };
3684
+ const provider = {
3685
+ category: "chain property",
3686
+ defaultValues: {},
3687
+ factory() {
3688
+ const { colors } = state;
3689
+ const parsed = /* @__PURE__ */ new Map();
3690
+ for (const [chainId, hex] of colors) parsed.set(chainId, themeColor(hex, DEFAULT_CHAIN_COLOR_HEX));
3691
+ return {
3692
+ color(location) {
3693
+ var _parsed$get;
3694
+ if (!molstar_lib_mol_model_structure.StructureElement.Location.is(location)) return DEFAULT_CHAIN_COLOR;
3695
+ const chainId = molstar_lib_mol_model_structure.StructureProperties.chain.auth_asym_id(location);
3696
+ return (_parsed$get = parsed.get(chainId)) !== null && _parsed$get !== void 0 ? _parsed$get : DEFAULT_CHAIN_COLOR;
3697
+ },
3698
+ contextHash: state.version,
3699
+ description: "Color residues by the chain they sit on",
3700
+ factory: provider.factory,
3701
+ granularity: "group",
3702
+ props: {}
3703
+ };
3704
+ },
3705
+ getParams: () => ({}),
3706
+ isApplicable: () => true,
3707
+ label: "Chain Color",
3708
+ name: CHAIN_COLOR_THEME_NAME
3709
+ };
3710
+ return {
3711
+ provider,
3712
+ setState(colors) {
3713
+ state.colors = colors;
3714
+ state.version++;
3715
+ }
3716
+ };
2363
3717
  }
2364
3718
  //#endregion
2365
3719
  //#region src/core/ProteinStructureViewer/utils/plddt.ts
@@ -2395,6 +3749,14 @@ const PLDDT_COLOR_SCALE = {
2395
3749
  ]
2396
3750
  };
2397
3751
  const PLDDT_BANDS = PLDDT_COLOR_SCALE.kind === "stepped" ? PLDDT_COLOR_SCALE.stops : [];
3752
+ /**
3753
+ * The band colors alone, ascending from least to most confident.
3754
+ *
3755
+ * What the chain legend shows in place of a single color while pLDDT is
3756
+ * painting the structure: no one color stands for a chain then, so the swatch
3757
+ * carries the whole key instead.
3758
+ */
3759
+ const PLDDT_BAND_COLORS = PLDDT_BANDS.map((stop) => stop.color);
2398
3760
  const DEFAULT_PLDDT_COLOR = molstar_lib_mol_util_color.Color.fromRgb(128, 128, 128);
2399
3761
  /**
2400
3762
  * Maps a pLDDT confidence score on the 0-100 B-factor scale to its band color.
@@ -2433,9 +3795,183 @@ function injectPlddtIntoPdb(pdbData, plddtValues) {
2433
3795
  return paddedLine.substring(0, 60) + bfactorStr + paddedLine.substring(66);
2434
3796
  }).join("\n");
2435
3797
  }
3798
+ const ATOM_SITE_PREFIX = "_atom_site.";
3799
+ const B_FACTOR_COLUMN = "B_iso_or_equiv";
3800
+ function skipCifWhitespace(line, index) {
3801
+ let i = index;
3802
+ while (i < line.length && /\s/.test(line.charAt(i))) i += 1;
3803
+ return i;
3804
+ }
3805
+ function readCifToken(line, start) {
3806
+ const quote = line.charAt(start);
3807
+ if (quote === "'" || quote === "\"") {
3808
+ const end = line.indexOf(quote, start + 1);
3809
+ if (end < 0) return {
3810
+ next: line.length,
3811
+ token: line.slice(start + 1)
3812
+ };
3813
+ return {
3814
+ next: end + 1,
3815
+ token: line.slice(start + 1, end)
3816
+ };
3817
+ }
3818
+ let i = start;
3819
+ while (i < line.length && !/\s/.test(line.charAt(i))) i += 1;
3820
+ return {
3821
+ next: i,
3822
+ token: line.slice(start, i)
3823
+ };
3824
+ }
3825
+ /**
3826
+ * Splits one CIF line into tokens, honouring `'quoted'` and `"quoted"` values.
3827
+ * Atom-site rows are one line each in practice; multiline `;text;` fields are
3828
+ * not used there and are not handled.
3829
+ */
3830
+ function tokenizeCifLine(line) {
3831
+ const tokens = [];
3832
+ let i = skipCifWhitespace(line, 0);
3833
+ while (i < line.length) {
3834
+ const { next, token } = readCifToken(line, i);
3835
+ tokens.push(token);
3836
+ i = skipCifWhitespace(line, next);
3837
+ }
3838
+ return tokens;
3839
+ }
3840
+ function isCifNull(value) {
3841
+ return value === void 0 || value === "." || value === "?";
3842
+ }
3843
+ function columnIndex(columns, name) {
3844
+ return columns.indexOf(name);
3845
+ }
3846
+ /**
3847
+ * Residue identity matching `injectPlddtIntoPdb`: chain + sequence number +
3848
+ * insertion code. Prefers the `auth_*` columns Mol* reports on the parsed
3849
+ * model, falling back to `label_*` when a file omitted them. `.` / `?`
3850
+ * insertion codes count as empty, the way a blank PDB column 27 does.
3851
+ */
3852
+ function mmcifResidueKey(tokens, columns) {
3853
+ var _ref, _ref2, _ref3, _ref4;
3854
+ const authAsym = columnIndex(columns, "auth_asym_id");
3855
+ const labelAsym = columnIndex(columns, "label_asym_id");
3856
+ const authSeq = columnIndex(columns, "auth_seq_id");
3857
+ const labelSeq = columnIndex(columns, "label_seq_id");
3858
+ const ins = columnIndex(columns, "pdbx_PDB_ins_code");
3859
+ return `${(_ref = (_ref2 = authAsym >= 0 ? tokens[authAsym] : void 0) !== null && _ref2 !== void 0 ? _ref2 : labelAsym >= 0 ? tokens[labelAsym] : void 0) !== null && _ref !== void 0 ? _ref : ""}|${(_ref3 = (_ref4 = authSeq >= 0 ? tokens[authSeq] : void 0) !== null && _ref4 !== void 0 ? _ref4 : labelSeq >= 0 ? tokens[labelSeq] : void 0) !== null && _ref3 !== void 0 ? _ref3 : ""}|${isCifNull(ins >= 0 ? tokens[ins] : void 0) ? "" : tokens[ins]}`;
3860
+ }
3861
+ function isAtomSiteRow(tokens, columns) {
3862
+ const group = columnIndex(columns, "group_PDB");
3863
+ if (group < 0) return true;
3864
+ const value = tokens[group];
3865
+ return value === "ATOM" || value === "HETATM";
3866
+ }
3867
+ function plddtBFactor(residueIndex, plddtValues) {
3868
+ var _plddtValues$residueI2;
3869
+ return (residueIndex < plddtValues.length ? ((_plddtValues$residueI2 = plddtValues[residueIndex]) !== null && _plddtValues$residueI2 !== void 0 ? _plddtValues$residueI2 : 0) * 100 : 50).toFixed(2);
3870
+ }
3871
+ function isLoopStart(line) {
3872
+ return line.trim() === "loop_" || line.trim().startsWith("loop_");
3873
+ }
3874
+ function isAtomSiteColumn(line) {
3875
+ return line.trim().startsWith(ATOM_SITE_PREFIX);
3876
+ }
3877
+ function endsAtomSiteLoop(line) {
3878
+ const trimmed = line.trim();
3879
+ if (trimmed === "") return false;
3880
+ if (trimmed.startsWith("#")) return true;
3881
+ if (isLoopStart(trimmed)) return true;
3882
+ if (trimmed.startsWith("data_") || trimmed.startsWith("save_")) return true;
3883
+ return trimmed.startsWith("_") && !trimmed.startsWith(ATOM_SITE_PREFIX);
3884
+ }
3885
+ function isAtomSiteLoop(lines, loopIndex) {
3886
+ let peek = loopIndex + 1;
3887
+ while (peek < lines.length && lines[peek].trim() === "") peek += 1;
3888
+ return peek < lines.length && isAtomSiteColumn(lines[peek]);
3889
+ }
3890
+ function ensureBFactorColumn(columns, headerLines) {
3891
+ const existing = columnIndex(columns, B_FACTOR_COLUMN);
3892
+ if (existing >= 0) return existing;
3893
+ columns.push(B_FACTOR_COLUMN);
3894
+ headerLines.push(`${ATOM_SITE_PREFIX}${B_FACTOR_COLUMN}`);
3895
+ return columns.length - 1;
3896
+ }
3897
+ function rewriteAtomSiteRow(dataLine, columns, bIndex, plddtValues, walk) {
3898
+ if (dataLine.trim() === "") return dataLine;
3899
+ const tokens = tokenizeCifLine(dataLine);
3900
+ if (tokens.length === 0) return dataLine;
3901
+ while (tokens.length < columns.length) tokens.push(".");
3902
+ if (isAtomSiteRow(tokens, columns)) {
3903
+ const residue = mmcifResidueKey(tokens, columns);
3904
+ if (residue !== walk.lastResidue) {
3905
+ walk.residueIndex += 1;
3906
+ walk.lastResidue = residue;
3907
+ }
3908
+ tokens[bIndex] = plddtBFactor(walk.residueIndex, plddtValues);
3909
+ }
3910
+ return tokens.join(" ");
3911
+ }
3912
+ function rewriteAtomSiteLoop(lines, loopIndex, plddtValues, walk) {
3913
+ const rows = [lines[loopIndex]];
3914
+ let i = loopIndex + 1;
3915
+ const columns = [];
3916
+ while (i < lines.length && isAtomSiteColumn(lines[i])) {
3917
+ columns.push(lines[i].trim().slice(11));
3918
+ rows.push(lines[i]);
3919
+ i += 1;
3920
+ }
3921
+ const bIndex = ensureBFactorColumn(columns, rows);
3922
+ while (i < lines.length && !endsAtomSiteLoop(lines[i])) {
3923
+ rows.push(rewriteAtomSiteRow(lines[i], columns, bIndex, plddtValues, walk));
3924
+ i += 1;
3925
+ }
3926
+ return {
3927
+ next: i,
3928
+ rows
3929
+ };
3930
+ }
3931
+ /**
3932
+ * Rewrites an mmCIF (PDBx) file's `_atom_site.B_iso_or_equiv` column with
3933
+ * pLDDT scores so Mol* can read them off the parsed model, the same way
3934
+ * `injectPlddtIntoPdb` rewrites the PDB B-factor column. Scores arrive on a
3935
+ * 0-1 scale and are stored on the conventional 0-100 one. Residues past the
3936
+ * end of `plddtValues` fall back to a mid-confidence 50.
3937
+ *
3938
+ * If the file has no B-factor column, one is appended to the atom-site loop
3939
+ * rather than leaving the scores behind.
3940
+ */
3941
+ function injectPlddtIntoMmcif(mmcifData, plddtValues) {
3942
+ const lines = mmcifData.split("\n");
3943
+ const result = [];
3944
+ const walk = {
3945
+ lastResidue: "",
3946
+ residueIndex: -1
3947
+ };
3948
+ let i = 0;
3949
+ let rewrittenLoop = false;
3950
+ while (i < lines.length) {
3951
+ const line = lines[i];
3952
+ if (!rewrittenLoop && isLoopStart(line) && isAtomSiteLoop(lines, i)) {
3953
+ const rewritten = rewriteAtomSiteLoop(lines, i, plddtValues, walk);
3954
+ result.push(...rewritten.rows);
3955
+ i = rewritten.next;
3956
+ rewrittenLoop = true;
3957
+ continue;
3958
+ }
3959
+ result.push(line);
3960
+ i += 1;
3961
+ }
3962
+ return result.join("\n");
3963
+ }
3964
+ /**
3965
+ * Rewrites a structure file's B-factors with pLDDT scores. Dispatches to
3966
+ * `injectPlddtIntoPdb` or `injectPlddtIntoMmcif` from `format`, or from
3967
+ * detecting the format of `data` when `format` is omitted.
3968
+ */
3969
+ function injectPlddt(data, plddtValues, format = detectStructureFormat(data)) {
3970
+ return format === "mmcif" ? injectPlddtIntoMmcif(data, plddtValues) : injectPlddtIntoPdb(data, plddtValues);
3971
+ }
2436
3972
  /**
2437
3973
  * Mol* color theme that reads pLDDT scores out of the B-factor column, paired
2438
- * with `injectPlddtIntoPdb`. Registered per plugin instance.
3974
+ * with `injectPlddt`. Registered per plugin instance.
2439
3975
  */
2440
3976
  const PlddtColoring = molstar_lib_mol_model_props_common_custom_element_property.CustomElementProperty.create({
2441
3977
  coloring: {
@@ -2452,31 +3988,7 @@ const PlddtColoring = molstar_lib_mol_model_props_common_custom_element_property
2452
3988
  },
2453
3989
  label: "pLDDT",
2454
3990
  name: PLDDT_THEME_NAME
2455
- });
2456
- //#endregion
2457
- //#region src/core/ProteinStructureViewer/utils/color.ts
2458
- /** Parses `#RRGGBB` (or `RRGGBB`) into a Mol* `Color`. */
2459
- function parseHexColor(hex) {
2460
- const h = hex.replace("#", "");
2461
- 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));
2462
- }
2463
- /**
2464
- * Parses a theme color into a Mol* `Color`, falling back when the token is
2465
- * missing or is a format Mol* cannot consume (theme values are hex in practice,
2466
- * but the palette types allow any CSS color string).
2467
- */
2468
- function themeColor(value, fallback) {
2469
- const candidate = value === null || value === void 0 ? void 0 : value.trim();
2470
- return parseHexColor(candidate !== void 0 && /^#?[0-9a-f]{6}$/i.test(candidate) ? candidate : fallback);
2471
- }
2472
- /**
2473
- * Neutral fill for residues with no overlay value. Sits between the light and
2474
- * dark backgrounds so unhighlighted geometry stays legible in both modes.
2475
- */
2476
- function neutralResidueColor(mode) {
2477
- const g = mode === "dark" ? 100 : 180;
2478
- return molstar_lib_mol_util_color.Color.fromRgb(g, g, g);
2479
- }
3991
+ });
2480
3992
  //#endregion
2481
3993
  //#region src/core/ProteinStructureViewer/utils/residueValueTheme.ts
2482
3994
  /** Mol* color theme name under which residue value overlays are registered. */
@@ -2535,10 +4047,13 @@ function createResidueValueTheme(mode) {
2535
4047
  }
2536
4048
  //#endregion
2537
4049
  //#region src/core/ProteinStructureViewer/hooks/useMolstarPlugin.ts
2538
- /** Structure representation Mol* builds for the loaded model. */
2539
- const REPRESENTATION_PRESET = "polymer-cartoon";
2540
- /** Theme used when no pLDDT scores are supplied. */
2541
- const FALLBACK_THEME_NAME = "chain-id";
4050
+ /**
4051
+ * Theme used when neither pLDDT scores nor an overlay are supplied. Our own
4052
+ * per-chain theme rather than Mol*'s built-in `chain-id`, so the chain legend
4053
+ * can show swatches that match the structure instead of guessing at the colors
4054
+ * Mol* picked.
4055
+ */
4056
+ const FALLBACK_THEME_NAME = CHAIN_COLOR_THEME_NAME;
2542
4057
  /** Delay before retrying initialization while the container has no size. */
2543
4058
  const LAYOUT_RETRY_MS = 100;
2544
4059
  /** How long to wait for a frame before starting without one. */
@@ -2575,38 +4090,6 @@ function nextFrame() {
2575
4090
  */
2576
4091
  const HIGHLIGHT_STRENGTH = .2;
2577
4092
  const SELECT_STRENGTH = 0;
2578
- const AXES_ON = {
2579
- name: "on",
2580
- params: {
2581
- alpha: .51,
2582
- colorX: 16711680,
2583
- colorY: 32768,
2584
- colorZ: 255,
2585
- labelColorX: 8421504,
2586
- labelColorY: 8421504,
2587
- labelColorZ: 8421504,
2588
- labelOpacity: 1,
2589
- labelScale: .25,
2590
- labelX: "X",
2591
- labelY: "Y",
2592
- labelZ: "Z",
2593
- location: "bottom-left",
2594
- locationOffsetX: 0,
2595
- locationOffsetY: 0,
2596
- originColor: 8421504,
2597
- planeColorXY: 8421504,
2598
- planeColorXZ: 8421504,
2599
- planeColorYZ: 8421504,
2600
- radiusScale: .075,
2601
- scale: .15,
2602
- showLabels: false,
2603
- showPlanes: true
2604
- }
2605
- };
2606
- const AXES_OFF = {
2607
- name: "off",
2608
- params: {}
2609
- };
2610
4093
  /**
2611
4094
  * Turns the screenshot controls off, on the versions of Mol* that have them.
2612
4095
  *
@@ -2632,13 +4115,13 @@ function createViewer(_x) {
2632
4115
  return _createViewer.apply(this, arguments);
2633
4116
  }
2634
4117
  function _createViewer() {
2635
- _createViewer = _asyncToGenerator(function* ({ backgroundColor, edgeColor, highlightColor, residueValueTheme, root, showAxes, showSequenceViewer, viewSettings }) {
4118
+ _createViewer = _asyncToGenerator(function* ({ backgroundColor, chainColorTheme, edgeColor, highlightColor, molstarSpec, residueValueTheme, root, showAxes, showSequenceViewer, viewSettings }) {
2636
4119
  var _spec$config;
2637
4120
  const spec = (0, molstar_lib_mol_plugin_ui_spec.DefaultPluginUISpec)();
2638
4121
  const behaviors = spec.behaviors.filter((b) => b.transformer !== molstar_lib_mol_plugin_behavior.PluginBehaviors.Camera.FocusLoci);
2639
4122
  const plugin = yield (0, molstar_lib_mol_plugin_ui.createPluginUI)({
2640
4123
  render: molstar_lib_mol_plugin_ui_react18.renderReact18,
2641
- spec: _objectSpread2(_objectSpread2({}, spec), {}, {
4124
+ spec: mergeMolstarSpec(_objectSpread2(_objectSpread2({}, spec), {}, {
2642
4125
  behaviors,
2643
4126
  canvas3d: {
2644
4127
  camera: { helper: { axes: showAxes ? AXES_ON : AXES_OFF } },
@@ -2654,10 +4137,16 @@ function _createViewer() {
2654
4137
  selectStrength: SELECT_STRENGTH
2655
4138
  }
2656
4139
  },
2657
- components: _objectSpread2({
4140
+ components: {
4141
+ controls: {
4142
+ bottom: "none",
4143
+ left: "none",
4144
+ right: "none"
4145
+ },
2658
4146
  remoteState: "none",
2659
- sequenceViewer: { view: createSequenceView(viewSettings) }
2660
- }, showAxes && { viewport: { view: createViewportView(viewSettings) } }),
4147
+ sequenceViewer: { view: createSequenceView(viewSettings) },
4148
+ viewport: { view: createViewportView(viewSettings) }
4149
+ },
2661
4150
  config: [
2662
4151
  ...(_spec$config = spec.config) !== null && _spec$config !== void 0 ? _spec$config : [],
2663
4152
  [molstar_lib_mol_plugin_config.PluginConfig.Viewport.ShowExpand, false],
@@ -2678,12 +4167,13 @@ function _createViewer() {
2678
4167
  top: showSequenceViewer ? "full" : "hidden"
2679
4168
  }
2680
4169
  } }
2681
- }),
4170
+ }), molstarSpec),
2682
4171
  target: root
2683
4172
  });
2684
4173
  const registry = plugin.representation.structure.themes.colorThemeRegistry;
2685
4174
  if (PlddtColoring.colorThemeProvider) registry.add(PlddtColoring.colorThemeProvider);
2686
4175
  registry.add(residueValueTheme.provider);
4176
+ registry.add(chainColorTheme.provider);
2687
4177
  for (const p of [...plugin.managers.lociLabels.providers]) plugin.managers.lociLabels.removeProvider(p);
2688
4178
  plugin.managers.structure.focus.behaviors.current.subscribe((entry) => {
2689
4179
  const select = plugin.managers.interactivity.lociSelects;
@@ -2694,44 +4184,190 @@ function _createViewer() {
2694
4184
  });
2695
4185
  return _createViewer.apply(this, arguments);
2696
4186
  }
2697
- function loadStructure(_x2, _x3, _x4, _x5) {
4187
+ /**
4188
+ * The representation parameters Mol*'s own presets compute, read from the same
4189
+ * manager state they read. Ported rather than hardcoded so a chain drawn here
4190
+ * looks exactly like the one the preset used to draw. Mol* hands the same set
4191
+ * to its cartoons and its ball-and-sticks, and so does this.
4192
+ */
4193
+ function representationTypeParams(plugin) {
4194
+ const { hydrogens, ignoreLight, visualQuality } = plugin.managers.structure.component.state.options;
4195
+ return {
4196
+ ignoreHydrogens: hydrogens !== "all",
4197
+ ignoreHydrogensVariant: hydrogens === "only-polar" ? "non-polar" : "all",
4198
+ ignoreLight,
4199
+ quality: visualQuality
4200
+ };
4201
+ }
4202
+ /**
4203
+ * The two halves of a chain the viewer draws. Each names the component its
4204
+ * half is built under and tags the representation drawn over it, and it is
4205
+ * that tag `applyColorTheme` reads back to tell the two apart.
4206
+ */
4207
+ const POLYMER_PART = "polymer";
4208
+ const LIGAND_PART = "ligand";
4209
+ /**
4210
+ * True for the representation drawing a chain's heteroatoms.
4211
+ *
4212
+ * Read off the tag put on the representation when it was built, rather than
4213
+ * off the component's key. Mol* does not store the key as it was given: it
4214
+ * files components under `structure-component-<key>`, so a test against the
4215
+ * key as written here silently matches nothing, and every ligand quietly takes
4216
+ * the structure-wide theme instead. The tag survives untouched.
4217
+ */
4218
+ function isLigandRepresentation(representation) {
4219
+ var _representation$cell$;
4220
+ return ((_representation$cell$ = representation.cell.transform.tags) === null || _representation$cell$ === void 0 ? void 0 : _representation$cell$.includes(LIGAND_PART)) === true;
4221
+ }
4222
+ /**
4223
+ * How heteroatoms are colored: the theme Mol* puts on a ball-and-stick when
4224
+ * left to itself, with the parameters it chooses too.
4225
+ *
4226
+ * Named rather than left blank only so that it survives `applyColorTheme`,
4227
+ * which would otherwise repaint the ligands along with everything else. The
4228
+ * structure-wide theme is the wrong answer for them twice over: a heme reads
4229
+ * as a heme because its iron is orange and its nitrogens are blue, which one
4230
+ * flat color throws away, and pLDDT and residue overlays have no value for a
4231
+ * HETATM to be colored by at all.
4232
+ */
4233
+ const ELEMENT_THEME = "element-symbol";
4234
+ /**
4235
+ * Draws one chain: its polymer as a cartoon, its ligands and ions as
4236
+ * ball-and-stick.
4237
+ *
4238
+ * Two components rather than one, because a component is what a representation
4239
+ * attaches to and neither representation suits both halves - a cartoon has no
4240
+ * backbone to trace through a heme, and a ball-and-stick over the whole chain
4241
+ * would draw every protein atom as a sphere.
4242
+ *
4243
+ * Hands back the refs of whichever components the chain turned out to have,
4244
+ * which is what `setSubtreeVisibility` is later pointed at. Grouping them
4245
+ * under the chain is what makes hiding a chain take its ligands with it.
4246
+ */
4247
+ function buildChainComponents(_x5, _x6, _x7, _x8) {
4248
+ return _buildChainComponents.apply(this, arguments);
4249
+ }
4250
+ function _buildChainComponents() {
4251
+ _buildChainComponents = _asyncToGenerator(function* (plugin, structure, chain, typeParams) {
4252
+ const builders = plugin.builders.structure;
4253
+ const refs = [];
4254
+ const addPart = function() {
4255
+ var _ref = _asyncToGenerator(function* (part, expression, props) {
4256
+ const component = yield builders.tryCreateComponentFromExpression(structure, expression, `${part}-${chain.chainId}`, { label: chain.label });
4257
+ if (!component) return;
4258
+ yield builders.representation.addRepresentation(component, props, { tag: part });
4259
+ refs.push(component.ref);
4260
+ });
4261
+ return function addPart(_x2, _x3, _x4) {
4262
+ return _ref.apply(this, arguments);
4263
+ };
4264
+ }();
4265
+ yield addPart(POLYMER_PART, chainPolymerExpression(chain.chainId), {
4266
+ type: "cartoon",
4267
+ typeParams
4268
+ });
4269
+ yield addPart(LIGAND_PART, chainLigandExpression(chain.chainId), {
4270
+ color: ELEMENT_THEME,
4271
+ type: "ball-and-stick",
4272
+ typeParams
4273
+ });
4274
+ return refs;
4275
+ });
4276
+ return _buildChainComponents.apply(this, arguments);
4277
+ }
4278
+ const NOTHING_LOADED = {
4279
+ chains: [],
4280
+ componentRefs: /* @__PURE__ */ new Map(),
4281
+ residuesByChain: /* @__PURE__ */ new Map()
4282
+ };
4283
+ /**
4284
+ * Parses the structure and draws each chain: a cartoon over its polymer, and
4285
+ * ball-and-stick over its ligands and ions.
4286
+ *
4287
+ * Mol*'s `default` hierarchy preset would be shorter, but it groups every
4288
+ * polymer chain into a single component, and a component is the unit Mol* can
4289
+ * hide. Building them per chain is what makes a chain individually hideable;
4290
+ * the refs handed back are what `setSubtreeVisibility` is later pointed at.
4291
+ *
4292
+ * The wrapper detects PDB vs mmCIF from the text; this just forwards that
4293
+ * guess to Mol*.
4294
+ */
4295
+ function loadStructure(_x9, _x10, _x11, _x12) {
2698
4296
  return _loadStructure.apply(this, arguments);
2699
4297
  }
2700
4298
  function _loadStructure() {
2701
- _loadStructure = _asyncToGenerator(function* (plugin, pdbData, usePlddtColoring, showAxes) {
4299
+ _loadStructure = _asyncToGenerator(function* (plugin, structureText, usePlddtColoring, showAxes) {
2702
4300
  try {
2703
4301
  var _plugin$canvas3d;
2704
4302
  yield plugin.clear();
2705
4303
  const data = yield plugin.builders.data.rawData({
2706
- data: pdbData,
4304
+ data: structureText,
2707
4305
  label: "Structure"
2708
4306
  });
2709
- const trajectory = yield plugin.builders.structure.parseTrajectory(data, "pdb");
2710
- yield plugin.builders.structure.hierarchy.applyPreset(trajectory, "default", {
2711
- representationPreset: REPRESENTATION_PRESET,
2712
- showUnitcell: false,
2713
- structure: {
2714
- name: "model",
2715
- params: {}
2716
- }
4307
+ const trajectory = yield plugin.builders.structure.parseTrajectory(data, detectStructureFormat(structureText));
4308
+ const model = yield plugin.builders.structure.createModel(trajectory);
4309
+ const structure = yield plugin.builders.structure.createStructure(model, {
4310
+ name: "model",
4311
+ params: {}
2717
4312
  });
4313
+ const data3d = structure.data;
4314
+ if (!data3d) return NOTHING_LOADED;
4315
+ const { chainLabels, chains, residuesByChain } = scanChains(data3d);
4316
+ const typeParams = representationTypeParams(plugin);
4317
+ const componentRefs = /* @__PURE__ */ new Map();
4318
+ for (const [chainId, label] of chainLabels) {
4319
+ const refs = yield buildChainComponents(plugin, structure, {
4320
+ chainId,
4321
+ label
4322
+ }, typeParams);
4323
+ if (refs.length > 0) componentRefs.set(chainId, refs);
4324
+ }
2718
4325
  if (usePlddtColoring) yield applyColorTheme(plugin, PLDDT_THEME_NAME);
2719
4326
  (_plugin$canvas3d = plugin.canvas3d) === null || _plugin$canvas3d === void 0 || _plugin$canvas3d.requestCameraReset();
2720
4327
  setAxes(plugin, showAxes);
4328
+ return {
4329
+ chains,
4330
+ componentRefs,
4331
+ residuesByChain
4332
+ };
2721
4333
  } catch (error) {
2722
4334
  console.error("Failed to load structure:", error);
4335
+ return NOTHING_LOADED;
2723
4336
  }
2724
4337
  });
2725
4338
  return _loadStructure.apply(this, arguments);
2726
4339
  }
2727
- /** Recolors every loaded structure's representations with the named theme. */
2728
- function applyColorTheme(_x6, _x7) {
4340
+ /**
4341
+ * Shows or hides everything each chain is drawn with, cartoon and sticks
4342
+ * alike. Every component under the chain, so hiding it takes its ligands with
4343
+ * it rather than leaving a heme floating where its protein used to be.
4344
+ *
4345
+ * The whole subtree of each, not just the component cell: visibility is
4346
+ * per-cell, and it is the representation underneath that actually draws.
4347
+ *
4348
+ * Set outright rather than through the component manager's `toggleVisibility`,
4349
+ * which flips whatever it finds: the hidden set is derived from props on every
4350
+ * pass, so it has to be applied as a statement of what should be true and not
4351
+ * as a change to what already is.
4352
+ */
4353
+ function applyChainVisibility(plugin, componentRefs, hidden) {
4354
+ 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));
4355
+ }
4356
+ /**
4357
+ * Recolors every loaded structure's representations with the named theme,
4358
+ * leaving the heteroatoms on `ELEMENT_THEME`.
4359
+ *
4360
+ * The per-representation form of `updateRepresentationsTheme`, so the two can
4361
+ * be told apart in one pass; handing it a filtered list instead would leave
4362
+ * the ligands on whichever theme happened to be current when they were built.
4363
+ */
4364
+ function applyColorTheme(_x13, _x14) {
2729
4365
  return _applyColorTheme.apply(this, arguments);
2730
4366
  }
2731
4367
  function _applyColorTheme() {
2732
4368
  _applyColorTheme = _asyncToGenerator(function* (plugin, colorTheme) {
2733
4369
  yield plugin.dataTransaction(_asyncToGenerator(function* () {
2734
- for (const s of plugin.managers.structure.hierarchy.current.structures) yield plugin.managers.structure.component.updateRepresentationsTheme(s.components, { color: colorTheme });
4370
+ 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 }));
2735
4371
  }));
2736
4372
  });
2737
4373
  return _applyColorTheme.apply(this, arguments);
@@ -2742,14 +4378,39 @@ function _applyColorTheme() {
2742
4378
  *
2743
4379
  * The plugin is built once and then mutated in place. Creating it is expensive
2744
4380
  * and destroys the camera, so prop changes that Mol* can absorb (background,
2745
- * axes, coloring, new PDB) are pushed in through the effects below and in the
2746
- * sibling hooks rather than by rebuilding.
4381
+ * axes, coloring, new structure) are pushed in through the effects below and in
4382
+ * the sibling hooks rather than by rebuilding.
2747
4383
  */
2748
- function useMolstarPlugin({ backgroundColor, containerRef, edgeColor, hasPlddt, highlightColor, mode, onResidueClick, onResidueHover, onSelectionClear, pdb, sequenceViewerBackgroundColor, showAxes, showSequenceViewer }) {
4384
+ function useMolstarPlugin({ backgroundColor, chainColors, containerRef, download, edgeColor, hasPlddt, hiddenChains, highlightColor, mode, molstarSpec, onChainHover, onChainSelect, onChainToggle, onResidueClick, onResidueHover, onSelectionChange, onSelectionClear, selectedChains, sequenceViewerBackgroundColor, showAxes, showSequenceViewer, structure }) {
2749
4385
  const pluginRef = (0, react.useRef)(null);
2750
4386
  const residueValueThemeRef = (0, react.useRef)(null);
2751
- const currentPdbRef = (0, react.useRef)(null);
4387
+ const chainColorThemeRef = (0, react.useRef)(null);
4388
+ const currentStructureRef = (0, react.useRef)(null);
2752
4389
  const [isReady, setIsReady] = (0, react.useState)(false);
4390
+ const [chains, setChains] = (0, react.useState)([]);
4391
+ const [loadCount, setLoadCount] = (0, react.useState)(0);
4392
+ /**
4393
+ * Where each chain's components live in the state tree, which is what the
4394
+ * visibility effect points `setSubtreeVisibility` at. Rewritten by every
4395
+ * load, since the refs do not survive `plugin.clear()`.
4396
+ */
4397
+ const componentRefsRef = (0, react.useRef)(/* @__PURE__ */ new Map());
4398
+ /**
4399
+ * Which residues sit on each chain, for the readout to average a score over
4400
+ * a whole-chain selection. A ref rather than state: nothing renders from it
4401
+ * directly, and it is rewritten in step with `chains`, which does.
4402
+ */
4403
+ const residuesByChainRef = (0, react.useRef)(/* @__PURE__ */ new Map());
4404
+ /**
4405
+ * The visibility and coloring to apply as soon as a structure exists. Read
4406
+ * through refs because both arrive as fresh objects on every render, and a
4407
+ * load has to be able to consult them without the load effect depending on
4408
+ * them - a new Set each render would reload the structure on every pass.
4409
+ */
4410
+ const hiddenChainsRef = (0, react.useRef)(hiddenChains);
4411
+ hiddenChainsRef.current = hiddenChains;
4412
+ const chainColorsRef = (0, react.useRef)(chainColors);
4413
+ chainColorsRef.current = chainColors;
2753
4414
  /**
2754
4415
  * Carries the theme-dependent props into the views Mol* renders in its own
2755
4416
  * React root. They cannot read them from context, and rebuilding the plugin
@@ -2758,7 +4419,8 @@ function useMolstarPlugin({ backgroundColor, containerRef, edgeColor, hasPlddt,
2758
4419
  const viewSettingsRef = (0, react.useRef)(null);
2759
4420
  if (viewSettingsRef.current === null) viewSettingsRef.current = new rxjs.BehaviorSubject({
2760
4421
  mode,
2761
- sequenceViewerBackgroundColor
4422
+ sequenceViewerBackgroundColor,
4423
+ showAxes
2762
4424
  });
2763
4425
  const viewSettings = viewSettingsRef.current;
2764
4426
  /**
@@ -2769,10 +4431,28 @@ function useMolstarPlugin({ backgroundColor, containerRef, edgeColor, hasPlddt,
2769
4431
  const setClipRatio = (0, react.useCallback)((ratio) => {
2770
4432
  clipRatioRef.current = ratio;
2771
4433
  }, []);
4434
+ /**
4435
+ * Takes up what a load produced: the component refs the chain-keyed props
4436
+ * address, the visibility they were already asking for, and the chain list
4437
+ * the consumer is told about.
4438
+ *
4439
+ * Chains are published only when they actually differ, so reloading the same
4440
+ * structure - a theme switch that happens to reload, a parent handing back an
4441
+ * equal PDB string - does not hand every consumer a new array to react to.
4442
+ */
4443
+ const adoptLoadedStructure = (0, react.useCallback)((plugin, loaded) => {
4444
+ componentRefsRef.current = loaded.componentRefs;
4445
+ residuesByChainRef.current = loaded.residuesByChain;
4446
+ applyChainVisibility(plugin, loaded.componentRefs, hiddenChainsRef.current);
4447
+ setChains((prev) => chainsEqual(prev, loaded.chains) ? prev : loaded.chains);
4448
+ setLoadCount((count) => count + 1);
4449
+ }, []);
2772
4450
  const onResidueClickRef = (0, react.useRef)(onResidueClick);
2773
4451
  onResidueClickRef.current = onResidueClick;
2774
4452
  const onSelectionClearRef = (0, react.useRef)(onSelectionClear);
2775
4453
  onSelectionClearRef.current = onSelectionClear;
4454
+ const onSelectionChangeRef = (0, react.useRef)(onSelectionChange);
4455
+ onSelectionChangeRef.current = onSelectionChange;
2776
4456
  const onResidueHoverRef = (0, react.useRef)(onResidueHover);
2777
4457
  onResidueHoverRef.current = onResidueHover;
2778
4458
  /** Last residue reported to `onResidueHover`, to suppress repeats. */
@@ -2783,9 +4463,10 @@ function useMolstarPlugin({ backgroundColor, containerRef, edgeColor, hasPlddt,
2783
4463
  hasPlddt,
2784
4464
  highlightColor,
2785
4465
  mode,
2786
- pdb,
4466
+ molstarSpec,
2787
4467
  showAxes,
2788
- showSequenceViewer
4468
+ showSequenceViewer,
4469
+ structure
2789
4470
  });
2790
4471
  initialPropsRef.current = {
2791
4472
  backgroundColor,
@@ -2793,17 +4474,32 @@ function useMolstarPlugin({ backgroundColor, containerRef, edgeColor, hasPlddt,
2793
4474
  hasPlddt,
2794
4475
  highlightColor,
2795
4476
  mode,
2796
- pdb,
4477
+ molstarSpec,
2797
4478
  showAxes,
2798
- showSequenceViewer
4479
+ showSequenceViewer,
4480
+ structure
2799
4481
  };
4482
+ /**
4483
+ * The consumer's `canvas3d` overrides, keyed by content. Unlike the rest of
4484
+ * the spec these are re-applied when they change, and they commonly arrive
4485
+ * as an object written inline - so the effect below turns on what they say
4486
+ * rather than on the identity of the object saying it.
4487
+ */
4488
+ const canvas3dOverrideKey = (() => {
4489
+ try {
4490
+ var _molstarSpec$canvas3d;
4491
+ return JSON.stringify((_molstarSpec$canvas3d = molstarSpec === null || molstarSpec === void 0 ? void 0 : molstarSpec.canvas3d) !== null && _molstarSpec$canvas3d !== void 0 ? _molstarSpec$canvas3d : null);
4492
+ } catch (_unused) {
4493
+ return String(Date.now());
4494
+ }
4495
+ })();
2800
4496
  (0, react.useEffect)(() => {
2801
4497
  const container = containerRef.current;
2802
4498
  if (!container) return;
2803
4499
  let cancelled = false;
2804
4500
  let clipSubscription;
2805
4501
  const init = function() {
2806
- var _ref = _asyncToGenerator(function* () {
4502
+ var _ref2 = _asyncToGenerator(function* () {
2807
4503
  yield nextFrame();
2808
4504
  if (cancelled) return;
2809
4505
  const rect = container.getBoundingClientRect();
@@ -2815,10 +4511,13 @@ function useMolstarPlugin({ backgroundColor, containerRef, edgeColor, hasPlddt,
2815
4511
  try {
2816
4512
  var _plugin$canvas3d2;
2817
4513
  const residueValueTheme = createResidueValueTheme(initial.mode);
4514
+ const chainColorTheme = createChainColorTheme();
2818
4515
  const plugin = yield createViewer({
2819
4516
  backgroundColor: initial.backgroundColor,
4517
+ chainColorTheme,
2820
4518
  edgeColor: initial.edgeColor,
2821
4519
  highlightColor: initial.highlightColor,
4520
+ molstarSpec: initial.molstarSpec,
2822
4521
  residueValueTheme,
2823
4522
  root: container,
2824
4523
  showAxes: initial.showAxes,
@@ -2831,11 +4530,28 @@ function useMolstarPlugin({ backgroundColor, containerRef, edgeColor, hasPlddt,
2831
4530
  }
2832
4531
  pluginRef.current = plugin;
2833
4532
  residueValueThemeRef.current = residueValueTheme;
4533
+ chainColorThemeRef.current = chainColorTheme;
2834
4534
  const latest = initialPropsRef.current;
2835
- yield loadStructure(plugin, latest.pdb, latest.hasPlddt, latest.showAxes);
2836
- currentPdbRef.current = latest.pdb;
4535
+ const loaded = yield loadStructure(plugin, latest.structure, latest.hasPlddt, latest.showAxes);
4536
+ currentStructureRef.current = latest.structure;
4537
+ adoptLoadedStructure(plugin, loaded);
4538
+ /**
4539
+ * Mol*'s click behavior is a BehaviorSubject, so subscribing replays
4540
+ * whatever it is currently holding - which, before anything has been
4541
+ * clicked, is an empty click. Acting on that would read as the user
4542
+ * clicking empty space and clear a selection the consumer had set
4543
+ * before the viewer was even interactive.
4544
+ *
4545
+ * Only the replay is skipped, not every empty click: clicking away
4546
+ * from the structure is how a selection is meant to be cleared.
4547
+ */
4548
+ let clickReplayConsumed = false;
2837
4549
  plugin.behaviors.interaction.click.subscribe((e) => {
2838
- var _onResidueClickRef$cu;
4550
+ var _onResidueClickRef$cu, _onSelectionChangeRef;
4551
+ if (!clickReplayConsumed) {
4552
+ clickReplayConsumed = true;
4553
+ if (molstar_lib_mol_repr_representation.Representation.Loci.isEmpty(e.current)) return;
4554
+ }
2839
4555
  if (molstar_lib_mol_repr_representation.Representation.Loci.isEmpty(e.current)) {
2840
4556
  var _onSelectionClearRef$;
2841
4557
  (_onSelectionClearRef$ = onSelectionClearRef.current) === null || _onSelectionClearRef$ === void 0 || _onSelectionClearRef$.call(onSelectionClearRef);
@@ -2846,6 +4562,7 @@ function useMolstarPlugin({ backgroundColor, containerRef, edgeColor, hasPlddt,
2846
4562
  const residue = residueRefFromLoci(loci);
2847
4563
  if (!residue) return;
2848
4564
  (_onResidueClickRef$cu = onResidueClickRef.current) === null || _onResidueClickRef$cu === void 0 || _onResidueClickRef$cu.call(onResidueClickRef, residue);
4565
+ (_onSelectionChangeRef = onSelectionChangeRef.current) === null || _onSelectionChangeRef === void 0 || _onSelectionChangeRef.call(onSelectionChangeRef, selectionFromLoci(loci));
2849
4566
  });
2850
4567
  plugin.behaviors.interaction.hover.subscribe((e) => {
2851
4568
  var _lastHoverRef$current, _onResidueHoverRef$cu;
@@ -2864,7 +4581,7 @@ function useMolstarPlugin({ backgroundColor, containerRef, edgeColor, hasPlddt,
2864
4581
  }
2865
4582
  });
2866
4583
  return function init() {
2867
- return _ref.apply(this, arguments);
4584
+ return _ref2.apply(this, arguments);
2868
4585
  };
2869
4586
  }();
2870
4587
  init();
@@ -2875,13 +4592,16 @@ function useMolstarPlugin({ backgroundColor, containerRef, edgeColor, hasPlddt,
2875
4592
  (_pluginRef$current = pluginRef.current) === null || _pluginRef$current === void 0 || _pluginRef$current.dispose();
2876
4593
  pluginRef.current = null;
2877
4594
  residueValueThemeRef.current = null;
2878
- currentPdbRef.current = null;
4595
+ chainColorThemeRef.current = null;
4596
+ componentRefsRef.current = /* @__PURE__ */ new Map();
4597
+ residuesByChainRef.current = /* @__PURE__ */ new Map();
4598
+ currentStructureRef.current = null;
2879
4599
  clipRatioRef.current = null;
2880
4600
  setIsReady(false);
2881
4601
  };
2882
4602
  }, []);
2883
4603
  (0, react.useEffect)(() => {
2884
- var _pluginRef$current2;
4604
+ var _pluginRef$current2, _initialPropsRef$curr;
2885
4605
  const canvas3d = (_pluginRef$current2 = pluginRef.current) === null || _pluginRef$current2 === void 0 ? void 0 : _pluginRef$current2.canvas3d;
2886
4606
  if (!canvas3d || !isReady) return;
2887
4607
  canvas3d.setProps({
@@ -2894,20 +4614,37 @@ function useMolstarPlugin({ backgroundColor, containerRef, edgeColor, hasPlddt,
2894
4614
  highlightColor
2895
4615
  }
2896
4616
  });
4617
+ const override = (_initialPropsRef$curr = initialPropsRef.current.molstarSpec) === null || _initialPropsRef$curr === void 0 ? void 0 : _initialPropsRef$curr.canvas3d;
4618
+ if (override) canvas3d.setProps(override);
2897
4619
  }, [
2898
4620
  backgroundColor,
4621
+ canvas3dOverrideKey,
2899
4622
  edgeColor,
2900
4623
  highlightColor,
2901
4624
  isReady
2902
4625
  ]);
2903
4626
  (0, react.useEffect)(() => {
2904
4627
  viewSettings.next({
4628
+ download,
4629
+ hiddenChains,
2905
4630
  mode,
2906
- sequenceViewerBackgroundColor
4631
+ onChainHover,
4632
+ onChainSelect,
4633
+ onChainToggle,
4634
+ selectedChains,
4635
+ sequenceViewerBackgroundColor,
4636
+ showAxes
2907
4637
  });
2908
4638
  }, [
4639
+ download,
4640
+ hiddenChains,
2909
4641
  mode,
4642
+ onChainHover,
4643
+ onChainSelect,
4644
+ onChainToggle,
4645
+ selectedChains,
2910
4646
  sequenceViewerBackgroundColor,
4647
+ showAxes,
2911
4648
  viewSettings
2912
4649
  ]);
2913
4650
  (0, react.useEffect)(() => {
@@ -2917,124 +4654,333 @@ function useMolstarPlugin({ backgroundColor, containerRef, edgeColor, hasPlddt,
2917
4654
  (0, react.useEffect)(() => {
2918
4655
  const plugin = pluginRef.current;
2919
4656
  if (!plugin || !isReady) return;
2920
- if (pdb === currentPdbRef.current) return;
2921
- currentPdbRef.current = pdb;
4657
+ if (structure === currentStructureRef.current) return;
4658
+ currentStructureRef.current = structure;
2922
4659
  clipRatioRef.current = null;
2923
4660
  lastHoverRef.current = null;
2924
- loadStructure(plugin, pdb, hasPlddt, showAxes);
4661
+ loadStructure(plugin, structure, hasPlddt, showAxes).then((loaded) => {
4662
+ if (pluginRef.current !== plugin) return;
4663
+ adoptLoadedStructure(plugin, loaded);
4664
+ });
2925
4665
  }, [
2926
- pdb,
4666
+ structure,
2927
4667
  hasPlddt,
2928
- isReady
4668
+ isReady,
4669
+ adoptLoadedStructure
2929
4670
  ]);
4671
+ (0, react.useEffect)(() => {
4672
+ const plugin = pluginRef.current;
4673
+ if (!plugin || !isReady) return;
4674
+ applyChainVisibility(plugin, componentRefsRef.current, hiddenChains);
4675
+ }, [hiddenChains, isReady]);
2930
4676
  return {
4677
+ chainColorThemeRef,
4678
+ chains,
2931
4679
  isReady,
4680
+ loadCount,
2932
4681
  pluginRef,
4682
+ residuesByChainRef,
2933
4683
  residueValueThemeRef,
2934
4684
  setClipRatio
2935
4685
  };
2936
4686
  }
2937
4687
  //#endregion
2938
- //#region src/core/ProteinStructureViewer/utils/residueLoci.ts
4688
+ //#region src/core/ProteinStructureViewer/utils/focusRepresentation.ts
4689
+ /**
4690
+ * What Mol* draws around a focused selection, as ball-and-stick over the
4691
+ * cartoon: the selection itself, the residues near it, and the non-covalent
4692
+ * contacts between the two. All three stay on; what changes is how far "near"
4693
+ * reaches.
4694
+ */
4695
+ const FOCUS_COMPONENTS = [
4696
+ "target",
4697
+ "surroundings",
4698
+ "interactions"
4699
+ ];
4700
+ /** Mol*'s own default, in angstroms. */
4701
+ const NEIGHBOUR_RADIUS = 5;
2939
4702
  /**
2940
- * Every atom of the residue at a 0-based index, as geometry Mol* can focus and
2941
- * frame, or undefined when the structure has no such residue.
4703
+ * Mol* builds the surroundings as `includeSurroundings(target, radius)` with
4704
+ * whole residues, so a radius of zero resolves to the residues of the target
4705
+ * and nothing else.
4706
+ */
4707
+ const OWN_RESIDUES_ONLY = 0;
4708
+ /**
4709
+ * Chooses how far the focus shell reaches around a selection.
2942
4710
  *
2943
- * The inverse of `residueRefFromLoci`, and keyed on the same residue ordinal,
2944
- * so a `selectedResidue` echoed back from a click resolves to the residue that
2945
- * was clicked - on a complex as much as on a single chain.
4711
+ * A residue, or a short range, is a site: the 5A shell around it is the point
4712
+ * of looking at it, and Mol*'s default earns its keep. A whole chain is a
4713
+ * structural unit instead, and its neighbours are the chain bound to it - so
4714
+ * on a complex the shell reaches straight across the interface and draws the
4715
+ * partner's contact face in atoms, which reads as the partner being selected
4716
+ * too.
2946
4717
  *
2947
- * The whole residue is collected rather than one atom, since the caller frames
2948
- * the camera on the loci's bounding sphere and a single atom would zoom far
2949
- * closer than the residue warrants.
4718
+ * Taking the radius to zero confines the shell to the selection rather than
4719
+ * turning it off: the surroundings and the interactions are still drawn, over
4720
+ * the selected chain's own residues, so a chain shows its own atoms and
4721
+ * contacts and no others'.
2950
4722
  */
2951
- function lociForResidueIndex(plugin, residueIndex) {
2952
- for (const entry of plugin.managers.structure.hierarchy.current.structures) {
2953
- var _entry$cell$obj;
2954
- const structure = (_entry$cell$obj = entry.cell.obj) === null || _entry$cell$obj === void 0 ? void 0 : _entry$cell$obj.data;
2955
- if (!structure) continue;
2956
- const location = molstar_lib_mol_model_structure.StructureElement.Location.create(structure);
2957
- for (const unit of structure.units) {
2958
- location.unit = unit;
2959
- const indices = [];
2960
- for (let i = 0; i < unit.elements.length; i++) {
2961
- location.element = unit.elements[i];
2962
- if (molstar_lib_mol_model_structure.StructureProperties.residue.key(location) === residueIndex) indices.push(i);
2963
- }
2964
- if (indices.length > 0) return molstar_lib_mol_model_structure.StructureElement.Loci(structure, [{
2965
- indices: molstar_lib_mol_data_int.OrderedSet.ofSortedArray(indices),
2966
- unit
2967
- }]);
2968
- }
2969
- }
4723
+ function setFocusNeighbourhood(_x, _x2) {
4724
+ return _setFocusNeighbourhood.apply(this, arguments);
4725
+ }
4726
+ function _setFocusNeighbourhood() {
4727
+ _setFocusNeighbourhood = _asyncToGenerator(function* (plugin, includeNeighbours) {
4728
+ const expandRadius = includeNeighbours ? NEIGHBOUR_RADIUS : OWN_RESIDUES_ONLY;
4729
+ yield plugin.state.updateBehavior(molstar_lib_mol_plugin_behavior_dynamic_selection_structure_focus_representation.StructureFocusRepresentation, (params) => {
4730
+ params.components = FOCUS_COMPONENTS;
4731
+ params.expandRadius = expandRadius;
4732
+ });
4733
+ });
4734
+ return _setFocusNeighbourhood.apply(this, arguments);
2970
4735
  }
2971
4736
  //#endregion
2972
- //#region src/core/ProteinStructureViewer/hooks/useResidueFocus.ts
4737
+ //#region src/core/ProteinStructureViewer/hooks/useSelectionFocus.ts
2973
4738
  /**
2974
- * Makes `selectedResidue` the one thing that decides where the camera sits.
4739
+ * Points the camera at a loci and marks it.
2975
4740
  *
2976
- * Setting the prop zooms in on that residue and marks it; clearing it drops the
2977
- * focus and zooms back out. The click subscription only reports the residue it
2978
- * was given, so a click moves the camera only once the consumer accepts it and
2979
- * echoes it back - which is what the prop being controlled is supposed to mean.
4741
+ * A click has already focused what the consumer echoes back by the time it
4742
+ * arrives, so the focus is only set when it is not already what is marked;
4743
+ * setting it again would restate the selection and repaint the sequence panel
4744
+ * for nothing.
4745
+ */
4746
+ function focusLoci(plugin, loci, setClipRatio) {
4747
+ const focused = plugin.managers.structure.focus.behaviors.current.value;
4748
+ if (!focused || !molstar_lib_mol_model_structure.StructureElement.Loci.areEqual(focused.loci, loci)) plugin.managers.structure.focus.setFromLoci(loci);
4749
+ setClipRatio(focusResidue(plugin, loci));
4750
+ }
4751
+ /**
4752
+ * Drops the focus, and the depth clipping anchored to it.
4753
+ *
4754
+ * `resetCamera` separates the two ways this happens. The consumer clearing the
4755
+ * selection means there is nothing to look at, so the view goes back to the
4756
+ * default. A selection that merely resolved to nothing visible - the chain it
4757
+ * covers having been hidden - still stands, so throwing the camera back would
4758
+ * be answering a question nobody asked.
4759
+ */
4760
+ function clearFocus(plugin, setClipRatio, resetCamera) {
4761
+ var _plugin$canvas3d;
4762
+ setClipRatio(null);
4763
+ plugin.managers.structure.focus.clear();
4764
+ if (resetCamera) (_plugin$canvas3d = plugin.canvas3d) === null || _plugin$canvas3d === void 0 || _plugin$canvas3d.requestCameraReset();
4765
+ }
4766
+ /**
4767
+ * Focuses a loci, resizing the focus shell first when this selection wants a
4768
+ * different reach than the last one did.
4769
+ *
4770
+ * The resize is asynchronous and the focus has to follow it - the shell is
4771
+ * sized before focusing so it is not drawn at the old reach and rebuilt - so
4772
+ * this hands back a teardown that abandons the focus if the effect is torn
4773
+ * down while the resize is still in flight.
4774
+ *
4775
+ * `currentShell` is the reach already configured, carried in a ref so that two
4776
+ * selections of the same kind do not each restate it.
4777
+ */
4778
+ function focusWithShell(plugin, loci, includeNeighbours, currentShell, pluginRef, setClipRatio) {
4779
+ if (currentShell.current === includeNeighbours) {
4780
+ focusLoci(plugin, loci, setClipRatio);
4781
+ return;
4782
+ }
4783
+ let cancelled = false;
4784
+ currentShell.current = includeNeighbours;
4785
+ setFocusNeighbourhood(plugin, includeNeighbours).then(() => {
4786
+ if (cancelled || pluginRef.current !== plugin) return;
4787
+ focusLoci(plugin, loci, setClipRatio);
4788
+ });
4789
+ return () => {
4790
+ cancelled = true;
4791
+ };
4792
+ }
4793
+ /**
4794
+ * Makes `selection` the one thing that decides where the camera sits.
4795
+ *
4796
+ * Setting the prop frames whatever it covers; clearing it drops the focus and
4797
+ * zooms back out. The camera frames the selection's bounding sphere, so a range
4798
+ * or a whole chain is fitted rather than approached as a point - one residue is
4799
+ * just the smallest case of that.
2980
4800
  *
2981
- * Clicking still marks the residue immediately, through Mol*'s own focus
2982
- * behavior, so the structure responds to a click whether or not the consumer
2983
- * wires the prop up.
4801
+ * Residues are also marked, which is what draws Mol*'s ball-and-stick and
4802
+ * outline over them. Whole chains are not: those are shown by dimming the
4803
+ * chains around them instead, in `useChainHighlight`.
4804
+ *
4805
+ * The click subscription only reports the selection it was given, so a click
4806
+ * moves the camera only once the consumer accepts it and echoes it back -
4807
+ * which is what the prop being controlled is supposed to mean. Clicking still
4808
+ * marks immediately, through Mol*'s own focus behavior, so the structure
4809
+ * responds to a click whether or not the consumer wires the prop up.
2984
4810
  */
2985
- function useResidueFocus({ isReady, pluginRef, selectedResidue, setClipRatio }) {
4811
+ function useSelectionFocus({ hiddenChains, isReady, pluginRef, selection, setClipRatio }) {
4812
+ var _selection$chains, _selection$residues;
2986
4813
  /**
2987
- * The residue this hook last pointed the camera at. Tracked rather than
2988
- * compared against the previous prop value so that a selection made before
2989
- * the plugin was ready is still applied once it is, and so that a plugin
2990
- * rebuilt underneath us - which comes up focused on nothing - is not assumed
2991
- * to still be holding the old one.
4814
+ * What this hook last pointed the camera at, as a key rather than the object
4815
+ * itself: a consumer echoing a selection back builds a new object each time,
4816
+ * and comparing identity would refocus on every render.
4817
+ *
4818
+ * Tracked here rather than compared against the previous prop so that a
4819
+ * selection made before the plugin was ready is still applied once it is,
4820
+ * and so that a plugin rebuilt underneath us - which comes up focused on
4821
+ * nothing - is not assumed to still be holding the old one.
2992
4822
  */
2993
4823
  const focusedRef = (0, react.useRef)(null);
4824
+ /**
4825
+ * Whether the focus shell currently reaches beyond the selection, so the
4826
+ * behavior is only updated when the answer actually changes rather than on
4827
+ * every selection.
4828
+ */
4829
+ const neighbourhoodRef = (0, react.useRef)(null);
4830
+ /**
4831
+ * What the camera should be pointed at, as a string. Hiding a chain changes
4832
+ * what there is to focus, so it belongs in here alongside the selection
4833
+ * itself - and comparing strings rather than objects is what keeps a consumer
4834
+ * echoing an equal selection back from refocusing on every render.
4835
+ */
4836
+ const focusKey = selection ? `${selectionKey(selection)}#${[...hiddenChains].sort().join(",")}` : null;
4837
+ /**
4838
+ * Whether the selection names chains and nothing else, which is the shape
4839
+ * shown by dimming the chains around it rather than by focusing it.
4840
+ */
4841
+ const chainsOnly = Boolean((selection === null || selection === void 0 || (_selection$chains = selection.chains) === null || _selection$chains === void 0 ? void 0 : _selection$chains.length) && !((_selection$residues = selection.residues) === null || _selection$residues === void 0 ? void 0 : _selection$residues.length));
2994
4842
  (0, react.useEffect)(() => {
4843
+ var _chosen$chains;
2995
4844
  const plugin = pluginRef.current;
2996
4845
  if (!plugin || !isReady) {
2997
4846
  focusedRef.current = null;
4847
+ neighbourhoodRef.current = null;
2998
4848
  return;
2999
4849
  }
3000
- const residue = selectedResidue !== null && selectedResidue !== void 0 ? selectedResidue : null;
3001
- if (residue === focusedRef.current) return;
3002
- focusedRef.current = residue;
3003
- if (residue === null) {
3004
- var _plugin$canvas3d;
3005
- setClipRatio(null);
4850
+ if (focusKey === focusedRef.current) return;
4851
+ focusedRef.current = focusKey;
4852
+ if (focusKey === null) {
4853
+ clearFocus(plugin, setClipRatio, true);
4854
+ return;
4855
+ }
4856
+ const chosen = selection;
4857
+ const loci = lociForSelection(plugin, chosen, hiddenChains);
4858
+ if (!loci) {
4859
+ clearFocus(plugin, setClipRatio, false);
4860
+ return;
4861
+ }
4862
+ if (chainsOnly) {
3006
4863
  plugin.managers.structure.focus.clear();
3007
- (_plugin$canvas3d = plugin.canvas3d) === null || _plugin$canvas3d === void 0 || _plugin$canvas3d.requestCameraReset();
4864
+ frameLoci(plugin, loci);
4865
+ setClipRatio(null);
3008
4866
  return;
3009
4867
  }
3010
- const loci = lociForResidueIndex(plugin, residue);
3011
- if (!loci) return;
3012
- const focused = plugin.managers.structure.focus.behaviors.current.value;
3013
- if (!focused || !molstar_lib_mol_model_structure.StructureElement.Loci.areEqual(focused.loci, loci)) plugin.managers.structure.focus.setFromLoci(loci);
3014
- setClipRatio(focusResidue(plugin, loci));
4868
+ return focusWithShell(plugin, loci, !((_chosen$chains = chosen.chains) === null || _chosen$chains === void 0 ? void 0 : _chosen$chains.length), neighbourhoodRef, pluginRef, setClipRatio);
3015
4869
  }, [
3016
- selectedResidue,
4870
+ focusKey,
3017
4871
  isReady,
3018
4872
  setClipRatio
3019
4873
  ]);
3020
4874
  }
3021
4875
  //#endregion
3022
- //#region src/core/ProteinStructureViewer/hooks/useResidueOverlay.ts
4876
+ //#region src/core/ProteinStructureViewer/hooks/useSelectionReadout.ts
4877
+ /**
4878
+ * Tracks the residue under the pointer, deduplicated, and passes the event on
4879
+ * to the consumer.
4880
+ *
4881
+ * Mol* emits hover events continuously, so a residue the pointer is resting on
4882
+ * would otherwise cost a state update per event. The index alone does not
4883
+ * identify one: loading a new structure into the same plugin renumbers from
4884
+ * zero, so the residue now at an index is not the one that was there before.
4885
+ * Comparing the label as well is enough, because the readout this drives is
4886
+ * built from the label and the index and from nothing else.
4887
+ */
4888
+ function useResidueHoverState(onResidueHover) {
4889
+ const [hoveredResidue, setHoveredResidue] = (0, react.useState)(null);
4890
+ return {
4891
+ handleResidueHover: (0, react.useCallback)((residue) => {
4892
+ setHoveredResidue((prev) => {
4893
+ if (residue === null) return prev === null ? prev : null;
4894
+ const label = residueLabel(residue);
4895
+ return prev !== null && prev.index === residue.index && prev.label === label ? prev : {
4896
+ index: residue.index,
4897
+ label
4898
+ };
4899
+ });
4900
+ onResidueHover === null || onResidueHover === void 0 || onResidueHover(residue);
4901
+ }, [onResidueHover]),
4902
+ hoveredResidue
4903
+ };
4904
+ }
4905
+ /**
4906
+ * Derives the legend's readouts from what is hovered and what is selected.
4907
+ *
4908
+ * The viewer already knows everything they need - the label, the pLDDT, the
4909
+ * overlay value - so they are worked out here rather than asked of the
4910
+ * consumer. A readout covering several residues reports the mean of each,
4911
+ * which is the only reading that survives a range or a whole chain.
4912
+ */
4913
+ function useSelectionReadout({ hoveredResidue, isReady, plddt, pluginRef, residueOverlay, residuesByChainRef, selection }) {
4914
+ const [resolved, setResolved] = (0, react.useState)(null);
4915
+ /**
4916
+ * What the selection covers has to be read off the structure, since a
4917
+ * selection can be made without a click ever naming anything: a lone residue
4918
+ * is named the way the click path names it, so the two readouts cannot
4919
+ * disagree, and a chain is expanded to the residues it actually holds rather
4920
+ * than to the span between its ends.
4921
+ */
4922
+ (0, react.useEffect)(() => {
4923
+ var _residuesByChainRef$c;
4924
+ const plugin = pluginRef.current;
4925
+ if (!plugin || !isReady || isEmptySelection(selection)) {
4926
+ setResolved(null);
4927
+ return;
4928
+ }
4929
+ const chosen = selection;
4930
+ const residues = selectionResidues(chosen, (_residuesByChainRef$c = residuesByChainRef.current) !== null && _residuesByChainRef$c !== void 0 ? _residuesByChainRef$c : /* @__PURE__ */ new Map());
4931
+ if (residues.length === 0) {
4932
+ setResolved(null);
4933
+ return;
4934
+ }
4935
+ if (residues.length > 1) {
4936
+ setResolved({
4937
+ label: selectionLabel(chosen, residues.length),
4938
+ residues
4939
+ });
4940
+ return;
4941
+ }
4942
+ const loci = lociForSelection(plugin, chosen);
4943
+ const residue = loci && residueRefFromLoci(loci);
4944
+ setResolved(residue ? {
4945
+ label: residueLabel(residue),
4946
+ residues
4947
+ } : null);
4948
+ }, [isReady, selection && selectionKey(selection)]);
4949
+ const buildReadout = (0, react.useCallback)((label, residues) => ({
4950
+ label,
4951
+ plddt: meanOverResidues(residues, (i) => plddt === null || plddt === void 0 ? void 0 : plddt[i]),
4952
+ value: meanOverResidues(residues, (i) => residueOverlay === null || residueOverlay === void 0 ? void 0 : residueOverlay.values.get(i)),
4953
+ residueCount: residues.length
4954
+ }), [plddt, residueOverlay]);
4955
+ return {
4956
+ hoveredReadout: (0, react.useMemo)(() => hoveredResidue === null ? null : buildReadout(hoveredResidue.label, [hoveredResidue.index]), [hoveredResidue, buildReadout]),
4957
+ selectedReadout: (0, react.useMemo)(() => resolved === null ? null : buildReadout(resolved.label, resolved.residues), [resolved, buildReadout])
4958
+ };
4959
+ }
4960
+ //#endregion
4961
+ //#region src/core/ProteinStructureViewer/hooks/useStructureColoring.ts
3023
4962
  /**
3024
- * Switches the structure's coloring between the residue value overlay and the
3025
- * default (pLDDT, or chain-id when no scores were supplied).
4963
+ * Decides what is coloring the structure and keeps that theme's data current.
4964
+ *
4965
+ * Three themes compete for the same representations, in a fixed order: a
4966
+ * residue value overlay, then pLDDT scores, then per-chain colors. Only one
4967
+ * can be applied, so the precedence lives here rather than being spread across
4968
+ * the hooks that own each theme's data - and the legend resolves what to
4969
+ * describe the same way, so the two cannot disagree about what is on screen.
3026
4970
  *
3027
4971
  * Recoloring happens in place: the camera is left where it is, so selecting or
3028
4972
  * switching an overlay does not zoom back out to the default view.
3029
4973
  */
3030
- function useResidueOverlay({ hasPlddt, isReady, mode, overlay, pluginRef, residueValueThemeRef }) {
4974
+ function useStructureColoring({ chainColorThemeRef, chainColors, hasPlddt, isReady, mode, overlay, pluginRef, residueValueThemeRef }) {
3031
4975
  (0, react.useEffect)(() => {
3032
4976
  const plugin = pluginRef.current;
3033
- const theme = residueValueThemeRef.current;
3034
- if (!plugin || !theme || !isReady) return;
4977
+ const residueTheme = residueValueThemeRef.current;
4978
+ const chainTheme = chainColorThemeRef.current;
4979
+ if (!plugin || !residueTheme || !chainTheme || !isReady) return;
4980
+ chainTheme.setState(chainColors);
3035
4981
  if (overlay) {
3036
4982
  var _overlay$colorScale, _overlay$min;
3037
- theme.setState({
4983
+ residueTheme.setState({
3038
4984
  colorScale: (_overlay$colorScale = overlay.colorScale) !== null && _overlay$colorScale !== void 0 ? _overlay$colorScale : PLASMA_COLOR_SCALE,
3039
4985
  max: overlay.max,
3040
4986
  min: (_overlay$min = overlay.min) !== null && _overlay$min !== void 0 ? _overlay$min : 0,
@@ -3042,33 +4988,51 @@ function useResidueOverlay({ hasPlddt, isReady, mode, overlay, pluginRef, residu
3042
4988
  values: overlay.values
3043
4989
  });
3044
4990
  applyColorTheme(plugin, RESIDUE_VALUE_THEME_NAME);
3045
- } else {
3046
- theme.setState({
3047
- mode,
3048
- values: /* @__PURE__ */ new Map()
3049
- });
3050
- applyColorTheme(plugin, hasPlddt ? PLDDT_THEME_NAME : FALLBACK_THEME_NAME);
4991
+ return;
3051
4992
  }
4993
+ residueTheme.setState({
4994
+ mode,
4995
+ values: /* @__PURE__ */ new Map()
4996
+ });
4997
+ applyColorTheme(plugin, hasPlddt ? PLDDT_THEME_NAME : FALLBACK_THEME_NAME);
3052
4998
  }, [
3053
4999
  overlay,
3054
5000
  hasPlddt,
3055
5001
  isReady,
3056
- mode
5002
+ mode,
5003
+ chainColors
3057
5004
  ]);
3058
5005
  }
3059
5006
  //#endregion
5007
+ //#region src/core/ProteinStructureViewer/ProteinStructureViewer.types.ts
5008
+ /** Pixel dimensions each resolution produces, for documentation and labels. */
5009
+ const DOWNLOAD_RESOLUTIONS = {
5010
+ high: "3840x2160",
5011
+ low: "1280x720",
5012
+ maximum: "7680x4320",
5013
+ medium: "1920x1080"
5014
+ };
5015
+ //#endregion
3060
5016
  //#region src/core/ProteinStructureViewer/index.tsx
3061
5017
  const _excluded$1 = [
3062
5018
  "backgroundColor",
5019
+ "chainColors",
5020
+ "disableChainHighlightOnHover",
5021
+ "download",
5022
+ "hiddenChains",
5023
+ "onChainVisibilityChange",
5024
+ "onChainsChange",
5025
+ "molstarSpec",
3063
5026
  "onResidueClick",
3064
5027
  "onResidueHover",
3065
- "onSelectionClear",
3066
- "pdb",
5028
+ "onSelectionChange",
5029
+ "structure",
3067
5030
  "plddt",
3068
5031
  "residueOverlay",
3069
- "selectedResidue",
5032
+ "selection",
3070
5033
  "sequenceViewerBackgroundColor",
3071
5034
  "showAxes",
5035
+ "showChainLegend",
3072
5036
  "showLegend",
3073
5037
  "showSequenceViewer",
3074
5038
  "stats"
@@ -3138,64 +5102,125 @@ function resolveScaleProps(overlay, hasPlddt) {
3138
5102
  * without the consumer round-tripping every pointer move.
3139
5103
  */
3140
5104
  const ProteinStructureViewer = (0, react.forwardRef)((props, ref) => {
3141
- const { backgroundColor, onResidueClick, onResidueHover, onSelectionClear, pdb, plddt, residueOverlay, selectedResidue = null, sequenceViewerBackgroundColor, showAxes = true, showLegend = true, showSequenceViewer = true, stats } = props, rest = _objectWithoutProperties(props, _excluded$1);
5105
+ var _selection$chains;
5106
+ const { backgroundColor, chainColors: chainColorOverrides, disableChainHighlightOnHover = false, download, hiddenChains: hiddenChainsProp, onChainVisibilityChange, onChainsChange, molstarSpec, onResidueClick, onResidueHover, onSelectionChange, structure, plddt, residueOverlay, selection = null, sequenceViewerBackgroundColor, showAxes = true, showChainLegend = true, showLegend = true, showSequenceViewer = true, stats } = props, rest = _objectWithoutProperties(props, _excluded$1);
3142
5107
  const theme = (0, _mui_material_styles.useTheme)();
3143
5108
  const mode = (0, _czi_sds_components.getMode)({ theme });
3144
5109
  const semanticColors = (0, _czi_sds_components.getSemanticColors)({ theme });
3145
5110
  const pluginMountRef = (0, react.useRef)(null);
3146
- const [hoveredResidue, setHoveredResidue] = (0, react.useState)(null);
3147
- /**
3148
- * Label for the selected residue, read back off the structure rather than
3149
- * remembered from the click that selected it, so the readout follows the
3150
- * prop however the selection was made.
3151
- */
3152
- const [selectedLabel, setSelectedLabel] = (0, react.useState)(null);
3153
5111
  const hasPlddt = Boolean(plddt && plddt.length > 0);
3154
- const processedPdb = (0, react.useMemo)(() => hasPlddt ? injectPlddtIntoPdb(pdb, plddt) : pdb, [
3155
- pdb,
5112
+ const processedStructure = (0, react.useMemo)(() => hasPlddt ? injectPlddt(structure, plddt) : structure, [
5113
+ structure,
3156
5114
  plddt,
3157
5115
  hasPlddt
3158
5116
  ]);
3159
- const { isReady, pluginRef, residueValueThemeRef, setClipRatio } = useMolstarPlugin({
3160
- backgroundColor: (0, react.useMemo)(() => {
3161
- var _semanticColors$base;
3162
- 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]);
3163
- }, [
3164
- backgroundColor,
3165
- mode,
3166
- semanticColors
3167
- ]),
5117
+ const bgColor = (0, react.useMemo)(() => {
5118
+ var _semanticColors$base;
5119
+ 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]);
5120
+ }, [
5121
+ backgroundColor,
5122
+ mode,
5123
+ semanticColors
5124
+ ]);
5125
+ const edgeColor = (0, react.useMemo)(() => {
5126
+ var _semanticColors$base2;
5127
+ return themeColor(semanticColors === null || semanticColors === void 0 || (_semanticColors$base2 = semanticColors.base) === null || _semanticColors$base2 === void 0 ? void 0 : _semanticColors$base2.borderPrimary, FALLBACK_EDGE_COLOR);
5128
+ }, [semanticColors]);
5129
+ const highlightColor = (0, react.useMemo)(() => {
5130
+ var _semanticColors$base3;
5131
+ return themeColor(semanticColors === null || semanticColors === void 0 || (_semanticColors$base3 = semanticColors.base) === null || _semanticColors$base3 === void 0 ? void 0 : _semanticColors$base3.fillSelected, FALLBACK_HIGHLIGHT_COLOR);
5132
+ }, [semanticColors]);
5133
+ const { handleResidueHover, hoveredResidue } = useResidueHoverState(onResidueHover);
5134
+ const handleSelectionClear = (0, react.useCallback)(() => {
5135
+ onSelectionChange === null || onSelectionChange === void 0 || onSelectionChange(null);
5136
+ }, [onSelectionChange]);
5137
+ /**
5138
+ * Chains the selection covers whole, which is what makes a chain caption a
5139
+ * toggle rather than a one-way switch. Keyed on its contents because the
5140
+ * set is pushed into the views Mol* renders, where a fresh object every
5141
+ * render would be a fresh push every render.
5142
+ */
5143
+ const selectedChainsKey = [...(_selection$chains = selection === null || selection === void 0 ? void 0 : selection.chains) !== null && _selection$chains !== void 0 ? _selection$chains : []].sort().join("\0");
5144
+ const selectedChains = (0, react.useMemo)(() => new Set(selectedChainsKey === "" ? [] : selectedChainsKey.split("\0")), [selectedChainsKey]);
5145
+ /**
5146
+ * Clicking a chain selects it; clicking the selected chain again clears it.
5147
+ *
5148
+ * A whole chain is reported as the chain it is, not as the hundreds of
5149
+ * residue indices it stands for, so echoing it back costs nothing. Only a
5150
+ * selection that is exactly this one chain toggles off - with a residue or
5151
+ * another chain also selected, the click is narrowing to this chain rather
5152
+ * than undoing itself.
5153
+ */
5154
+ const handleChainSelect = (0, react.useCallback)((chainId) => {
5155
+ var _selection$chains2, _selection$residues;
5156
+ 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);
5157
+ onSelectionChange === null || onSelectionChange === void 0 || onSelectionChange(isOnlyThisChain ? null : { chains: [chainId] });
5158
+ }, [onSelectionChange, selection]);
5159
+ /**
5160
+ * Dims every other chain while a chain's name is pointed at, in the legend
5161
+ * or in the sequence panel's captions, and leaves the selected chain lit
5162
+ * once the pointer has gone.
5163
+ *
5164
+ * Reached through a ref because the two ends need each other: dimming
5165
+ * other chains takes the plugin, which the hook below creates, and that
5166
+ * hook has to be handed the callback to give the captions it renders. This
5167
+ * one is stable and forwards to whatever the highlight hook installs once
5168
+ * there is a plugin for it to talk to.
5169
+ */
5170
+ const highlightChainRef = (0, react.useRef)(() => void 0);
5171
+ const highlightChain = (0, react.useCallback)((chainId) => {
5172
+ highlightChainRef.current(chainId);
5173
+ }, []);
5174
+ const [chains, setChains] = (0, react.useState)([]);
5175
+ const { colors: chainColors, hidden: hiddenChains, toggleChain } = useChains({
5176
+ chainColors: chainColorOverrides,
5177
+ chains,
5178
+ hiddenChains: hiddenChainsProp,
5179
+ onChainVisibilityChange
5180
+ });
5181
+ const { chainColorThemeRef, chains: loadedChains, isReady, loadCount, pluginRef, residuesByChainRef, residueValueThemeRef, setClipRatio } = useMolstarPlugin({
5182
+ backgroundColor: bgColor,
5183
+ chainColors,
3168
5184
  containerRef: pluginMountRef,
3169
- edgeColor: (0, react.useMemo)(() => {
3170
- var _semanticColors$base2;
3171
- return themeColor(semanticColors === null || semanticColors === void 0 || (_semanticColors$base2 = semanticColors.base) === null || _semanticColors$base2 === void 0 ? void 0 : _semanticColors$base2.borderPrimary, FALLBACK_EDGE_COLOR);
3172
- }, [semanticColors]),
5185
+ download,
5186
+ edgeColor,
3173
5187
  hasPlddt,
3174
- highlightColor: (0, react.useMemo)(() => {
3175
- var _semanticColors$base3;
3176
- return themeColor(semanticColors === null || semanticColors === void 0 || (_semanticColors$base3 = semanticColors.base) === null || _semanticColors$base3 === void 0 ? void 0 : _semanticColors$base3.fillSelected, FALLBACK_HIGHLIGHT_COLOR);
3177
- }, [semanticColors]),
5188
+ hiddenChains,
5189
+ highlightColor,
3178
5190
  mode,
5191
+ molstarSpec,
5192
+ onChainHover: highlightChain,
5193
+ onChainSelect: handleChainSelect,
5194
+ onChainToggle: toggleChain,
3179
5195
  onResidueClick,
3180
- onResidueHover: (0, react.useCallback)((residue) => {
3181
- setHoveredResidue((prev) => {
3182
- if (residue === null) return prev === null ? prev : null;
3183
- const label = residueLabel(residue);
3184
- if (prev !== null && prev.index === residue.index && prev.label === label) return prev;
3185
- return {
3186
- index: residue.index,
3187
- label
3188
- };
3189
- });
3190
- onResidueHover === null || onResidueHover === void 0 || onResidueHover(residue);
3191
- }, [onResidueHover]),
3192
- onSelectionClear,
3193
- pdb: processedPdb,
5196
+ onResidueHover: handleResidueHover,
5197
+ onSelectionChange,
5198
+ onSelectionClear: handleSelectionClear,
5199
+ selectedChains,
3194
5200
  sequenceViewerBackgroundColor,
3195
5201
  showAxes,
3196
- showSequenceViewer
5202
+ showSequenceViewer,
5203
+ structure: processedStructure
5204
+ });
5205
+ highlightChainRef.current = useChainHighlight({
5206
+ disabled: disableChainHighlightOnHover,
5207
+ hiddenChains,
5208
+ loadCount,
5209
+ pluginRef,
5210
+ selectedChains
3197
5211
  });
3198
- useResidueOverlay({
5212
+ (0, react.useEffect)(() => {
5213
+ setChains(loadedChains);
5214
+ }, [loadedChains]);
5215
+ const onChainsChangeRef = (0, react.useRef)(onChainsChange);
5216
+ onChainsChangeRef.current = onChainsChange;
5217
+ (0, react.useEffect)(() => {
5218
+ var _onChainsChangeRef$cu;
5219
+ (_onChainsChangeRef$cu = onChainsChangeRef.current) === null || _onChainsChangeRef$cu === void 0 || _onChainsChangeRef$cu.call(onChainsChangeRef, loadedChains);
5220
+ }, [loadedChains]);
5221
+ useStructureColoring({
5222
+ chainColorThemeRef,
5223
+ chainColors,
3199
5224
  hasPlddt,
3200
5225
  isReady,
3201
5226
  mode,
@@ -3203,46 +5228,49 @@ const ProteinStructureViewer = (0, react.forwardRef)((props, ref) => {
3203
5228
  pluginRef,
3204
5229
  residueValueThemeRef
3205
5230
  });
3206
- useResidueFocus({
5231
+ useSelectionFocus({
5232
+ hiddenChains,
3207
5233
  isReady,
3208
5234
  pluginRef,
3209
- selectedResidue,
5235
+ selection,
3210
5236
  setClipRatio
3211
5237
  });
3212
- (0, react.useEffect)(() => {
3213
- const plugin = pluginRef.current;
3214
- if (!plugin || !isReady || selectedResidue === null) {
3215
- setSelectedLabel(null);
3216
- return;
3217
- }
3218
- const loci = lociForResidueIndex(plugin, selectedResidue);
3219
- const residue = loci && residueRefFromLoci(loci);
3220
- setSelectedLabel(residue ? residueLabel(residue) : null);
3221
- }, [
5238
+ const { hoveredReadout, selectedReadout } = useSelectionReadout({
5239
+ hoveredResidue,
3222
5240
  isReady,
5241
+ plddt,
3223
5242
  pluginRef,
3224
- selectedResidue
3225
- ]);
3226
- const buildReadout = (0, react.useCallback)((label, index) => {
3227
- var _plddt$index, _residueOverlay$value;
3228
- return {
3229
- label,
3230
- plddt: (_plddt$index = plddt === null || plddt === void 0 ? void 0 : plddt[index]) !== null && _plddt$index !== void 0 ? _plddt$index : null,
3231
- value: (_residueOverlay$value = residueOverlay === null || residueOverlay === void 0 ? void 0 : residueOverlay.values.get(index)) !== null && _residueOverlay$value !== void 0 ? _residueOverlay$value : null
3232
- };
3233
- }, [plddt, residueOverlay]);
3234
- const hoveredReadout = (0, react.useMemo)(() => hoveredResidue === null ? null : buildReadout(hoveredResidue.label, hoveredResidue.index), [hoveredResidue, buildReadout]);
3235
- const selectedReadout = (0, react.useMemo)(() => selectedResidue === null || selectedLabel === null ? null : buildReadout(selectedLabel, selectedResidue), [
3236
- selectedResidue,
3237
- selectedLabel,
3238
- buildReadout
3239
- ]);
5243
+ residueOverlay,
5244
+ residuesByChainRef,
5245
+ selection
5246
+ });
3240
5247
  const scaleProps = (0, react.useMemo)(() => resolveScaleProps(residueOverlay, hasPlddt), [residueOverlay, hasPlddt]);
5248
+ /**
5249
+ * What a chain's swatch can show follows whatever is painting the
5250
+ * structure, in the same order the color key resolves in.
5251
+ *
5252
+ * Chain coloring gives each chain one color of its own. pLDDT gives every
5253
+ * chain the same four bands, which says they are colored by confidence
5254
+ * rather than telling them apart - so the swatch carries the whole key,
5255
+ * quartered. An overlay is continuous and has no discrete colors to put in
5256
+ * a swatch at all, so the rows keep their labels and toggles and the color
5257
+ * key beside them does the describing.
5258
+ */
5259
+ const chainColoringActive = !residueOverlay && !hasPlddt;
5260
+ const plddtColoringActive = !residueOverlay && hasPlddt;
3241
5261
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ViewerRoot, _objectSpread2(_objectSpread2({
3242
5262
  ref,
3243
5263
  showSequenceViewer
3244
5264
  }, rest), {}, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PluginMount, { ref: pluginMountRef }), showLegend && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StructureLegend, _objectSpread2({
5265
+ chainBandColors: plddtColoringActive ? PLDDT_BAND_COLORS : void 0,
5266
+ chainColors: chainColoringActive ? chainColors : void 0,
5267
+ chains: showChainLegend ? chains : [],
5268
+ hiddenChains,
3245
5269
  hoveredResidue: hoveredReadout,
5270
+ onChainHover: highlightChain,
5271
+ onChainSelect: handleChainSelect,
5272
+ onChainToggle: toggleChain,
5273
+ selectedChains,
3246
5274
  selectedResidue: selectedReadout,
3247
5275
  showSequenceViewer,
3248
5276
  stats: stats !== null && stats !== void 0 ? stats : []
@@ -3725,11 +5753,15 @@ const StackedBarChart = (props) => {
3725
5753
  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] }));
3726
5754
  };
3727
5755
  //#endregion
5756
+ exports.DOWNLOAD_RESOLUTIONS = DOWNLOAD_RESOLUTIONS;
3728
5757
  exports.HeatmapChart = HeatmapChart_default;
3729
5758
  exports.PLASMA_COLOR_SCALE = PLASMA_COLOR_SCALE;
3730
5759
  exports.PLDDT_COLOR_SCALE = PLDDT_COLOR_SCALE;
3731
5760
  exports.ProteinStructureViewer = ProteinStructureViewer_default;
3732
5761
  exports.StackedBarChart = StackedBarChart;
5762
+ exports.detectStructureFormat = detectStructureFormat;
5763
+ exports.injectPlddt = injectPlddt;
5764
+ exports.injectPlddtIntoMmcif = injectPlddtIntoMmcif;
3733
5765
  exports.injectPlddtIntoPdb = injectPlddtIntoPdb;
3734
5766
  exports.sampleColorScale = sampleColorScale;
3735
5767
  exports.toCssGradient = toCssGradient;