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