@czi-sds/data-viz 1.0.1 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs.js CHANGED
@@ -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),
@@ -245,7 +372,7 @@ function generateDefaultValues(props) {
245
372
  //#endregion
246
373
  //#region src/core/HeatmapChart/hooks/useUpdateChart.ts
247
374
  const UPDATE_THROTTLE_MS = 100;
248
- function useUpdateChart({ axisPointer, camera, chart, data, emphasis, xAxisData, yAxisData, width, height, encode, itemStyle, symbol, symbolSize, grid, options, onEvents }) {
375
+ function useUpdateChart({ axisPointer, camera, chart, data, dataZoom, emphasis, xAxisData, yAxisData, width, height, encode, itemStyle, symbol, symbolSize, grid, options, onEvents }) {
249
376
  const throttledUpdateChart = (0, react.useMemo)(() => {
250
377
  return (0, lodash.throttle)(() => {
251
378
  if (!chart || !data || !xAxisData || !yAxisData) return;
@@ -254,6 +381,7 @@ function useUpdateChart({ axisPointer, camera, chart, data, emphasis, xAxisData,
254
381
  axisPointer,
255
382
  camera,
256
383
  data,
384
+ dataZoom,
257
385
  emphasis,
258
386
  encode,
259
387
  grid,
@@ -285,6 +413,7 @@ function useUpdateChart({ axisPointer, camera, chart, data, emphasis, xAxisData,
285
413
  camera,
286
414
  chart,
287
415
  data,
416
+ dataZoom,
288
417
  emphasis,
289
418
  xAxisData,
290
419
  yAxisData,
@@ -308,6 +437,7 @@ function useUpdateChart({ axisPointer, camera, chart, data, emphasis, xAxisData,
308
437
  camera,
309
438
  chart,
310
439
  data,
440
+ dataZoom,
311
441
  emphasis,
312
442
  xAxisData,
313
443
  yAxisData,
@@ -336,12 +466,13 @@ function getWidthAndHeight({ width, height }) {
336
466
  }
337
467
  //#endregion
338
468
  //#region src/core/HeatmapChart/index.tsx
339
- const _excluded$1 = [
469
+ const _excluded$2 = [
340
470
  "axisPointer",
341
471
  "width",
342
472
  "height",
343
473
  "echartsRendererMode",
344
474
  "camera",
475
+ "dataZoom",
345
476
  "onEvents",
346
477
  "xAxisData",
347
478
  "yAxisData",
@@ -355,7 +486,7 @@ const _excluded$1 = [
355
486
  "options"
356
487
  ];
357
488
  const HeatmapChart = (0, react.forwardRef)((props, ref) => {
358
- const { axisPointer, width, height, echartsRendererMode = "svg", camera, 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);
359
490
  if (!width || !height) throw Error("Heatmap must have width and height bigger than Zero!");
360
491
  const innerRef = (0, react.useRef)(null);
361
492
  /**
@@ -408,6 +539,7 @@ const HeatmapChart = (0, react.forwardRef)((props, ref) => {
408
539
  camera,
409
540
  chart,
410
541
  data,
542
+ dataZoom,
411
543
  emphasis,
412
544
  encode,
413
545
  grid,
@@ -442,6 +574,2626 @@ const HeatmapChart = (0, react.forwardRef)((props, ref) => {
442
574
  }
443
575
  });
444
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
+ const ScaleCaption = (0, _emotion_styled.default)("span")`
897
+ ${_czi_sds_components.fontBodyXxxs}
898
+
899
+ text-align: left;
900
+ display: flex;
901
+ align-items: center;
902
+
903
+ ${(props) => {
904
+ var _semanticColors$base3;
905
+ const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
906
+ return `
907
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base3 = semanticColors.base) === null || _semanticColors$base3 === void 0 ? void 0 : _semanticColors$base3.textTertiaryOnDark};
908
+ `;
909
+ }}
910
+ `;
911
+ /** Re-enables pointer events for the one interactive element in the overlay. */
912
+ const TooltipAnchor = (0, _emotion_styled.default)("span")`
913
+ pointer-events: auto;
914
+
915
+ ${(props) => {
916
+ var _semanticColors$base4;
917
+ const spaces = (0, _czi_sds_components.getSpaces)(props);
918
+ const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
919
+ return `
920
+ padding-left: ${spaces === null || spaces === void 0 ? void 0 : spaces.xxxs}px;
921
+ margin-bottom: -${spaces === null || spaces === void 0 ? void 0 : spaces.xxxs}px;
922
+
923
+ &:hover {
924
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base4 = semanticColors.base) === null || _semanticColors$base4 === void 0 ? void 0 : _semanticColors$base4.ornamentSecondary};
925
+ }
926
+ `;
927
+ }}
928
+ `;
929
+ //#endregion
930
+ //#region src/core/ProteinStructureViewer/components/StructureLegend/index.tsx
931
+ /** Number of stat columns, fixed so the grid tracks never move. */
932
+ const SLOT_COUNT = 3;
933
+ /** Decimal places for stat and readout values. */
934
+ const VALUE_PRECISION = 3;
935
+ /** Shown in place of a number the residue has none of. */
936
+ const MISSING_VALUE = "–";
937
+ function StatColumn({ label, value }) {
938
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(StatColumnWrapper, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatValue, { children: value }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatLabel, { children: label })] });
939
+ }
940
+ /**
941
+ * Stats and color key overlaid on the structure viewer.
942
+ *
943
+ * When a residue is selected or hovered, the whole-structure stats are replaced
944
+ * in place by that residue's readout: slot 0 becomes the residue label, slot 1
945
+ * the overlay value (only when an overlay is active, otherwise the stat stays),
946
+ * and slot 2 the residue pLDDT. A selection pins the readout; hovering another
947
+ * residue takes priority so the user can inspect others without losing their
948
+ * selection.
949
+ */
950
+ function StructureLegend({ hoveredResidue = null, scale, scaleLabel, scaleMax = null, scaleMin = 0, scaleTooltip, selectedResidue = null, showSequenceViewer, stats, valueLabel }) {
951
+ var _stats$, _stats$2, _stats$3;
952
+ const activeResidue = hoveredResidue !== null && hoveredResidue !== void 0 ? hoveredResidue : selectedResidue;
953
+ const showResidue = activeResidue !== null;
954
+ const showValue = showResidue && valueLabel !== void 0;
955
+ const slots = [
956
+ showResidue ? {
957
+ label: "Residue",
958
+ value: activeResidue.label
959
+ } : (_stats$ = stats[0]) !== null && _stats$ !== void 0 ? _stats$ : null,
960
+ showValue ? {
961
+ label: valueLabel,
962
+ value: activeResidue.value !== null ? activeResidue.value.toFixed(VALUE_PRECISION) : MISSING_VALUE
963
+ } : (_stats$2 = stats[1]) !== null && _stats$2 !== void 0 ? _stats$2 : null,
964
+ showResidue ? {
965
+ label: "pLDDT",
966
+ value: activeResidue.plddt !== null ? activeResidue.plddt.toFixed(VALUE_PRECISION) : MISSING_VALUE
967
+ } : (_stats$3 = stats[2]) !== null && _stats$3 !== void 0 ? _stats$3 : null
968
+ ];
969
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(LegendOverlay, {
970
+ showSequenceViewer,
971
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatsGrid, { children: Array.from({ length: SLOT_COUNT }, (_, i) => {
972
+ var _slots$i;
973
+ const slot = (_slots$i = slots[i]) !== null && _slots$i !== void 0 ? _slots$i : null;
974
+ return slot ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatColumn, {
975
+ label: slot.label,
976
+ value: slot.value
977
+ }, i) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {}, i);
978
+ }) }), scale && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ScaleColumn, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ColorScaleLegend, {
979
+ max: scaleMax,
980
+ min: scaleMin,
981
+ scale
982
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ScaleCaption, { children: [scaleLabel, scaleTooltip !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_czi_sds_components.Tooltip, {
983
+ arrow: true,
984
+ placement: "bottom",
985
+ title: scaleTooltip,
986
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipAnchor, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_phosphor_icons_react.QuestionIcon, {
987
+ size: 10,
988
+ weight: "bold"
989
+ }) })
990
+ })] })] })]
991
+ });
992
+ }
993
+ //#endregion
994
+ //#region src/core/ProteinStructureViewer/utils/theme.ts
995
+ const themeCache = /* @__PURE__ */ new Map();
996
+ /**
997
+ * The MUI theme for a mode, built at most once. Mol*'s view components rebuild
998
+ * their `ThemeProvider` on every render, and `Theme()` is not cheap.
999
+ */
1000
+ function themeForMode(mode) {
1001
+ const cached = themeCache.get(mode);
1002
+ if (cached) return cached;
1003
+ const theme = (0, _czi_sds_components.Theme)(mode);
1004
+ themeCache.set(mode, theme);
1005
+ return theme;
1006
+ }
1007
+ const residueColorCache = /* @__PURE__ */ new Map();
1008
+ function residueColorsForMode(mode) {
1009
+ var _getSemanticColors, _base$fillSecondaryIn, _base$textPrimary, _base$textSecondary, _base$textTertiary;
1010
+ const cached = residueColorCache.get(mode);
1011
+ if (cached) return cached;
1012
+ const base = (_getSemanticColors = (0, _czi_sds_components.getSemanticColors)({ theme: themeForMode(mode) })) === null || _getSemanticColors === void 0 ? void 0 : _getSemanticColors.base;
1013
+ const colors = {
1014
+ activeBackground: (_base$fillSecondaryIn = base === null || base === void 0 ? void 0 : base.fillSecondaryInteraction) !== null && _base$fillSecondaryIn !== void 0 ? _base$fillSecondaryIn : "transparent",
1015
+ activeText: (_base$textPrimary = base === null || base === void 0 ? void 0 : base.textPrimary) !== null && _base$textPrimary !== void 0 ? _base$textPrimary : "inherit",
1016
+ defaultText: (_base$textSecondary = base === null || base === void 0 ? void 0 : base.textSecondary) !== null && _base$textSecondary !== void 0 ? _base$textSecondary : "inherit",
1017
+ inactiveText: (_base$textTertiary = base === null || base === void 0 ? void 0 : base.textTertiary) !== null && _base$textTertiary !== void 0 ? _base$textTertiary : "inherit"
1018
+ };
1019
+ residueColorCache.set(mode, colors);
1020
+ return colors;
1021
+ }
1022
+ //#endregion
1023
+ //#region src/core/ProteinStructureViewer/hooks/useSubscribe.ts
1024
+ /**
1025
+ * Subscribes to an observable for as long as the component is mounted, the
1026
+ * hook equivalent of `PluginUIComponent`'s `subscribe`.
1027
+ *
1028
+ * The handler is read through a ref so that passing a fresh closure on every
1029
+ * render - which is the normal case, since these handlers read props and state
1030
+ * - does not tear down and rebuild the subscription.
1031
+ */
1032
+ function useSubscribe(observable, handler) {
1033
+ const handlerRef = (0, react.useRef)(handler);
1034
+ handlerRef.current = handler;
1035
+ (0, react.useEffect)(() => {
1036
+ if (!observable) return;
1037
+ const subscription = observable.subscribe((value) => handlerRef.current(value));
1038
+ return () => subscription.unsubscribe();
1039
+ }, [observable]);
1040
+ }
1041
+ //#endregion
1042
+ //#region src/core/ProteinStructureViewer/hooks/useViewSetting.ts
1043
+ /**
1044
+ * One field of the settings Mol*'s own React root cannot read from context.
1045
+ *
1046
+ * The subject emits a whole new settings object on every push, so each field is
1047
+ * selected out separately: React's equality bailout then absorbs the pushes
1048
+ * that left this particular value alone, and a component only re-renders for
1049
+ * the settings it actually reads.
1050
+ */
1051
+ function useViewSetting(settings, select) {
1052
+ const [value, setValue] = (0, react.useState)(() => select(settings.value));
1053
+ useSubscribe(settings, (next) => setValue(select(next)));
1054
+ return value;
1055
+ }
1056
+ //#endregion
1057
+ //#region src/core/ProteinStructureViewer/hooks/useMolstarTheme.ts
1058
+ /**
1059
+ * The active theme for a component Mol* renders in its own React root.
1060
+ *
1061
+ * Those components sit outside the consumer's provider tree, so they cannot
1062
+ * read the theme from context; the mode reaches them through the settings
1063
+ * subject instead. Callers re-supply the theme to their subtree via a local
1064
+ * `ThemeProvider` so Emotion styles resolve SDS tokens correctly.
1065
+ */
1066
+ function useMolstarTheme(settings) {
1067
+ const mode = useViewSetting(settings, (s) => s.mode);
1068
+ return {
1069
+ mode,
1070
+ theme: themeForMode(mode)
1071
+ };
1072
+ }
1073
+ //#endregion
1074
+ //#region src/core/ProteinStructureViewer/components/SequenceView/constants.ts
1075
+ /** Attribute carrying a residue's sequence index, used for hit testing. */
1076
+ const SEQ_ID_ATTR = "data-seqid";
1077
+ /** Hover updates are coalesced to roughly one every three frames. */
1078
+ const HOVER_THROTTLE_MS = 3 * 16.666;
1079
+ /** How long the copy button shows its confirmation state. */
1080
+ const COPIED_FEEDBACK_MS = 2e3;
1081
+ //#endregion
1082
+ //#region src/core/ProteinStructureViewer/hooks/usePlugin.ts
1083
+ /**
1084
+ * The plugin owning the React root this component renders in.
1085
+ *
1086
+ * Mol* renders its view components inside its own root and provides the plugin
1087
+ * through this context, which is what `PluginUIComponent` reads to populate
1088
+ * `this.plugin`.
1089
+ */
1090
+ function usePlugin() {
1091
+ return (0, react.useContext)(molstar_lib_mol_plugin_ui_base.PluginReactContext);
1092
+ }
1093
+ //#endregion
1094
+ //#region src/core/ProteinStructureViewer/components/SequenceView/utils/residue.ts
1095
+ /** Reads the sequence index off a residue span, or undefined if it is not one. */
1096
+ function readSeqIdx(el) {
1097
+ if (!el || !el.getAttribute) return void 0;
1098
+ return el.hasAttribute("data-seqid") ? +el.getAttribute(SEQ_ID_ATTR) : void 0;
1099
+ }
1100
+ /**
1101
+ * Nearest ancestor that scrolls vertically, or null if none. Used to scope the
1102
+ * residue tooltip's hide-on-scroll listener to the sequence's own overflow
1103
+ * container instead of a global window listener.
1104
+ */
1105
+ function getScrollParent(node) {
1106
+ var _node$parentElement;
1107
+ let el = (_node$parentElement = node === null || node === void 0 ? void 0 : node.parentElement) !== null && _node$parentElement !== void 0 ? _node$parentElement : null;
1108
+ while (el) {
1109
+ const overflowY = getComputedStyle(el).overflowY;
1110
+ if (overflowY === "auto" || overflowY === "scroll") return el;
1111
+ el = el.parentElement;
1112
+ }
1113
+ return null;
1114
+ }
1115
+ function getResidueClass(sequenceWrapper, seqIdx) {
1116
+ return sequenceWrapper.isHighlighted(seqIdx) || sequenceWrapper.isSelected(seqIdx) || sequenceWrapper.isFocused(seqIdx) ? "msp-sequence-present msp-sequence-residue-active" : "msp-sequence-present msp-sequence-residue";
1117
+ }
1118
+ /**
1119
+ * The label shown above a residue: its author sequence id for atomic units,
1120
+ * or a 1-based position for coarse ones.
1121
+ *
1122
+ * `location` is a scratch object the caller reuses across residues, since this
1123
+ * runs once per numbered residue on every render.
1124
+ */
1125
+ function getSequenceNumber(sequenceWrapper, seqIdx, location) {
1126
+ let seqNum = "";
1127
+ const loci = sequenceWrapper.getLoci(seqIdx);
1128
+ const l = molstar_lib_mol_model_structure.StructureElement.Loci.getFirstLocation(loci, location);
1129
+ if (l) {
1130
+ if (molstar_lib_mol_model_structure.Unit.isAtomic(l.unit)) {
1131
+ const seqId = molstar_lib_mol_model_structure.StructureProperties.residue.auth_seq_id(l);
1132
+ const insCode = molstar_lib_mol_model_structure.StructureProperties.residue.pdbx_PDB_ins_code(l);
1133
+ seqNum = `${seqId}${insCode ? insCode : ""}`;
1134
+ } else if (molstar_lib_mol_model_structure.Unit.isCoarse(l.unit)) seqNum = `${seqIdx + 1}`;
1135
+ }
1136
+ return seqNum;
1137
+ }
1138
+ /**
1139
+ * How often a sequence number is drawn above the residues.
1140
+ *
1141
+ * Long sequences get one every ten residues. Short ones number every residue,
1142
+ * unless their numbers run to more than one digit, which would crowd them.
1143
+ */
1144
+ function resolveSequenceNumberPeriod(sequenceWrapper, location, sequenceNumberPeriod) {
1145
+ if (sequenceNumberPeriod !== void 0) return sequenceNumberPeriod;
1146
+ if (sequenceWrapper.length > 10) return 10;
1147
+ if (getSequenceNumber(sequenceWrapper, sequenceWrapper.length - 1, location).length > 1) return 5;
1148
+ return 1;
1149
+ }
1150
+ /**
1151
+ * The grid's accessible name.
1152
+ *
1153
+ * The grid is a rendering of the structure rather than a control of its own,
1154
+ * so it is exposed as an image; that makes this label the only thing an
1155
+ * assistive technology gets, which is why it carries the residues themselves
1156
+ * rather than just how many there are. They are spaced apart so they are read
1157
+ * as individual letters instead of running together into a word.
1158
+ *
1159
+ * A complex draws one grid per chain, and the caption naming each is a visual
1160
+ * sibling rather than a label, so the chain is named here too. Without it every
1161
+ * grid announces the same name and which chain a reader is on is apparent only
1162
+ * by sight.
1163
+ */
1164
+ function buildSequenceLabel(sequenceWrapper, chainLabel) {
1165
+ const letters = [];
1166
+ for (let i = 0; i < sequenceWrapper.length; i++) letters.push(sequenceWrapper.residueLabel(i));
1167
+ const name = chainLabel ? `Chain ${chainLabel}, residue sequence, ${sequenceWrapper.length} residues` : `Residue sequence, ${sequenceWrapper.length} residues`;
1168
+ return letters.length > 0 ? `${name}: ${letters.join(" ")}` : name;
1169
+ }
1170
+ //#endregion
1171
+ //#region src/core/ProteinStructureViewer/components/SequenceView/hooks/useResidueMarkers.ts
1172
+ /**
1173
+ * Keeps the residue grid's hover and selection colors in sync with the 3D
1174
+ * view.
1175
+ *
1176
+ * The colors are written straight to the residue spans rather than rendered,
1177
+ * because a marker change would otherwise re-render a grid that can hold
1178
+ * thousands of nodes.
1179
+ */
1180
+ function useResidueMarkers({ containerRef, residueColors, sequenceWrapper }) {
1181
+ const plugin = usePlugin();
1182
+ /**
1183
+ * Residues sit at the default color until something is active. A hovered,
1184
+ * selected, or focused residue brightens and takes a translucent fill; while
1185
+ * any residue is selected, the rest dim to draw attention to the selection.
1186
+ */
1187
+ const paintMarkers = (0, react.useCallback)(() => {
1188
+ const container = containerRef.current;
1189
+ if (!container) return;
1190
+ const { activeBackground, activeText, defaultText, inactiveText } = residueColors;
1191
+ const spans = container.querySelectorAll(`[${SEQ_ID_ATTR}]`);
1192
+ let anySelected = false;
1193
+ for (let i = 0; i < sequenceWrapper.length; i++) if (sequenceWrapper.isSelected(i)) {
1194
+ anySelected = true;
1195
+ break;
1196
+ }
1197
+ const inactiveColor = anySelected ? inactiveText : defaultText;
1198
+ for (let i = 0; i < sequenceWrapper.length; i++) {
1199
+ const span = spans[i];
1200
+ if (!span) return;
1201
+ const className = getResidueClass(sequenceWrapper, i);
1202
+ if (span.className !== className) span.className = className;
1203
+ const active = sequenceWrapper.isSelected(i) || sequenceWrapper.isFocused(i) || sequenceWrapper.isHighlighted(i);
1204
+ span.style.color = active ? activeText : inactiveColor;
1205
+ span.style.backgroundColor = active ? activeBackground : "";
1206
+ }
1207
+ }, [
1208
+ containerRef,
1209
+ residueColors,
1210
+ sequenceWrapper
1211
+ ]);
1212
+ const markFocus = (0, react.useCallback)((loci) => {
1213
+ sequenceWrapper.markResidue(molstar_lib_mol_model_loci.EveryLoci, "unfocus");
1214
+ if (loci) sequenceWrapper.markResidue(loci, "focus");
1215
+ }, [sequenceWrapper]);
1216
+ const paintMarkersRef = (0, react.useRef)(paintMarkers);
1217
+ paintMarkersRef.current = paintMarkers;
1218
+ const sequenceWrapperRef = (0, react.useRef)(sequenceWrapper);
1219
+ sequenceWrapperRef.current = sequenceWrapper;
1220
+ (0, react.useEffect)(() => {
1221
+ const markProvider = (loci, action) => {
1222
+ if (sequenceWrapperRef.current.markResidue(loci.loci, action)) paintMarkersRef.current();
1223
+ };
1224
+ const { lociHighlights, lociSelects } = plugin.managers.interactivity;
1225
+ lociHighlights.addProvider(markProvider);
1226
+ lociSelects.addProvider(markProvider);
1227
+ return () => {
1228
+ lociHighlights.removeProvider(markProvider);
1229
+ lociSelects.removeProvider(markProvider);
1230
+ };
1231
+ }, [plugin]);
1232
+ useSubscribe(plugin.managers.structure.focus.behaviors.current, (focus) => {
1233
+ markFocus(focus === null || focus === void 0 ? void 0 : focus.loci);
1234
+ paintMarkers();
1235
+ });
1236
+ (0, react.useLayoutEffect)(() => {
1237
+ var _plugin$managers$stru;
1238
+ markFocus((_plugin$managers$stru = plugin.managers.structure.focus.behaviors.current.value) === null || _plugin$managers$stru === void 0 ? void 0 : _plugin$managers$stru.loci);
1239
+ paintMarkers();
1240
+ });
1241
+ }
1242
+ //#endregion
1243
+ //#region src/core/ProteinStructureViewer/components/SequenceView/utils/loci.ts
1244
+ /** The loci for a residue, or undefined when there is no residue there. */
1245
+ function getLoci(sequenceWrapper, seqIdx) {
1246
+ if (seqIdx !== void 0) {
1247
+ const loci = sequenceWrapper.getLoci(seqIdx);
1248
+ if (!molstar_lib_mol_model_structure.StructureElement.Loci.isEmpty(loci)) return loci;
1249
+ }
1250
+ }
1251
+ /**
1252
+ * Extends a loci to the range between the mouse-down anchor and the current
1253
+ * residue, so dragging across the sequence selects a contiguous span.
1254
+ *
1255
+ * Element indices are only comparable within a single unit, and the range below
1256
+ * is built on the anchor's unit alone. So a drag is only extended when both
1257
+ * endpoints are a single element of the same unit; anything else -- different
1258
+ * units, or an endpoint spanning several because the chain has more than one
1259
+ * symmetry operator -- is left unextended rather than collapsed onto one unit
1260
+ * and silently reinterpreted as a different range.
1261
+ */
1262
+ function extendToRange(loci, anchor) {
1263
+ if (anchor.elements.length !== 1 || loci.elements.length !== 1) return loci;
1264
+ const ref = anchor.elements[0];
1265
+ const ext = loci.elements[0];
1266
+ if (!ref || !ext) return loci;
1267
+ if (ref.unit !== ext.unit) return loci;
1268
+ const min = Math.min(molstar_lib_mol_data_int.OrderedSet.min(ref.indices), molstar_lib_mol_data_int.OrderedSet.min(ext.indices));
1269
+ const max = Math.max(molstar_lib_mol_data_int.OrderedSet.max(ref.indices), molstar_lib_mol_data_int.OrderedSet.max(ext.indices));
1270
+ return molstar_lib_mol_model_structure.StructureElement.Loci(loci.structure, [{
1271
+ indices: molstar_lib_mol_data_int.OrderedSet.ofRange(min, max),
1272
+ unit: ref.unit
1273
+ }]);
1274
+ }
1275
+ //#endregion
1276
+ //#region src/core/ProteinStructureViewer/components/SequenceView/hooks/useHoveredResidue.ts
1277
+ /**
1278
+ * Tracks which residue the pointer is over and keeps the tooltip pinned to it.
1279
+ *
1280
+ * The tracked index also deduplicates pointer moves: the grid sees a move for
1281
+ * every pixel, but only the ones that cross into a different residue matter.
1282
+ */
1283
+ function useHoveredResidue({ containerRef, sequenceWrapper }) {
1284
+ const tooltipRef = (0, react.useRef)(null);
1285
+ /** Index of the tracked residue, or -1 when the pointer is off the grid. */
1286
+ const seqIdxRef = (0, react.useRef)(-1);
1287
+ const hide = (0, react.useCallback)(() => {
1288
+ var _tooltipRef$current;
1289
+ return (_tooltipRef$current = tooltipRef.current) === null || _tooltipRef$current === void 0 ? void 0 : _tooltipRef$current.hide();
1290
+ }, []);
1291
+ const clear = (0, react.useCallback)(() => {
1292
+ var _tooltipRef$current2;
1293
+ if (seqIdxRef.current === -1) return false;
1294
+ seqIdxRef.current = -1;
1295
+ (_tooltipRef$current2 = tooltipRef.current) === null || _tooltipRef$current2 === void 0 || _tooltipRef$current2.hide();
1296
+ return true;
1297
+ }, []);
1298
+ const moveTo = (0, react.useCallback)((seqIdx, el) => {
1299
+ var _tooltipRef$current3, _tooltipRef$current4;
1300
+ if (seqIdxRef.current === seqIdx) return false;
1301
+ seqIdxRef.current = seqIdx;
1302
+ if (seqIdx < 0) (_tooltipRef$current3 = tooltipRef.current) === null || _tooltipRef$current3 === void 0 || _tooltipRef$current3.hide();
1303
+ else (_tooltipRef$current4 = tooltipRef.current) === null || _tooltipRef$current4 === void 0 || _tooltipRef$current4.show(el, sequenceWrapper.residueLabel(seqIdx));
1304
+ return true;
1305
+ }, [sequenceWrapper]);
1306
+ (0, react.useEffect)(() => {
1307
+ const scrollTarget = getScrollParent(containerRef.current);
1308
+ if (!scrollTarget) return;
1309
+ const handleScroll = () => clear();
1310
+ scrollTarget.addEventListener("scroll", handleScroll, { passive: true });
1311
+ return () => scrollTarget.removeEventListener("scroll", handleScroll);
1312
+ }, [clear, containerRef]);
1313
+ return (0, react.useMemo)(() => ({
1314
+ clear,
1315
+ hide,
1316
+ moveTo,
1317
+ tooltipRef
1318
+ }), [
1319
+ clear,
1320
+ hide,
1321
+ moveTo
1322
+ ]);
1323
+ }
1324
+ //#endregion
1325
+ //#region src/core/ProteinStructureViewer/components/SequenceView/hooks/useLociDispatch.ts
1326
+ /**
1327
+ * Wraps a residue loci and the mouse state into a Mol* interaction event. An
1328
+ * `anchor` extends the loci to the range between it and `loci`, which is how a
1329
+ * drag across the sequence comes out as a contiguous span.
1330
+ */
1331
+ function buildEvent(loci, buttons, button, modifiers, anchor) {
1332
+ const ev = {
1333
+ button,
1334
+ buttons,
1335
+ current: molstar_lib_mol_repr_representation.Representation.Loci.Empty,
1336
+ modifiers
1337
+ };
1338
+ if (loci === void 0 || molstar_lib_mol_model_structure.StructureElement.Loci.isEmpty(loci)) return ev;
1339
+ ev.current = { loci: anchor ? extendToRange(loci, anchor) : loci };
1340
+ return ev;
1341
+ }
1342
+ /**
1343
+ * Reports the grid's hover and click to Mol*, on the same behaviors the 3D
1344
+ * view publishes to, so an interaction in either surface reaches the other.
1345
+ */
1346
+ function useLociDispatch() {
1347
+ const plugin = usePlugin();
1348
+ const hover = (0, react.useCallback)((loci, buttons, button, modifiers, anchor) => {
1349
+ plugin.behaviors.interaction.hover.next(buildEvent(loci, buttons, button, modifiers, anchor));
1350
+ }, [plugin]);
1351
+ const click = (0, react.useCallback)((loci, buttons, button, modifiers, anchor) => {
1352
+ plugin.behaviors.interaction.click.next(buildEvent(loci, buttons, button, modifiers, anchor));
1353
+ }, [plugin]);
1354
+ return (0, react.useMemo)(() => ({
1355
+ click,
1356
+ hover
1357
+ }), [click, hover]);
1358
+ }
1359
+ //#endregion
1360
+ //#region src/core/ProteinStructureViewer/components/SequenceView/hooks/useResidueSelection.ts
1361
+ /**
1362
+ * Turns a press and release on the grid into a selection: one residue when
1363
+ * both land on the same one, otherwise the contiguous range dragged across.
1364
+ */
1365
+ function useResidueSelection({ sequenceWrapper }) {
1366
+ const anchorRef = (0, react.useRef)(void 0);
1367
+ const { click } = useLociDispatch();
1368
+ const onMouseDown = (e) => {
1369
+ e.stopPropagation();
1370
+ anchorRef.current = getLoci(sequenceWrapper, readSeqIdx(e.target));
1371
+ };
1372
+ const onMouseUp = (e) => {
1373
+ e.stopPropagation();
1374
+ const anchor = anchorRef.current;
1375
+ if (anchor === void 0) return;
1376
+ const loci = getLoci(sequenceWrapper, readSeqIdx(e.target));
1377
+ if (loci) {
1378
+ const dragged = !molstar_lib_mol_model_structure.StructureElement.Loci.areEqual(anchor, loci);
1379
+ click(loci, (0, molstar_lib_mol_util_input_input_observer.getButtons)(e.nativeEvent), (0, molstar_lib_mol_util_input_input_observer.getButton)(e.nativeEvent), (0, molstar_lib_mol_util_input_input_observer.getModifiers)(e.nativeEvent), dragged ? anchor : void 0);
1380
+ }
1381
+ anchorRef.current = void 0;
1382
+ };
1383
+ return {
1384
+ clearAnchor: () => {
1385
+ anchorRef.current = void 0;
1386
+ },
1387
+ getAnchor: () => anchorRef.current,
1388
+ onMouseDown,
1389
+ onMouseUp
1390
+ };
1391
+ }
1392
+ //#endregion
1393
+ //#region src/core/ProteinStructureViewer/components/SequenceView/hooks/useResiduePointer.ts
1394
+ /**
1395
+ * Everything the grid does with a pointer: hover on move, and the drag-to-range
1396
+ * selection delegated to `useResidueSelection`.
1397
+ *
1398
+ * Hover reaches the 3D view through a throttled queue, so that sweeping the
1399
+ * pointer across the grid does not ask Mol* to re-mark on every residue
1400
+ * crossed. A drag is exempt: its hover carries the growing range, so it goes
1401
+ * out immediately to keep the preview under the pointer.
1402
+ */
1403
+ function useResiduePointer({ containerRef, sequenceWrapper }) {
1404
+ const { hover } = useLociDispatch();
1405
+ const { clear, hide, moveTo, tooltipRef } = useHoveredResidue({
1406
+ containerRef,
1407
+ sequenceWrapper
1408
+ });
1409
+ const { clearAnchor, getAnchor, onMouseDown, onMouseUp } = useResidueSelection({ sequenceWrapper });
1410
+ const queueRef = (0, react.useRef)(null);
1411
+ if (queueRef.current === null) queueRef.current = new rxjs.Subject();
1412
+ const queue = queueRef.current;
1413
+ useSubscribe((0, react.useMemo)(() => queue.pipe((0, rxjs_operators.throttleTime)(HOVER_THROTTLE_MS, void 0, {
1414
+ leading: true,
1415
+ trailing: true
1416
+ })), [queue]), (e) => {
1417
+ hover(getLoci(sequenceWrapper, e.seqIdx < 0 ? void 0 : e.seqIdx), e.buttons, e.button, e.modifiers, getAnchor());
1418
+ });
1419
+ const queueHover = (e, seqIdx) => queue.next({
1420
+ button: (0, molstar_lib_mol_util_input_input_observer.getButton)(e.nativeEvent),
1421
+ buttons: (0, molstar_lib_mol_util_input_input_observer.getButtons)(e.nativeEvent),
1422
+ modifiers: (0, molstar_lib_mol_util_input_input_observer.getModifiers)(e.nativeEvent),
1423
+ seqIdx
1424
+ });
1425
+ /** Asks the 3D view to drop its hover, once, if the grid still holds one. */
1426
+ const releaseHover = (e) => {
1427
+ if (clear()) queueHover(e, -1);
1428
+ };
1429
+ const onContextMenu = (e) => {
1430
+ e.preventDefault();
1431
+ };
1432
+ const onMouseMove = (e) => {
1433
+ var _readSeqIdx;
1434
+ e.stopPropagation();
1435
+ const el = e.target;
1436
+ if (!el || !el.getAttribute) {
1437
+ releaseHover(e);
1438
+ return;
1439
+ }
1440
+ const seqIdx = (_readSeqIdx = readSeqIdx(el)) !== null && _readSeqIdx !== void 0 ? _readSeqIdx : -1;
1441
+ if (!moveTo(seqIdx, el)) return;
1442
+ const anchor = getAnchor();
1443
+ if (anchor === void 0) {
1444
+ queueHover(e, seqIdx);
1445
+ return;
1446
+ }
1447
+ hover(getLoci(sequenceWrapper, seqIdx), molstar_lib_mol_util_input_input_observer.ButtonsType.Flag.None, molstar_lib_mol_util_input_input_observer.ButtonsType.Flag.None, (0, molstar_lib_mol_util_input_input_observer.getModifiers)(e.nativeEvent), anchor);
1448
+ };
1449
+ const onMouseLeave = (e) => {
1450
+ e.stopPropagation();
1451
+ clearAnchor();
1452
+ hide();
1453
+ releaseHover(e);
1454
+ };
1455
+ return {
1456
+ handlers: {
1457
+ onContextMenu,
1458
+ onMouseDown,
1459
+ onMouseLeave,
1460
+ onMouseMove,
1461
+ onMouseUp
1462
+ },
1463
+ tooltipRef
1464
+ };
1465
+ }
1466
+ //#endregion
1467
+ //#region src/core/ProteinStructureViewer/components/SequenceView/style.ts
1468
+ /** Height of the fade that masks residues scrolling under the panel header. */
1469
+ const SCROLL_FADE_HEIGHT = 8;
1470
+ const SequencePanel = (0, _emotion_styled.default)("div")`
1471
+ display: flex;
1472
+ flex-direction: column;
1473
+ height: 100%;
1474
+
1475
+ ${(props) => {
1476
+ var _semanticColors$base, _props$backgroundColo, _semanticColors$base2;
1477
+ const spaces = (0, _czi_sds_components.getSpaces)(props);
1478
+ const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
1479
+ return `
1480
+ padding: ${spaces === null || spaces === void 0 ? void 0 : spaces.m}px ${spaces === null || spaces === void 0 ? void 0 : spaces.m}px 0;
1481
+ border-top: 1px solid ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base = semanticColors.base) === null || _semanticColors$base === void 0 ? void 0 : _semanticColors$base.divider};
1482
+ background-color: ${(_props$backgroundColo = props.backgroundColor) !== null && _props$backgroundColo !== void 0 ? _props$backgroundColo : semanticColors === null || semanticColors === void 0 || (_semanticColors$base2 = semanticColors.base) === null || _semanticColors$base2 === void 0 ? void 0 : _semanticColors$base2.surfacePrimary};
1483
+ `;
1484
+ }}
1485
+ `;
1486
+ const EmptyState = (0, _emotion_styled.default)("div")`
1487
+ ${_czi_sds_components.fontBodyXs}
1488
+
1489
+ display: flex;
1490
+ align-items: center;
1491
+ justify-content: center;
1492
+ height: 100%;
1493
+
1494
+ ${(props) => {
1495
+ var _props$backgroundColo2, _semanticColors$base3, _semanticColors$base4;
1496
+ const spaces = (0, _czi_sds_components.getSpaces)(props);
1497
+ const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
1498
+ return `
1499
+ padding: ${spaces === null || spaces === void 0 ? void 0 : spaces.m}px;
1500
+ background-color: ${(_props$backgroundColo2 = props.backgroundColor) !== null && _props$backgroundColo2 !== void 0 ? _props$backgroundColo2 : semanticColors === null || semanticColors === void 0 || (_semanticColors$base3 = semanticColors.base) === null || _semanticColors$base3 === void 0 ? void 0 : _semanticColors$base3.surfaceSecondary};
1501
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base4 = semanticColors.base) === null || _semanticColors$base4 === void 0 ? void 0 : _semanticColors$base4.textTertiaryOnDark};
1502
+ `;
1503
+ }}
1504
+ `;
1505
+ const PanelHeader = (0, _emotion_styled.default)("div")`
1506
+ display: flex;
1507
+ align-items: center;
1508
+
1509
+ ${(props) => {
1510
+ const spaces = (0, _czi_sds_components.getSpaces)(props);
1511
+ return `
1512
+ gap: ${spaces === null || spaces === void 0 ? void 0 : spaces.xs}px;
1513
+ `;
1514
+ }}
1515
+ `;
1516
+ const PanelTitle = (0, _emotion_styled.default)("span")`
1517
+ ${_czi_sds_components.fontHeaderM}
1518
+
1519
+ ${(props) => {
1520
+ var _semanticColors$base5;
1521
+ const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
1522
+ return `
1523
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base5 = semanticColors.base) === null || _semanticColors$base5 === void 0 ? void 0 : _semanticColors$base5.textPrimary};
1524
+ `;
1525
+ }}
1526
+ `;
1527
+ const CopyButton = (0, _emotion_styled.default)("button")`
1528
+ display: flex;
1529
+ align-items: center;
1530
+ padding: 0;
1531
+ border: none;
1532
+ background: none;
1533
+ cursor: default;
1534
+
1535
+ ${(props) => {
1536
+ var _semanticColors$base6, _semanticColors$base7;
1537
+ const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
1538
+ return `
1539
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base6 = semanticColors.base) === null || _semanticColors$base6 === void 0 ? void 0 : _semanticColors$base6.ornamentSecondary};
1540
+
1541
+ &:hover {
1542
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base7 = semanticColors.base) === null || _semanticColors$base7 === void 0 ? void 0 : _semanticColors$base7.textPrimary};
1543
+ }
1544
+ `;
1545
+ }}
1546
+ `;
1547
+ const ResidueCount = (0, _emotion_styled.default)("div")`
1548
+ display: flex;
1549
+ align-items: center;
1550
+ margin-left: auto;
1551
+
1552
+ ${(props) => {
1553
+ const spaces = (0, _czi_sds_components.getSpaces)(props);
1554
+ return `
1555
+ gap: ${spaces === null || spaces === void 0 ? void 0 : spaces.xxs}px;
1556
+ `;
1557
+ }}
1558
+ `;
1559
+ const ResidueCountLabel = (0, _emotion_styled.default)("span")`
1560
+ ${_czi_sds_components.fontBodyXs}
1561
+
1562
+ ${(props) => {
1563
+ var _semanticColors$base8;
1564
+ const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
1565
+ return `
1566
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base8 = semanticColors.base) === null || _semanticColors$base8 === void 0 ? void 0 : _semanticColors$base8.textTertiary};
1567
+ `;
1568
+ }}
1569
+ `;
1570
+ const ResidueCountValue = (0, _emotion_styled.default)("span")`
1571
+ ${_czi_sds_components.fontBodyXs}
1572
+
1573
+ ${(props) => {
1574
+ var _semanticColors$base9;
1575
+ const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
1576
+ return `
1577
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base9 = semanticColors.base) === null || _semanticColors$base9 === void 0 ? void 0 : _semanticColors$base9.textPrimary};
1578
+ `;
1579
+ }}
1580
+ `;
1581
+ /**
1582
+ * Scroll region for the residue grid. The `::before` fade masks residues as
1583
+ * they scroll up under the header, so it has to start from whatever the panel
1584
+ * is painted with or it leaves a mismatched strip.
1585
+ */
1586
+ const SequenceScroller = (0, _emotion_styled.default)("div")`
1587
+ position: relative;
1588
+ flex: 1;
1589
+ min-height: 0;
1590
+
1591
+ ${(props) => {
1592
+ var _props$backgroundColo3, _semanticColors$base10;
1593
+ const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
1594
+ return `
1595
+ &::before {
1596
+ content: "";
1597
+ position: absolute;
1598
+ inset-inline: 0;
1599
+ top: 0;
1600
+ z-index: 10;
1601
+ height: ${SCROLL_FADE_HEIGHT}px;
1602
+ pointer-events: none;
1603
+ background: linear-gradient(to bottom, ${(_props$backgroundColo3 = props.backgroundColor) !== null && _props$backgroundColo3 !== void 0 ? _props$backgroundColo3 : semanticColors === null || semanticColors === void 0 || (_semanticColors$base10 = semanticColors.base) === null || _semanticColors$base10 === void 0 ? void 0 : _semanticColors$base10.surfacePrimary}, transparent);
1604
+ }
1605
+ `;
1606
+ }}
1607
+ `;
1608
+ /**
1609
+ * Residue spans are written to directly by `useResidueMarkers` rather than
1610
+ * re-rendered, so their hover and selection colors are set inline. Everything
1611
+ * here is the static styling underneath that: the monospace type, the pointer
1612
+ * affordance, and the superscript sequence numbers (which need their own color
1613
+ * rule, since they are nested inside residue spans and would otherwise inherit
1614
+ * the inline color).
1615
+ */
1616
+ const SequenceScrollArea = (0, _emotion_styled.default)("div")`
1617
+ height: 100%;
1618
+ overflow-y: auto;
1619
+
1620
+ .msp-sequence-wrapper {
1621
+ ${_czi_sds_components.fontCodeXs};
1622
+ font-size: 11px;
1623
+ line-height: 1.2;
1624
+ }
1625
+
1626
+ .msp-sequence-present {
1627
+ cursor: pointer;
1628
+ }
1629
+
1630
+ ${(props) => {
1631
+ var _semanticColors$base11;
1632
+ const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
1633
+ return `
1634
+ .msp-sequence-number {
1635
+ font-size: 8px;
1636
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base11 = semanticColors.base) === null || _semanticColors$base11 === void 0 ? void 0 : _semanticColors$base11.textTertiaryOnDark};
1637
+ }
1638
+ `;
1639
+ }}
1640
+ `;
1641
+ /**
1642
+ * Layout for one chain's residues.
1643
+ *
1644
+ * Residues are grouped into fixed-size sections, each one grid cell. The
1645
+ * equal-width auto-fill tracks and uniform gap keep the spacing between
1646
+ * sections identical on every row - including a partial last row, which keeps
1647
+ * the same column width (rather than stretching) and stacks from the leading
1648
+ * edge.
1649
+ */
1650
+ const ResidueGrid = (0, _emotion_styled.default)("div")`
1651
+ user-select: none;
1652
+ display: grid;
1653
+ grid-template-columns: repeat(
1654
+ auto-fill,
1655
+ minmax(${10}ch, 1fr)
1656
+ );
1657
+ gap: 8px;
1658
+ justify-items: start;
1659
+ padding-top: 6px;
1660
+
1661
+ /* Section wrappers, so a section's letters never split across lines. */
1662
+ & > span {
1663
+ white-space: nowrap;
1664
+ }
1665
+
1666
+ .msp-sequence-present {
1667
+ position: relative;
1668
+ display: inline-block;
1669
+ width: 1ch;
1670
+ /*
1671
+ * Reserves room above the letter for its superscript sequence number.
1672
+ * content-box clipping keeps the active fill off that gutter, so a
1673
+ * highlighted residue does not tint the number.
1674
+ */
1675
+ padding-top: 8px;
1676
+ background-clip: content-box;
1677
+ text-align: center;
1678
+ }
1679
+
1680
+ .msp-sequence-number {
1681
+ position: absolute;
1682
+ top: 0;
1683
+ line-height: 1.2;
1684
+ white-space: nowrap;
1685
+ pointer-events: none;
1686
+ }
1687
+ `;
1688
+ /**
1689
+ * Caption naming the chain a sequence grid belongs to. A structure with more
1690
+ * than one chain renders one grid per chain, and without a caption between them
1691
+ * the grids read as a single continuous protein.
1692
+ */
1693
+ const ChainLabel = (0, _emotion_styled.default)("div")`
1694
+ ${_czi_sds_components.fontBodyXs}
1695
+
1696
+ ${(props) => {
1697
+ var _semanticColors$base12;
1698
+ const spaces = (0, _czi_sds_components.getSpaces)(props);
1699
+ const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
1700
+ return `
1701
+ margin-top: ${spaces === null || spaces === void 0 ? void 0 : spaces.xs}px;
1702
+ margin-bottom: ${spaces === null || spaces === void 0 ? void 0 : spaces.xxxs}px;
1703
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base12 = semanticColors.base) === null || _semanticColors$base12 === void 0 ? void 0 : _semanticColors$base12.textSecondary};
1704
+ `;
1705
+ }}
1706
+ `;
1707
+ //#endregion
1708
+ //#region src/core/ProteinStructureViewer/components/SequenceView/components/ResidueTooltip/index.tsx
1709
+ var _getSpaces$m, _getSpaces;
1710
+ /**
1711
+ * Pull the tooltip toward the residue by the SDS `m` spacing token. Spacing
1712
+ * tokens are theme-invariant, so reading from the light theme is fine; popper's
1713
+ * offset modifier needs a number, hence deriving it from the token.
1714
+ */
1715
+ 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);
1716
+ /**
1717
+ * A single controlled tooltip anchored to the residue span currently under the
1718
+ * cursor. Driven imperatively through its ref so that hover updates re-render
1719
+ * only the tooltip, not the residue grid.
1720
+ */
1721
+ const ResidueTooltip = (0, react.forwardRef)((_props, ref) => {
1722
+ var _getSemanticColors;
1723
+ const [{ anchorEl, label }, setState] = (0, react.useState)({
1724
+ anchorEl: null,
1725
+ label: ""
1726
+ });
1727
+ 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;
1728
+ (0, react.useImperativeHandle)(ref, () => ({
1729
+ hide: () => setState((prev) => prev.anchorEl === null ? prev : _objectSpread2(_objectSpread2({}, prev), {}, { anchorEl: null })),
1730
+ show: (nextAnchorEl, nextLabel) => setState({
1731
+ anchorEl: nextAnchorEl,
1732
+ label: nextLabel
1733
+ })
1734
+ }), []);
1735
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_czi_sds_components.Tooltip, {
1736
+ arrow: true,
1737
+ disableInteractive: true,
1738
+ open: anchorEl !== null,
1739
+ placement: "top",
1740
+ slotProps: {
1741
+ popper: {
1742
+ anchorEl,
1743
+ modifiers: [{
1744
+ name: "offset",
1745
+ options: { offset: [0, MAIN_AXIS_OFFSET] }
1746
+ }]
1747
+ },
1748
+ tooltip: { style: { backgroundColor: residueTooltipBackgroundColor } },
1749
+ arrow: { style: { color: residueTooltipBackgroundColor } }
1750
+ },
1751
+ title: label,
1752
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: {
1753
+ height: 0,
1754
+ position: "fixed",
1755
+ width: 0
1756
+ } })
1757
+ });
1758
+ });
1759
+ //#endregion
1760
+ //#region src/core/ProteinStructureViewer/components/SequenceView/components/Sequence/index.tsx
1761
+ /**
1762
+ * One residue. Not a component: the grid can hold thousands of these, and
1763
+ * giving each its own fiber costs more than it saves, since the markers that
1764
+ * actually change bypass React anyway.
1765
+ */
1766
+ function renderResidue(sequenceWrapper, seqIdx, seqNum) {
1767
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1768
+ className: getResidueClass(sequenceWrapper, seqIdx),
1769
+ "data-seqid": seqIdx,
1770
+ children: [seqNum && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1771
+ className: "msp-sequence-number",
1772
+ children: seqNum
1773
+ }), sequenceWrapper.residueLabel(seqIdx)]
1774
+ }, seqIdx);
1775
+ }
1776
+ /** Interactive residue grid, kept in sync with the 3D view's hover and selection. */
1777
+ const Sequence = ({ chainLabel, hideSequenceNumbers, residueColors, sequenceNumberPeriod, sequenceWrapper }) => {
1778
+ const containerRef = (0, react.useRef)(null);
1779
+ /** Scratch location reused by every sequence-number lookup below. */
1780
+ const locationRef = (0, react.useRef)(molstar_lib_mol_model_structure.StructureElement.Location.create(void 0));
1781
+ useResidueMarkers({
1782
+ containerRef,
1783
+ residueColors,
1784
+ sequenceWrapper
1785
+ });
1786
+ const { handlers, tooltipRef } = useResiduePointer({
1787
+ containerRef,
1788
+ sequenceWrapper
1789
+ });
1790
+ const period = resolveSequenceNumberPeriod(sequenceWrapper, locationRef.current, sequenceNumberPeriod);
1791
+ const groups = [];
1792
+ for (let start = 0; start < sequenceWrapper.length; start += 10) {
1793
+ const end = Math.min(start + 10, sequenceWrapper.length);
1794
+ const residues = [];
1795
+ for (let i = start; i < end; i++) {
1796
+ const showNum = !hideSequenceNumbers && i % period === 0;
1797
+ residues.push(renderResidue(sequenceWrapper, i, showNum ? getSequenceNumber(sequenceWrapper, i, locationRef.current) : null));
1798
+ }
1799
+ groups.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: residues }, `group-${start}`));
1800
+ }
1801
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResidueGrid, _objectSpread2(_objectSpread2({
1802
+ "aria-label": buildSequenceLabel(sequenceWrapper, chainLabel),
1803
+ className: "msp-sequence-wrapper",
1804
+ ref: containerRef,
1805
+ role: "img"
1806
+ }, handlers), {}, { children: groups })), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResidueTooltip, { ref: tooltipRef })] });
1807
+ };
1808
+ //#endregion
1809
+ //#region src/core/ProteinStructureViewer/components/SequenceView/utils/sequenceText.ts
1810
+ /**
1811
+ * The structure's residues as text, one section per chain separated by
1812
+ * `CHAIN_SEPARATOR`. Placeholder entries contribute nothing, since they have no
1813
+ * residues to write.
1814
+ */
1815
+ function sequenceTextFromEntries(entries) {
1816
+ return entries.filter((entry) => typeof entry.wrapper !== "string").map((entry) => {
1817
+ const wrapper = entry.wrapper;
1818
+ return Array.from({ length: wrapper.length }, (_, i) => wrapper.residueLabel(i)).join("");
1819
+ }).join("|");
1820
+ }
1821
+ //#endregion
1822
+ //#region src/core/ProteinStructureViewer/components/SequenceView/hooks/useCopySequence.ts
1823
+ /**
1824
+ * Copies the structure's residues to the clipboard, chains separated so a
1825
+ * complex does not paste as one sequence, and holds a confirmation flag for a
1826
+ * moment afterwards.
1827
+ */
1828
+ function useCopySequence(entries) {
1829
+ const [copied, setCopied] = (0, react.useState)(false);
1830
+ const timeoutRef = (0, react.useRef)(null);
1831
+ (0, react.useEffect)(() => () => {
1832
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
1833
+ }, []);
1834
+ return {
1835
+ copied,
1836
+ copySequence: (0, react.useCallback)(() => {
1837
+ const sequence = sequenceTextFromEntries(entries);
1838
+ navigator.clipboard.writeText(sequence).then(() => {
1839
+ setCopied(true);
1840
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
1841
+ timeoutRef.current = setTimeout(() => setCopied(false), COPIED_FEEDBACK_MS);
1842
+ });
1843
+ }, [entries])
1844
+ };
1845
+ }
1846
+ //#endregion
1847
+ //#region src/core/ProteinStructureViewer/components/SequenceView/hooks/useSequenceWrappers.ts
1848
+ const EMPTY_STATE = {
1849
+ chainGroupId: -1,
1850
+ modelEntityId: "",
1851
+ operatorKey: "",
1852
+ structure: molstar_lib_mol_model_structure.Structure.Empty,
1853
+ structureOptions: {
1854
+ all: [],
1855
+ options: []
1856
+ },
1857
+ structureRef: ""
1858
+ };
1859
+ function readStructure(plugin, ref) {
1860
+ const cell = plugin.state.data.select(ref)[0];
1861
+ if (!ref || !cell || !cell.obj) return molstar_lib_mol_model_structure.Structure.Empty;
1862
+ return cell.obj.data;
1863
+ }
1864
+ /** Picks the first structure, entity, chain, and operator Mol* offers. */
1865
+ function readInitialState(plugin) {
1866
+ const structureOptions = (0, molstar_lib_mol_plugin_ui_sequence.getStructureOptions)(plugin.state.data);
1867
+ const structureRef = structureOptions.options[0][0];
1868
+ const structure = readStructure(plugin, structureRef);
1869
+ const modelEntityId = (0, molstar_lib_mol_plugin_ui_sequence.getModelEntityOptions)(structure)[0][0];
1870
+ const chainGroupId = (0, molstar_lib_mol_plugin_ui_sequence.getChainOptions)(structure, modelEntityId)[0][0];
1871
+ return {
1872
+ chainGroupId,
1873
+ modelEntityId,
1874
+ operatorKey: (0, molstar_lib_mol_plugin_ui_sequence.getOperatorOptions)(structure, modelEntityId, chainGroupId)[0][0],
1875
+ structure,
1876
+ structureOptions,
1877
+ structureRef
1878
+ };
1879
+ }
1880
+ function buildParams(state) {
1881
+ const { modelEntityId, structure, structureOptions } = state;
1882
+ const chainOptions = (0, molstar_lib_mol_plugin_ui_sequence.getChainOptions)(structure, modelEntityId);
1883
+ return {
1884
+ chain: molstar_lib_mol_util_param_definition.ParamDefinition.Select(chainOptions[0][0], chainOptions, { shortLabel: true }),
1885
+ structure: molstar_lib_mol_util_param_definition.ParamDefinition.Select(structureOptions.options[0][0], structureOptions.options, { shortLabel: true })
1886
+ };
1887
+ }
1888
+ /** One entry per chain, in Mol*'s entity / chain / operator order. */
1889
+ function buildEntries(structure, selection, _params) {
1890
+ const entries = [];
1891
+ for (const [modelEntityId] of (0, molstar_lib_mol_plugin_ui_sequence.getModelEntityOptions)(structure)) for (const [chainGroupId, cLabel] of (0, molstar_lib_mol_plugin_ui_sequence.getChainOptions)(structure, modelEntityId)) for (const [operatorKey] of (0, molstar_lib_mol_plugin_ui_sequence.getOperatorOptions)(structure, modelEntityId, chainGroupId)) entries.push({
1892
+ id: `${modelEntityId}:${chainGroupId}:${operatorKey}`,
1893
+ label: cLabel,
1894
+ wrapper: (0, molstar_lib_mol_plugin_ui_sequence.getSequenceWrapper)({
1895
+ chainGroupId,
1896
+ modelEntityId,
1897
+ operatorKey,
1898
+ structure
1899
+ }, selection)
1900
+ });
1901
+ return entries;
1902
+ }
1903
+ function countResidues(entries) {
1904
+ return entries.reduce((sum, entry) => sum + (typeof entry.wrapper === "string" ? 0 : entry.wrapper.length), 0);
1905
+ }
1906
+ /**
1907
+ * The residue wrappers for every chain of the loaded structure, resynced
1908
+ * whenever Mol* creates, updates, or removes a structure object.
1909
+ *
1910
+ * The wrappers read the live selection, so they are rebuilt on each render
1911
+ * rather than memoized; that is what keeps the grid's dimming in step with a
1912
+ * selection made in the 3D view.
1913
+ */
1914
+ function useSequenceWrappers() {
1915
+ const plugin = usePlugin();
1916
+ const [state, setState] = (0, react.useState)(EMPTY_STATE);
1917
+ const sync = (0, react.useCallback)(() => {
1918
+ if ((0, molstar_lib_mol_util.arrayEqual)((0, molstar_lib_mol_plugin_ui_sequence.getStructureOptions)(plugin.state.data).all, state.structureOptions.all)) return;
1919
+ setState(readInitialState(plugin));
1920
+ }, [plugin, state.structureOptions.all]);
1921
+ (0, react.useEffect)(() => {
1922
+ 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));
1923
+ }, []);
1924
+ useSubscribe(plugin.state.events.object.updated, ({ ref, obj }) => {
1925
+ if (ref === state.structureRef && obj && obj.type === molstar_lib_mol_plugin_state_objects.PluginStateObject.Molecule.Structure.type && obj.data !== state.structure) sync();
1926
+ });
1927
+ useSubscribe(plugin.state.events.object.created, ({ obj }) => {
1928
+ if (obj && obj.type === molstar_lib_mol_plugin_state_objects.PluginStateObject.Molecule.Structure.type) sync();
1929
+ });
1930
+ useSubscribe(plugin.state.events.object.removed, ({ obj }) => {
1931
+ if (obj && obj.type === molstar_lib_mol_plugin_state_objects.PluginStateObject.Molecule.Structure.type) sync();
1932
+ });
1933
+ const structure = readStructure(plugin, state.structureRef);
1934
+ if (structure === molstar_lib_mol_model_structure.Structure.Empty) return {
1935
+ entries: [],
1936
+ isEmpty: true,
1937
+ residueCount: 0
1938
+ };
1939
+ const entries = buildEntries(structure, plugin.managers.structure.selection, buildParams(state));
1940
+ return {
1941
+ entries,
1942
+ isEmpty: false,
1943
+ residueCount: countResidues(entries)
1944
+ };
1945
+ }
1946
+ //#endregion
1947
+ //#region src/core/ProteinStructureViewer/components/SequenceView/index.tsx
1948
+ /**
1949
+ * Builds the Mol* sequence panel view.
1950
+ *
1951
+ * Like the viewport, Mol* renders this inside its own React root, so the theme
1952
+ * mode and the panel's background arrive through `viewSettings` and are watched
1953
+ * for changes rather than read once. The theme is re-supplied through a local
1954
+ * `ThemeProvider` for the Emotion styles and the SDS `Tooltip` below.
1955
+ */
1956
+ function createSequenceView(viewSettings) {
1957
+ return function SequenceView() {
1958
+ const { mode, theme } = useMolstarTheme(viewSettings);
1959
+ const backgroundColor = useViewSetting(viewSettings, (s) => s.sequenceViewerBackgroundColor);
1960
+ const { entries, isEmpty, residueCount } = useSequenceWrappers();
1961
+ const { copied, copySequence } = useCopySequence(entries);
1962
+ if (isEmpty) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_mui_material_styles.ThemeProvider, {
1963
+ theme,
1964
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmptyState, {
1965
+ backgroundColor,
1966
+ className: "msp-sequence",
1967
+ children: "No structure available"
1968
+ })
1969
+ });
1970
+ const residueColors = residueColorsForMode(mode);
1971
+ const showChainLabels = entries.length > 1;
1972
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_mui_material_styles.ThemeProvider, {
1973
+ theme,
1974
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(SequencePanel, {
1975
+ backgroundColor,
1976
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(PanelHeader, { children: [
1977
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PanelTitle, { children: "Sequence" }),
1978
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_czi_sds_components.Tooltip, {
1979
+ arrow: true,
1980
+ placement: "top",
1981
+ title: copied ? "Copied!" : "Copy",
1982
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CopyButton, {
1983
+ "aria-label": "Copy sequence to clipboard",
1984
+ onClick: copySequence,
1985
+ type: "button",
1986
+ children: copied ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_phosphor_icons_react.CheckIcon, { size: 16 }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_phosphor_icons_react.CopyIcon, { size: 16 })
1987
+ })
1988
+ }),
1989
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ResidueCount, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResidueCountLabel, { children: "Residues" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResidueCountValue, { children: residueCount })] })
1990
+ ] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SequenceScroller, {
1991
+ backgroundColor,
1992
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SequenceScrollArea, {
1993
+ className: "msp-sequence msp-sequence-wrapper-non-empty",
1994
+ children: entries.map((s) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [showChainLabels && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChainLabel, { children: s.label }), typeof s.wrapper === "string" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1995
+ className: "msp-sequence-wrapper",
1996
+ children: s.wrapper
1997
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Sequence, {
1998
+ chainLabel: showChainLabels ? s.label : void 0,
1999
+ residueColors,
2000
+ sequenceWrapper: s.wrapper
2001
+ })] }, s.id))
2002
+ })
2003
+ })]
2004
+ })
2005
+ });
2006
+ };
2007
+ }
2008
+ //#endregion
2009
+ //#region src/core/ProteinStructureViewer/components/Viewport/constants.ts
2010
+ /**
2011
+ * Fraction of the bounding sphere radius within which the camera still counts
2012
+ * as sitting at the default view, absorbing easing and float error.
2013
+ */
2014
+ const DEFAULT_VIEW_TOLERANCE = .02;
2015
+ //#endregion
2016
+ //#region src/core/ProteinStructureViewer/components/Viewport/utils/camera.ts
2017
+ /** True when the camera's zoom/center differs from the default fit. */
2018
+ function isZoomedOffDefault(canvas3d) {
2019
+ const { center, radius } = canvas3d.boundingSphereVisible;
2020
+ if (radius <= 0) return false;
2021
+ const { state } = canvas3d.camera;
2022
+ const tolerance = radius * DEFAULT_VIEW_TOLERANCE;
2023
+ return Math.abs(state.radius - radius) > tolerance || molstar_lib_mol_math_linear_algebra.Vec3.distance(state.target, center) > tolerance;
2024
+ }
2025
+ //#endregion
2026
+ //#region \0@oxc-project+runtime@0.134.0/helpers/esm/asyncToGenerator.js
2027
+ function asyncGeneratorStep(n, t, e, r, o, a, c) {
2028
+ try {
2029
+ var i = n[a](c), u = i.value;
2030
+ } catch (n) {
2031
+ e(n);
2032
+ return;
2033
+ }
2034
+ i.done ? t(u) : Promise.resolve(u).then(r, o);
2035
+ }
2036
+ function _asyncToGenerator(n) {
2037
+ return function() {
2038
+ var t = this, e = arguments;
2039
+ return new Promise(function(r, o) {
2040
+ var a = n.apply(t, e);
2041
+ function _next(n) {
2042
+ asyncGeneratorStep(a, r, o, _next, _throw, "next", n);
2043
+ }
2044
+ function _throw(n) {
2045
+ asyncGeneratorStep(a, r, o, _next, _throw, "throw", n);
2046
+ }
2047
+ _next(void 0);
2048
+ });
2049
+ };
2050
+ }
2051
+ //#endregion
2052
+ //#region src/core/ProteinStructureViewer/components/Viewport/hooks/useCameraReset.ts
2053
+ /**
2054
+ * Tracks whether the camera has moved off the default view, and puts it back.
2055
+ *
2056
+ * The default view is the auto-fit to the structure's bounding sphere, so
2057
+ * "moved" means either the zoom/center has drifted off that fit or a manual
2058
+ * drag has rotated it - rotation has to be tracked separately, since it leaves
2059
+ * the zoom and center at the default and the bounding-sphere check cannot see
2060
+ * it.
2061
+ */
2062
+ function useCameraReset() {
2063
+ const plugin = usePlugin();
2064
+ const [cameraChanged, setCameraChanged] = (0, react.useState)(false);
2065
+ /**
2066
+ * Mirrors the state above so the per-frame handler below can skip redundant
2067
+ * updates without re-reading state through a stale closure.
2068
+ */
2069
+ const cameraChangedRef = (0, react.useRef)(false);
2070
+ const updateCameraChanged = (0, react.useCallback)((changed) => {
2071
+ if (changed === cameraChangedRef.current) return;
2072
+ cameraChangedRef.current = changed;
2073
+ setCameraChanged(changed);
2074
+ }, []);
2075
+ /**
2076
+ * A manual gesture (rotate via drag) moves the camera off the default view.
2077
+ * Tracked separately from zoom because rotation leaves the zoom/center at
2078
+ * the default fit, so the bounding-sphere check cannot detect it.
2079
+ */
2080
+ const gesturedRef = (0, react.useRef)(false);
2081
+ /**
2082
+ * A (re)loaded structure auto-fits to the default view. Ignore camera
2083
+ * changes until that fit transition settles so the button does not flash
2084
+ * during load.
2085
+ */
2086
+ const awaitingFitRef = (0, react.useRef)(true);
2087
+ const fitStartedRef = (0, react.useRef)(false);
2088
+ /**
2089
+ * Reconciles the button visibility with the current camera. The button
2090
+ * shows when the camera differs from the default view: either the
2091
+ * zoom/center has moved off the bounding-sphere fit, or a manual gesture
2092
+ * rotated the view.
2093
+ */
2094
+ const syncCameraChanged = (0, react.useCallback)(() => {
2095
+ const canvas3d = plugin.canvas3d;
2096
+ if (!canvas3d) return;
2097
+ const inTransition = canvas3d.camera.transition.inTransition;
2098
+ if (awaitingFitRef.current) {
2099
+ if (inTransition) {
2100
+ fitStartedRef.current = true;
2101
+ return;
2102
+ }
2103
+ if (!fitStartedRef.current && isZoomedOffDefault(canvas3d)) return;
2104
+ awaitingFitRef.current = false;
2105
+ }
2106
+ if (inTransition) return;
2107
+ updateCameraChanged(gesturedRef.current || isZoomedOffDefault(canvas3d));
2108
+ }, [plugin, updateCameraChanged]);
2109
+ (0, react.useEffect)(() => {
2110
+ const subscriptions = [];
2111
+ let cameraBound = false;
2112
+ const bindCamera = () => {
2113
+ const canvas3d = plugin.canvas3d;
2114
+ if (!canvas3d || cameraBound) return;
2115
+ cameraBound = true;
2116
+ const setGestured = () => {
2117
+ gesturedRef.current = true;
2118
+ syncCameraChanged();
2119
+ };
2120
+ subscriptions.push(canvas3d.input.drag.subscribe(setGestured), canvas3d.input.wheel.subscribe(setGestured), plugin.state.data.events.object.created.subscribe(() => {
2121
+ gesturedRef.current = false;
2122
+ awaitingFitRef.current = true;
2123
+ fitStartedRef.current = false;
2124
+ updateCameraChanged(false);
2125
+ }), canvas3d.didDraw.subscribe(syncCameraChanged));
2126
+ };
2127
+ subscriptions.push(plugin.behaviors.canvas3d.initialized.subscribe(bindCamera));
2128
+ bindCamera();
2129
+ return () => subscriptions.forEach((s) => s.unsubscribe());
2130
+ }, [
2131
+ plugin,
2132
+ syncCameraChanged,
2133
+ updateCameraChanged
2134
+ ]);
2135
+ return {
2136
+ cameraChanged,
2137
+ resetCamera: (0, react.useCallback)(_asyncToGenerator(function* () {
2138
+ yield molstar_lib_mol_plugin_commands.PluginCommands.Camera.Reset(plugin, { durationMs: 250 });
2139
+ yield new Promise((resolve) => setTimeout(resolve, 250));
2140
+ yield molstar_lib_mol_plugin_commands.PluginCommands.Camera.ResetAxes(plugin, { durationMs: 250 });
2141
+ yield new Promise((resolve) => setTimeout(resolve, 250));
2142
+ gesturedRef.current = false;
2143
+ updateCameraChanged(false);
2144
+ }), [plugin, updateCameraChanged])
2145
+ };
2146
+ }
2147
+ //#endregion
2148
+ //#region src/core/ProteinStructureViewer/components/Viewport/style.ts
2149
+ /**
2150
+ * Pinned to the lower-left of the viewport, clear of the sequence panel below
2151
+ * and the axes widget in the corner.
2152
+ */
2153
+ const ResetCameraSlot = (0, _emotion_styled.default)("div")`
2154
+ position: absolute;
2155
+ bottom: 48px;
2156
+ z-index: 10;
2157
+
2158
+ ${(props) => {
2159
+ const spaces = (0, _czi_sds_components.getSpaces)(props);
2160
+ return `
2161
+ left: ${spaces === null || spaces === void 0 ? void 0 : spaces.m}px;
2162
+ `;
2163
+ }}
2164
+ `;
2165
+ //#endregion
2166
+ //#region src/core/ProteinStructureViewer/components/Viewport/index.tsx
2167
+ /**
2168
+ * Builds the Mol* viewport view.
2169
+ *
2170
+ * Mol* instantiates view components inside its own React root, so they render
2171
+ * outside the consumer's provider tree and cannot read the theme from context.
2172
+ * `viewSettings` carries the mode in instead, and is watched rather than read
2173
+ * once so that a theme change after the plugin was created still lands here;
2174
+ * the mode is re-supplied via a local `ThemeProvider` so the Emotion styles
2175
+ * below resolve SDS tokens correctly.
2176
+ */
2177
+ function createViewportView(viewSettings) {
2178
+ return function ViewportWithReset() {
2179
+ const { theme } = useMolstarTheme(viewSettings);
2180
+ const { cameraChanged, resetCamera } = useCameraReset();
2181
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2182
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(molstar_lib_mol_plugin_ui_viewport.Viewport, {}),
2183
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2184
+ className: "msp-highlight-toast-wrapper",
2185
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(molstar_lib_mol_plugin_ui_toast.Toasts, {})
2186
+ }),
2187
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_mui_material_styles.ThemeProvider, {
2188
+ theme,
2189
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResetCameraSlot, {
2190
+ hidden: !cameraChanged,
2191
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_czi_sds_components.Button, {
2192
+ "aria-label": "Reset camera",
2193
+ onClick: resetCamera,
2194
+ title: "Reset camera",
2195
+ sdsStyle: "minimal",
2196
+ sdsType: "secondary",
2197
+ size: "medium",
2198
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_phosphor_icons_react.ArrowCounterClockwiseIcon, {})
2199
+ })
2200
+ })
2201
+ })
2202
+ ] });
2203
+ };
2204
+ }
2205
+ //#endregion
2206
+ //#region src/core/ProteinStructureViewer/utils/cameraFocus.ts
2207
+ /**
2208
+ * Margin (angstroms) added around a residue's bounding sphere when focusing,
2209
+ * matching Mol*'s default `FocusLoci` extra radius. Used both for the framing
2210
+ * radius that sets the zoom-in distance (floored at `FOCUS_MIN_RADIUS`) and for
2211
+ * the depth-clip anchor, so the focus crop matches Mol*'s default depth slab.
2212
+ */
2213
+ const FOCUS_MIN_RADIUS = 8;
2214
+ const FOCUS_EXTRA_RADIUS = 4;
2215
+ /**
2216
+ * Never let the clip window collapse below this, so the focused residue and its
2217
+ * immediate neighborhood stay visible even at extreme zoom-in.
2218
+ */
2219
+ const MIN_CLIP_RADIUS = 4;
2220
+ /** Camera transition length (ms) when zooming in on a residue. */
2221
+ const FOCUS_DURATION_MS = 250;
2222
+ /** Clip radius has to drift this far before it is worth redrawing. */
2223
+ const CLIP_EPSILON = .5;
2224
+ /**
2225
+ * Depth-clip radius at a given camera distance. The clip scales linearly with
2226
+ * distance, `ratio` being the clip radius per unit distance (fixed at focus so
2227
+ * the clip hugs the residue there), clamped to the scene. This keeps clipping
2228
+ * tied to the zoom:
2229
+ * - at the focus zoom the clip hugs the residue, peeling the structure so the
2230
+ * selected residue reads clearly;
2231
+ * - zooming in shrinks the clip further, peeling deeper toward the residue;
2232
+ * - zooming out grows the clip, revealing more of the structure, up to the
2233
+ * whole scene (`radiusMax`) - the point at which nothing is cropped.
2234
+ */
2235
+ function clipRadiusForDistance(distance, ratio, radiusMax) {
2236
+ return Math.max(MIN_CLIP_RADIUS, Math.min(ratio * distance, radiusMax));
2237
+ }
2238
+ /**
2239
+ * Zooms the camera in on a residue. Uses Mol*'s focus framing for the camera
2240
+ * *position* (so the zoom level matches the built-in focus) but anchors the
2241
+ * clip tightly on the residue itself, and returns the clip-radius-per-distance
2242
+ * ratio used to keep clipping in sync with the zoom afterwards.
2243
+ */
2244
+ function focusResidue(plugin, loci) {
2245
+ var _plugin$canvas3d;
2246
+ const camera = (_plugin$canvas3d = plugin.canvas3d) === null || _plugin$canvas3d === void 0 ? void 0 : _plugin$canvas3d.camera;
2247
+ if (!camera) return null;
2248
+ const sphere = molstar_lib_mol_model_loci.Loci.getBoundingSphere(loci);
2249
+ if (!sphere) return null;
2250
+ const framingRadius = Math.max(sphere.radius + FOCUS_EXTRA_RADIUS, FOCUS_MIN_RADIUS);
2251
+ const snapshot = camera.getFocus(sphere.center, framingRadius);
2252
+ const focusDistance = camera.getTargetDistance(framingRadius);
2253
+ const clipRatio = (sphere.radius + FOCUS_EXTRA_RADIUS) / focusDistance;
2254
+ const radius = clipRadiusForDistance(focusDistance, clipRatio, camera.state.radiusMax);
2255
+ camera.setState(_objectSpread2(_objectSpread2({}, snapshot), {}, { radius }), FOCUS_DURATION_MS);
2256
+ return clipRatio;
2257
+ }
2258
+ /**
2259
+ * Keeps the depth clip in sync with the zoom while a residue is focused.
2260
+ *
2261
+ * Mol*'s trackball zoom moves the camera without touching the clip radius, so
2262
+ * this recomputes it after each draw: zooming in tightens the clip (peeling
2263
+ * toward the residue), zooming out widens it toward the whole structure. It is
2264
+ * a no-op when `getClipRatio` returns null (nothing focused) and while the
2265
+ * camera is mid-transition, so it does not fight the focus animation.
2266
+ */
2267
+ function syncClipToZoom(plugin, getClipRatio) {
2268
+ var _plugin$canvas3d2;
2269
+ const camera = (_plugin$canvas3d2 = plugin.canvas3d) === null || _plugin$canvas3d2 === void 0 ? void 0 : _plugin$canvas3d2.camera;
2270
+ const clipRatio = getClipRatio();
2271
+ if (!camera || clipRatio === null || camera.transition.inTransition) return;
2272
+ const radius = clipRadiusForDistance(molstar_lib_mol_math_linear_algebra.Vec3.distance(camera.state.position, camera.state.target), clipRatio, camera.state.radiusMax);
2273
+ if (Math.abs(camera.state.radius - radius) > CLIP_EPSILON) {
2274
+ var _plugin$canvas3d3;
2275
+ camera.setState({ radius });
2276
+ (_plugin$canvas3d3 = plugin.canvas3d) === null || _plugin$canvas3d3 === void 0 || _plugin$canvas3d3.requestDraw();
2277
+ }
2278
+ }
2279
+ //#endregion
2280
+ //#region src/core/ProteinStructureViewer/utils/residueRef.ts
2281
+ /**
2282
+ * Reads the residue a loci points at, or null when it points at nothing the
2283
+ * viewer can address.
2284
+ *
2285
+ * `index` comes from Mol*'s residue key, which counts residues in file order
2286
+ * from zero and is the same counter `injectPlddtIntoPdb` walks. Deriving it
2287
+ * from the residue's *number* instead would tie it to how the file happens to
2288
+ * be numbered: a crop beginning at residue 200 would index `plddt` at 199, and
2289
+ * a file numbering each chain from 1 would give two residues the same index.
2290
+ */
2291
+ function residueRefFromLoci(loci) {
2292
+ var _StructureProperties$;
2293
+ const location = molstar_lib_mol_model_structure.StructureElement.Location.create(void 0);
2294
+ const firstLoc = molstar_lib_mol_model_structure.StructureElement.Loci.getFirstLocation(loci, location);
2295
+ if (!firstLoc) return null;
2296
+ return {
2297
+ chainId: molstar_lib_mol_model_structure.StructureProperties.chain.auth_asym_id(firstLoc),
2298
+ compId: molstar_lib_mol_model_structure.StructureProperties.atom.label_comp_id(firstLoc),
2299
+ insCode: (_StructureProperties$ = molstar_lib_mol_model_structure.StructureProperties.residue.pdbx_PDB_ins_code(firstLoc)) !== null && _StructureProperties$ !== void 0 ? _StructureProperties$ : "",
2300
+ index: molstar_lib_mol_model_structure.StructureProperties.residue.key(firstLoc),
2301
+ seqId: molstar_lib_mol_model_structure.StructureProperties.residue.auth_seq_id(firstLoc)
2302
+ };
2303
+ }
2304
+ /**
2305
+ * How a residue is named in the readout, e.g. `"LYS 111"`. Takes the number and
2306
+ * the insertion code from the file so that the readout and the sequence panel
2307
+ * beneath it agree on what to call a residue.
2308
+ */
2309
+ function residueLabel(residue) {
2310
+ return `${residue.compId} ${residue.seqId}${residue.insCode}`;
2311
+ }
2312
+ //#endregion
2313
+ //#region src/core/ProteinStructureViewer/utils/plddt.ts
2314
+ /** Mol* color theme name under which pLDDT coloring is registered. */
2315
+ const PLDDT_THEME_NAME = "plddt-bfactor";
2316
+ /**
2317
+ * AlphaFold's pLDDT confidence bands. Thresholds are on the 0-1 scale that
2318
+ * `plddt` values use, ascending from lowest to highest confidence.
2319
+ */
2320
+ const PLDDT_COLOR_SCALE = {
2321
+ kind: "stepped",
2322
+ stops: [
2323
+ {
2324
+ color: "#FF7C45",
2325
+ label: "0.5",
2326
+ threshold: .5
2327
+ },
2328
+ {
2329
+ color: "#FFDB11",
2330
+ label: "0.7",
2331
+ threshold: .7
2332
+ },
2333
+ {
2334
+ color: "#64CBF3",
2335
+ label: "0.9",
2336
+ threshold: .9
2337
+ },
2338
+ {
2339
+ color: "#0053D5",
2340
+ label: "1.0",
2341
+ threshold: 1
2342
+ }
2343
+ ]
2344
+ };
2345
+ const PLDDT_BANDS = PLDDT_COLOR_SCALE.kind === "stepped" ? PLDDT_COLOR_SCALE.stops : [];
2346
+ const DEFAULT_PLDDT_COLOR = molstar_lib_mol_util_color.Color.fromRgb(128, 128, 128);
2347
+ /**
2348
+ * Maps a pLDDT confidence score on the 0-100 B-factor scale to its band color.
2349
+ * `PLDDT_COLOR_SCALE` thresholds are on the 0-1 scale, so the score is
2350
+ * normalized before it is sampled.
2351
+ */
2352
+ function plddtToColor(value) {
2353
+ const [r, g, b] = sampleSteppedScale(PLDDT_BANDS, value / 100);
2354
+ return molstar_lib_mol_util_color.Color.fromRgb(r, g, b);
2355
+ }
2356
+ /**
2357
+ * Rewrites a PDB file's B-factor column with pLDDT scores so Mol* can read them
2358
+ * off the parsed model. B-factors occupy columns 60-66 of ATOM/HETATM lines;
2359
+ * scores arrive on a 0-1 scale and are stored on the conventional 0-100 one.
2360
+ * Residues past the end of `plddtValues` fall back to a mid-confidence 50.
2361
+ *
2362
+ * A residue is identified by chain, sequence number and insertion code. The
2363
+ * code has to be part of that: `10` and `10A` are two residues, Mol* counts
2364
+ * them as two, and treating them as one here would hand every residue after
2365
+ * them its neighbour's score.
2366
+ */
2367
+ function injectPlddtIntoPdb(pdbData, plddtValues) {
2368
+ const lines = pdbData.split("\n");
2369
+ let residueIndex = -1;
2370
+ let lastResidue = "";
2371
+ return lines.map((line) => {
2372
+ var _plddtValues$residueI;
2373
+ if (!line.startsWith("ATOM") && !line.startsWith("HETATM")) return line;
2374
+ const residue = line.substring(21, 27);
2375
+ if (residue !== lastResidue) {
2376
+ residueIndex++;
2377
+ lastResidue = residue;
2378
+ }
2379
+ const bfactorStr = (residueIndex < plddtValues.length ? ((_plddtValues$residueI = plddtValues[residueIndex]) !== null && _plddtValues$residueI !== void 0 ? _plddtValues$residueI : 0) * 100 : 50).toFixed(2).padStart(6, " ");
2380
+ const paddedLine = line.padEnd(66, " ");
2381
+ return paddedLine.substring(0, 60) + bfactorStr + paddedLine.substring(66);
2382
+ }).join("\n");
2383
+ }
2384
+ /**
2385
+ * Mol* color theme that reads pLDDT scores out of the B-factor column, paired
2386
+ * with `injectPlddtIntoPdb`. Registered per plugin instance.
2387
+ */
2388
+ const PlddtColoring = molstar_lib_mol_model_props_common_custom_element_property.CustomElementProperty.create({
2389
+ coloring: {
2390
+ defaultColor: DEFAULT_PLDDT_COLOR,
2391
+ getColor(e) {
2392
+ return plddtToColor(e);
2393
+ }
2394
+ },
2395
+ getData(model) {
2396
+ const map = /* @__PURE__ */ new Map();
2397
+ const atomCount = model.atomicHierarchy.atoms._rowCount;
2398
+ for (let i = 0; i < atomCount; i++) map.set(i, model.atomicConformation.B_iso_or_equiv.value(i));
2399
+ return { value: map };
2400
+ },
2401
+ label: "pLDDT",
2402
+ name: PLDDT_THEME_NAME
2403
+ });
2404
+ //#endregion
2405
+ //#region src/core/ProteinStructureViewer/utils/color.ts
2406
+ /** Parses `#RRGGBB` (or `RRGGBB`) into a Mol* `Color`. */
2407
+ function parseHexColor(hex) {
2408
+ const h = hex.replace("#", "");
2409
+ return molstar_lib_mol_util_color.Color.fromRgb(parseInt(h.substring(0, 2), 16), parseInt(h.substring(2, 4), 16), parseInt(h.substring(4, 6), 16));
2410
+ }
2411
+ /**
2412
+ * Parses a theme color into a Mol* `Color`, falling back when the token is
2413
+ * missing or is a format Mol* cannot consume (theme values are hex in practice,
2414
+ * but the palette types allow any CSS color string).
2415
+ */
2416
+ function themeColor(value, fallback) {
2417
+ const candidate = value === null || value === void 0 ? void 0 : value.trim();
2418
+ return parseHexColor(candidate !== void 0 && /^#?[0-9a-f]{6}$/i.test(candidate) ? candidate : fallback);
2419
+ }
2420
+ /**
2421
+ * Neutral fill for residues with no overlay value. Sits between the light and
2422
+ * dark backgrounds so unhighlighted geometry stays legible in both modes.
2423
+ */
2424
+ function neutralResidueColor(mode) {
2425
+ const g = mode === "dark" ? 100 : 180;
2426
+ return molstar_lib_mol_util_color.Color.fromRgb(g, g, g);
2427
+ }
2428
+ //#endregion
2429
+ //#region src/core/ProteinStructureViewer/utils/residueValueTheme.ts
2430
+ /** Mol* color theme name under which residue value overlays are registered. */
2431
+ const RESIDUE_VALUE_THEME_NAME = "residue-value";
2432
+ /**
2433
+ * Builds a Mol* color theme that paints residues by an arbitrary per-residue
2434
+ * value.
2435
+ *
2436
+ * The theme is created per plugin instance rather than shared at module scope,
2437
+ * so several viewers can render different overlays on the same page. Its state
2438
+ * lives in the closure below and is read live inside `color()`, which is what
2439
+ * lets an overlay update recolor the structure without rebuilding it.
2440
+ */
2441
+ function createResidueValueTheme(mode) {
2442
+ const state = {
2443
+ colorScale: PLASMA_COLOR_SCALE,
2444
+ max: 1,
2445
+ min: 0,
2446
+ mode,
2447
+ values: /* @__PURE__ */ new Map(),
2448
+ version: 0
2449
+ };
2450
+ const provider = {
2451
+ category: "residue property",
2452
+ defaultValues: {},
2453
+ factory() {
2454
+ const { colorScale, max, min, mode: currentMode, values } = state;
2455
+ const neutral = neutralResidueColor(currentMode);
2456
+ return {
2457
+ color(location) {
2458
+ if (!molstar_lib_mol_model_structure.StructureElement.Location.is(location)) return neutral;
2459
+ const value = values.get(molstar_lib_mol_model_structure.StructureProperties.residue.key(location));
2460
+ if (value === void 0) return neutral;
2461
+ const rgb = sampleColorScale(colorScale, value, max, min);
2462
+ return rgb ? molstar_lib_mol_util_color.Color.fromRgb(rgb[0], rgb[1], rgb[2]) : neutral;
2463
+ },
2464
+ contextHash: state.version,
2465
+ description: "Color residues by a per-residue value",
2466
+ factory: provider.factory,
2467
+ granularity: "group",
2468
+ props: {}
2469
+ };
2470
+ },
2471
+ getParams: () => ({}),
2472
+ isApplicable: () => true,
2473
+ label: "Residue Value",
2474
+ name: RESIDUE_VALUE_THEME_NAME
2475
+ };
2476
+ return {
2477
+ provider,
2478
+ setState(next) {
2479
+ Object.assign(state, next);
2480
+ state.version++;
2481
+ }
2482
+ };
2483
+ }
2484
+ //#endregion
2485
+ //#region src/core/ProteinStructureViewer/hooks/useMolstarPlugin.ts
2486
+ /** Structure representation Mol* builds for the loaded model. */
2487
+ const REPRESENTATION_PRESET = "polymer-cartoon";
2488
+ /** Theme used when no pLDDT scores are supplied. */
2489
+ const FALLBACK_THEME_NAME = "chain-id";
2490
+ /** Delay before retrying initialization while the container has no size. */
2491
+ const LAYOUT_RETRY_MS = 100;
2492
+ /** How long to wait for a frame before starting without one. */
2493
+ const FRAME_WAIT_MS = 100;
2494
+ /**
2495
+ * Yield once so the container can be laid out before it is measured.
2496
+ *
2497
+ * `requestAnimationFrame` does not fire while the document is hidden -- a
2498
+ * background tab, a collapsed pane, or a headless capture that never paints --
2499
+ * and awaiting it alone leaves the viewer parked forever on a page that has in
2500
+ * fact been laid out. Racing it against a timeout keeps the fast path on a
2501
+ * visible page and still starts on a hidden one, where the size check below is
2502
+ * what actually guards against measuring too early.
2503
+ *
2504
+ * The loser of the race is cancelled rather than left to fire: on a visible
2505
+ * page this runs once per viewer, and a grid of them would otherwise each keep
2506
+ * a stray timer alive past the frame that already resolved.
2507
+ */
2508
+ function nextFrame() {
2509
+ return new Promise((resolve) => {
2510
+ const done = () => {
2511
+ cancelAnimationFrame(frame);
2512
+ clearTimeout(timer);
2513
+ resolve();
2514
+ };
2515
+ const frame = requestAnimationFrame(done);
2516
+ const timer = setTimeout(done, FRAME_WAIT_MS);
2517
+ });
2518
+ }
2519
+ /**
2520
+ * Hover fills the geometry with a tint; selection stays outline-only. Mol*'s
2521
+ * `colorMarker` is a global toggle for the per-fragment marker tint, so it is
2522
+ * scoped to hover via the per-state strengths below.
2523
+ */
2524
+ const HIGHLIGHT_STRENGTH = .2;
2525
+ const SELECT_STRENGTH = 0;
2526
+ const AXES_ON = {
2527
+ name: "on",
2528
+ params: {
2529
+ alpha: .51,
2530
+ colorX: 16711680,
2531
+ colorY: 32768,
2532
+ colorZ: 255,
2533
+ labelColorX: 8421504,
2534
+ labelColorY: 8421504,
2535
+ labelColorZ: 8421504,
2536
+ labelOpacity: 1,
2537
+ labelScale: .25,
2538
+ labelX: "X",
2539
+ labelY: "Y",
2540
+ labelZ: "Z",
2541
+ location: "bottom-left",
2542
+ locationOffsetX: 0,
2543
+ locationOffsetY: 0,
2544
+ originColor: 8421504,
2545
+ planeColorXY: 8421504,
2546
+ planeColorXZ: 8421504,
2547
+ planeColorYZ: 8421504,
2548
+ radiusScale: .075,
2549
+ scale: .15,
2550
+ showLabels: false,
2551
+ showPlanes: true
2552
+ }
2553
+ };
2554
+ const AXES_OFF = {
2555
+ name: "off",
2556
+ params: {}
2557
+ };
2558
+ /**
2559
+ * Turns the screenshot controls off, on the versions of Mol* that have them.
2560
+ *
2561
+ * `ShowScreenshotControls` arrived in Mol* 5 and this package's peer range
2562
+ * reaches back to 4, so the key is read through an index rather than named
2563
+ * directly: naming it would not compile against 4, and passing the resulting
2564
+ * `undefined` would ask Mol* to set a config item it has no name for. On 4
2565
+ * there are no screenshot controls to hide in the first place.
2566
+ */
2567
+ const SCREENSHOT_CONTROLS_CONFIG = (() => {
2568
+ const item = molstar_lib_mol_plugin_config.PluginConfig.Viewport.ShowScreenshotControls;
2569
+ return item ? [[item, false]] : [];
2570
+ })();
2571
+ function setAxes(plugin, enabled) {
2572
+ if (!plugin.canvas3d) return;
2573
+ try {
2574
+ plugin.canvas3d.setProps({ camera: { helper: { axes: enabled ? AXES_ON : AXES_OFF } } });
2575
+ } catch (e) {
2576
+ console.warn("Could not set axes:", e);
2577
+ }
2578
+ }
2579
+ function createViewer(_x) {
2580
+ return _createViewer.apply(this, arguments);
2581
+ }
2582
+ function _createViewer() {
2583
+ _createViewer = _asyncToGenerator(function* ({ backgroundColor, edgeColor, highlightColor, residueValueTheme, root, showAxes, showSequenceViewer, viewSettings }) {
2584
+ var _spec$config;
2585
+ const spec = (0, molstar_lib_mol_plugin_ui_spec.DefaultPluginUISpec)();
2586
+ const behaviors = spec.behaviors.filter((b) => b.transformer !== molstar_lib_mol_plugin_behavior.PluginBehaviors.Camera.FocusLoci);
2587
+ const plugin = yield (0, molstar_lib_mol_plugin_ui.createPluginUI)({
2588
+ render: molstar_lib_mol_plugin_ui_react18.renderReact18,
2589
+ spec: _objectSpread2(_objectSpread2({}, spec), {}, {
2590
+ behaviors,
2591
+ canvas3d: {
2592
+ camera: { helper: { axes: showAxes ? AXES_ON : AXES_OFF } },
2593
+ marking: {
2594
+ highlightEdgeColor: edgeColor,
2595
+ selectEdgeColor: edgeColor
2596
+ },
2597
+ renderer: {
2598
+ backgroundColor,
2599
+ colorMarker: true,
2600
+ highlightColor,
2601
+ highlightStrength: HIGHLIGHT_STRENGTH,
2602
+ selectStrength: SELECT_STRENGTH
2603
+ }
2604
+ },
2605
+ components: _objectSpread2({
2606
+ remoteState: "none",
2607
+ sequenceViewer: { view: createSequenceView(viewSettings) }
2608
+ }, showAxes && { viewport: { view: createViewportView(viewSettings) } }),
2609
+ config: [
2610
+ ...(_spec$config = spec.config) !== null && _spec$config !== void 0 ? _spec$config : [],
2611
+ [molstar_lib_mol_plugin_config.PluginConfig.Viewport.ShowExpand, false],
2612
+ [molstar_lib_mol_plugin_config.PluginConfig.Viewport.ShowControls, false],
2613
+ [molstar_lib_mol_plugin_config.PluginConfig.Viewport.ShowSettings, false],
2614
+ [molstar_lib_mol_plugin_config.PluginConfig.Viewport.ShowSelectionMode, false],
2615
+ [molstar_lib_mol_plugin_config.PluginConfig.Viewport.ShowAnimation, false],
2616
+ [molstar_lib_mol_plugin_config.PluginConfig.Viewport.ShowTrajectoryControls, false],
2617
+ ...SCREENSHOT_CONTROLS_CONFIG
2618
+ ],
2619
+ layout: { initial: {
2620
+ controlsDisplay: "reactive",
2621
+ isExpanded: false,
2622
+ regionState: {
2623
+ bottom: "hidden",
2624
+ left: "hidden",
2625
+ right: "hidden",
2626
+ top: showSequenceViewer ? "full" : "hidden"
2627
+ }
2628
+ } }
2629
+ }),
2630
+ target: root
2631
+ });
2632
+ const registry = plugin.representation.structure.themes.colorThemeRegistry;
2633
+ if (PlddtColoring.colorThemeProvider) registry.add(PlddtColoring.colorThemeProvider);
2634
+ registry.add(residueValueTheme.provider);
2635
+ for (const p of [...plugin.managers.lociLabels.providers]) plugin.managers.lociLabels.removeProvider(p);
2636
+ plugin.managers.structure.focus.behaviors.current.subscribe((entry) => {
2637
+ const select = plugin.managers.interactivity.lociSelects;
2638
+ if (entry === null || entry === void 0 ? void 0 : entry.loci) select.selectOnly({ loci: entry.loci });
2639
+ else select.deselectAll();
2640
+ });
2641
+ return plugin;
2642
+ });
2643
+ return _createViewer.apply(this, arguments);
2644
+ }
2645
+ function loadStructure(_x2, _x3, _x4, _x5) {
2646
+ return _loadStructure.apply(this, arguments);
2647
+ }
2648
+ function _loadStructure() {
2649
+ _loadStructure = _asyncToGenerator(function* (plugin, pdbData, usePlddtColoring, showAxes) {
2650
+ try {
2651
+ var _plugin$canvas3d;
2652
+ yield plugin.clear();
2653
+ const data = yield plugin.builders.data.rawData({
2654
+ data: pdbData,
2655
+ label: "Structure"
2656
+ });
2657
+ const trajectory = yield plugin.builders.structure.parseTrajectory(data, "pdb");
2658
+ yield plugin.builders.structure.hierarchy.applyPreset(trajectory, "default", {
2659
+ representationPreset: REPRESENTATION_PRESET,
2660
+ showUnitcell: false,
2661
+ structure: {
2662
+ name: "model",
2663
+ params: {}
2664
+ }
2665
+ });
2666
+ if (usePlddtColoring) yield applyColorTheme(plugin, PLDDT_THEME_NAME);
2667
+ (_plugin$canvas3d = plugin.canvas3d) === null || _plugin$canvas3d === void 0 || _plugin$canvas3d.requestCameraReset();
2668
+ setAxes(plugin, showAxes);
2669
+ } catch (error) {
2670
+ console.error("Failed to load structure:", error);
2671
+ }
2672
+ });
2673
+ return _loadStructure.apply(this, arguments);
2674
+ }
2675
+ /** Recolors every loaded structure's representations with the named theme. */
2676
+ function applyColorTheme(_x6, _x7) {
2677
+ return _applyColorTheme.apply(this, arguments);
2678
+ }
2679
+ function _applyColorTheme() {
2680
+ _applyColorTheme = _asyncToGenerator(function* (plugin, colorTheme) {
2681
+ yield plugin.dataTransaction(_asyncToGenerator(function* () {
2682
+ for (const s of plugin.managers.structure.hierarchy.current.structures) yield plugin.managers.structure.component.updateRepresentationsTheme(s.components, { color: colorTheme });
2683
+ }));
2684
+ });
2685
+ return _applyColorTheme.apply(this, arguments);
2686
+ }
2687
+ /**
2688
+ * Owns the Mol* plugin for one viewer: creates it, loads the structure, wires
2689
+ * interaction callbacks, and disposes of it on unmount.
2690
+ *
2691
+ * The plugin is built once and then mutated in place. Creating it is expensive
2692
+ * and destroys the camera, so prop changes that Mol* can absorb (background,
2693
+ * axes, coloring, new PDB) are pushed in through the effects below and in the
2694
+ * sibling hooks rather than by rebuilding.
2695
+ */
2696
+ function useMolstarPlugin({ backgroundColor, containerRef, edgeColor, hasPlddt, highlightColor, mode, onResidueClick, onResidueHover, onSelectionClear, pdb, sequenceViewerBackgroundColor, showAxes, showSequenceViewer }) {
2697
+ const pluginRef = (0, react.useRef)(null);
2698
+ const residueValueThemeRef = (0, react.useRef)(null);
2699
+ const currentPdbRef = (0, react.useRef)(null);
2700
+ const [isReady, setIsReady] = (0, react.useState)(false);
2701
+ /**
2702
+ * Carries the theme-dependent props into the views Mol* renders in its own
2703
+ * React root. They cannot read them from context, and rebuilding the plugin
2704
+ * would throw away the camera, so the values are pushed to them instead.
2705
+ */
2706
+ const viewSettingsRef = (0, react.useRef)(null);
2707
+ if (viewSettingsRef.current === null) viewSettingsRef.current = new rxjs.BehaviorSubject({
2708
+ mode,
2709
+ sequenceViewerBackgroundColor
2710
+ });
2711
+ const viewSettings = viewSettingsRef.current;
2712
+ /**
2713
+ * Clip-radius-per-camera-distance ratio recorded at the last residue focus,
2714
+ * or null when no residue is focused. Anchors the adaptive depth clipping.
2715
+ */
2716
+ const clipRatioRef = (0, react.useRef)(null);
2717
+ const setClipRatio = (0, react.useCallback)((ratio) => {
2718
+ clipRatioRef.current = ratio;
2719
+ }, []);
2720
+ const onResidueClickRef = (0, react.useRef)(onResidueClick);
2721
+ onResidueClickRef.current = onResidueClick;
2722
+ const onSelectionClearRef = (0, react.useRef)(onSelectionClear);
2723
+ onSelectionClearRef.current = onSelectionClear;
2724
+ const onResidueHoverRef = (0, react.useRef)(onResidueHover);
2725
+ onResidueHoverRef.current = onResidueHover;
2726
+ /** Last residue reported to `onResidueHover`, to suppress repeats. */
2727
+ const lastHoverRef = (0, react.useRef)(null);
2728
+ const initialPropsRef = (0, react.useRef)({
2729
+ backgroundColor,
2730
+ edgeColor,
2731
+ hasPlddt,
2732
+ highlightColor,
2733
+ mode,
2734
+ pdb,
2735
+ showAxes,
2736
+ showSequenceViewer
2737
+ });
2738
+ initialPropsRef.current = {
2739
+ backgroundColor,
2740
+ edgeColor,
2741
+ hasPlddt,
2742
+ highlightColor,
2743
+ mode,
2744
+ pdb,
2745
+ showAxes,
2746
+ showSequenceViewer
2747
+ };
2748
+ (0, react.useEffect)(() => {
2749
+ const container = containerRef.current;
2750
+ if (!container) return;
2751
+ let cancelled = false;
2752
+ let clipSubscription;
2753
+ const init = function() {
2754
+ var _ref = _asyncToGenerator(function* () {
2755
+ yield nextFrame();
2756
+ if (cancelled) return;
2757
+ const rect = container.getBoundingClientRect();
2758
+ if (rect.width === 0 || rect.height === 0) {
2759
+ setTimeout(init, LAYOUT_RETRY_MS);
2760
+ return;
2761
+ }
2762
+ const initial = initialPropsRef.current;
2763
+ try {
2764
+ var _plugin$canvas3d2;
2765
+ const residueValueTheme = createResidueValueTheme(initial.mode);
2766
+ const plugin = yield createViewer({
2767
+ backgroundColor: initial.backgroundColor,
2768
+ edgeColor: initial.edgeColor,
2769
+ highlightColor: initial.highlightColor,
2770
+ residueValueTheme,
2771
+ root: container,
2772
+ showAxes: initial.showAxes,
2773
+ showSequenceViewer: initial.showSequenceViewer,
2774
+ viewSettings
2775
+ });
2776
+ if (cancelled) {
2777
+ plugin.dispose();
2778
+ return;
2779
+ }
2780
+ pluginRef.current = plugin;
2781
+ residueValueThemeRef.current = residueValueTheme;
2782
+ const latest = initialPropsRef.current;
2783
+ yield loadStructure(plugin, latest.pdb, latest.hasPlddt, latest.showAxes);
2784
+ currentPdbRef.current = latest.pdb;
2785
+ plugin.behaviors.interaction.click.subscribe((e) => {
2786
+ var _onResidueClickRef$cu;
2787
+ if (molstar_lib_mol_repr_representation.Representation.Loci.isEmpty(e.current)) {
2788
+ var _onSelectionClearRef$;
2789
+ (_onSelectionClearRef$ = onSelectionClearRef.current) === null || _onSelectionClearRef$ === void 0 || _onSelectionClearRef$.call(onSelectionClearRef);
2790
+ return;
2791
+ }
2792
+ const loci = e.current.loci;
2793
+ if (!molstar_lib_mol_model_structure.StructureElement.Loci.is(loci)) return;
2794
+ const residue = residueRefFromLoci(loci);
2795
+ if (!residue) return;
2796
+ (_onResidueClickRef$cu = onResidueClickRef.current) === null || _onResidueClickRef$cu === void 0 || _onResidueClickRef$cu.call(onResidueClickRef, residue);
2797
+ });
2798
+ plugin.behaviors.interaction.hover.subscribe((e) => {
2799
+ var _lastHoverRef$current, _onResidueHoverRef$cu;
2800
+ const loci = e.current.loci;
2801
+ const residue = molstar_lib_mol_model_structure.StructureElement.Loci.is(loci) ? residueRefFromLoci(loci) : null;
2802
+ 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;
2803
+ lastHoverRef.current = residue;
2804
+ (_onResidueHoverRef$cu = onResidueHoverRef.current) === null || _onResidueHoverRef$cu === void 0 || _onResidueHoverRef$cu.call(onResidueHoverRef, residue);
2805
+ });
2806
+ clipSubscription = (_plugin$canvas3d2 = plugin.canvas3d) === null || _plugin$canvas3d2 === void 0 ? void 0 : _plugin$canvas3d2.didDraw.subscribe(() => {
2807
+ syncClipToZoom(plugin, () => clipRatioRef.current);
2808
+ });
2809
+ if (!cancelled) setIsReady(true);
2810
+ } catch (error) {
2811
+ console.error("Failed to initialize Mol* viewer:", error);
2812
+ }
2813
+ });
2814
+ return function init() {
2815
+ return _ref.apply(this, arguments);
2816
+ };
2817
+ }();
2818
+ init();
2819
+ return () => {
2820
+ var _pluginRef$current;
2821
+ cancelled = true;
2822
+ clipSubscription === null || clipSubscription === void 0 || clipSubscription.unsubscribe();
2823
+ (_pluginRef$current = pluginRef.current) === null || _pluginRef$current === void 0 || _pluginRef$current.dispose();
2824
+ pluginRef.current = null;
2825
+ residueValueThemeRef.current = null;
2826
+ currentPdbRef.current = null;
2827
+ clipRatioRef.current = null;
2828
+ setIsReady(false);
2829
+ };
2830
+ }, []);
2831
+ (0, react.useEffect)(() => {
2832
+ var _pluginRef$current2;
2833
+ const canvas3d = (_pluginRef$current2 = pluginRef.current) === null || _pluginRef$current2 === void 0 ? void 0 : _pluginRef$current2.canvas3d;
2834
+ if (!canvas3d || !isReady) return;
2835
+ canvas3d.setProps({
2836
+ marking: {
2837
+ highlightEdgeColor: edgeColor,
2838
+ selectEdgeColor: edgeColor
2839
+ },
2840
+ renderer: {
2841
+ backgroundColor,
2842
+ highlightColor
2843
+ }
2844
+ });
2845
+ }, [
2846
+ backgroundColor,
2847
+ edgeColor,
2848
+ highlightColor,
2849
+ isReady
2850
+ ]);
2851
+ (0, react.useEffect)(() => {
2852
+ viewSettings.next({
2853
+ mode,
2854
+ sequenceViewerBackgroundColor
2855
+ });
2856
+ }, [
2857
+ mode,
2858
+ sequenceViewerBackgroundColor,
2859
+ viewSettings
2860
+ ]);
2861
+ (0, react.useEffect)(() => {
2862
+ if (!isReady || !pluginRef.current) return;
2863
+ setAxes(pluginRef.current, showAxes);
2864
+ }, [showAxes, isReady]);
2865
+ (0, react.useEffect)(() => {
2866
+ const plugin = pluginRef.current;
2867
+ if (!plugin || !isReady) return;
2868
+ if (pdb === currentPdbRef.current) return;
2869
+ currentPdbRef.current = pdb;
2870
+ clipRatioRef.current = null;
2871
+ lastHoverRef.current = null;
2872
+ loadStructure(plugin, pdb, hasPlddt, showAxes);
2873
+ }, [
2874
+ pdb,
2875
+ hasPlddt,
2876
+ isReady
2877
+ ]);
2878
+ return {
2879
+ isReady,
2880
+ pluginRef,
2881
+ residueValueThemeRef,
2882
+ setClipRatio
2883
+ };
2884
+ }
2885
+ //#endregion
2886
+ //#region src/core/ProteinStructureViewer/utils/residueLoci.ts
2887
+ /**
2888
+ * Every atom of the residue at a 0-based index, as geometry Mol* can focus and
2889
+ * frame, or undefined when the structure has no such residue.
2890
+ *
2891
+ * The inverse of `residueRefFromLoci`, and keyed on the same residue ordinal,
2892
+ * so a `selectedResidue` echoed back from a click resolves to the residue that
2893
+ * was clicked - on a complex as much as on a single chain.
2894
+ *
2895
+ * The whole residue is collected rather than one atom, since the caller frames
2896
+ * the camera on the loci's bounding sphere and a single atom would zoom far
2897
+ * closer than the residue warrants.
2898
+ */
2899
+ function lociForResidueIndex(plugin, residueIndex) {
2900
+ for (const entry of plugin.managers.structure.hierarchy.current.structures) {
2901
+ var _entry$cell$obj;
2902
+ const structure = (_entry$cell$obj = entry.cell.obj) === null || _entry$cell$obj === void 0 ? void 0 : _entry$cell$obj.data;
2903
+ if (!structure) continue;
2904
+ const location = molstar_lib_mol_model_structure.StructureElement.Location.create(structure);
2905
+ for (const unit of structure.units) {
2906
+ location.unit = unit;
2907
+ const indices = [];
2908
+ for (let i = 0; i < unit.elements.length; i++) {
2909
+ location.element = unit.elements[i];
2910
+ if (molstar_lib_mol_model_structure.StructureProperties.residue.key(location) === residueIndex) indices.push(i);
2911
+ }
2912
+ if (indices.length > 0) return molstar_lib_mol_model_structure.StructureElement.Loci(structure, [{
2913
+ indices: molstar_lib_mol_data_int.OrderedSet.ofSortedArray(indices),
2914
+ unit
2915
+ }]);
2916
+ }
2917
+ }
2918
+ }
2919
+ //#endregion
2920
+ //#region src/core/ProteinStructureViewer/hooks/useResidueFocus.ts
2921
+ /**
2922
+ * Makes `selectedResidue` the one thing that decides where the camera sits.
2923
+ *
2924
+ * Setting the prop zooms in on that residue and marks it; clearing it drops the
2925
+ * focus and zooms back out. The click subscription only reports the residue it
2926
+ * was given, so a click moves the camera only once the consumer accepts it and
2927
+ * echoes it back - which is what the prop being controlled is supposed to mean.
2928
+ *
2929
+ * Clicking still marks the residue immediately, through Mol*'s own focus
2930
+ * behavior, so the structure responds to a click whether or not the consumer
2931
+ * wires the prop up.
2932
+ */
2933
+ function useResidueFocus({ isReady, pluginRef, selectedResidue, setClipRatio }) {
2934
+ /**
2935
+ * The residue this hook last pointed the camera at. Tracked rather than
2936
+ * compared against the previous prop value so that a selection made before
2937
+ * the plugin was ready is still applied once it is, and so that a plugin
2938
+ * rebuilt underneath us - which comes up focused on nothing - is not assumed
2939
+ * to still be holding the old one.
2940
+ */
2941
+ const focusedRef = (0, react.useRef)(null);
2942
+ (0, react.useEffect)(() => {
2943
+ const plugin = pluginRef.current;
2944
+ if (!plugin || !isReady) {
2945
+ focusedRef.current = null;
2946
+ return;
2947
+ }
2948
+ const residue = selectedResidue !== null && selectedResidue !== void 0 ? selectedResidue : null;
2949
+ if (residue === focusedRef.current) return;
2950
+ focusedRef.current = residue;
2951
+ if (residue === null) {
2952
+ var _plugin$canvas3d;
2953
+ setClipRatio(null);
2954
+ plugin.managers.structure.focus.clear();
2955
+ (_plugin$canvas3d = plugin.canvas3d) === null || _plugin$canvas3d === void 0 || _plugin$canvas3d.requestCameraReset();
2956
+ return;
2957
+ }
2958
+ const loci = lociForResidueIndex(plugin, residue);
2959
+ if (!loci) return;
2960
+ const focused = plugin.managers.structure.focus.behaviors.current.value;
2961
+ if (!focused || !molstar_lib_mol_model_structure.StructureElement.Loci.areEqual(focused.loci, loci)) plugin.managers.structure.focus.setFromLoci(loci);
2962
+ setClipRatio(focusResidue(plugin, loci));
2963
+ }, [
2964
+ selectedResidue,
2965
+ isReady,
2966
+ setClipRatio
2967
+ ]);
2968
+ }
2969
+ //#endregion
2970
+ //#region src/core/ProteinStructureViewer/hooks/useResidueOverlay.ts
2971
+ /**
2972
+ * Switches the structure's coloring between the residue value overlay and the
2973
+ * default (pLDDT, or chain-id when no scores were supplied).
2974
+ *
2975
+ * Recoloring happens in place: the camera is left where it is, so selecting or
2976
+ * switching an overlay does not zoom back out to the default view.
2977
+ */
2978
+ function useResidueOverlay({ hasPlddt, isReady, mode, overlay, pluginRef, residueValueThemeRef }) {
2979
+ (0, react.useEffect)(() => {
2980
+ const plugin = pluginRef.current;
2981
+ const theme = residueValueThemeRef.current;
2982
+ if (!plugin || !theme || !isReady) return;
2983
+ if (overlay) {
2984
+ var _overlay$colorScale, _overlay$min;
2985
+ theme.setState({
2986
+ colorScale: (_overlay$colorScale = overlay.colorScale) !== null && _overlay$colorScale !== void 0 ? _overlay$colorScale : PLASMA_COLOR_SCALE,
2987
+ max: overlay.max,
2988
+ min: (_overlay$min = overlay.min) !== null && _overlay$min !== void 0 ? _overlay$min : 0,
2989
+ mode,
2990
+ values: overlay.values
2991
+ });
2992
+ applyColorTheme(plugin, RESIDUE_VALUE_THEME_NAME);
2993
+ } else {
2994
+ theme.setState({
2995
+ mode,
2996
+ values: /* @__PURE__ */ new Map()
2997
+ });
2998
+ applyColorTheme(plugin, hasPlddt ? PLDDT_THEME_NAME : FALLBACK_THEME_NAME);
2999
+ }
3000
+ }, [
3001
+ overlay,
3002
+ hasPlddt,
3003
+ isReady,
3004
+ mode
3005
+ ]);
3006
+ }
3007
+ //#endregion
3008
+ //#region src/core/ProteinStructureViewer/index.tsx
3009
+ const _excluded$1 = [
3010
+ "backgroundColor",
3011
+ "onResidueClick",
3012
+ "onResidueHover",
3013
+ "onSelectionClear",
3014
+ "pdb",
3015
+ "plddt",
3016
+ "residueOverlay",
3017
+ "selectedResidue",
3018
+ "sequenceViewerBackgroundColor",
3019
+ "showAxes",
3020
+ "showLegend",
3021
+ "showSequenceViewer",
3022
+ "stats"
3023
+ ];
3024
+ /** Fallbacks for theme tokens Mol* needs as concrete hex colors. */
3025
+ const FALLBACK_EDGE_COLOR = "#6c6c6c";
3026
+ const FALLBACK_HIGHLIGHT_COLOR = "#1b1b1b";
3027
+ /**
3028
+ * Keyed by mode: unlike the two above, the canvas has to invert with it. Only
3029
+ * reached when the viewer renders outside an SDS theme; inside one the
3030
+ * `base.backgroundPrimary` token wins.
3031
+ */
3032
+ const FALLBACK_BACKGROUND_COLOR = {
3033
+ dark: "#000000",
3034
+ light: "#ffffff"
3035
+ };
3036
+ /** Caption shown beneath the color key when no overlay is active. */
3037
+ const PLDDT_SCALE_LABEL = "pLDDT";
3038
+ /** Caption used when an overlay does not name itself. */
3039
+ const DEFAULT_OVERLAY_LABEL = "Value";
3040
+ /** Readout slot label used when an overlay does not name one. */
3041
+ const DEFAULT_READOUT_LABEL = "Value";
3042
+ /**
3043
+ * Chooses what the legend describes, following whatever is actually coloring
3044
+ * the structure: the overlay when one is set, the pLDDT bands when scores were
3045
+ * supplied, and nothing at all otherwise - Mol* falls back to chain coloring
3046
+ * there, which no per-residue scale describes, so a key would be labelling
3047
+ * colors that are not on screen.
3048
+ */
3049
+ function resolveScaleProps(overlay, hasPlddt) {
3050
+ if (overlay) {
3051
+ var _overlay$colorScale, _overlay$label, _overlay$min, _overlay$readoutLabel;
3052
+ return {
3053
+ scale: (_overlay$colorScale = overlay.colorScale) !== null && _overlay$colorScale !== void 0 ? _overlay$colorScale : PLASMA_COLOR_SCALE,
3054
+ scaleLabel: (_overlay$label = overlay.label) !== null && _overlay$label !== void 0 ? _overlay$label : DEFAULT_OVERLAY_LABEL,
3055
+ scaleMax: overlay.max,
3056
+ scaleMin: (_overlay$min = overlay.min) !== null && _overlay$min !== void 0 ? _overlay$min : 0,
3057
+ scaleTooltip: overlay.tooltip,
3058
+ valueLabel: (_overlay$readoutLabel = overlay.readoutLabel) !== null && _overlay$readoutLabel !== void 0 ? _overlay$readoutLabel : DEFAULT_READOUT_LABEL
3059
+ };
3060
+ }
3061
+ if (hasPlddt) return {
3062
+ scale: PLDDT_COLOR_SCALE,
3063
+ scaleLabel: PLDDT_SCALE_LABEL,
3064
+ scaleMax: null,
3065
+ scaleTooltip: void 0,
3066
+ valueLabel: void 0
3067
+ };
3068
+ return {
3069
+ scale: null,
3070
+ scaleLabel: void 0,
3071
+ scaleMax: null,
3072
+ scaleTooltip: void 0,
3073
+ valueLabel: void 0
3074
+ };
3075
+ }
3076
+ /**
3077
+ * Interactive 3D protein structure viewer built on Mol*, with a sequence panel,
3078
+ * pLDDT confidence coloring, per-residue value overlays, and a stats legend.
3079
+ *
3080
+ * The viewer is controlled for selection: `selectedResidue` drives the camera
3081
+ * (clicking a residue zooms in on it, clearing the selection zooms back out),
3082
+ * while hover state is owned internally so the legend can show a live readout
3083
+ * without the consumer round-tripping every pointer move.
3084
+ */
3085
+ const ProteinStructureViewer = (0, react.forwardRef)((props, ref) => {
3086
+ const { backgroundColor, onResidueClick, onResidueHover, onSelectionClear, pdb, plddt, residueOverlay, selectedResidue = null, sequenceViewerBackgroundColor, showAxes = true, showLegend = true, showSequenceViewer = true, stats } = props, rest = _objectWithoutProperties(props, _excluded$1);
3087
+ const theme = (0, _mui_material_styles.useTheme)();
3088
+ const mode = (0, _czi_sds_components.getMode)({ theme });
3089
+ const semanticColors = (0, _czi_sds_components.getSemanticColors)({ theme });
3090
+ const pluginMountRef = (0, react.useRef)(null);
3091
+ const [hoveredResidue, setHoveredResidue] = (0, react.useState)(null);
3092
+ /**
3093
+ * Label for the selected residue, read back off the structure rather than
3094
+ * remembered from the click that selected it, so the readout follows the
3095
+ * prop however the selection was made.
3096
+ */
3097
+ const [selectedLabel, setSelectedLabel] = (0, react.useState)(null);
3098
+ const hasPlddt = Boolean(plddt && plddt.length > 0);
3099
+ const processedPdb = (0, react.useMemo)(() => hasPlddt ? injectPlddtIntoPdb(pdb, plddt) : pdb, [
3100
+ pdb,
3101
+ plddt,
3102
+ hasPlddt
3103
+ ]);
3104
+ const { isReady, pluginRef, residueValueThemeRef, setClipRatio } = useMolstarPlugin({
3105
+ backgroundColor: (0, react.useMemo)(() => {
3106
+ var _semanticColors$base;
3107
+ return themeColor(backgroundColor !== null && backgroundColor !== void 0 ? backgroundColor : semanticColors === null || semanticColors === void 0 || (_semanticColors$base = semanticColors.base) === null || _semanticColors$base === void 0 ? void 0 : _semanticColors$base.backgroundPrimary, FALLBACK_BACKGROUND_COLOR[mode]);
3108
+ }, [
3109
+ backgroundColor,
3110
+ mode,
3111
+ semanticColors
3112
+ ]),
3113
+ containerRef: pluginMountRef,
3114
+ edgeColor: (0, react.useMemo)(() => {
3115
+ var _semanticColors$base2;
3116
+ return themeColor(semanticColors === null || semanticColors === void 0 || (_semanticColors$base2 = semanticColors.base) === null || _semanticColors$base2 === void 0 ? void 0 : _semanticColors$base2.borderPrimary, FALLBACK_EDGE_COLOR);
3117
+ }, [semanticColors]),
3118
+ hasPlddt,
3119
+ highlightColor: (0, react.useMemo)(() => {
3120
+ var _semanticColors$base3;
3121
+ return themeColor(semanticColors === null || semanticColors === void 0 || (_semanticColors$base3 = semanticColors.base) === null || _semanticColors$base3 === void 0 ? void 0 : _semanticColors$base3.fillSelected, FALLBACK_HIGHLIGHT_COLOR);
3122
+ }, [semanticColors]),
3123
+ mode,
3124
+ onResidueClick,
3125
+ onResidueHover: (0, react.useCallback)((residue) => {
3126
+ setHoveredResidue((prev) => {
3127
+ if (residue === null) return prev === null ? prev : null;
3128
+ const label = residueLabel(residue);
3129
+ if (prev !== null && prev.index === residue.index && prev.label === label) return prev;
3130
+ return {
3131
+ index: residue.index,
3132
+ label
3133
+ };
3134
+ });
3135
+ onResidueHover === null || onResidueHover === void 0 || onResidueHover(residue);
3136
+ }, [onResidueHover]),
3137
+ onSelectionClear,
3138
+ pdb: processedPdb,
3139
+ sequenceViewerBackgroundColor,
3140
+ showAxes,
3141
+ showSequenceViewer
3142
+ });
3143
+ useResidueOverlay({
3144
+ hasPlddt,
3145
+ isReady,
3146
+ mode,
3147
+ overlay: residueOverlay,
3148
+ pluginRef,
3149
+ residueValueThemeRef
3150
+ });
3151
+ useResidueFocus({
3152
+ isReady,
3153
+ pluginRef,
3154
+ selectedResidue,
3155
+ setClipRatio
3156
+ });
3157
+ (0, react.useEffect)(() => {
3158
+ const plugin = pluginRef.current;
3159
+ if (!plugin || !isReady || selectedResidue === null) {
3160
+ setSelectedLabel(null);
3161
+ return;
3162
+ }
3163
+ const loci = lociForResidueIndex(plugin, selectedResidue);
3164
+ const residue = loci && residueRefFromLoci(loci);
3165
+ setSelectedLabel(residue ? residueLabel(residue) : null);
3166
+ }, [
3167
+ isReady,
3168
+ pluginRef,
3169
+ selectedResidue
3170
+ ]);
3171
+ const buildReadout = (0, react.useCallback)((label, index) => {
3172
+ var _plddt$index, _residueOverlay$value;
3173
+ return {
3174
+ label,
3175
+ plddt: (_plddt$index = plddt === null || plddt === void 0 ? void 0 : plddt[index]) !== null && _plddt$index !== void 0 ? _plddt$index : null,
3176
+ value: (_residueOverlay$value = residueOverlay === null || residueOverlay === void 0 ? void 0 : residueOverlay.values.get(index)) !== null && _residueOverlay$value !== void 0 ? _residueOverlay$value : null
3177
+ };
3178
+ }, [plddt, residueOverlay]);
3179
+ const hoveredReadout = (0, react.useMemo)(() => hoveredResidue === null ? null : buildReadout(hoveredResidue.label, hoveredResidue.index), [hoveredResidue, buildReadout]);
3180
+ const selectedReadout = (0, react.useMemo)(() => selectedResidue === null || selectedLabel === null ? null : buildReadout(selectedLabel, selectedResidue), [
3181
+ selectedResidue,
3182
+ selectedLabel,
3183
+ buildReadout
3184
+ ]);
3185
+ const scaleProps = (0, react.useMemo)(() => resolveScaleProps(residueOverlay, hasPlddt), [residueOverlay, hasPlddt]);
3186
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ViewerRoot, _objectSpread2(_objectSpread2({
3187
+ ref,
3188
+ showSequenceViewer
3189
+ }, rest), {}, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PluginMount, { ref: pluginMountRef }), showLegend && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StructureLegend, _objectSpread2({
3190
+ hoveredResidue: hoveredReadout,
3191
+ selectedResidue: selectedReadout,
3192
+ showSequenceViewer,
3193
+ stats: stats !== null && stats !== void 0 ? stats : []
3194
+ }, scaleProps))] }));
3195
+ });
3196
+ var ProteinStructureViewer_default = (0, react.memo)(ProteinStructureViewer);
445
3197
  //#endregion
