@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/README.md +2 -2
- package/dist/index.cjs.d.ts +296 -60
- package/dist/index.cjs.js +2763 -6
- package/dist/index.esm.d.ts +297 -60
- package/dist/index.esm.js +2869 -128
- package/package.json +13 -6
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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;
|