@czi-sds/data-viz 2.0.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs.js CHANGED
@@ -29,7 +29,134 @@ let _emotion_styled = require("@emotion/styled");
29
29
  _emotion_styled = __toESM(_emotion_styled);
30
30
  let react_jsx_runtime = require("react/jsx-runtime");
31
31
  let _czi_sds_components = require("@czi-sds/components");
32
+ let _mui_material_styles = require("@mui/material/styles");
33
+ let _phosphor_icons_react = require("@phosphor-icons/react");
34
+ let molstar_lib_mol_model_structure = require("molstar/lib/mol-model/structure");
35
+ let molstar_lib_mol_plugin_ui = require("molstar/lib/mol-plugin-ui");
36
+ let molstar_lib_mol_plugin_ui_react18 = require("molstar/lib/mol-plugin-ui/react18");
37
+ let molstar_lib_mol_plugin_ui_spec = require("molstar/lib/mol-plugin-ui/spec");
38
+ let molstar_lib_mol_plugin_behavior = require("molstar/lib/mol-plugin/behavior");
39
+ let molstar_lib_mol_plugin_config = require("molstar/lib/mol-plugin/config");
40
+ let molstar_lib_mol_repr_representation = require("molstar/lib/mol-repr/representation");
41
+ let rxjs = require("rxjs");
42
+ let molstar_lib_mol_model_loci = require("molstar/lib/mol-model/loci");
43
+ let molstar_lib_mol_plugin_ui_base = require("molstar/lib/mol-plugin-ui/base");
44
+ let molstar_lib_mol_util_input_input_observer = require("molstar/lib/mol-util/input/input-observer");
45
+ let rxjs_operators = require("rxjs/operators");
46
+ let molstar_lib_mol_data_int = require("molstar/lib/mol-data/int");
32
47
  let _mui_material = require("@mui/material");
48
+ let molstar_lib_mol_plugin_state_objects = require("molstar/lib/mol-plugin-state/objects");
49
+ let molstar_lib_mol_plugin_ui_sequence = require("molstar/lib/mol-plugin-ui/sequence");
50
+ let molstar_lib_mol_state_state_selection = require("molstar/lib/mol-state/state/selection");
51
+ let molstar_lib_mol_util = require("molstar/lib/mol-util");
52
+ let molstar_lib_mol_util_param_definition = require("molstar/lib/mol-util/param-definition");
53
+ let molstar_lib_mol_plugin_ui_toast = require("molstar/lib/mol-plugin-ui/toast");
54
+ let molstar_lib_mol_plugin_ui_viewport = require("molstar/lib/mol-plugin-ui/viewport");
55
+ let molstar_lib_mol_plugin_commands = require("molstar/lib/mol-plugin/commands");
56
+ let molstar_lib_mol_math_linear_algebra = require("molstar/lib/mol-math/linear-algebra");
57
+ 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");
59
+ //#region src/common/colorScales.ts
60
+ /**
61
+ * Perceptually uniform sequential scale (matplotlib's plasma), the default for
62
+ * value overlays.
63
+ */
64
+ const PLASMA_COLOR_SCALE = {
65
+ kind: "continuous",
66
+ stops: [
67
+ {
68
+ color: "#0D0887",
69
+ t: 0
70
+ },
71
+ {
72
+ color: "#7E03A8",
73
+ t: .25
74
+ },
75
+ {
76
+ color: "#CC4778",
77
+ t: .5
78
+ },
79
+ {
80
+ color: "#F89441",
81
+ t: .75
82
+ },
83
+ {
84
+ color: "#F0F921",
85
+ t: 1
86
+ }
87
+ ]
88
+ };
89
+ /** Parses `#RRGGBB` (or `RRGGBB`) into an RGB triplet. */
90
+ function hexToRgb(hex) {
91
+ const h = hex.replace("#", "");
92
+ return [
93
+ parseInt(h.substring(0, 2), 16),
94
+ parseInt(h.substring(2, 4), 16),
95
+ parseInt(h.substring(4, 6), 16)
96
+ ];
97
+ }
98
+ /** Linearly interpolates a continuous scale at `t`, clamped to 0-1. */
99
+ function sampleContinuousScale(stops, t) {
100
+ if (stops.length === 0) return [
101
+ 0,
102
+ 0,
103
+ 0
104
+ ];
105
+ const first = stops[0];
106
+ const last = stops[stops.length - 1];
107
+ const clamped = Math.min(1, Math.max(0, t));
108
+ let lo = first;
109
+ let hi = last;
110
+ for (let i = 0; i < stops.length - 1; i++) {
111
+ const current = stops[i];
112
+ const next = stops[i + 1];
113
+ if (clamped >= current.t && clamped <= next.t) {
114
+ lo = current;
115
+ hi = next;
116
+ break;
117
+ }
118
+ }
119
+ const loRgb = hexToRgb(lo.color);
120
+ const hiRgb = hexToRgb(hi.color);
121
+ const f = hi.t === lo.t ? 0 : (clamped - lo.t) / (hi.t - lo.t);
122
+ return [
123
+ Math.round(loRgb[0] + f * (hiRgb[0] - loRgb[0])),
124
+ Math.round(loRgb[1] + f * (hiRgb[1] - loRgb[1])),
125
+ Math.round(loRgb[2] + f * (hiRgb[2] - loRgb[2]))
126
+ ];
127
+ }
128
+ /**
129
+ * Picks the band a normalized value falls into on a stepped scale.
130
+ *
131
+ * A value lands in the first band whose threshold it falls short of, which puts
132
+ * each threshold at the bottom of the band above it: against thresholds of 0.5,
133
+ * 0.7 and 0.9, a value of exactly 0.7 reads as the 0.7-0.9 band, not the one
134
+ * below.
135
+ */
136
+ function sampleSteppedScale(stops, t) {
137
+ for (const stop of stops) if (t < stop.threshold) return hexToRgb(stop.color);
138
+ const last = stops[stops.length - 1];
139
+ return last ? hexToRgb(last.color) : [
140
+ 0,
141
+ 0,
142
+ 0
143
+ ];
144
+ }
145
+ /**
146
+ * Colors a raw value against a scale, normalizing it into `min`-`max` first.
147
+ * Returns null when the value sits at or below `min`, or when the range is
148
+ * degenerate, so callers can fall back to a neutral color.
149
+ */
150
+ function sampleColorScale(scale, value, max, min = 0) {
151
+ if (max <= min || value <= min) return null;
152
+ const t = Math.min(1, (value - min) / (max - min));
153
+ return scale.kind === "continuous" ? sampleContinuousScale(scale.stops, t) : sampleSteppedScale(scale.stops, t);
154
+ }
155
+ /** CSS `linear-gradient` spanning a continuous scale, for the legend bar. */
156
+ function toCssGradient(stops) {
157
+ return `linear-gradient(to right, ${stops.map((stop) => `${stop.color} ${stop.t * 100}%`).join(", ")})`;
158
+ }
159
+ //#endregion
33
160
  //#region src/common/utils.ts
34
161
  const EMPTY_OBJECT = {};
35
162
  //#endregion
@@ -117,7 +244,7 @@ function _objectSpread2(e) {
117
244
  }
118
245
  //#endregion
119
246
  //#region src/core/HeatmapChart/hooks/utils.ts