446
3198
  //#region src/core/StackedBarChart/style.ts
447
3199
  const ChartWrapper = (0, _emotion_styled.default)("div")`
@@ -660,7 +3412,6 @@ const renderBarSegment = (item, options) => {
660
3412
  title: null,
661
3413
  componentSlot: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_czi_sds_components.TooltipTable, _objectSpread2({}, item.tooltip)),
662
3414
  placement: "top",
663
- hasInvertedStyle: false,
664
3415
  open: hoveredIndex === item.originalIndex && !isHidden,
665
3416
  disableInteractive: true,
666
3417
  slotProps: { popper: {
@@ -920,4 +3671,10 @@ const StackedBarChart = (props) => {
920
3671
  };
921
3672
  //#endregion
922
3673
  exports.HeatmapChart = HeatmapChart_default;
3674
+ exports.PLASMA_COLOR_SCALE = PLASMA_COLOR_SCALE;
3675
+ exports.PLDDT_COLOR_SCALE = PLDDT_COLOR_SCALE;
3676
+ exports.ProteinStructureViewer = ProteinStructureViewer_default;
923
3677
  exports.StackedBarChart = StackedBarChart;
3678
+ exports.injectPlddtIntoPdb = injectPlddtIntoPdb;
3679
+ exports.sampleColorScale = sampleColorScale;
3680
+ exports.toCssGradient = toCssGradient;