120
- const _excluded$2 = [
247
+ const _excluded$3 = [
121
248
  "axisPointer",
122
249
  "dataZoom",
123
250
  "series",
@@ -131,7 +258,7 @@ function createChartOptions(props) {
131
258
  const { axisPointer, camera, data, dataZoom, emphasis, encode, grid: gridProp, itemStyle = DEFAULT_ITEM_STYLE, options, symbolSize, symbol = "rect" } = props;
132
259
  const { defaultAxisPointer, defaultDataZoom, defaultEmphasis, defaultGrid, defaultXAxis, defaultYAxis } = generateDefaultValues(props);
133
260
  const customGrid = typeof gridProp === "function" ? gridProp(defaultGrid) : gridProp;
134
- const _ref = options || {}, { axisPointer: optionsAxisPointer, dataZoom: optionsDataZoom, series: optionsSeries, xAxis: optionsXAxis, yAxis: optionsYAxis } = _ref, optionsRest = _objectWithoutProperties(_ref, _excluded$2);
261
+ const _ref = options || {}, { axisPointer: optionsAxisPointer, dataZoom: optionsDataZoom, series: optionsSeries, xAxis: optionsXAxis, yAxis: optionsYAxis } = _ref, optionsRest = _objectWithoutProperties(_ref, _excluded$3);
135
262
  return _objectSpread2({
136
263
  animation: false,
137
264
  axisPointer: mergeAxisPointer(defaultAxisPointer, axisPointer, optionsAxisPointer),
@@ -339,7 +466,7 @@ function getWidthAndHeight({ width, height }) {
339
466
  }
340
467
  //#endregion
341
468
  //#region src/core/HeatmapChart/index.tsx
342
- const _excluded$1 = [
469
+ const _excluded$2 = [
343
470
  "axisPointer",
344
471
  "width",
345
472
  "height",
@@ -359,7 +486,7 @@ const _excluded$1 = [
359
486
  "options"
360
487
  ];
361
488
  const HeatmapChart = (0, react.forwardRef)((props, ref) => {
362
- const { axisPointer, width, height, echartsRendererMode = "svg", camera, dataZoom, onEvents, xAxisData, yAxisData, data, encode, emphasis, itemStyle, symbol, symbolSize = 5, grid, options } = props, rest = _objectWithoutProperties(props, _excluded$1);
489
+ const { axisPointer, width, height, echartsRendererMode = "svg", camera, dataZoom, onEvents, xAxisData, yAxisData, data, encode, emphasis, itemStyle, symbol, symbolSize = 5, grid, options } = props, rest = _objectWithoutProperties(props, _excluded$2);
363
490
  if (!width || !height) throw Error("Heatmap must have width and height bigger than Zero!");
364
491
  const innerRef = (0, react.useRef)(null);
365
492
  /**
@@ -447,6 +574,2681 @@ const HeatmapChart = (0, react.forwardRef)((props, ref) => {
447
574
  }
448
575
  });
449
576
  var HeatmapChart_default = (0, react.memo)(HeatmapChart);
577
+ /** Height of the color bar itself. */
578
+ const BAR_HEIGHT = 8;
579
+ /**
580
+ * Inset applied to the outer tick labels on a stepped scale. Labels sit on the
581
+ * boundaries *between* bands, so the row is padded by half a band to line each
582
+ * label up with its boundary rather than with a band's center.
583
+ */
584
+ const STEPPED_LABEL_INSET = 5;
585
+ const LegendWrapper = (0, _emotion_styled.default)("div")`
586
+ width: ${142}px;
587
+ `;
588
+ const LegendLabels = (0, _emotion_styled.default)("div")`
589
+ ${_czi_sds_components.fontBodyXxxs}
590
+
591
+ display: flex;
592
+ justify-content: space-between;
593
+ width: 100%;
594
+
595
+ ${(props) => {
596
+ var _semanticColors$base;
597
+ const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
598
+ return `
599
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base = semanticColors.base) === null || _semanticColors$base === void 0 ? void 0 : _semanticColors$base.textTertiaryOnDark};
600
+ `;
601
+ }}
602
+ `;
603
+ const SteppedLabels = (0, _emotion_styled.default)(LegendLabels)`
604
+ &::before,
605
+ &::after {
606
+ content: "";
607
+ width: ${STEPPED_LABEL_INSET}px;
608
+ }
609
+ `;
610
+ const LegendBar = (0, _emotion_styled.default)("div")`
611
+ display: flex;
612
+ height: ${BAR_HEIGHT}px;
613
+ width: 100%;
614
+ `;
615
+ const LegendBand = (0, _emotion_styled.default)("div")`
616
+ flex: 1;
617
+ background-color: ${(props) => props.color};
618
+ `;
619
+ const LegendGradient = (0, _emotion_styled.default)("div")`
620
+ height: ${BAR_HEIGHT}px;
621
+ width: 100%;
622
+ background: ${(props) => props.gradient};
623
+ `;
624
+ //#endregion
625
+ //#region src/core/ProteinStructureViewer/components/ColorScaleLegend/index.tsx
626
+ /** Decimal places used for the ticks on a continuous scale. */
627
+ const TICK_PRECISION = 2;
628
+ /**
629
+ * The color key beneath the structure viewer. A stepped scale renders as
630
+ * discrete bands with a tick at each boundary; a continuous scale renders as a
631
+ * gradient running from `min` to `max`, the range its values are normalized
632
+ * into before they are sampled.
633
+ */
634
+ function ColorScaleLegend({ max = null, min = 0, scale }) {
635
+ if (scale.kind === "stepped") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(LegendWrapper, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SteppedLabels, { children: scale.stops.slice(0, -1).map((stop) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: stop.label }, stop.label)) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LegendBar, { children: scale.stops.map((stop) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LegendBand, { color: stop.color }, stop.label)) })] });
636
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(LegendWrapper, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(LegendLabels, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: min.toFixed(TICK_PRECISION) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: max !== null ? max.toFixed(TICK_PRECISION) : "" })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LegendGradient, { gradient: toCssGradient(scale.stops) })] });
637
+ }
638
+ //#endregion
639
+ //#region src/core/ProteinStructureViewer/style.ts
640
+ /** Smallest box in which Mol* can still lay out a usable viewport. */
641
+ const MIN_HEIGHT = 190;
642
+ const MIN_WIDTH = 200;
643
+ /**
644
+ * Name of the size container the root declares below, which is what lets the
645
+ * sequence panel and the legend reflow to the viewer's own width rather than
646
+ * the page's.
647
+ */
648
+ const VIEWER_CONTAINER_NAME = "sds-structure-viewer";
649
+ /**
650
+ * Height reserved for the sequence panel along the bottom, and the viewer
651
+ * width above which it grows. Measured against the container above rather than
652
+ * the page, so a viewer embedded in a wider page sizes to the room it has.
653
+ *
654
+ * The legend sits directly on top of the panel and offsets itself by these
655
+ * same three values, so it reads them from here rather than restating them.
656
+ * Copies that drifted would leave the legend floating off the panel.
657
+ */
658
+ const SEQUENCE_HEIGHT = "max(104px, 30%)";
659
+ const SEQUENCE_HEIGHT_WIDE = "max(134px, 32%)";
660
+ /**
661
+ * The element Mol* mounts into.
662
+ *
663
+ * This has to be its own node rather than the root: Mol* takes ownership of its
664
+ * target and replaces whatever is in it, so anything the viewer renders
665
+ * alongside the plugin (the legend) has to be a sibling of this, not a child.
666
+ */
667
+ const PluginMount = (0, _emotion_styled.default)("div")`
668
+ position: absolute;
669
+ inset: 0;
670
+ `;
671
+ /**
672
+ * Root container for the viewer.
673
+ *
674
+ * Mol* ships no stylesheet of its own here: the viewport and sequence panel are
675
+ * replaced with custom components and the remaining chrome is hidden, so all
676
+ * that is left to style is the layout scaffolding. Those overrides are nested
677
+ * below rather than declared globally, so consumers do not have to import
678
+ * anything extra and two viewers on a page cannot fight over the same
679
+ * selectors.
680
+ *
681
+ * The element also declares itself a size container, which is what lets the
682
+ * legend reflow to the viewer's own width instead of the page's.
683
+ */
684
+ const ViewerRoot = (0, _emotion_styled.default)("div")`
685
+ position: relative;
686
+ overflow: hidden;
687
+ width: 100%;
688
+ height: 100%;
689
+ min-width: ${MIN_WIDTH}px;
690
+ min-height: ${MIN_HEIGHT}px;
691
+ container-type: inline-size;
692
+ container-name: ${VIEWER_CONTAINER_NAME};
693
+
694
+ .msp-plugin {
695
+ position: absolute !important;
696
+ inset: 0 !important;
697
+ width: 100% !important;
698
+ height: 100% !important;
699
+ }
700
+
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 {
709
+ display: none !important;
710
+ }
711
+
712
+ .msp-plugin canvas {
713
+ display: block !important;
714
+ }
715
+
716
+ .msp-plugin .msp-layout-static {
717
+ height: 100%;
718
+ }
719
+
720
+ .msp-plugin .msp-sequence-wrapper-non-empty {
721
+ overflow-y: auto;
722
+ }
723
+
724
+ /*
725
+ * Mol* puts the sequence panel in its "top" region; move it to the bottom and
726
+ * shrink the main viewport region to match, so the structure sits above the
727
+ * sequence rather than behind it. The panel grows on wider viewers, and the
728
+ * viewport region's offset follows it.
729
+ */
730
+ ${(props) => {
731
+ const { showSequenceViewer } = props;
732
+ return `
733
+ .msp-plugin .msp-layout-region.msp-layout-top {
734
+ display: ${showSequenceViewer ? "block" : "none"} !important;
735
+ position: absolute !important;
736
+ inset: auto 0 0 0 !important;
737
+ height: ${SEQUENCE_HEIGHT} !important;
738
+ }
739
+
740
+ .msp-plugin .msp-layout-region.msp-layout-main {
741
+ position: absolute !important;
742
+ top: 0 !important;
743
+ left: 0 !important;
744
+ right: 0 !important;
745
+ bottom: ${showSequenceViewer ? SEQUENCE_HEIGHT : "0"} !important;
746
+ }
747
+
748
+ @container ${VIEWER_CONTAINER_NAME} (min-width: 880px) {
749
+ .msp-plugin .msp-layout-region.msp-layout-top {
750
+ height: ${SEQUENCE_HEIGHT_WIDE} !important;
751
+ }
752
+
753
+ .msp-plugin .msp-layout-region.msp-layout-main {
754
+ bottom: ${showSequenceViewer ? SEQUENCE_HEIGHT_WIDE : "0"} !important;
755
+ }
756
+ }
757
+ `;
758
+ }}
759
+
760
+ /*
761
+ * Toast slot rendered by the custom viewport. No loci label providers are
762
+ * registered (residue info goes to the legend instead), so this only carries
763
+ * Mol*'s own transient messages.
764
+ */
765
+ .msp-plugin .msp-highlight-toast-wrapper {
766
+ position: absolute !important;
767
+ left: 10px !important;
768
+ max-width: 95% !important;
769
+ z-index: 10000 !important;
770
+ pointer-events: none;
771
+ }
772
+ `;
773
+ //#endregion
774
+ //#region src/core/ProteinStructureViewer/components/StructureLegend/style.ts
775
+ /** Width below which the legend switches to its stacked layout. */
776
+ const NARROW_BREAKPOINT = 480;
777
+ /** Fixed stat column width, so labels swapping on hover never shift the grid. */
778
+ const STAT_COLUMN_WIDTH = "4.5rem";
779
+ /**
780
+ * Overlaid along the bottom of the viewer, above the sequence panel. Its bottom
781
+ * offset is the panel's own height, taken from the root style so the two cannot
782
+ * disagree; with no panel it drops to the bottom edge, where its own padding
783
+ * provides the gap. Pointer events are off so the legend never intercepts drags
784
+ * meant for the structure; the help tooltip re-enables them on itself.
785
+ */
786
+ const LegendOverlay = (0, _emotion_styled.default)("div")`
787
+ position: absolute;
788
+ z-index: 1;
789
+ display: flex;
790
+ align-items: flex-end;
791
+ width: 100%;
792
+ pointer-events: none;
793
+
794
+ ${(props) => {
795
+ const spaces = (0, _czi_sds_components.getSpaces)(props);
796
+ const { showSequenceViewer } = props;
797
+ return `
798
+ right: ${spaces === null || spaces === void 0 ? void 0 : spaces.m}px;
799
+ gap: ${spaces === null || spaces === void 0 ? void 0 : spaces.xs}px;
800
+ padding-bottom: ${spaces === null || spaces === void 0 ? void 0 : spaces.m}px;
801
+ bottom: ${showSequenceViewer ? SEQUENCE_HEIGHT : "0"};
802
+
803
+ /*
804
+ * Track the sequence panel as it grows on wider viewers. The panel is
805
+ * sized against this same container, so answering to the page instead
806
+ * would part the legend from it on a viewer narrower than its page.
807
+ */
808
+ @container ${VIEWER_CONTAINER_NAME} (min-width: 880px) {
809
+ bottom: ${showSequenceViewer ? SEQUENCE_HEIGHT_WIDE : "0"};
810
+ }
811
+
812
+ /* Spacer balancing the axes widget pinned in the opposite corner. */
813
+ &::before {
814
+ content: "";
815
+ display: block;
816
+ width: ${spaces === null || spaces === void 0 ? void 0 : spaces.xxl}px;
817
+ }
818
+
819
+ @container ${VIEWER_CONTAINER_NAME} (max-width: ${NARROW_BREAKPOINT}px) {
820
+ width: auto;
821
+ flex-direction: column;
822
+ align-items: flex-end;
823
+ gap: ${spaces === null || spaces === void 0 ? void 0 : spaces.l}px;
824
+
825
+ /* The horizontal spacer only makes sense in the bottom-bar layout. */
826
+ &::before {
827
+ display: none;
828
+ }
829
+ }
830
+ `;
831
+ }}
832
+ `;
833
+ /**
834
+ * Fills the space between the axes widget on the left and the color legend on
835
+ * the right. Three fixed-width columns distributed with space-evenly, so the
836
+ * side padding equals the inter-column gap and both flex with the viewer width.
837
+ */
838
+ const StatsGrid = (0, _emotion_styled.default)("div")`
839
+ display: grid;
840
+ flex: 1;
841
+ grid-template-columns: repeat(3, ${STAT_COLUMN_WIDTH});
842
+ align-items: end;
843
+ justify-content: space-evenly;
844
+ white-space: nowrap;
845
+
846
+ ${(props) => {
847
+ const spaces = (0, _czi_sds_components.getSpaces)(props);
848
+ return `
849
+ @container ${VIEWER_CONTAINER_NAME} (max-width: ${NARROW_BREAKPOINT}px) {
850
+ flex: none;
851
+ grid-template-columns: minmax(0, 1fr);
852
+ justify-items: end;
853
+ text-align: right;
854
+ row-gap: ${spaces === null || spaces === void 0 ? void 0 : spaces.xl}px;
855
+ }
856
+ `;
857
+ }}
858
+ `;
859
+ const StatColumnWrapper = (0, _emotion_styled.default)("div")`
860
+ display: flex;
861
+ flex-direction: column;
862
+ `;
863
+ const StatValue = (0, _emotion_styled.default)("span")`
864
+ ${_czi_sds_components.fontBodyXxxs}
865
+
866
+ ${(props) => {
867
+ var _semanticColors$base;
868
+ const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
869
+ return `
870
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base = semanticColors.base) === null || _semanticColors$base === void 0 ? void 0 : _semanticColors$base.textPrimary};
871
+ `;
872
+ }}
873
+ `;
874
+ const StatLabel = (0, _emotion_styled.default)("span")`
875
+ ${_czi_sds_components.fontBodyXxxs}
876
+
877
+ ${(props) => {
878
+ var _semanticColors$base2;
879
+ const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
880
+ return `
881
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base2 = semanticColors.base) === null || _semanticColors$base2 === void 0 ? void 0 : _semanticColors$base2.textTertiaryOnDark};
882
+ `;
883
+ }}
884
+ `;
885
+ const ScaleColumn = (0, _emotion_styled.default)("div")`
886
+ display: flex;
887
+ flex-direction: column;
888
+
889
+ ${(props) => {
890
+ const spaces = (0, _czi_sds_components.getSpaces)(props);
891
+ return `
892
+ gap: ${spaces === null || spaces === void 0 ? void 0 : spaces.xxs}px;
893
+ `;
894
+ }}
895
+ `;
896
+ /**
897
+ * A wrapping text line, not a flex row: the help icon has to sit at the end of
898
+ * the last line of the caption rather than beside the whole wrapped block.
899
+ */
900
+ const ScaleCaption = (0, _emotion_styled.default)("span")`
901
+ ${_czi_sds_components.fontBodyXxxs}
902
+
903
+ display: block;
904
+ text-align: left;
905
+
906
+ ${(props) => {
907
+ var _semanticColors$base3;
908
+ const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
909
+ return `
910
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base3 = semanticColors.base) === null || _semanticColors$base3 === void 0 ? void 0 : _semanticColors$base3.textTertiaryOnDark};
911
+ max-width: 142px;
912
+ `;
913
+ }}
914
+ `;
915
+ /** Re-enables pointer events for the one interactive element in the overlay. */
916
+ const TooltipAnchor = (0, _emotion_styled.default)("span")`
917
+ pointer-events: auto;
918
+
919
+ ${(props) => {
920
+ var _semanticColors$base4;
921
+ const spaces = (0, _czi_sds_components.getSpaces)(props);
922
+ const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
923
+ return `
924
+ display: inline-block;
925
+ padding-left: ${spaces === null || spaces === void 0 ? void 0 : spaces.xxxs}px;
926
+ margin-bottom: 1px;
927
+ vertical-align: text-bottom;
928
+
929
+ &:hover {
930
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base4 = semanticColors.base) === null || _semanticColors$base4 === void 0 ? void 0 : _semanticColors$base4.ornamentSecondary};
931
+ }
932
+
933
+ svg {
934
+ display: block;
935
+ }
936
+ `;
937
+ }}
938
+ `;
939
+ //#endregion
940
+ //#region src/core/ProteinStructureViewer/components/StructureLegend/index.tsx
941
+ /** Number of stat columns, fixed so the grid tracks never move. */
942
+ const SLOT_COUNT = 3;
943
+ /** Decimal places for stat and readout values. */
944
+ const VALUE_PRECISION = 3;
945
+ /** Shown in place of a number the residue has none of. */
946
+ const MISSING_VALUE = "–";
947
+ function StatColumn({ label, value }) {
948
+ 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
+ }
950
+ /**
951
+ * Stats and color key overlaid on the structure viewer.
952
+ *
953
+ * When a residue is selected or hovered, the whole-structure stats are replaced
954
+ * in place by that residue's readout: slot 0 becomes the residue label, slot 1
955
+ * the overlay value (only when an overlay is active, otherwise the stat stays),
956
+ * and slot 2 the residue pLDDT. A selection pins the readout; hovering another
957
+ * residue takes priority so the user can inspect others without losing their
958
+ * selection.
959
+ */
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;
962
+ const activeResidue = hoveredResidue !== null && hoveredResidue !== void 0 ? hoveredResidue : selectedResidue;
963
+ const showResidue = activeResidue !== null;
964
+ const showValue = showResidue && valueLabel !== void 0;
965
+ const slots = [
966
+ showResidue ? {
967
+ label: "Residue",
968
+ value: activeResidue.label
969
+ } : (_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
978
+ ];
979
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(LegendOverlay, {
980
+ showSequenceViewer,
981
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatsGrid, { children: Array.from({ length: SLOT_COUNT }, (_, i) => {
982
+ var _slots$i;
983
+ const slot = (_slots$i = slots[i]) !== null && _slots$i !== void 0 ? _slots$i : null;
984
+ return slot ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatColumn, {
985
+ label: slot.label,
986
+ value: slot.value
987
+ }, 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, {
989
+ max: scaleMax,
990
+ min: scaleMin,
991
+ scale
992
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ScaleCaption, { children: [scaleLabel, (scaleTooltip !== void 0 || scaleTooltipProps !== void 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_czi_sds_components.Tooltip, _objectSpread2(_objectSpread2({
993
+ arrow: true,
994
+ placement: "bottom"
995
+ }, scaleTooltipProps), {}, {
996
+ title: scaleTooltip !== null && scaleTooltip !== void 0 ? scaleTooltip : scaleTooltipProps === null || scaleTooltipProps === void 0 ? void 0 : scaleTooltipProps.title,
997
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipAnchor, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_phosphor_icons_react.QuestionIcon, {
998
+ size: 10,
999
+ weight: "bold"
1000
+ }) })
1001
+ }))] })] })]
1002
+ });
1003
+ }
1004
+ //#endregion
1005
+ //#region src/core/ProteinStructureViewer/utils/theme.ts
1006
+ const themeCache = /* @__PURE__ */ new Map();
1007
+ /**
1008
+ * The MUI theme for a mode, built at most once. Mol*'s view components rebuild
1009
+ * their `ThemeProvider` on every render, and `Theme()` is not cheap.
1010
+ */
1011
+ function themeForMode(mode) {
1012
+ const cached = themeCache.get(mode);
1013
+ if (cached) return cached;
1014
+ const theme = (0, _czi_sds_components.Theme)(mode);
1015
+ themeCache.set(mode, theme);
1016
+ return theme;
1017
+ }
1018
+ const residueColorCache = /* @__PURE__ */ new Map();
1019
+ function residueColorsForMode(mode) {
1020
+ var _getSemanticColors, _base$fillSecondaryIn, _base$textPrimary, _base$textSecondary, _base$textTertiary;
1021
+ const cached = residueColorCache.get(mode);
1022
+ if (cached) return cached;
1023
+ const base = (_getSemanticColors = (0, _czi_sds_components.getSemanticColors)({ theme: themeForMode(mode) })) === null || _getSemanticColors === void 0 ? void 0 : _getSemanticColors.base;
1024
+ const colors = {
1025
+ activeBackground: (_base$fillSecondaryIn = base === null || base === void 0 ? void 0 : base.fillSecondaryInteraction) !== null && _base$fillSecondaryIn !== void 0 ? _base$fillSecondaryIn : "transparent",
1026
+ activeText: (_base$textPrimary = base === null || base === void 0 ? void 0 : base.textPrimary) !== null && _base$textPrimary !== void 0 ? _base$textPrimary : "inherit",
1027
+ defaultText: (_base$textSecondary = base === null || base === void 0 ? void 0 : base.textSecondary) !== null && _base$textSecondary !== void 0 ? _base$textSecondary : "inherit",
1028
+ inactiveText: (_base$textTertiary = base === null || base === void 0 ? void 0 : base.textTertiary) !== null && _base$textTertiary !== void 0 ? _base$textTertiary : "inherit"
1029
+ };
1030
+ residueColorCache.set(mode, colors);
1031
+ return colors;
1032
+ }
1033
+ //#endregion
1034
+ //#region src/core/ProteinStructureViewer/hooks/useSubscribe.ts
1035
+ /**
1036
+ * Subscribes to an observable for as long as the component is mounted, the
1037
+ * hook equivalent of `PluginUIComponent`'s `subscribe`.
1038
+ *
1039
+ * The handler is read through a ref so that passing a fresh closure on every
1040
+ * render - which is the normal case, since these handlers read props and state
1041
+ * - does not tear down and rebuild the subscription.
1042
+ */
1043
+ function useSubscribe(observable, handler) {
1044
+ const handlerRef = (0, react.useRef)(handler);
1045
+ handlerRef.current = handler;
1046
+ (0, react.useEffect)(() => {
1047
+ if (!observable) return;
1048
+ const subscription = observable.subscribe((value) => handlerRef.current(value));
1049
+ return () => subscription.unsubscribe();
1050
+ }, [observable]);
1051
+ }
1052
+ //#endregion
1053
+ //#region src/core/ProteinStructureViewer/hooks/useViewSetting.ts
1054
+ /**
1055
+ * One field of the settings Mol*'s own React root cannot read from context.
1056
+ *
1057
+ * The subject emits a whole new settings object on every push, so each field is
1058
+ * selected out separately: React's equality bailout then absorbs the pushes
1059
+ * that left this particular value alone, and a component only re-renders for
1060
+ * the settings it actually reads.
1061
+ */
1062
+ function useViewSetting(settings, select) {
1063
+ const [value, setValue] = (0, react.useState)(() => select(settings.value));
1064
+ useSubscribe(settings, (next) => setValue(select(next)));
1065
+ return value;
1066
+ }
1067
+ //#endregion
1068
+ //#region src/core/ProteinStructureViewer/hooks/useMolstarTheme.ts
1069
+ /**
1070
+ * The active theme for a component Mol* renders in its own React root.
1071
+ *
1072
+ * Those components sit outside the consumer's provider tree, so they cannot
1073
+ * read the theme from context; the mode reaches them through the settings
1074
+ * subject instead. Callers re-supply the theme to their subtree via a local
1075
+ * `ThemeProvider` so Emotion styles resolve SDS tokens correctly.
1076
+ */
1077
+ function useMolstarTheme(settings) {
1078
+ const mode = useViewSetting(settings, (s) => s.mode);
1079
+ return {
1080
+ mode,
1081
+ theme: themeForMode(mode)
1082
+ };
1083
+ }
1084
+ //#endregion
1085
+ //#region src/core/ProteinStructureViewer/components/SequenceView/constants.ts
1086
+ /** Attribute carrying a residue's sequence index, used for hit testing. */
1087
+ const SEQ_ID_ATTR = "data-seqid";
1088
+ /** Hover updates are coalesced to roughly one every three frames. */
1089
+ const HOVER_THROTTLE_MS = 3 * 16.666;
1090
+ /** How long the copy button shows its confirmation state. */
1091
+ const COPIED_FEEDBACK_MS = 2e3;
1092
+ //#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;
1159
+ return 1;
1160
+ }
1161
+ /**
1162
+ * The grid's accessible name.
1163
+ *
1164
+ * The grid is a rendering of the structure rather than a control of its own,
1165
+ * so it is exposed as an image; that makes this label the only thing an
1166
+ * assistive technology gets, which is why it carries the residues themselves
1167
+ * rather than just how many there are. They are spaced apart so they are read
1168
+ * as individual letters instead of running together into a word.
1169
+ *
1170
+ * A complex draws one grid per chain, and the caption naming each is a visual
1171
+ * sibling rather than a label, so the chain is named here too. Without it every
1172
+ * grid announces the same name and which chain a reader is on is apparent only
1173
+ * by sight.
1174
+ */
1175
+ function buildSequenceLabel(sequenceWrapper, chainLabel) {
1176
+ const letters = [];
1177
+ for (let i = 0; i < sequenceWrapper.length; i++) letters.push(sequenceWrapper.residueLabel(i));
1178
+ const name = chainLabel ? `Chain ${chainLabel}, residue sequence, ${sequenceWrapper.length} residues` : `Residue sequence, ${sequenceWrapper.length} residues`;
1179
+ return letters.length > 0 ? `${name}: ${letters.join(" ")}` : name;
1180
+ }
1181
+ //#endregion
1182
+ //#region src/core/ProteinStructureViewer/components/SequenceView/hooks/useResidueMarkers.ts
1183
+ /**
1184
+ * Keeps the residue grid's hover and selection colors in sync with the 3D
1185
+ * view.
1186
+ *
1187
+ * The colors are written straight to the residue spans rather than rendered,
1188
+ * because a marker change would otherwise re-render a grid that can hold
1189
+ * thousands of nodes.
1190
+ */
1191
+ function useResidueMarkers({ containerRef, residueColors, sequenceWrapper }) {
1192
+ const plugin = usePlugin();
1193
+ /**
1194
+ * Residues sit at the default color until something is active. A hovered,
1195
+ * selected, or focused residue brightens and takes a translucent fill; while
1196
+ * any residue is selected, the rest dim to draw attention to the selection.
1197
+ */
1198
+ const paintMarkers = (0, react.useCallback)(() => {
1199
+ const container = containerRef.current;
1200
+ if (!container) return;
1201
+ const { activeBackground, activeText, defaultText, inactiveText } = residueColors;
1202
+ const spans = container.querySelectorAll(`[${SEQ_ID_ATTR}]`);
1203
+ let anySelected = false;
1204
+ for (let i = 0; i < sequenceWrapper.length; i++) if (sequenceWrapper.isSelected(i)) {
1205
+ anySelected = true;
1206
+ break;
1207
+ }
1208
+ const inactiveColor = anySelected ? inactiveText : defaultText;
1209
+ for (let i = 0; i < sequenceWrapper.length; i++) {
1210
+ const span = spans[i];
1211
+ if (!span) return;
1212
+ const className = getResidueClass(sequenceWrapper, i);
1213
+ if (span.className !== className) span.className = className;
1214
+ const active = sequenceWrapper.isSelected(i) || sequenceWrapper.isFocused(i) || sequenceWrapper.isHighlighted(i);
1215
+ span.style.color = active ? activeText : inactiveColor;
1216
+ span.style.backgroundColor = active ? activeBackground : "";
1217
+ }
1218
+ }, [
1219
+ containerRef,
1220
+ residueColors,
1221
+ sequenceWrapper
1222
+ ]);
1223
+ const markFocus = (0, react.useCallback)((loci) => {
1224
+ sequenceWrapper.markResidue(molstar_lib_mol_model_loci.EveryLoci, "unfocus");
1225
+ if (loci) sequenceWrapper.markResidue(loci, "focus");
1226
+ }, [sequenceWrapper]);
1227
+ const paintMarkersRef = (0, react.useRef)(paintMarkers);
1228
+ paintMarkersRef.current = paintMarkers;
1229
+ const sequenceWrapperRef = (0, react.useRef)(sequenceWrapper);
1230
+ sequenceWrapperRef.current = sequenceWrapper;
1231
+ (0, react.useEffect)(() => {
1232
+ const markProvider = (loci, action) => {
1233
+ if (sequenceWrapperRef.current.markResidue(loci.loci, action)) paintMarkersRef.current();
1234
+ };
1235
+ const { lociHighlights, lociSelects } = plugin.managers.interactivity;
1236
+ lociHighlights.addProvider(markProvider);
1237
+ lociSelects.addProvider(markProvider);
1238
+ return () => {
1239
+ lociHighlights.removeProvider(markProvider);
1240
+ lociSelects.removeProvider(markProvider);
1241
+ };
1242
+ }, [plugin]);
1243
+ useSubscribe(plugin.managers.structure.focus.behaviors.current, (focus) => {
1244
+ markFocus(focus === null || focus === void 0 ? void 0 : focus.loci);
1245
+ paintMarkers();
1246
+ });
1247
+ (0, react.useLayoutEffect)(() => {
1248
+ var _plugin$managers$stru;
1249
+ markFocus((_plugin$managers$stru = plugin.managers.structure.focus.behaviors.current.value) === null || _plugin$managers$stru === void 0 ? void 0 : _plugin$managers$stru.loci);
1250
+ paintMarkers();
1251
+ });
1252
+ }
1253
+ //#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;
1260
+ }
1261
+ }
1262
+ /**
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.
1265
+ *
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.
1272
+ */
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
+ }]);
1285
+ }
1286
+ //#endregion
1287
+ //#region src/core/ProteinStructureViewer/components/SequenceView/hooks/useHoveredResidue.ts
1288
+ /**
1289
+ * Tracks which residue the pointer is over and keeps the tooltip pinned to it.
1290
+ *
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.
1293
+ */
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
+ ]);
1334
+ }
1335
+ //#endregion
1336
+ //#region src/core/ProteinStructureViewer/components/SequenceView/hooks/useLociDispatch.ts
1337
+ /**
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.
1341
+ */
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;
1352
+ }
1353
+ /**
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.
1356
+ */
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]);
1369
+ }
1370
+ //#endregion
1371
+ //#region src/core/ProteinStructureViewer/components/SequenceView/hooks/useResidueSelection.ts
1372
+ /**
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.
1375
+ */
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
+ };
1402
+ }
1403
+ //#endregion
1404
+ //#region src/core/ProteinStructureViewer/components/SequenceView/hooks/useResiduePointer.ts
1405
+ /**
1406
+ * Everything the grid does with a pointer: hover on move, and the drag-to-range
1407
+ * selection delegated to `useResidueSelection`.
1408
+ *
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.
1413
+ */
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
+ };
1466
+ return {
1467
+ handlers: {
1468
+ onContextMenu,
1469
+ onMouseDown,
1470
+ onMouseLeave,
1471
+ onMouseMove,
1472
+ onMouseUp
1473
+ },
1474
+ tooltipRef
1475
+ };
1476
+ }
1477
+ //#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
+ `;
1592
+ /**
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.
1596
+ */
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
+ `;
1619
+ /**
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).
1626
+ */
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
+ `;
1652
+ /**
1653
+ * Layout for one chain's residues.
1654
+ *
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.
1660
+ */
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
+ `;
1699
+ /**
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.
1703
+ */
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
+ `;
1718
+ //#endregion
1719
+ //#region src/core/ProteinStructureViewer/components/SequenceView/components/ResidueTooltip/index.tsx
1720
+ var _getSpaces$m, _getSpaces;
1721
+ /**
1722
+ * Pull the tooltip toward the residue by the SDS `m` spacing token. Spacing
1723
+ * tokens are theme-invariant, so reading from the light theme is fine; popper's
1724
+ * offset modifier needs a number, hence deriving it from the token.
1725
+ */
1726
+ const MAIN_AXIS_OFFSET = -((_getSpaces$m = (_getSpaces = (0, _czi_sds_components.getSpaces)({ theme: _czi_sds_components.defaultTheme })) === null || _getSpaces === void 0 ? void 0 : _getSpaces.m) !== null && _getSpaces$m !== void 0 ? _getSpaces$m : 12);
1727
+ /**
1728
+ * A single controlled tooltip anchored to the residue span currently under the
1729
+ * cursor. Driven imperatively through its ref so that hover updates re-render
1730
+ * only the tooltip, not the residue grid.
1731
+ */
1732
+ const ResidueTooltip = (0, react.forwardRef)((_props, ref) => {
1733
+ var _getSemanticColors;
1734
+ const [{ anchorEl, label }, setState] = (0, react.useState)({
1735
+ anchorEl: null,
1736
+ label: ""
1737
+ });
1738
+ const residueTooltipBackgroundColor = (_getSemanticColors = (0, _czi_sds_components.getSemanticColors)({ theme: (0, _mui_material.useTheme)() })) === null || _getSemanticColors === void 0 || (_getSemanticColors = _getSemanticColors.base) === null || _getSemanticColors === void 0 ? void 0 : _getSemanticColors.backgroundPrimary;
1739
+ (0, react.useImperativeHandle)(ref, () => ({
1740
+ hide: () => setState((prev) => prev.anchorEl === null ? prev : _objectSpread2(_objectSpread2({}, prev), {}, { anchorEl: null })),
1741
+ show: (nextAnchorEl, nextLabel) => setState({
1742
+ anchorEl: nextAnchorEl,
1743
+ label: nextLabel
1744
+ })
1745
+ }), []);
1746
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_czi_sds_components.Tooltip, {
1747
+ arrow: true,
1748
+ disableInteractive: true,
1749
+ open: anchorEl !== null,
1750
+ placement: "top",
1751
+ slotProps: {
1752
+ popper: {
1753
+ anchorEl,
1754
+ modifiers: [{
1755
+ name: "offset",
1756
+ options: { offset: [0, MAIN_AXIS_OFFSET] }
1757
+ }]
1758
+ },
1759
+ tooltip: { style: { backgroundColor: residueTooltipBackgroundColor } },
1760
+ arrow: { style: { color: residueTooltipBackgroundColor } }
1761
+ },
1762
+ title: label,
1763
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: {
1764
+ height: 0,
1765
+ position: "fixed",
1766
+ width: 0
1767
+ } })
1768
+ });
1769
+ });
1770
+ //#endregion
1771
+ //#region src/core/ProteinStructureViewer/components/SequenceView/components/Sequence/index.tsx
1772
+ /**
1773
+ * One residue. Not a component: the grid can hold thousands of these, and
1774
+ * giving each its own fiber costs more than it saves, since the markers that
1775
+ * actually change bypass React anyway.
1776
+ */
1777
+ function renderResidue(sequenceWrapper, seqIdx, seqNum) {
1778
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1779
+ className: getResidueClass(sequenceWrapper, seqIdx),
1780
+ "data-seqid": seqIdx,
1781
+ children: [seqNum && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1782
+ className: "msp-sequence-number",
1783
+ children: seqNum
1784
+ }), sequenceWrapper.residueLabel(seqIdx)]
1785
+ }, seqIdx);
1786
+ }
1787
+ /** Interactive residue grid, kept in sync with the 3D view's hover and selection. */
1788
+ const Sequence = ({ chainLabel, hideSequenceNumbers, residueColors, sequenceNumberPeriod, sequenceWrapper }) => {
1789
+ const containerRef = (0, react.useRef)(null);
1790
+ /** Scratch location reused by every sequence-number lookup below. */
1791
+ const locationRef = (0, react.useRef)(molstar_lib_mol_model_structure.StructureElement.Location.create(void 0));
1792
+ useResidueMarkers({
1793
+ containerRef,
1794
+ residueColors,
1795
+ sequenceWrapper
1796
+ });
1797
+ const { handlers, tooltipRef } = useResiduePointer({
1798
+ containerRef,
1799
+ sequenceWrapper
1800
+ });
1801
+ const period = resolveSequenceNumberPeriod(sequenceWrapper, locationRef.current, sequenceNumberPeriod);
1802
+ const groups = [];
1803
+ for (let start = 0; start < sequenceWrapper.length; start += 10) {
1804
+ const end = Math.min(start + 10, sequenceWrapper.length);
1805
+ const residues = [];
1806
+ for (let i = start; i < end; i++) {
1807
+ const showNum = !hideSequenceNumbers && i % period === 0;
1808
+ residues.push(renderResidue(sequenceWrapper, i, showNum ? getSequenceNumber(sequenceWrapper, i, locationRef.current) : null));
1809
+ }
1810
+ groups.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: residues }, `group-${start}`));
1811
+ }
1812
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResidueGrid, _objectSpread2(_objectSpread2({
1813
+ "aria-label": buildSequenceLabel(sequenceWrapper, chainLabel),
1814
+ className: "msp-sequence-wrapper",
1815
+ ref: containerRef,
1816
+ role: "img"
1817
+ }, handlers), {}, { children: groups })), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResidueTooltip, { ref: tooltipRef })] });
1818
+ };
1819
+ //#endregion
1820
+ //#region src/core/ProteinStructureViewer/components/SequenceView/utils/copyText.ts
1821
+ /**
1822
+ * Writes `text` to the clipboard.
1823
+ *
1824
+ * The async Clipboard API is preferred, but nested srcdoc sandboxes — MCP
1825
+ * inspectors, Claude Desktop, and similar hosts — often omit `clipboard-write`
1826
+ * from their Permissions Policy, so `navigator.clipboard.writeText` rejects
1827
+ * even on a user click. `document.execCommand("copy")` only needs that click, so
1828
+ * it covers those hosts without the consumer having to patch the bundle.
1829
+ */
1830
+ function copyText(text) {
1831
+ var _navigator$clipboard;
1832
+ if (!((_navigator$clipboard = navigator.clipboard) === null || _navigator$clipboard === void 0 ? void 0 : _navigator$clipboard.writeText)) return copyWithSelection(text);
1833
+ return navigator.clipboard.writeText(text).catch(() => copyWithSelection(text));
1834
+ }
1835
+ /**
1836
+ * Copies by selecting a transient textarea. Must run under user activation,
1837
+ * which a click still has after the Clipboard API has been refused.
1838
+ */
1839
+ function copyWithSelection(text) {
1840
+ return new Promise((resolve, reject) => {
1841
+ const area = document.createElement("textarea");
1842
+ area.value = text;
1843
+ area.setAttribute("readonly", "");
1844
+ area.setAttribute("aria-hidden", "true");
1845
+ area.style.position = "fixed";
1846
+ area.style.top = "-1000px";
1847
+ document.body.appendChild(area);
1848
+ area.select();
1849
+ area.setSelectionRange(0, text.length);
1850
+ let copied = false;
1851
+ try {
1852
+ copied = document.execCommand("copy");
1853
+ } catch (_unused) {
1854
+ copied = false;
1855
+ }
1856
+ area.remove();
1857
+ if (copied) resolve();
1858
+ else reject(/* @__PURE__ */ new Error("The host blocked clipboard access."));
1859
+ });
1860
+ }
1861
+ //#endregion
1862
+ //#region src/core/ProteinStructureViewer/components/SequenceView/utils/sequenceText.ts
1863
+ /**
1864
+ * The structure's residues as text, one section per chain separated by
1865
+ * `CHAIN_SEPARATOR`. Placeholder entries contribute nothing, since they have no
1866
+ * residues to write.
1867
+ */
1868
+ function sequenceTextFromEntries(entries) {
1869
+ return entries.filter((entry) => typeof entry.wrapper !== "string").map((entry) => {
1870
+ const wrapper = entry.wrapper;
1871
+ return Array.from({ length: wrapper.length }, (_, i) => wrapper.residueLabel(i)).join("");
1872
+ }).join("|");
1873
+ }
1874
+ //#endregion
1875
+ //#region src/core/ProteinStructureViewer/components/SequenceView/hooks/useCopySequence.ts
1876
+ /**
1877
+ * Copies the structure's residues to the clipboard, chains separated so a
1878
+ * complex does not paste as one sequence, and holds a confirmation flag for a
1879
+ * moment afterwards.
1880
+ */
1881
+ function useCopySequence(entries) {
1882
+ const [copied, setCopied] = (0, react.useState)(false);
1883
+ const timeoutRef = (0, react.useRef)(null);
1884
+ (0, react.useEffect)(() => () => {
1885
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
1886
+ }, []);
1887
+ return {
1888
+ copied,
1889
+ copySequence: (0, react.useCallback)(() => {
1890
+ copyText(sequenceTextFromEntries(entries)).then(() => {
1891
+ setCopied(true);
1892
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
1893
+ timeoutRef.current = setTimeout(() => setCopied(false), COPIED_FEEDBACK_MS);
1894
+ }).catch(() => {});
1895
+ }, [entries])
1896
+ };
1897
+ }
1898
+ //#endregion
1899
+ //#region src/core/ProteinStructureViewer/components/SequenceView/hooks/useSequenceWrappers.ts
1900
+ const EMPTY_STATE = {
1901
+ chainGroupId: -1,
1902
+ modelEntityId: "",
1903
+ operatorKey: "",
1904
+ structure: molstar_lib_mol_model_structure.Structure.Empty,
1905
+ structureOptions: {
1906
+ all: [],
1907
+ options: []
1908
+ },
1909
+ structureRef: ""
1910
+ };
1911
+ function readStructure(plugin, ref) {
1912
+ const cell = plugin.state.data.select(ref)[0];
1913
+ if (!ref || !cell || !cell.obj) return molstar_lib_mol_model_structure.Structure.Empty;
1914
+ return cell.obj.data;
1915
+ }
1916
+ /** Picks the first structure, entity, chain, and operator Mol* offers. */
1917
+ function readInitialState(plugin) {
1918
+ const structureOptions = (0, molstar_lib_mol_plugin_ui_sequence.getStructureOptions)(plugin.state.data);
1919
+ const structureRef = structureOptions.options[0][0];
1920
+ const structure = readStructure(plugin, structureRef);
1921
+ const modelEntityId = (0, molstar_lib_mol_plugin_ui_sequence.getModelEntityOptions)(structure)[0][0];
1922
+ const chainGroupId = (0, molstar_lib_mol_plugin_ui_sequence.getChainOptions)(structure, modelEntityId)[0][0];
1923
+ return {
1924
+ chainGroupId,
1925
+ modelEntityId,
1926
+ operatorKey: (0, molstar_lib_mol_plugin_ui_sequence.getOperatorOptions)(structure, modelEntityId, chainGroupId)[0][0],
1927
+ structure,
1928
+ structureOptions,
1929
+ structureRef
1930
+ };
1931
+ }
1932
+ function buildParams(state) {
1933
+ const { modelEntityId, structure, structureOptions } = state;
1934
+ const chainOptions = (0, molstar_lib_mol_plugin_ui_sequence.getChainOptions)(structure, modelEntityId);
1935
+ return {
1936
+ chain: molstar_lib_mol_util_param_definition.ParamDefinition.Select(chainOptions[0][0], chainOptions, { shortLabel: true }),
1937
+ structure: molstar_lib_mol_util_param_definition.ParamDefinition.Select(structureOptions.options[0][0], structureOptions.options, { shortLabel: true })
1938
+ };
1939
+ }
1940
+ /** One entry per chain, in Mol*'s entity / chain / operator order. */
1941
+ function buildEntries(structure, selection, _params) {
1942
+ 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({
1944
+ id: `${modelEntityId}:${chainGroupId}:${operatorKey}`,
1945
+ label: cLabel,
1946
+ wrapper: (0, molstar_lib_mol_plugin_ui_sequence.getSequenceWrapper)({
1947
+ chainGroupId,
1948
+ modelEntityId,
1949
+ operatorKey,
1950
+ structure
1951
+ }, selection)
1952
+ });
1953
+ return entries;
1954
+ }
1955
+ function countResidues(entries) {
1956
+ return entries.reduce((sum, entry) => sum + (typeof entry.wrapper === "string" ? 0 : entry.wrapper.length), 0);
1957
+ }
1958
+ /**
1959
+ * The residue wrappers for every chain of the loaded structure, resynced
1960
+ * whenever Mol* creates, updates, or removes a structure object.
1961
+ *
1962
+ * The wrappers read the live selection, so they are rebuilt on each render
1963
+ * rather than memoized; that is what keeps the grid's dimming in step with a
1964
+ * selection made in the 3D view.
1965
+ */
1966
+ function useSequenceWrappers() {
1967
+ const plugin = usePlugin();
1968
+ const [state, setState] = (0, react.useState)(EMPTY_STATE);
1969
+ const sync = (0, react.useCallback)(() => {
1970
+ if ((0, molstar_lib_mol_util.arrayEqual)((0, molstar_lib_mol_plugin_ui_sequence.getStructureOptions)(plugin.state.data).all, state.structureOptions.all)) return;
1971
+ setState(readInitialState(plugin));
1972
+ }, [plugin, state.structureOptions.all]);
1973
+ (0, react.useEffect)(() => {
1974
+ if (plugin.state.data.select(molstar_lib_mol_state_state_selection.StateSelection.Generators.rootsOfType(molstar_lib_mol_plugin_state_objects.PluginStateObject.Molecule.Structure)).length > 0) setState(readInitialState(plugin));
1975
+ }, []);
1976
+ useSubscribe(plugin.state.events.object.updated, ({ ref, obj }) => {
1977
+ if (ref === state.structureRef && obj && obj.type === molstar_lib_mol_plugin_state_objects.PluginStateObject.Molecule.Structure.type && obj.data !== state.structure) sync();
1978
+ });
1979
+ useSubscribe(plugin.state.events.object.created, ({ obj }) => {
1980
+ if (obj && obj.type === molstar_lib_mol_plugin_state_objects.PluginStateObject.Molecule.Structure.type) sync();
1981
+ });
1982
+ useSubscribe(plugin.state.events.object.removed, ({ obj }) => {
1983
+ if (obj && obj.type === molstar_lib_mol_plugin_state_objects.PluginStateObject.Molecule.Structure.type) sync();
1984
+ });
1985
+ const structure = readStructure(plugin, state.structureRef);
1986
+ if (structure === molstar_lib_mol_model_structure.Structure.Empty) return {
1987
+ entries: [],
1988
+ isEmpty: true,
1989
+ residueCount: 0
1990
+ };
1991
+ const entries = buildEntries(structure, plugin.managers.structure.selection, buildParams(state));
1992
+ return {
1993
+ entries,
1994
+ isEmpty: false,
1995
+ residueCount: countResidues(entries)
1996
+ };
1997
+ }
1998
+ //#endregion
1999
+ //#region src/core/ProteinStructureViewer/components/SequenceView/index.tsx
2000
+ /**
2001
+ * Builds the Mol* sequence panel view.
2002
+ *
2003
+ * Like the viewport, Mol* renders this inside its own React root, so the theme
2004
+ * mode and the panel's background arrive through `viewSettings` and are watched
2005
+ * for changes rather than read once. The theme is re-supplied through a local
2006
+ * `ThemeProvider` for the Emotion styles and the SDS `Tooltip` below.
2007
+ */
2008
+ function createSequenceView(viewSettings) {
2009
+ return function SequenceView() {
2010
+ const { mode, theme } = useMolstarTheme(viewSettings);
2011
+ const backgroundColor = useViewSetting(viewSettings, (s) => s.sequenceViewerBackgroundColor);
2012
+ const { entries, isEmpty, residueCount } = useSequenceWrappers();
2013
+ const { copied, copySequence } = useCopySequence(entries);
2014
+ if (isEmpty) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_mui_material_styles.ThemeProvider, {
2015
+ theme,
2016
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmptyState, {
2017
+ backgroundColor,
2018
+ className: "msp-sequence",
2019
+ children: "No structure available"
2020
+ })
2021
+ });
2022
+ const residueColors = residueColorsForMode(mode);
2023
+ const showChainLabels = entries.length > 1;
2024
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_mui_material_styles.ThemeProvider, {
2025
+ theme,
2026
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(SequencePanel, {
2027
+ backgroundColor,
2028
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(PanelHeader, { children: [
2029
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PanelTitle, { children: "Sequence" }),
2030
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_czi_sds_components.Tooltip, {
2031
+ arrow: true,
2032
+ placement: "top",
2033
+ title: copied ? "Copied!" : "Copy",
2034
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CopyButton, {
2035
+ "aria-label": "Copy sequence to clipboard",
2036
+ onClick: copySequence,
2037
+ type: "button",
2038
+ children: copied ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_phosphor_icons_react.CheckIcon, { size: 16 }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_phosphor_icons_react.CopyIcon, { size: 16 })
2039
+ })
2040
+ }),
2041
+ /* @__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
+ ] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SequenceScroller, {
2043
+ 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))
2054
+ })
2055
+ })]
2056
+ })
2057
+ });
2058
+ };
2059
+ }
2060
+ //#endregion
2061
+ //#region src/core/ProteinStructureViewer/components/Viewport/constants.ts
2062
+ /**
2063
+ * Fraction of the bounding sphere radius within which the camera still counts
2064
+ * as sitting at the default view, absorbing easing and float error.
2065
+ */
2066
+ const DEFAULT_VIEW_TOLERANCE = .02;
2067
+ //#endregion
2068
+ //#region src/core/ProteinStructureViewer/components/Viewport/utils/camera.ts
2069
+ /** True when the camera's zoom/center differs from the default fit. */
2070
+ function isZoomedOffDefault(canvas3d) {
2071
+ const { center, radius } = canvas3d.boundingSphereVisible;
2072
+ if (radius <= 0) return false;
2073
+ const { state } = canvas3d.camera;
2074
+ const tolerance = radius * DEFAULT_VIEW_TOLERANCE;
2075
+ return Math.abs(state.radius - radius) > tolerance || molstar_lib_mol_math_linear_algebra.Vec3.distance(state.target, center) > tolerance;
2076
+ }
2077
+ //#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
+ //#region src/core/ProteinStructureViewer/components/Viewport/hooks/useCameraReset.ts
2105
+ /**
2106
+ * Tracks whether the camera has moved off the default view, and puts it back.
2107
+ *
2108
+ * The default view is the auto-fit to the structure's bounding sphere, so
2109
+ * "moved" means either the zoom/center has drifted off that fit or a manual
2110
+ * drag has rotated it - rotation has to be tracked separately, since it leaves
2111
+ * the zoom and center at the default and the bounding-sphere check cannot see
2112
+ * it.
2113
+ */
2114
+ function useCameraReset() {
2115
+ const plugin = usePlugin();
2116
+ const [cameraChanged, setCameraChanged] = (0, react.useState)(false);
2117
+ /**
2118
+ * Mirrors the state above so the per-frame handler below can skip redundant
2119
+ * updates without re-reading state through a stale closure.
2120
+ */
2121
+ const cameraChangedRef = (0, react.useRef)(false);
2122
+ const updateCameraChanged = (0, react.useCallback)((changed) => {
2123
+ if (changed === cameraChangedRef.current) return;
2124
+ cameraChangedRef.current = changed;
2125
+ setCameraChanged(changed);
2126
+ }, []);
2127
+ /**
2128
+ * A manual gesture (rotate via drag) moves the camera off the default view.
2129
+ * Tracked separately from zoom because rotation leaves the zoom/center at
2130
+ * the default fit, so the bounding-sphere check cannot detect it.
2131
+ */
2132
+ const gesturedRef = (0, react.useRef)(false);
2133
+ /**
2134
+ * A (re)loaded structure auto-fits to the default view. Ignore camera
2135
+ * changes until that fit transition settles so the button does not flash
2136
+ * during load.
2137
+ */
2138
+ const awaitingFitRef = (0, react.useRef)(true);
2139
+ const fitStartedRef = (0, react.useRef)(false);
2140
+ /**
2141
+ * Reconciles the button visibility with the current camera. The button
2142
+ * shows when the camera differs from the default view: either the
2143
+ * zoom/center has moved off the bounding-sphere fit, or a manual gesture
2144
+ * rotated the view.
2145
+ */
2146
+ const syncCameraChanged = (0, react.useCallback)(() => {
2147
+ const canvas3d = plugin.canvas3d;
2148
+ if (!canvas3d) return;
2149
+ const inTransition = canvas3d.camera.transition.inTransition;
2150
+ if (awaitingFitRef.current) {
2151
+ if (inTransition) {
2152
+ fitStartedRef.current = true;
2153
+ return;
2154
+ }
2155
+ if (!fitStartedRef.current && isZoomedOffDefault(canvas3d)) return;
2156
+ awaitingFitRef.current = false;
2157
+ }
2158
+ if (inTransition) return;
2159
+ updateCameraChanged(gesturedRef.current || isZoomedOffDefault(canvas3d));
2160
+ }, [plugin, updateCameraChanged]);
2161
+ (0, react.useEffect)(() => {
2162
+ const subscriptions = [];
2163
+ let cameraBound = false;
2164
+ const bindCamera = () => {
2165
+ const canvas3d = plugin.canvas3d;
2166
+ if (!canvas3d || cameraBound) return;
2167
+ cameraBound = true;
2168
+ const setGestured = () => {
2169
+ gesturedRef.current = true;
2170
+ syncCameraChanged();
2171
+ };
2172
+ subscriptions.push(canvas3d.input.drag.subscribe(setGestured), canvas3d.input.wheel.subscribe(setGestured), plugin.state.data.events.object.created.subscribe(() => {
2173
+ gesturedRef.current = false;
2174
+ awaitingFitRef.current = true;
2175
+ fitStartedRef.current = false;
2176
+ updateCameraChanged(false);
2177
+ }), canvas3d.didDraw.subscribe(syncCameraChanged));
2178
+ };
2179
+ subscriptions.push(plugin.behaviors.canvas3d.initialized.subscribe(bindCamera));
2180
+ bindCamera();
2181
+ return () => subscriptions.forEach((s) => s.unsubscribe());
2182
+ }, [
2183
+ plugin,
2184
+ syncCameraChanged,
2185
+ updateCameraChanged
2186
+ ]);
2187
+ return {
2188
+ cameraChanged,
2189
+ resetCamera: (0, react.useCallback)(_asyncToGenerator(function* () {
2190
+ yield molstar_lib_mol_plugin_commands.PluginCommands.Camera.Reset(plugin, { durationMs: 250 });
2191
+ yield new Promise((resolve) => setTimeout(resolve, 250));
2192
+ yield molstar_lib_mol_plugin_commands.PluginCommands.Camera.ResetAxes(plugin, { durationMs: 250 });
2193
+ yield new Promise((resolve) => setTimeout(resolve, 250));
2194
+ gesturedRef.current = false;
2195
+ updateCameraChanged(false);
2196
+ }), [plugin, updateCameraChanged])
2197
+ };
2198
+ }
2199
+ //#endregion
2200
+ //#region src/core/ProteinStructureViewer/components/Viewport/style.ts
2201
+ /**
2202
+ * Pinned to the lower-left of the viewport, clear of the sequence panel below
2203
+ * and the axes widget in the corner.
2204
+ */
2205
+ const ResetCameraSlot = (0, _emotion_styled.default)("div")`
2206
+ position: absolute;
2207
+ bottom: 48px;
2208
+ z-index: 10;
2209
+
2210
+ ${(props) => {
2211
+ const spaces = (0, _czi_sds_components.getSpaces)(props);
2212
+ return `
2213
+ left: ${spaces === null || spaces === void 0 ? void 0 : spaces.m}px;
2214
+ `;
2215
+ }}
2216
+ `;
2217
+ //#endregion
2218
+ //#region src/core/ProteinStructureViewer/components/Viewport/index.tsx
2219
+ /**
2220
+ * Builds the Mol* viewport view.
2221
+ *
2222
+ * Mol* instantiates view components inside its own React root, so they render
2223
+ * outside the consumer's provider tree and cannot read the theme from context.
2224
+ * `viewSettings` carries the mode in instead, and is watched rather than read
2225
+ * once so that a theme change after the plugin was created still lands here;
2226
+ * the mode is re-supplied via a local `ThemeProvider` so the Emotion styles
2227
+ * below resolve SDS tokens correctly.
2228
+ */
2229
+ function createViewportView(viewSettings) {
2230
+ return function ViewportWithReset() {
2231
+ const { theme } = useMolstarTheme(viewSettings);
2232
+ const { cameraChanged, resetCamera } = useCameraReset();
2233
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2234
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(molstar_lib_mol_plugin_ui_viewport.Viewport, {}),
2235
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2236
+ className: "msp-highlight-toast-wrapper",
2237
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(molstar_lib_mol_plugin_ui_toast.Toasts, {})
2238
+ }),
2239
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_mui_material_styles.ThemeProvider, {
2240
+ 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,
2246
+ title: "Reset camera",
2247
+ sdsStyle: "minimal",
2248
+ sdsType: "secondary",
2249
+ size: "medium",
2250
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_phosphor_icons_react.ArrowCounterClockwiseIcon, {})
2251
+ })
2252
+ })
2253
+ })
2254
+ ] });
2255
+ };
2256
+ }
2257
+ //#endregion
2258
+ //#region src/core/ProteinStructureViewer/utils/cameraFocus.ts
2259
+ /**
2260
+ * Margin (angstroms) added around a residue's bounding sphere when focusing,
2261
+ * matching Mol*'s default `FocusLoci` extra radius. Used both for the framing
2262
+ * radius that sets the zoom-in distance (floored at `FOCUS_MIN_RADIUS`) and for
2263
+ * the depth-clip anchor, so the focus crop matches Mol*'s default depth slab.
2264
+ */
2265
+ const FOCUS_MIN_RADIUS = 8;
2266
+ const FOCUS_EXTRA_RADIUS = 4;
2267
+ /**
2268
+ * Never let the clip window collapse below this, so the focused residue and its
2269
+ * immediate neighborhood stay visible even at extreme zoom-in.
2270
+ */
2271
+ const MIN_CLIP_RADIUS = 4;
2272
+ /** Camera transition length (ms) when zooming in on a residue. */
2273
+ const FOCUS_DURATION_MS = 250;
2274
+ /** Clip radius has to drift this far before it is worth redrawing. */
2275
+ const CLIP_EPSILON = .5;
2276
+ /**
2277
+ * Depth-clip radius at a given camera distance. The clip scales linearly with
2278
+ * distance, `ratio` being the clip radius per unit distance (fixed at focus so
2279
+ * the clip hugs the residue there), clamped to the scene. This keeps clipping
2280
+ * tied to the zoom:
2281
+ * - at the focus zoom the clip hugs the residue, peeling the structure so the
2282
+ * selected residue reads clearly;
2283
+ * - zooming in shrinks the clip further, peeling deeper toward the residue;
2284
+ * - zooming out grows the clip, revealing more of the structure, up to the
2285
+ * whole scene (`radiusMax`) - the point at which nothing is cropped.
2286
+ */
2287
+ function clipRadiusForDistance(distance, ratio, radiusMax) {
2288
+ return Math.max(MIN_CLIP_RADIUS, Math.min(ratio * distance, radiusMax));
2289
+ }
2290
+ /**
2291
+ * Zooms the camera in on a residue. Uses Mol*'s focus framing for the camera
2292
+ * *position* (so the zoom level matches the built-in focus) but anchors the
2293
+ * clip tightly on the residue itself, and returns the clip-radius-per-distance
2294
+ * ratio used to keep clipping in sync with the zoom afterwards.
2295
+ */
2296
+ function focusResidue(plugin, loci) {
2297
+ var _plugin$canvas3d;
2298
+ const camera = (_plugin$canvas3d = plugin.canvas3d) === null || _plugin$canvas3d === void 0 ? void 0 : _plugin$canvas3d.camera;
2299
+ if (!camera) return null;
2300
+ const sphere = molstar_lib_mol_model_loci.Loci.getBoundingSphere(loci);
2301
+ if (!sphere) return null;
2302
+ const framingRadius = Math.max(sphere.radius + FOCUS_EXTRA_RADIUS, FOCUS_MIN_RADIUS);
2303
+ const snapshot = camera.getFocus(sphere.center, framingRadius);
2304
+ const focusDistance = camera.getTargetDistance(framingRadius);
2305
+ const clipRatio = (sphere.radius + FOCUS_EXTRA_RADIUS) / focusDistance;
2306
+ const radius = clipRadiusForDistance(focusDistance, clipRatio, camera.state.radiusMax);
2307
+ camera.setState(_objectSpread2(_objectSpread2({}, snapshot), {}, { radius }), FOCUS_DURATION_MS);
2308
+ return clipRatio;
2309
+ }
2310
+ /**
2311
+ * Keeps the depth clip in sync with the zoom while a residue is focused.
2312
+ *
2313
+ * Mol*'s trackball zoom moves the camera without touching the clip radius, so
2314
+ * this recomputes it after each draw: zooming in tightens the clip (peeling
2315
+ * toward the residue), zooming out widens it toward the whole structure. It is
2316
+ * a no-op when `getClipRatio` returns null (nothing focused) and while the
2317
+ * camera is mid-transition, so it does not fight the focus animation.
2318
+ */
2319
+ 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;
2322
+ const clipRatio = getClipRatio();
2323
+ if (!camera || clipRatio === null || camera.transition.inTransition) return;
2324
+ const radius = clipRadiusForDistance(molstar_lib_mol_math_linear_algebra.Vec3.distance(camera.state.position, camera.state.target), clipRatio, camera.state.radiusMax);
2325
+ if (Math.abs(camera.state.radius - radius) > CLIP_EPSILON) {
2326
+ var _plugin$canvas3d3;
2327
+ camera.setState({ radius });
2328
+ (_plugin$canvas3d3 = plugin.canvas3d) === null || _plugin$canvas3d3 === void 0 || _plugin$canvas3d3.requestDraw();
2329
+ }
2330
+ }
2331
+ //#endregion
2332
+ //#region src/core/ProteinStructureViewer/utils/residueRef.ts
2333
+ /**
2334
+ * Reads the residue a loci points at, or null when it points at nothing the
2335
+ * viewer can address.
2336
+ *
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.
2342
+ */
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;
2348
+ 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)
2354
+ };
2355
+ }
2356
+ /**
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.
2360
+ */
2361
+ function residueLabel(residue) {
2362
+ return `${residue.compId} ${residue.seqId}${residue.insCode}`;
2363
+ }
2364
+ //#endregion
2365
+ //#region src/core/ProteinStructureViewer/utils/plddt.ts
2366
+ /** Mol* color theme name under which pLDDT coloring is registered. */
2367
+ const PLDDT_THEME_NAME = "plddt-bfactor";
2368
+ /**
2369
+ * AlphaFold's pLDDT confidence bands. Thresholds are on the 0-1 scale that
2370
+ * `plddt` values use, ascending from lowest to highest confidence.
2371
+ */
2372
+ const PLDDT_COLOR_SCALE = {
2373
+ kind: "stepped",
2374
+ stops: [
2375
+ {
2376
+ color: "#FF7C45",
2377
+ label: "0.5",
2378
+ threshold: .5
2379
+ },
2380
+ {
2381
+ color: "#FFDB11",
2382
+ label: "0.7",
2383
+ threshold: .7
2384
+ },
2385
+ {
2386
+ color: "#64CBF3",
2387
+ label: "0.9",
2388
+ threshold: .9
2389
+ },
2390
+ {
2391
+ color: "#0053D5",
2392
+ label: "1.0",
2393
+ threshold: 1
2394
+ }
2395
+ ]
2396
+ };
2397
+ const PLDDT_BANDS = PLDDT_COLOR_SCALE.kind === "stepped" ? PLDDT_COLOR_SCALE.stops : [];
2398
+ const DEFAULT_PLDDT_COLOR = molstar_lib_mol_util_color.Color.fromRgb(128, 128, 128);
2399
+ /**
2400
+ * Maps a pLDDT confidence score on the 0-100 B-factor scale to its band color.
2401
+ * `PLDDT_COLOR_SCALE` thresholds are on the 0-1 scale, so the score is
2402
+ * normalized before it is sampled.
2403
+ */
2404
+ function plddtToColor(value) {
2405
+ const [r, g, b] = sampleSteppedScale(PLDDT_BANDS, value / 100);
2406
+ return molstar_lib_mol_util_color.Color.fromRgb(r, g, b);
2407
+ }
2408
+ /**
2409
+ * Rewrites a PDB file's B-factor column with pLDDT scores so Mol* can read them
2410
+ * off the parsed model. B-factors occupy columns 60-66 of ATOM/HETATM lines;
2411
+ * scores arrive on a 0-1 scale and are stored on the conventional 0-100 one.
2412
+ * Residues past the end of `plddtValues` fall back to a mid-confidence 50.
2413
+ *
2414
+ * A residue is identified by chain, sequence number and insertion code. The
2415
+ * code has to be part of that: `10` and `10A` are two residues, Mol* counts
2416
+ * them as two, and treating them as one here would hand every residue after
2417
+ * them its neighbour's score.
2418
+ */
2419
+ function injectPlddtIntoPdb(pdbData, plddtValues) {
2420
+ const lines = pdbData.split("\n");
2421
+ let residueIndex = -1;
2422
+ let lastResidue = "";
2423
+ return lines.map((line) => {
2424
+ var _plddtValues$residueI;
2425
+ if (!line.startsWith("ATOM") && !line.startsWith("HETATM")) return line;
2426
+ const residue = line.substring(21, 27);
2427
+ if (residue !== lastResidue) {
2428
+ residueIndex++;
2429
+ lastResidue = residue;
2430
+ }
2431
+ const bfactorStr = (residueIndex < plddtValues.length ? ((_plddtValues$residueI = plddtValues[residueIndex]) !== null && _plddtValues$residueI !== void 0 ? _plddtValues$residueI : 0) * 100 : 50).toFixed(2).padStart(6, " ");
2432
+ const paddedLine = line.padEnd(66, " ");
2433
+ return paddedLine.substring(0, 60) + bfactorStr + paddedLine.substring(66);
2434
+ }).join("\n");
2435
+ }
2436
+ /**
2437
+ * Mol* color theme that reads pLDDT scores out of the B-factor column, paired
2438
+ * with `injectPlddtIntoPdb`. Registered per plugin instance.
2439
+ */
2440
+ const PlddtColoring = molstar_lib_mol_model_props_common_custom_element_property.CustomElementProperty.create({
2441
+ coloring: {
2442
+ defaultColor: DEFAULT_PLDDT_COLOR,
2443
+ getColor(e) {
2444
+ return plddtToColor(e);
2445
+ }
2446
+ },
2447
+ getData(model) {
2448
+ const map = /* @__PURE__ */ new Map();
2449
+ const atomCount = model.atomicHierarchy.atoms._rowCount;
2450
+ for (let i = 0; i < atomCount; i++) map.set(i, model.atomicConformation.B_iso_or_equiv.value(i));
2451
+ return { value: map };
2452
+ },
2453
+ label: "pLDDT",
2454
+ 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
+ }
2480
+ //#endregion
2481
+ //#region src/core/ProteinStructureViewer/utils/residueValueTheme.ts
2482
+ /** Mol* color theme name under which residue value overlays are registered. */
2483
+ const RESIDUE_VALUE_THEME_NAME = "residue-value";
2484
+ /**
2485
+ * Builds a Mol* color theme that paints residues by an arbitrary per-residue
2486
+ * value.
2487
+ *
2488
+ * The theme is created per plugin instance rather than shared at module scope,
2489
+ * so several viewers can render different overlays on the same page. Its state
2490
+ * lives in the closure below and is read live inside `color()`, which is what
2491
+ * lets an overlay update recolor the structure without rebuilding it.
2492
+ */
2493
+ function createResidueValueTheme(mode) {
2494
+ const state = {
2495
+ colorScale: PLASMA_COLOR_SCALE,
2496
+ max: 1,
2497
+ min: 0,
2498
+ mode,
2499
+ values: /* @__PURE__ */ new Map(),
2500
+ version: 0
2501
+ };
2502
+ const provider = {
2503
+ category: "residue property",
2504
+ defaultValues: {},
2505
+ factory() {
2506
+ const { colorScale, max, min, mode: currentMode, values } = state;
2507
+ const neutral = neutralResidueColor(currentMode);
2508
+ return {
2509
+ color(location) {
2510
+ if (!molstar_lib_mol_model_structure.StructureElement.Location.is(location)) return neutral;
2511
+ const value = values.get(molstar_lib_mol_model_structure.StructureProperties.residue.key(location));
2512
+ if (value === void 0) return neutral;
2513
+ const rgb = sampleColorScale(colorScale, value, max, min);
2514
+ return rgb ? molstar_lib_mol_util_color.Color.fromRgb(rgb[0], rgb[1], rgb[2]) : neutral;
2515
+ },
2516
+ contextHash: state.version,
2517
+ description: "Color residues by a per-residue value",
2518
+ factory: provider.factory,
2519
+ granularity: "group",
2520
+ props: {}
2521
+ };
2522
+ },
2523
+ getParams: () => ({}),
2524
+ isApplicable: () => true,
2525
+ label: "Residue Value",
2526
+ name: RESIDUE_VALUE_THEME_NAME
2527
+ };
2528
+ return {
2529
+ provider,
2530
+ setState(next) {
2531
+ Object.assign(state, next);
2532
+ state.version++;
2533
+ }
2534
+ };
2535
+ }
2536
+ //#endregion
2537
+ //#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";
2542
+ /** Delay before retrying initialization while the container has no size. */
2543
+ const LAYOUT_RETRY_MS = 100;
2544
+ /** How long to wait for a frame before starting without one. */
2545
+ const FRAME_WAIT_MS = 100;
2546
+ /**
2547
+ * Yield once so the container can be laid out before it is measured.
2548
+ *
2549
+ * `requestAnimationFrame` does not fire while the document is hidden -- a
2550
+ * background tab, a collapsed pane, or a headless capture that never paints --
2551
+ * and awaiting it alone leaves the viewer parked forever on a page that has in
2552
+ * fact been laid out. Racing it against a timeout keeps the fast path on a
2553
+ * visible page and still starts on a hidden one, where the size check below is
2554
+ * what actually guards against measuring too early.
2555
+ *
2556
+ * The loser of the race is cancelled rather than left to fire: on a visible
2557
+ * page this runs once per viewer, and a grid of them would otherwise each keep
2558
+ * a stray timer alive past the frame that already resolved.
2559
+ */
2560
+ function nextFrame() {
2561
+ return new Promise((resolve) => {
2562
+ const done = () => {
2563
+ cancelAnimationFrame(frame);
2564
+ clearTimeout(timer);
2565
+ resolve();
2566
+ };
2567
+ const frame = requestAnimationFrame(done);
2568
+ const timer = setTimeout(done, FRAME_WAIT_MS);
2569
+ });
2570
+ }
2571
+ /**
2572
+ * Hover fills the geometry with a tint; selection stays outline-only. Mol*'s
2573
+ * `colorMarker` is a global toggle for the per-fragment marker tint, so it is
2574
+ * scoped to hover via the per-state strengths below.
2575
+ */
2576
+ const HIGHLIGHT_STRENGTH = .2;
2577
+ 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
+ /**
2611
+ * Turns the screenshot controls off, on the versions of Mol* that have them.
2612
+ *
2613
+ * `ShowScreenshotControls` arrived in Mol* 5 and this package's peer range
2614
+ * reaches back to 4, so the key is read through an index rather than named
2615
+ * directly: naming it would not compile against 4, and passing the resulting
2616
+ * `undefined` would ask Mol* to set a config item it has no name for. On 4
2617
+ * there are no screenshot controls to hide in the first place.
2618
+ */
2619
+ const SCREENSHOT_CONTROLS_CONFIG = (() => {
2620
+ const item = molstar_lib_mol_plugin_config.PluginConfig.Viewport.ShowScreenshotControls;
2621
+ return item ? [[item, false]] : [];
2622
+ })();
2623
+ function setAxes(plugin, enabled) {
2624
+ if (!plugin.canvas3d) return;
2625
+ try {
2626
+ plugin.canvas3d.setProps({ camera: { helper: { axes: enabled ? AXES_ON : AXES_OFF } } });
2627
+ } catch (e) {
2628
+ console.warn("Could not set axes:", e);
2629
+ }
2630
+ }
2631
+ function createViewer(_x) {
2632
+ return _createViewer.apply(this, arguments);
2633
+ }
2634
+ function _createViewer() {
2635
+ _createViewer = _asyncToGenerator(function* ({ backgroundColor, edgeColor, highlightColor, residueValueTheme, root, showAxes, showSequenceViewer, viewSettings }) {
2636
+ var _spec$config;
2637
+ const spec = (0, molstar_lib_mol_plugin_ui_spec.DefaultPluginUISpec)();
2638
+ const behaviors = spec.behaviors.filter((b) => b.transformer !== molstar_lib_mol_plugin_behavior.PluginBehaviors.Camera.FocusLoci);
2639
+ const plugin = yield (0, molstar_lib_mol_plugin_ui.createPluginUI)({
2640
+ render: molstar_lib_mol_plugin_ui_react18.renderReact18,
2641
+ spec: _objectSpread2(_objectSpread2({}, spec), {}, {
2642
+ behaviors,
2643
+ canvas3d: {
2644
+ camera: { helper: { axes: showAxes ? AXES_ON : AXES_OFF } },
2645
+ marking: {
2646
+ highlightEdgeColor: edgeColor,
2647
+ selectEdgeColor: edgeColor
2648
+ },
2649
+ renderer: {
2650
+ backgroundColor,
2651
+ colorMarker: true,
2652
+ highlightColor,
2653
+ highlightStrength: HIGHLIGHT_STRENGTH,
2654
+ selectStrength: SELECT_STRENGTH
2655
+ }
2656
+ },
2657
+ components: _objectSpread2({
2658
+ remoteState: "none",
2659
+ sequenceViewer: { view: createSequenceView(viewSettings) }
2660
+ }, showAxes && { viewport: { view: createViewportView(viewSettings) } }),
2661
+ config: [
2662
+ ...(_spec$config = spec.config) !== null && _spec$config !== void 0 ? _spec$config : [],
2663
+ [molstar_lib_mol_plugin_config.PluginConfig.Viewport.ShowExpand, false],
2664
+ [molstar_lib_mol_plugin_config.PluginConfig.Viewport.ShowControls, false],
2665
+ [molstar_lib_mol_plugin_config.PluginConfig.Viewport.ShowSettings, false],
2666
+ [molstar_lib_mol_plugin_config.PluginConfig.Viewport.ShowSelectionMode, false],
2667
+ [molstar_lib_mol_plugin_config.PluginConfig.Viewport.ShowAnimation, false],
2668
+ [molstar_lib_mol_plugin_config.PluginConfig.Viewport.ShowTrajectoryControls, false],
2669
+ ...SCREENSHOT_CONTROLS_CONFIG
2670
+ ],
2671
+ layout: { initial: {
2672
+ controlsDisplay: "reactive",
2673
+ isExpanded: false,
2674
+ regionState: {
2675
+ bottom: "hidden",
2676
+ left: "hidden",
2677
+ right: "hidden",
2678
+ top: showSequenceViewer ? "full" : "hidden"
2679
+ }
2680
+ } }
2681
+ }),
2682
+ target: root
2683
+ });
2684
+ const registry = plugin.representation.structure.themes.colorThemeRegistry;
2685
+ if (PlddtColoring.colorThemeProvider) registry.add(PlddtColoring.colorThemeProvider);
2686
+ registry.add(residueValueTheme.provider);
2687
+ for (const p of [...plugin.managers.lociLabels.providers]) plugin.managers.lociLabels.removeProvider(p);
2688
+ plugin.managers.structure.focus.behaviors.current.subscribe((entry) => {
2689
+ const select = plugin.managers.interactivity.lociSelects;
2690
+ if (entry === null || entry === void 0 ? void 0 : entry.loci) select.selectOnly({ loci: entry.loci });
2691
+ else select.deselectAll();
2692
+ });
2693
+ return plugin;
2694
+ });
2695
+ return _createViewer.apply(this, arguments);
2696
+ }
2697
+ function loadStructure(_x2, _x3, _x4, _x5) {
2698
+ return _loadStructure.apply(this, arguments);
2699
+ }
2700
+ function _loadStructure() {
2701
+ _loadStructure = _asyncToGenerator(function* (plugin, pdbData, usePlddtColoring, showAxes) {
2702
+ try {
2703
+ var _plugin$canvas3d;
2704
+ yield plugin.clear();
2705
+ const data = yield plugin.builders.data.rawData({
2706
+ data: pdbData,
2707
+ label: "Structure"
2708
+ });
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
+ }
2717
+ });
2718
+ if (usePlddtColoring) yield applyColorTheme(plugin, PLDDT_THEME_NAME);
2719
+ (_plugin$canvas3d = plugin.canvas3d) === null || _plugin$canvas3d === void 0 || _plugin$canvas3d.requestCameraReset();
2720
+ setAxes(plugin, showAxes);
2721
+ } catch (error) {
2722
+ console.error("Failed to load structure:", error);
2723
+ }
2724
+ });
2725
+ return _loadStructure.apply(this, arguments);
2726
+ }
2727
+ /** Recolors every loaded structure's representations with the named theme. */
2728
+ function applyColorTheme(_x6, _x7) {
2729
+ return _applyColorTheme.apply(this, arguments);
2730
+ }
2731
+ function _applyColorTheme() {
2732
+ _applyColorTheme = _asyncToGenerator(function* (plugin, colorTheme) {
2733
+ 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 });
2735
+ }));
2736
+ });
2737
+ return _applyColorTheme.apply(this, arguments);
2738
+ }
2739
+ /**
2740
+ * Owns the Mol* plugin for one viewer: creates it, loads the structure, wires
2741
+ * interaction callbacks, and disposes of it on unmount.
2742
+ *
2743
+ * The plugin is built once and then mutated in place. Creating it is expensive
2744
+ * 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.
2747
+ */
2748
+ function useMolstarPlugin({ backgroundColor, containerRef, edgeColor, hasPlddt, highlightColor, mode, onResidueClick, onResidueHover, onSelectionClear, pdb, sequenceViewerBackgroundColor, showAxes, showSequenceViewer }) {
2749
+ const pluginRef = (0, react.useRef)(null);
2750
+ const residueValueThemeRef = (0, react.useRef)(null);
2751
+ const currentPdbRef = (0, react.useRef)(null);
2752
+ const [isReady, setIsReady] = (0, react.useState)(false);
2753
+ /**
2754
+ * Carries the theme-dependent props into the views Mol* renders in its own
2755
+ * React root. They cannot read them from context, and rebuilding the plugin
2756
+ * would throw away the camera, so the values are pushed to them instead.
2757
+ */
2758
+ const viewSettingsRef = (0, react.useRef)(null);
2759
+ if (viewSettingsRef.current === null) viewSettingsRef.current = new rxjs.BehaviorSubject({
2760
+ mode,
2761
+ sequenceViewerBackgroundColor
2762
+ });
2763
+ const viewSettings = viewSettingsRef.current;
2764
+ /**
2765
+ * Clip-radius-per-camera-distance ratio recorded at the last residue focus,
2766
+ * or null when no residue is focused. Anchors the adaptive depth clipping.
2767
+ */
2768
+ const clipRatioRef = (0, react.useRef)(null);
2769
+ const setClipRatio = (0, react.useCallback)((ratio) => {
2770
+ clipRatioRef.current = ratio;
2771
+ }, []);
2772
+ const onResidueClickRef = (0, react.useRef)(onResidueClick);
2773
+ onResidueClickRef.current = onResidueClick;
2774
+ const onSelectionClearRef = (0, react.useRef)(onSelectionClear);
2775
+ onSelectionClearRef.current = onSelectionClear;
2776
+ const onResidueHoverRef = (0, react.useRef)(onResidueHover);
2777
+ onResidueHoverRef.current = onResidueHover;
2778
+ /** Last residue reported to `onResidueHover`, to suppress repeats. */
2779
+ const lastHoverRef = (0, react.useRef)(null);
2780
+ const initialPropsRef = (0, react.useRef)({
2781
+ backgroundColor,
2782
+ edgeColor,
2783
+ hasPlddt,
2784
+ highlightColor,
2785
+ mode,
2786
+ pdb,
2787
+ showAxes,
2788
+ showSequenceViewer
2789
+ });
2790
+ initialPropsRef.current = {
2791
+ backgroundColor,
2792
+ edgeColor,
2793
+ hasPlddt,
2794
+ highlightColor,
2795
+ mode,
2796
+ pdb,
2797
+ showAxes,
2798
+ showSequenceViewer
2799
+ };
2800
+ (0, react.useEffect)(() => {
2801
+ const container = containerRef.current;
2802
+ if (!container) return;
2803
+ let cancelled = false;
2804
+ let clipSubscription;
2805
+ const init = function() {
2806
+ var _ref = _asyncToGenerator(function* () {
2807
+ yield nextFrame();
2808
+ if (cancelled) return;
2809
+ const rect = container.getBoundingClientRect();
2810
+ if (rect.width === 0 || rect.height === 0) {
2811
+ setTimeout(init, LAYOUT_RETRY_MS);
2812
+ return;
2813
+ }
2814
+ const initial = initialPropsRef.current;
2815
+ try {
2816
+ var _plugin$canvas3d2;
2817
+ const residueValueTheme = createResidueValueTheme(initial.mode);
2818
+ const plugin = yield createViewer({
2819
+ backgroundColor: initial.backgroundColor,
2820
+ edgeColor: initial.edgeColor,
2821
+ highlightColor: initial.highlightColor,
2822
+ residueValueTheme,
2823
+ root: container,
2824
+ showAxes: initial.showAxes,
2825
+ showSequenceViewer: initial.showSequenceViewer,
2826
+ viewSettings
2827
+ });
2828
+ if (cancelled) {
2829
+ plugin.dispose();
2830
+ return;
2831
+ }
2832
+ pluginRef.current = plugin;
2833
+ residueValueThemeRef.current = residueValueTheme;
2834
+ const latest = initialPropsRef.current;
2835
+ yield loadStructure(plugin, latest.pdb, latest.hasPlddt, latest.showAxes);
2836
+ currentPdbRef.current = latest.pdb;
2837
+ plugin.behaviors.interaction.click.subscribe((e) => {
2838
+ var _onResidueClickRef$cu;
2839
+ if (molstar_lib_mol_repr_representation.Representation.Loci.isEmpty(e.current)) {
2840
+ var _onSelectionClearRef$;
2841
+ (_onSelectionClearRef$ = onSelectionClearRef.current) === null || _onSelectionClearRef$ === void 0 || _onSelectionClearRef$.call(onSelectionClearRef);
2842
+ return;
2843
+ }
2844
+ const loci = e.current.loci;
2845
+ if (!molstar_lib_mol_model_structure.StructureElement.Loci.is(loci)) return;
2846
+ const residue = residueRefFromLoci(loci);
2847
+ if (!residue) return;
2848
+ (_onResidueClickRef$cu = onResidueClickRef.current) === null || _onResidueClickRef$cu === void 0 || _onResidueClickRef$cu.call(onResidueClickRef, residue);
2849
+ });
2850
+ plugin.behaviors.interaction.hover.subscribe((e) => {
2851
+ var _lastHoverRef$current, _onResidueHoverRef$cu;
2852
+ const loci = e.current.loci;
2853
+ const residue = molstar_lib_mol_model_structure.StructureElement.Loci.is(loci) ? residueRefFromLoci(loci) : null;
2854
+ if ((residue === null || residue === void 0 ? void 0 : residue.index) === ((_lastHoverRef$current = lastHoverRef.current) === null || _lastHoverRef$current === void 0 ? void 0 : _lastHoverRef$current.index)) return;
2855
+ lastHoverRef.current = residue;
2856
+ (_onResidueHoverRef$cu = onResidueHoverRef.current) === null || _onResidueHoverRef$cu === void 0 || _onResidueHoverRef$cu.call(onResidueHoverRef, residue);
2857
+ });
2858
+ clipSubscription = (_plugin$canvas3d2 = plugin.canvas3d) === null || _plugin$canvas3d2 === void 0 ? void 0 : _plugin$canvas3d2.didDraw.subscribe(() => {
2859
+ syncClipToZoom(plugin, () => clipRatioRef.current);
2860
+ });
2861
+ if (!cancelled) setIsReady(true);
2862
+ } catch (error) {
2863
+ console.error("Failed to initialize Mol* viewer:", error);
2864
+ }
2865
+ });
2866
+ return function init() {
2867
+ return _ref.apply(this, arguments);
2868
+ };
2869
+ }();
2870
+ init();
2871
+ return () => {
2872
+ var _pluginRef$current;
2873
+ cancelled = true;
2874
+ clipSubscription === null || clipSubscription === void 0 || clipSubscription.unsubscribe();
2875
+ (_pluginRef$current = pluginRef.current) === null || _pluginRef$current === void 0 || _pluginRef$current.dispose();
2876
+ pluginRef.current = null;
2877
+ residueValueThemeRef.current = null;
2878
+ currentPdbRef.current = null;
2879
+ clipRatioRef.current = null;
2880
+ setIsReady(false);
2881
+ };
2882
+ }, []);
2883
+ (0, react.useEffect)(() => {
2884
+ var _pluginRef$current2;
2885
+ const canvas3d = (_pluginRef$current2 = pluginRef.current) === null || _pluginRef$current2 === void 0 ? void 0 : _pluginRef$current2.canvas3d;
2886
+ if (!canvas3d || !isReady) return;
2887
+ canvas3d.setProps({
2888
+ marking: {
2889
+ highlightEdgeColor: edgeColor,
2890
+ selectEdgeColor: edgeColor
2891
+ },
2892
+ renderer: {
2893
+ backgroundColor,
2894
+ highlightColor
2895
+ }
2896
+ });
2897
+ }, [
2898
+ backgroundColor,
2899
+ edgeColor,
2900
+ highlightColor,
2901
+ isReady
2902
+ ]);
2903
+ (0, react.useEffect)(() => {
2904
+ viewSettings.next({
2905
+ mode,
2906
+ sequenceViewerBackgroundColor
2907
+ });
2908
+ }, [
2909
+ mode,
2910
+ sequenceViewerBackgroundColor,
2911
+ viewSettings
2912
+ ]);
2913
+ (0, react.useEffect)(() => {
2914
+ if (!isReady || !pluginRef.current) return;
2915
+ setAxes(pluginRef.current, showAxes);
2916
+ }, [showAxes, isReady]);
2917
+ (0, react.useEffect)(() => {
2918
+ const plugin = pluginRef.current;
2919
+ if (!plugin || !isReady) return;
2920
+ if (pdb === currentPdbRef.current) return;
2921
+ currentPdbRef.current = pdb;
2922
+ clipRatioRef.current = null;
2923
+ lastHoverRef.current = null;
2924
+ loadStructure(plugin, pdb, hasPlddt, showAxes);
2925
+ }, [
2926
+ pdb,
2927
+ hasPlddt,
2928
+ isReady
2929
+ ]);
2930
+ return {
2931
+ isReady,
2932
+ pluginRef,
2933
+ residueValueThemeRef,
2934
+ setClipRatio
2935
+ };
2936
+ }
2937
+ //#endregion
2938
+ //#region src/core/ProteinStructureViewer/utils/residueLoci.ts
2939
+ /**
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.
2942
+ *
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.
2946
+ *
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.
2950
+ */
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
+ }
2970
+ }
2971
+ //#endregion
2972
+ //#region src/core/ProteinStructureViewer/hooks/useResidueFocus.ts
2973
+ /**
2974
+ * Makes `selectedResidue` the one thing that decides where the camera sits.
2975
+ *
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.
2980
+ *
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.
2984
+ */
2985
+ function useResidueFocus({ isReady, pluginRef, selectedResidue, setClipRatio }) {
2986
+ /**
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.
2992
+ */
2993
+ const focusedRef = (0, react.useRef)(null);
2994
+ (0, react.useEffect)(() => {
2995
+ const plugin = pluginRef.current;
2996
+ if (!plugin || !isReady) {
2997
+ focusedRef.current = null;
2998
+ return;
2999
+ }
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);
3006
+ plugin.managers.structure.focus.clear();
3007
+ (_plugin$canvas3d = plugin.canvas3d) === null || _plugin$canvas3d === void 0 || _plugin$canvas3d.requestCameraReset();
3008
+ return;
3009
+ }
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));
3015
+ }, [
3016
+ selectedResidue,
3017
+ isReady,
3018
+ setClipRatio
3019
+ ]);
3020
+ }
3021
+ //#endregion
3022
+ //#region src/core/ProteinStructureViewer/hooks/useResidueOverlay.ts
3023
+ /**
3024
+ * Switches the structure's coloring between the residue value overlay and the
3025
+ * default (pLDDT, or chain-id when no scores were supplied).
3026
+ *
3027
+ * Recoloring happens in place: the camera is left where it is, so selecting or
3028
+ * switching an overlay does not zoom back out to the default view.
3029
+ */
3030
+ function useResidueOverlay({ hasPlddt, isReady, mode, overlay, pluginRef, residueValueThemeRef }) {
3031
+ (0, react.useEffect)(() => {
3032
+ const plugin = pluginRef.current;
3033
+ const theme = residueValueThemeRef.current;
3034
+ if (!plugin || !theme || !isReady) return;
3035
+ if (overlay) {
3036
+ var _overlay$colorScale, _overlay$min;
3037
+ theme.setState({
3038
+ colorScale: (_overlay$colorScale = overlay.colorScale) !== null && _overlay$colorScale !== void 0 ? _overlay$colorScale : PLASMA_COLOR_SCALE,
3039
+ max: overlay.max,
3040
+ min: (_overlay$min = overlay.min) !== null && _overlay$min !== void 0 ? _overlay$min : 0,
3041
+ mode,
3042
+ values: overlay.values
3043
+ });
3044
+ 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);
3051
+ }
3052
+ }, [
3053
+ overlay,
3054
+ hasPlddt,
3055
+ isReady,
3056
+ mode
3057
+ ]);
3058
+ }
3059
+ //#endregion
3060
+ //#region src/core/ProteinStructureViewer/index.tsx
3061
+ const _excluded$1 = [
3062
+ "backgroundColor",
3063
+ "onResidueClick",
3064
+ "onResidueHover",
3065
+ "onSelectionClear",
3066
+ "pdb",
3067
+ "plddt",
3068
+ "residueOverlay",
3069
+ "selectedResidue",
3070
+ "sequenceViewerBackgroundColor",
3071
+ "showAxes",
3072
+ "showLegend",
3073
+ "showSequenceViewer",
3074
+ "stats"
3075
+ ];
3076
+ /** Fallbacks for theme tokens Mol* needs as concrete hex colors. */
3077
+ const FALLBACK_EDGE_COLOR = "#6c6c6c";
3078
+ const FALLBACK_HIGHLIGHT_COLOR = "#1b1b1b";
3079
+ /**
3080
+ * Keyed by mode: unlike the two above, the canvas has to invert with it. Only
3081
+ * reached when the viewer renders outside an SDS theme; inside one the
3082
+ * `base.backgroundPrimary` token wins.
3083
+ */
3084
+ const FALLBACK_BACKGROUND_COLOR = {
3085
+ dark: "#000000",
3086
+ light: "#ffffff"
3087
+ };
3088
+ /** Caption shown beneath the color key when no overlay is active. */
3089
+ const PLDDT_SCALE_LABEL = "pLDDT";
3090
+ /** Caption used when an overlay does not name itself. */
3091
+ const DEFAULT_OVERLAY_LABEL = "Value";
3092
+ /** Readout slot label used when an overlay does not name one. */
3093
+ const DEFAULT_READOUT_LABEL = "Value";
3094
+ /**
3095
+ * Chooses what the legend describes, following whatever is actually coloring
3096
+ * the structure: the overlay when one is set, the pLDDT bands when scores were
3097
+ * supplied, and nothing at all otherwise - Mol* falls back to chain coloring
3098
+ * there, which no per-residue scale describes, so a key would be labelling
3099
+ * colors that are not on screen.
3100
+ */
3101
+ function resolveScaleProps(overlay, hasPlddt) {
3102
+ if (overlay) {
3103
+ var _overlay$colorScale, _overlay$label, _overlay$min, _overlay$readoutLabel;
3104
+ return {
3105
+ scale: (_overlay$colorScale = overlay.colorScale) !== null && _overlay$colorScale !== void 0 ? _overlay$colorScale : PLASMA_COLOR_SCALE,
3106
+ scaleLabel: (_overlay$label = overlay.label) !== null && _overlay$label !== void 0 ? _overlay$label : DEFAULT_OVERLAY_LABEL,
3107
+ scaleMax: overlay.max,
3108
+ scaleMin: (_overlay$min = overlay.min) !== null && _overlay$min !== void 0 ? _overlay$min : 0,
3109
+ scaleTooltip: overlay.tooltip,
3110
+ scaleTooltipProps: overlay.tooltipProps,
3111
+ valueLabel: (_overlay$readoutLabel = overlay.readoutLabel) !== null && _overlay$readoutLabel !== void 0 ? _overlay$readoutLabel : DEFAULT_READOUT_LABEL
3112
+ };
3113
+ }
3114
+ if (hasPlddt) return {
3115
+ scale: PLDDT_COLOR_SCALE,
3116
+ scaleLabel: PLDDT_SCALE_LABEL,
3117
+ scaleMax: null,
3118
+ scaleTooltip: void 0,
3119
+ scaleTooltipProps: void 0,
3120
+ valueLabel: void 0
3121
+ };
3122
+ return {
3123
+ scale: null,
3124
+ scaleLabel: void 0,
3125
+ scaleMax: null,
3126
+ scaleTooltip: void 0,
3127
+ scaleTooltipProps: void 0,
3128
+ valueLabel: void 0
3129
+ };
3130
+ }
3131
+ /**
3132
+ * Interactive 3D protein structure viewer built on Mol*, with a sequence panel,
3133
+ * pLDDT confidence coloring, per-residue value overlays, and a stats legend.
3134
+ *
3135
+ * The viewer is controlled for selection: `selectedResidue` drives the camera
3136
+ * (clicking a residue zooms in on it, clearing the selection zooms back out),
3137
+ * while hover state is owned internally so the legend can show a live readout
3138
+ * without the consumer round-tripping every pointer move.
3139
+ */
3140
+ 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);
3142
+ const theme = (0, _mui_material_styles.useTheme)();
3143
+ const mode = (0, _czi_sds_components.getMode)({ theme });
3144
+ const semanticColors = (0, _czi_sds_components.getSemanticColors)({ theme });
3145
+ 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
+ const hasPlddt = Boolean(plddt && plddt.length > 0);
3154
+ const processedPdb = (0, react.useMemo)(() => hasPlddt ? injectPlddtIntoPdb(pdb, plddt) : pdb, [
3155
+ pdb,
3156
+ plddt,
3157
+ hasPlddt
3158
+ ]);
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
+ ]),
3168
+ 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]),
3173
+ 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]),
3178
+ mode,
3179
+ 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,
3194
+ sequenceViewerBackgroundColor,
3195
+ showAxes,
3196
+ showSequenceViewer
3197
+ });
3198
+ useResidueOverlay({
3199
+ hasPlddt,
3200
+ isReady,
3201
+ mode,
3202
+ overlay: residueOverlay,
3203
+ pluginRef,
3204
+ residueValueThemeRef
3205
+ });
3206
+ useResidueFocus({
3207
+ isReady,
3208
+ pluginRef,
3209
+ selectedResidue,
3210
+ setClipRatio
3211
+ });
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
+ }, [
3222
+ isReady,
3223
+ 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
+ ]);
3240
+ const scaleProps = (0, react.useMemo)(() => resolveScaleProps(residueOverlay, hasPlddt), [residueOverlay, hasPlddt]);
3241
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ViewerRoot, _objectSpread2(_objectSpread2({
3242
+ ref,
3243
+ showSequenceViewer
3244
+ }, rest), {}, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PluginMount, { ref: pluginMountRef }), showLegend && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StructureLegend, _objectSpread2({
3245
+ hoveredResidue: hoveredReadout,
3246
+ selectedResidue: selectedReadout,
3247
+ showSequenceViewer,
3248
+ stats: stats !== null && stats !== void 0 ? stats : []
3249
+ }, scaleProps))] }));
3250
+ });
3251
+ var ProteinStructureViewer_default = (0, react.memo)(ProteinStructureViewer);
450
3252
  //#endregion
451
3253
  //#region src/core/StackedBarChart/style.ts
452
3254
  const ChartWrapper = (0, _emotion_styled.default)("div")`
@@ -924,4 +3726,10 @@ const StackedBarChart = (props) => {
924
3726
  };
925
3727
  //#endregion
926
3728
  exports.HeatmapChart = HeatmapChart_default;
3729
+ exports.PLASMA_COLOR_SCALE = PLASMA_COLOR_SCALE;
3730
+ exports.PLDDT_COLOR_SCALE = PLDDT_COLOR_SCALE;
3731
+ exports.ProteinStructureViewer = ProteinStructureViewer_default;
927
3732
  exports.StackedBarChart = StackedBarChart;
3733
+ exports.injectPlddtIntoPdb = injectPlddtIntoPdb;
3734
+ exports.sampleColorScale = sampleColorScale;
3735
+ exports.toCssGradient = toCssGradient;