@czi-sds/data-viz 2.3.0 → 2.4.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.
Files changed (39) hide show
  1. package/dist/HeatmapChart.cjs.d.ts +2 -0
  2. package/dist/HeatmapChart.cjs.js +8 -0
  3. package/dist/HeatmapChart.esm.d.ts +2 -0
  4. package/dist/HeatmapChart.esm.js +3 -0
  5. package/dist/ProteinStructureViewer.cjs.d.ts +2 -0
  6. package/dist/ProteinStructureViewer.cjs.js +14 -0
  7. package/dist/ProteinStructureViewer.esm.d.ts +2 -0
  8. package/dist/ProteinStructureViewer.esm.js +3 -0
  9. package/dist/StackedBarChart.cjs.d.ts +2 -0
  10. package/dist/StackedBarChart.cjs.js +8 -0
  11. package/dist/StackedBarChart.esm.d.ts +2 -0
  12. package/dist/StackedBarChart.esm.js +3 -0
  13. package/dist/colorScales.cjs.d.ts +2 -0
  14. package/dist/colorScales.cjs.js +6 -0
  15. package/dist/colorScales.esm.d.ts +2 -0
  16. package/dist/colorScales.esm.js +3 -0
  17. package/dist/index.cjs.d.ts +4 -772
  18. package/dist/index.cjs.js +16 -5765
  19. package/dist/index.esm.d.ts +4 -774
  20. package/dist/index.esm.js +4 -5719
  21. package/dist/shared/HeatmapChart.cjs.js +349 -0
  22. package/dist/shared/HeatmapChart.esm.js +342 -0
  23. package/dist/shared/ProteinStructureViewer.cjs.js +4780 -0
  24. package/dist/shared/ProteinStructureViewer.esm.js +4728 -0
  25. package/dist/shared/StackedBarChart.cjs.js +489 -0
  26. package/dist/shared/StackedBarChart.esm.js +483 -0
  27. package/dist/shared/colorScales.cjs.d.ts +45 -0
  28. package/dist/shared/colorScales.cjs.js +126 -0
  29. package/dist/shared/colorScales.esm.d.ts +45 -0
  30. package/dist/shared/colorScales.esm.js +103 -0
  31. package/dist/shared/index.cjs.d.ts +183 -0
  32. package/dist/shared/index.esm.d.ts +183 -0
  33. package/dist/shared/index2.cjs.d.ts +389 -0
  34. package/dist/shared/index2.esm.d.ts +391 -0
  35. package/dist/shared/index3.cjs.d.ts +166 -0
  36. package/dist/shared/index3.esm.d.ts +166 -0
  37. package/dist/shared/objectSpread2.cjs.js +124 -0
  38. package/dist/shared/objectSpread2.esm.js +85 -0
  39. package/package.json +64 -2
@@ -0,0 +1,4780 @@
1
+ "use client";
2
+ const require_objectSpread2 = require("./objectSpread2.cjs.js");
3
+ const require_colorScales = require("./colorScales.cjs.js");
4
+ let react = require("react");
5
+ let _emotion_styled = require("@emotion/styled");
6
+ _emotion_styled = require_objectSpread2.__toESM(_emotion_styled);
7
+ let react_jsx_runtime = require("react/jsx-runtime");
8
+ let _czi_sds_components = require("@czi-sds/components");
9
+ let _mui_material_styles = require("@mui/material/styles");
10
+ let _phosphor_icons_react = require("@phosphor-icons/react");
11
+ let molstar_lib_mol_model_structure = require("molstar/lib/mol-model/structure");
12
+ let molstar_lib_mol_plugin_state_helpers_structure_transparency = require("molstar/lib/mol-plugin-state/helpers/structure-transparency");
13
+ let molstar_lib_mol_plugin_ui = require("molstar/lib/mol-plugin-ui");
14
+ let molstar_lib_mol_plugin_ui_react18 = require("molstar/lib/mol-plugin-ui/react18");
15
+ let molstar_lib_mol_plugin_ui_spec = require("molstar/lib/mol-plugin-ui/spec");
16
+ let molstar_lib_mol_plugin_behavior = require("molstar/lib/mol-plugin/behavior");
17
+ let molstar_lib_mol_plugin_behavior_static_state = require("molstar/lib/mol-plugin/behavior/static/state");
18
+ let molstar_lib_mol_plugin_config = require("molstar/lib/mol-plugin/config");
19
+ let molstar_lib_mol_repr_representation = require("molstar/lib/mol-repr/representation");
20
+ let rxjs = require("rxjs");
21
+ let molstar_lib_mol_model_loci = require("molstar/lib/mol-model/loci");
22
+ let molstar_lib_mol_plugin_ui_base = require("molstar/lib/mol-plugin-ui/base");
23
+ let molstar_lib_mol_util_input_input_observer = require("molstar/lib/mol-util/input/input-observer");
24
+ let rxjs_operators = require("rxjs/operators");
25
+ let molstar_lib_mol_data_int = require("molstar/lib/mol-data/int");
26
+ let _mui_material = require("@mui/material");
27
+ let molstar_lib_mol_plugin_state_objects = require("molstar/lib/mol-plugin-state/objects");
28
+ let molstar_lib_mol_plugin_ui_sequence = require("molstar/lib/mol-plugin-ui/sequence");
29
+ let molstar_lib_mol_state_state_selection = require("molstar/lib/mol-state/state/selection");
30
+ let molstar_lib_mol_util = require("molstar/lib/mol-util");
31
+ let molstar_lib_mol_util_param_definition = require("molstar/lib/mol-util/param-definition");
32
+ let molstar_lib_mol_plugin_ui_toast = require("molstar/lib/mol-plugin-ui/toast");
33
+ let molstar_lib_mol_plugin_ui_viewport = require("molstar/lib/mol-plugin-ui/viewport");
34
+ let molstar_lib_mol_util_color = require("molstar/lib/mol-util/color");
35
+ let molstar_lib_mol_plugin_commands = require("molstar/lib/mol-plugin/commands");
36
+ let molstar_lib_mol_math_linear_algebra = require("molstar/lib/mol-math/linear-algebra");
37
+ let molstar_lib_mol_model_structure_model_types = require("molstar/lib/mol-model/structure/model/types");
38
+ let molstar_lib_mol_theme_label = require("molstar/lib/mol-theme/label");
39
+ let molstar_lib_mol_script_language_builder = require("molstar/lib/mol-script/language/builder");
40
+ let molstar_lib_mol_model_props_common_custom_element_property = require("molstar/lib/mol-model-props/common/custom-element-property");
41
+ let molstar_lib_mol_plugin_behavior_dynamic_selection_structure_focus_representation = require("molstar/lib/mol-plugin/behavior/dynamic/selection/structure-focus-representation");
42
+ //#region src/core/ProteinStructureViewer/components/ChainLegend/style.ts
43
+ /** Opacity applied to a hidden chain's row, so it reads as off but reachable. */
44
+ const HIDDEN_OPACITY = .4;
45
+ /** Side of the color swatch. */
46
+ const SWATCH_SIZE = 8;
47
+ /**
48
+ * The rows, stacked and right-aligned against the color key beside them.
49
+ *
50
+ * Pointer events are switched back on here: the legend overlay turns them off
51
+ * so drags meant for the structure are not intercepted, and this is one of the
52
+ * two places in it that has to be clickable.
53
+ */
54
+ const ChainRowList = (0, _emotion_styled.default)("div")`
55
+ display: flex;
56
+ flex-direction: column;
57
+ align-items: flex-end;
58
+ pointer-events: auto;
59
+ max-height: 80px;
60
+ overflow-y: auto;
61
+ `;
62
+ const ChainRow = (0, _emotion_styled.default)("div")`
63
+ display: flex;
64
+ align-items: center;
65
+
66
+ ${(props) => {
67
+ const spaces = (0, _czi_sds_components.getSpaces)(props);
68
+ return `
69
+ gap: ${spaces === null || spaces === void 0 ? void 0 : spaces.xs}px;
70
+ `;
71
+ }}
72
+ `;
73
+ const ChainSwatch = (0, _emotion_styled.default)("div")`
74
+ flex: none;
75
+ width: ${SWATCH_SIZE}px;
76
+ height: ${SWATCH_SIZE}px;
77
+ border-radius: 1px;
78
+
79
+ ${(props) => `
80
+ background-color: ${props.swatchColor};
81
+ opacity: ${props.isHidden ? HIDDEN_OPACITY : 1};
82
+ `}
83
+ `;
84
+ /**
85
+ * A swatch quartered into a grid, for a coloring no single color stands for.
86
+ *
87
+ * Occupies the same box as the solid swatch so the rows stay aligned whichever
88
+ * one a chain gets, with a hairline gap between the quadrants to read as four
89
+ * colors rather than one blurred square.
90
+ */
91
+ const ChainSwatchGrid = (0, _emotion_styled.default)("div")`
92
+ flex: none;
93
+ display: grid;
94
+ grid-template-columns: repeat(2, 1fr);
95
+ grid-template-rows: repeat(2, 1fr);
96
+ gap: 0px;
97
+ width: ${SWATCH_SIZE}px;
98
+ height: ${SWATCH_SIZE}px;
99
+ border-radius: 1px;
100
+ overflow: hidden;
101
+
102
+ ${(props) => `
103
+ opacity: ${props.isHidden ? HIDDEN_OPACITY : 1};
104
+ `}
105
+ `;
106
+ const ChainSwatchQuadrant = (0, _emotion_styled.default)("div")`
107
+ ${(props) => `
108
+ background-color: ${props.swatchColor};
109
+ `}
110
+ `;
111
+ /**
112
+ * Selects the whole chain, or clears it when it is already the selection. A
113
+ * button so it is reachable from the keyboard.
114
+ */
115
+ const ChainLabel$1 = (0, _emotion_styled.default)("button")`
116
+ ${_czi_sds_components.fontBodyXxxs}
117
+
118
+ padding: 0;
119
+ border: none;
120
+ background: none;
121
+
122
+ ${(props) => {
123
+ var _semanticColors$base, _semanticColors$base2, _semanticColors$base3;
124
+ const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
125
+ return `
126
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base = semanticColors.base) === null || _semanticColors$base === void 0 ? void 0 : _semanticColors$base.textSecondary};
127
+ cursor: ${props.isHidden ? "default" : "pointer"};
128
+ font-weight: ${props.isSelected ? 600 : "inherit"};
129
+ opacity: ${props.isHidden ? HIDDEN_OPACITY : 1};
130
+
131
+ &:hover {
132
+ 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};
133
+ }
134
+ `;
135
+ }}
136
+ `;
137
+ const VisibilityToggle = (0, _emotion_styled.default)("button")`
138
+ display: flex;
139
+ align-items: center;
140
+ padding: 0;
141
+ border: none;
142
+ background: none;
143
+ cursor: pointer;
144
+ margin-top: 1px;
145
+
146
+ ${(props) => {
147
+ var _semanticColors$base4, _semanticColors$base5;
148
+ const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
149
+ return `
150
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base4 = semanticColors.base) === null || _semanticColors$base4 === void 0 ? void 0 : _semanticColors$base4.textTertiaryOnDark};
151
+
152
+ &:hover {
153
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base5 = semanticColors.base) === null || _semanticColors$base5 === void 0 ? void 0 : _semanticColors$base5.textPrimary};
154
+ }
155
+
156
+ svg {
157
+ display: block;
158
+ }
159
+ `;
160
+ }}
161
+ `;
162
+ //#endregion
163
+ //#region src/core/ProteinStructureViewer/components/ChainLegend/index.tsx
164
+ /**
165
+ * Lists the structure's chains, each with its color and a visibility toggle.
166
+ *
167
+ * Only drawn for a complex: one chain needs no key to tell it from anything,
168
+ * and hiding it would leave an empty canvas with no way back. Its home is the
169
+ * legend rather than the sequence panel so that it survives
170
+ * `showSequenceViewer={false}`, where the chains would otherwise have nowhere
171
+ * to be listed.
172
+ */
173
+ function ChainLegend({ bandColors, chainColors, chains, hiddenChains, onChainHover, onChainSelect, onChainToggle, selectedChains }) {
174
+ if (chains.length < 2) return null;
175
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChainRowList, { children: chains.map((chain) => {
176
+ var _selectedChains$has;
177
+ const hidden = hiddenChains.has(chain.chainId);
178
+ const selected = (_selectedChains$has = selectedChains === null || selectedChains === void 0 ? void 0 : selectedChains.has(chain.chainId)) !== null && _selectedChains$has !== void 0 ? _selectedChains$has : false;
179
+ const color = chainColors === null || chainColors === void 0 ? void 0 : chainColors.get(chain.chainId);
180
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ChainRow, {
181
+ onBlur: () => onChainHover === null || onChainHover === void 0 ? void 0 : onChainHover(null),
182
+ onFocus: () => onChainHover === null || onChainHover === void 0 ? void 0 : onChainHover(chain.chainId),
183
+ onMouseEnter: () => onChainHover === null || onChainHover === void 0 ? void 0 : onChainHover(chain.chainId),
184
+ onMouseLeave: () => onChainHover === null || onChainHover === void 0 ? void 0 : onChainHover(null),
185
+ children: [
186
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_czi_sds_components.Tooltip, {
187
+ arrow: true,
188
+ placement: "top",
189
+ disableInteractive: true,
190
+ title: hidden ? `Chain ${chain.label} is hidden` : `Click to ${selected ? "deselect" : "select"} Chain ${chain.label}`,
191
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ChainLabel$1, {
192
+ "aria-pressed": selected,
193
+ disabled: hidden,
194
+ isHidden: hidden,
195
+ isSelected: selected,
196
+ onClick: () => onChainSelect(chain.chainId),
197
+ type: "button",
198
+ children: ["Chain ", chain.label]
199
+ })
200
+ }),
201
+ color !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChainSwatch, {
202
+ isHidden: hidden,
203
+ swatchColor: color
204
+ }),
205
+ color === void 0 && bandColors !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChainSwatchGrid, {
206
+ isHidden: hidden,
207
+ children: bandColors.map((band) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChainSwatchQuadrant, { swatchColor: band }, band))
208
+ }),
209
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_czi_sds_components.Tooltip, {
210
+ arrow: true,
211
+ placement: "top",
212
+ disableInteractive: true,
213
+ title: hidden ? `Show Chain ${chain.label}` : `Hide Chain ${chain.label}`,
214
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VisibilityToggle, {
215
+ "aria-label": `${hidden ? "Show" : "Hide"} chain ${chain.label}`,
216
+ "aria-pressed": hidden,
217
+ onClick: () => onChainToggle(chain.chainId),
218
+ type: "button",
219
+ 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 })
220
+ })
221
+ })
222
+ ]
223
+ }, chain.chainId);
224
+ }) });
225
+ }
226
+ /** Height of the color bar itself. */
227
+ const BAR_HEIGHT = 8;
228
+ /**
229
+ * Inset applied to the outer tick labels on a stepped scale. Labels sit on the
230
+ * boundaries *between* bands, so the row is padded by half a band to line each
231
+ * label up with its boundary rather than with a band's center.
232
+ */
233
+ const STEPPED_LABEL_INSET = 5;
234
+ const LegendWrapper = (0, _emotion_styled.default)("div")`
235
+ width: ${142}px;
236
+ `;
237
+ const LegendLabels = (0, _emotion_styled.default)("div")`
238
+ ${_czi_sds_components.fontBodyXxxs}
239
+
240
+ display: flex;
241
+ justify-content: space-between;
242
+ width: 100%;
243
+
244
+ ${(props) => {
245
+ var _semanticColors$base;
246
+ const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
247
+ return `
248
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base = semanticColors.base) === null || _semanticColors$base === void 0 ? void 0 : _semanticColors$base.textTertiaryOnDark};
249
+ `;
250
+ }}
251
+ `;
252
+ const SteppedLabels = (0, _emotion_styled.default)(LegendLabels)`
253
+ &::before,
254
+ &::after {
255
+ content: "";
256
+ width: ${STEPPED_LABEL_INSET}px;
257
+ }
258
+ `;
259
+ const LegendBar = (0, _emotion_styled.default)("div")`
260
+ display: flex;
261
+ height: ${BAR_HEIGHT}px;
262
+ width: 100%;
263
+ `;
264
+ const LegendBand = (0, _emotion_styled.default)("div")`
265
+ flex: 1;
266
+ background-color: ${(props) => props.color};
267
+ `;
268
+ const LegendGradient = (0, _emotion_styled.default)("div")`
269
+ height: ${BAR_HEIGHT}px;
270
+ width: 100%;
271
+ background: ${(props) => props.gradient};
272
+ `;
273
+ //#endregion
274
+ //#region src/core/ProteinStructureViewer/components/ColorScaleLegend/index.tsx
275
+ /** Decimal places used for the ticks on a continuous scale. */
276
+ const TICK_PRECISION = 2;
277
+ /**
278
+ * The color key beneath the structure viewer. A stepped scale renders as
279
+ * discrete bands with a tick at each boundary; a continuous scale renders as a
280
+ * gradient running from `min` to `max`, the range its values are normalized
281
+ * into before they are sampled.
282
+ */
283
+ function ColorScaleLegend({ max = null, min = 0, scale }) {
284
+ if (scale.kind === "stepped") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(LegendWrapper, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SteppedLabels, { children: scale.stops.slice(0, -1).map((stop) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: stop.label }, stop.label)) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LegendBar, { children: scale.stops.map((stop) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LegendBand, { color: stop.color }, stop.label)) })] });
285
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(LegendWrapper, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(LegendLabels, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: min.toFixed(TICK_PRECISION) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: max !== null ? max.toFixed(TICK_PRECISION) : "" })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LegendGradient, { gradient: require_colorScales.toCssGradient(scale.stops) })] });
286
+ }
287
+ //#endregion
288
+ //#region src/core/ProteinStructureViewer/style.ts
289
+ /** Smallest box in which Mol* can still lay out a usable viewport. */
290
+ const MIN_HEIGHT = 190;
291
+ const MIN_WIDTH = 200;
292
+ /**
293
+ * Name of the size container the root declares below, which is what lets the
294
+ * sequence panel and the legend reflow to the viewer's own width rather than
295
+ * the page's.
296
+ */
297
+ const VIEWER_CONTAINER_NAME = "sds-structure-viewer";
298
+ /**
299
+ * Height reserved for the sequence panel along the bottom, and the viewer
300
+ * width above which it grows. Measured against the container above rather than
301
+ * the page, so a viewer embedded in a wider page sizes to the room it has.
302
+ *
303
+ * The legend sits directly on top of the panel and offsets itself by these
304
+ * same three values, so it reads them from here rather than restating them.
305
+ * Copies that drifted would leave the legend floating off the panel.
306
+ */
307
+ const SEQUENCE_HEIGHT = "max(104px, 30%)";
308
+ const SEQUENCE_HEIGHT_WIDE = "max(134px, 32%)";
309
+ /**
310
+ * The element Mol* mounts into.
311
+ *
312
+ * This has to be its own node rather than the root: Mol* takes ownership of its
313
+ * target and replaces whatever is in it, so anything the viewer renders
314
+ * alongside the plugin (the legend) has to be a sibling of this, not a child.
315
+ */
316
+ const PluginMount = (0, _emotion_styled.default)("div")`
317
+ position: absolute;
318
+ inset: 0;
319
+ `;
320
+ /**
321
+ * Root container for the viewer.
322
+ *
323
+ * Mol* ships no stylesheet of its own here: the viewport and sequence panel are
324
+ * replaced with custom components and the remaining chrome is hidden, so all
325
+ * that is left to style is the layout scaffolding. Those overrides are nested
326
+ * below rather than declared globally, so consumers do not have to import
327
+ * anything extra and two viewers on a page cannot fight over the same
328
+ * selectors.
329
+ *
330
+ * The element also declares itself a size container, which is what lets the
331
+ * legend reflow to the viewer's own width instead of the page's.
332
+ */
333
+ const ViewerRoot = (0, _emotion_styled.default)("div")`
334
+ position: relative;
335
+ overflow: hidden;
336
+ width: 100%;
337
+ height: 100%;
338
+ min-width: ${MIN_WIDTH}px;
339
+ min-height: ${MIN_HEIGHT}px;
340
+ container-type: inline-size;
341
+ container-name: ${VIEWER_CONTAINER_NAME};
342
+
343
+ .msp-plugin {
344
+ position: absolute !important;
345
+ inset: 0 !important;
346
+ width: 100% !important;
347
+ height: 100% !important;
348
+ }
349
+
350
+ /*
351
+ * The expanded layout is kept hidden because it is the one piece of Mol*
352
+ * chrome that would escape this container: it takes the plugin out of the
353
+ * box it was given and over the page.
354
+ *
355
+ * Its neighbours used to be here too - the viewport's icon column, and the
356
+ * left, right and bottom regions. They are not any more, because hiding them
357
+ * from here made them unreachable: Mol* already gates each of them behind a
358
+ * PluginConfig.Viewport flag and behind layout.initial.regionState, both of
359
+ * which a consumer can set through molstarSpec, and a display:none on top of
360
+ * that overrode the answer. None of them shows by default - the viewer's own
361
+ * flags and region state are what keep them away - so this only has to stay
362
+ * out of their way.
363
+ *
364
+ * No backticks in here: this is inside a template literal, and one would end
365
+ * it.
366
+ */
367
+ .msp-plugin .msp-layout-expanded {
368
+ display: none !important;
369
+ }
370
+
371
+ .msp-plugin canvas {
372
+ display: block !important;
373
+ }
374
+
375
+ .msp-plugin .msp-layout-static {
376
+ height: 100%;
377
+ }
378
+
379
+ .msp-plugin .msp-sequence-wrapper-non-empty {
380
+ overflow-y: auto;
381
+ }
382
+
383
+ /*
384
+ * Mol* puts the sequence panel in its "top" region; move it to the bottom and
385
+ * shrink the main viewport region to match, so the structure sits above the
386
+ * sequence rather than behind it. The panel grows on wider viewers, and the
387
+ * viewport region's offset follows it.
388
+ */
389
+ ${(props) => {
390
+ const { showSequenceViewer } = props;
391
+ return `
392
+ .msp-plugin .msp-layout-region.msp-layout-top {
393
+ display: ${showSequenceViewer ? "block" : "none"} !important;
394
+ position: absolute !important;
395
+ inset: auto 0 0 0 !important;
396
+ height: ${SEQUENCE_HEIGHT} !important;
397
+ }
398
+
399
+ .msp-plugin .msp-layout-region.msp-layout-main {
400
+ position: absolute !important;
401
+ top: 0 !important;
402
+ left: 0 !important;
403
+ right: 0 !important;
404
+ bottom: ${showSequenceViewer ? SEQUENCE_HEIGHT : "0"} !important;
405
+ }
406
+
407
+ @container ${VIEWER_CONTAINER_NAME} (min-width: 880px) {
408
+ .msp-plugin .msp-layout-region.msp-layout-top {
409
+ height: ${SEQUENCE_HEIGHT_WIDE} !important;
410
+ }
411
+
412
+ .msp-plugin .msp-layout-region.msp-layout-main {
413
+ bottom: ${showSequenceViewer ? SEQUENCE_HEIGHT_WIDE : "0"} !important;
414
+ }
415
+ }
416
+ `;
417
+ }}
418
+
419
+ /*
420
+ * Toast slot rendered by the custom viewport. No loci label providers are
421
+ * registered (residue info goes to the legend instead), so this only carries
422
+ * Mol*'s own transient messages.
423
+ */
424
+ .msp-plugin .msp-highlight-toast-wrapper {
425
+ position: absolute !important;
426
+ left: 10px !important;
427
+ max-width: 95% !important;
428
+ z-index: 10000 !important;
429
+ pointer-events: none;
430
+ }
431
+ `;
432
+ //#endregion
433
+ //#region src/core/ProteinStructureViewer/components/StructureLegend/style.ts
434
+ /** Width below which the legend switches to its stacked layout. */
435
+ const NARROW_BREAKPOINT = 480;
436
+ /** Fixed stat column width, so labels swapping on hover never shift the grid. */
437
+ const STAT_COLUMN_WIDTH = "4.5rem";
438
+ /**
439
+ * Overlaid along the bottom of the viewer, above the sequence panel. Its bottom
440
+ * offset is the panel's own height, taken from the root style so the two cannot
441
+ * disagree; with no panel it drops to the bottom edge, where its own padding
442
+ * provides the gap. Pointer events are off so the legend never intercepts drags
443
+ * meant for the structure; the help tooltip re-enables them on itself.
444
+ */
445
+ const LegendOverlay = (0, _emotion_styled.default)("div")`
446
+ position: absolute;
447
+ z-index: 1;
448
+ display: flex;
449
+ align-items: flex-end;
450
+ width: 100%;
451
+ pointer-events: none;
452
+
453
+ ${(props) => {
454
+ const spaces = (0, _czi_sds_components.getSpaces)(props);
455
+ const { showSequenceViewer } = props;
456
+ return `
457
+ right: ${spaces === null || spaces === void 0 ? void 0 : spaces.m}px;
458
+ gap: ${spaces === null || spaces === void 0 ? void 0 : spaces.xs}px;
459
+ padding-bottom: ${spaces === null || spaces === void 0 ? void 0 : spaces.m}px;
460
+ bottom: ${showSequenceViewer ? SEQUENCE_HEIGHT : "0"};
461
+
462
+ /*
463
+ * Track the sequence panel as it grows on wider viewers. The panel is
464
+ * sized against this same container, so answering to the page instead
465
+ * would part the legend from it on a viewer narrower than its page.
466
+ */
467
+ @container ${VIEWER_CONTAINER_NAME} (min-width: 880px) {
468
+ bottom: ${showSequenceViewer ? SEQUENCE_HEIGHT_WIDE : "0"};
469
+ }
470
+
471
+ /* Spacer balancing the axes widget pinned in the opposite corner. */
472
+ &::before {
473
+ content: "";
474
+ display: block;
475
+ width: ${spaces === null || spaces === void 0 ? void 0 : spaces.xxl}px;
476
+ }
477
+
478
+ @container ${VIEWER_CONTAINER_NAME} (max-width: ${NARROW_BREAKPOINT}px) {
479
+ width: auto;
480
+ flex-direction: column;
481
+ align-items: flex-end;
482
+ gap: ${spaces === null || spaces === void 0 ? void 0 : spaces.l}px;
483
+
484
+ /* The horizontal spacer only makes sense in the bottom-bar layout. */
485
+ &::before {
486
+ display: none;
487
+ }
488
+ }
489
+ `;
490
+ }}
491
+ `;
492
+ /**
493
+ * Fills the space between the axes widget on the left and the color legend on
494
+ * the right. Three fixed-width columns distributed with space-evenly, so the
495
+ * side padding equals the inter-column gap and both flex with the viewer width.
496
+ */
497
+ const StatsGrid = (0, _emotion_styled.default)("div")`
498
+ display: grid;
499
+ flex: 1;
500
+ grid-template-columns: repeat(3, ${STAT_COLUMN_WIDTH});
501
+ align-items: end;
502
+ justify-content: space-evenly;
503
+ white-space: nowrap;
504
+
505
+ ${(props) => {
506
+ const spaces = (0, _czi_sds_components.getSpaces)(props);
507
+ return `
508
+ @container ${VIEWER_CONTAINER_NAME} (max-width: ${NARROW_BREAKPOINT}px) {
509
+ flex: none;
510
+ grid-template-columns: minmax(0, 1fr);
511
+ justify-items: end;
512
+ text-align: right;
513
+ row-gap: ${spaces === null || spaces === void 0 ? void 0 : spaces.xl}px;
514
+ }
515
+ `;
516
+ }}
517
+ `;
518
+ const ChainScaleColumn = (0, _emotion_styled.default)("div")`
519
+ display: flex;
520
+ flex-direction: column;
521
+ align-items: flex-end;
522
+
523
+ ${(props) => {
524
+ const spaces = (0, _czi_sds_components.getSpaces)(props);
525
+ return `
526
+ gap: ${spaces === null || spaces === void 0 ? void 0 : spaces.l}px;
527
+ `;
528
+ }}
529
+ `;
530
+ const StatColumnWrapper = (0, _emotion_styled.default)("div")`
531
+ display: flex;
532
+ flex-direction: column;
533
+ `;
534
+ const StatValue = (0, _emotion_styled.default)("span")`
535
+ ${_czi_sds_components.fontBodyXxxs}
536
+
537
+ ${(props) => {
538
+ var _semanticColors$base;
539
+ const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
540
+ return `
541
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base = semanticColors.base) === null || _semanticColors$base === void 0 ? void 0 : _semanticColors$base.textPrimary};
542
+ `;
543
+ }}
544
+ `;
545
+ const StatLabel = (0, _emotion_styled.default)("span")`
546
+ ${_czi_sds_components.fontBodyXxxs}
547
+
548
+ ${(props) => {
549
+ var _semanticColors$base2;
550
+ const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
551
+ return `
552
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base2 = semanticColors.base) === null || _semanticColors$base2 === void 0 ? void 0 : _semanticColors$base2.textTertiaryOnDark};
553
+ `;
554
+ }}
555
+ `;
556
+ const ScaleColumn = (0, _emotion_styled.default)("div")`
557
+ display: flex;
558
+ flex-direction: column;
559
+
560
+ ${(props) => {
561
+ const spaces = (0, _czi_sds_components.getSpaces)(props);
562
+ return `
563
+ gap: ${spaces === null || spaces === void 0 ? void 0 : spaces.xxs}px;
564
+ `;
565
+ }}
566
+ `;
567
+ /**
568
+ * A wrapping text line, not a flex row: the help icon has to sit at the end of
569
+ * the last line of the caption rather than beside the whole wrapped block.
570
+ */
571
+ const ScaleCaption = (0, _emotion_styled.default)("span")`
572
+ ${_czi_sds_components.fontBodyXxxs}
573
+
574
+ display: block;
575
+ text-align: left;
576
+
577
+ ${(props) => {
578
+ var _semanticColors$base3;
579
+ const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
580
+ return `
581
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base3 = semanticColors.base) === null || _semanticColors$base3 === void 0 ? void 0 : _semanticColors$base3.textTertiaryOnDark};
582
+ max-width: 142px;
583
+ `;
584
+ }}
585
+ `;
586
+ /** Re-enables pointer events for the one interactive element in the overlay. */
587
+ const TooltipAnchor = (0, _emotion_styled.default)("span")`
588
+ pointer-events: auto;
589
+
590
+ ${(props) => {
591
+ var _semanticColors$base4;
592
+ const spaces = (0, _czi_sds_components.getSpaces)(props);
593
+ const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
594
+ return `
595
+ display: inline-block;
596
+ padding-left: ${spaces === null || spaces === void 0 ? void 0 : spaces.xxxs}px;
597
+ margin-bottom: 1px;
598
+ vertical-align: text-bottom;
599
+
600
+ &:hover {
601
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base4 = semanticColors.base) === null || _semanticColors$base4 === void 0 ? void 0 : _semanticColors$base4.ornamentSecondary};
602
+ }
603
+
604
+ svg {
605
+ display: block;
606
+ }
607
+ `;
608
+ }}
609
+ `;
610
+ //#endregion
611
+ //#region src/core/ProteinStructureViewer/components/StructureLegend/index.tsx
612
+ /** Number of stat columns, fixed so the grid tracks never move. */
613
+ const SLOT_COUNT = 3;
614
+ /** Decimal places for stat and readout values. */
615
+ const VALUE_PRECISION = 3;
616
+ /** Shown in place of a number the residue has none of. */
617
+ const MISSING_VALUE = "–";
618
+ function StatColumn({ label, value }) {
619
+ 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 })] });
620
+ }
621
+ /**
622
+ * Builds one readout slot.
623
+ *
624
+ * A readout covering several residues reports a mean, and the label says so:
625
+ * calling it "pLDDT" while it averages ninety of them would read as a single
626
+ * residue's score.
627
+ */
628
+ function readoutSlot(label, value, residueCount) {
629
+ return {
630
+ label: residueCount > 1 ? `Mean ${label}` : label,
631
+ value: value !== null ? value.toFixed(VALUE_PRECISION) : MISSING_VALUE
632
+ };
633
+ }
634
+ /**
635
+ * Stats and color key overlaid on the structure viewer.
636
+ *
637
+ * When a residue is selected or hovered, the whole-structure stats are replaced
638
+ * in place by that residue's readout: slot 0 becomes the residue label, slot 1
639
+ * the overlay value (only when an overlay is active, otherwise the stat stays),
640
+ * and slot 2 the residue pLDDT. A selection pins the readout; hovering another
641
+ * residue takes priority so the user can inspect others without losing their
642
+ * selection.
643
+ */
644
+ 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 }) {
645
+ var _activeResidue$residu, _stats$, _stats$2, _stats$3;
646
+ const activeResidue = hoveredResidue !== null && hoveredResidue !== void 0 ? hoveredResidue : selectedResidue;
647
+ const showResidue = activeResidue !== null;
648
+ const showValue = showResidue && valueLabel !== void 0;
649
+ const residueCount = (_activeResidue$residu = activeResidue === null || activeResidue === void 0 ? void 0 : activeResidue.residueCount) !== null && _activeResidue$residu !== void 0 ? _activeResidue$residu : 1;
650
+ const slots = [
651
+ showResidue ? {
652
+ label: residueCount > 1 ? "Selection" : "Residue",
653
+ value: activeResidue.label
654
+ } : (_stats$ = stats[0]) !== null && _stats$ !== void 0 ? _stats$ : null,
655
+ showValue ? readoutSlot(valueLabel, activeResidue.value, residueCount) : (_stats$2 = stats[1]) !== null && _stats$2 !== void 0 ? _stats$2 : null,
656
+ showResidue ? readoutSlot("pLDDT", activeResidue.plddt, residueCount) : (_stats$3 = stats[2]) !== null && _stats$3 !== void 0 ? _stats$3 : null
657
+ ];
658
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(LegendOverlay, {
659
+ showSequenceViewer,
660
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatsGrid, { children: Array.from({ length: SLOT_COUNT }, (_, i) => {
661
+ var _slots$i;
662
+ const slot = (_slots$i = slots[i]) !== null && _slots$i !== void 0 ? _slots$i : null;
663
+ return slot ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatColumn, {
664
+ label: slot.label,
665
+ value: slot.value
666
+ }, i) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {}, i);
667
+ }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ChainScaleColumn, { children: [onChainToggle && onChainSelect && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChainLegend, {
668
+ bandColors: chainBandColors,
669
+ chainColors,
670
+ chains,
671
+ hiddenChains: hiddenChains !== null && hiddenChains !== void 0 ? hiddenChains : /* @__PURE__ */ new Set(),
672
+ onChainHover,
673
+ onChainSelect,
674
+ onChainToggle,
675
+ selectedChains
676
+ }), scale && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ScaleColumn, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ColorScaleLegend, {
677
+ max: scaleMax,
678
+ min: scaleMin,
679
+ scale
680
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ScaleCaption, { children: [scaleLabel, (scaleTooltip !== void 0 || scaleTooltipProps !== void 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_czi_sds_components.Tooltip, require_objectSpread2._objectSpread2(require_objectSpread2._objectSpread2({
681
+ arrow: true,
682
+ placement: "bottom"
683
+ }, scaleTooltipProps), {}, {
684
+ title: scaleTooltip !== null && scaleTooltip !== void 0 ? scaleTooltip : scaleTooltipProps === null || scaleTooltipProps === void 0 ? void 0 : scaleTooltipProps.title,
685
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipAnchor, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_phosphor_icons_react.QuestionIcon, {
686
+ size: 10,
687
+ weight: "bold"
688
+ }) })
689
+ }))] })] })] })]
690
+ });
691
+ }
692
+ //#endregion
693
+ //#region \0@oxc-project+runtime@0.134.0/helpers/esm/asyncToGenerator.js
694
+ function asyncGeneratorStep(n, t, e, r, o, a, c) {
695
+ try {
696
+ var i = n[a](c), u = i.value;
697
+ } catch (n) {
698
+ e(n);
699
+ return;
700
+ }
701
+ i.done ? t(u) : Promise.resolve(u).then(r, o);
702
+ }
703
+ function _asyncToGenerator(n) {
704
+ return function() {
705
+ var t = this, e = arguments;
706
+ return new Promise(function(r, o) {
707
+ var a = n.apply(t, e);
708
+ function _next(n) {
709
+ asyncGeneratorStep(a, r, o, _next, _throw, "next", n);
710
+ }
711
+ function _throw(n) {
712
+ asyncGeneratorStep(a, r, o, _next, _throw, "throw", n);
713
+ }
714
+ _next(void 0);
715
+ });
716
+ };
717
+ }
718
+ //#endregion
719
+ //#region src/core/ProteinStructureViewer/hooks/useChainHighlight.ts
720
+ /**
721
+ * How transparent the other chains go while one is pointed at.
722
+ *
723
+ * Enough that the chain under the pointer is the one being read, not enough
724
+ * that the rest of the complex stops being context for it. 0 is opaque, 1 is
725
+ * invisible.
726
+ */
727
+ const CHAIN_DIM_TRANSPARENCY = .65;
728
+ /**
729
+ * The chain id in a component key.
730
+ *
731
+ * Chain components are tagged `structure-component-<part>-<chainId>`, and a
732
+ * key is the component's tags sorted and joined with commas. Anchoring on the
733
+ * part name and stopping at the comma is what keeps `A` from also matching the
734
+ * chain named `AA`.
735
+ */
736
+ const COMPONENT_CHAIN_ID = /structure-component-(?:polymer|ligand)-([^,]+)/;
737
+ /** The chain a component was built for, or undefined if it is not a chain's. */
738
+ function componentChainId(component) {
739
+ var _component$key, _COMPONENT_CHAIN_ID$e;
740
+ const key = (_component$key = component.key) !== null && _component$key !== void 0 ? _component$key : component.cell.transform.ref;
741
+ return (_COMPONENT_CHAIN_ID$e = COMPONENT_CHAIN_ID.exec(key)) === null || _COMPONENT_CHAIN_ID$e === void 0 ? void 0 : _COMPONENT_CHAIN_ID$e[1];
742
+ }
743
+ /** Every chain component of every loaded structure, polymers and ligands. */
744
+ function structureComponents(plugin) {
745
+ return plugin.managers.structure.hierarchy.current.structures.flatMap((entry) => entry.components);
746
+ }
747
+ /** A set of chain ids as one comparable string. */
748
+ function chainsKey(chains) {
749
+ return [...chains].sort().join("\0");
750
+ }
751
+ /**
752
+ * Makes every chain outside `lit` transparent, or all of them opaque again when
753
+ * nothing is lit.
754
+ *
755
+ * Transparency layers accumulate, so what is already there is cleared before
756
+ * the next chain is dimmed rather than added to - otherwise moving along the
757
+ * list would leave every chain it passed dimmed behind it.
758
+ *
759
+ * False when there is no structure to write to yet, which is what a selection
760
+ * arriving before the first load looks like. It is applied again after the load
761
+ * rather than being taken as done.
762
+ */
763
+ function dimChainsOutside(_x2, _x3) {
764
+ return _dimChainsOutside.apply(this, arguments);
765
+ }
766
+ function _dimChainsOutside() {
767
+ _dimChainsOutside = _asyncToGenerator(function* (plugin, lit) {
768
+ const components = structureComponents(plugin);
769
+ if (components.length === 0) return false;
770
+ yield (0, molstar_lib_mol_plugin_state_helpers_structure_transparency.clearStructureTransparency)(plugin, components);
771
+ if (lit.size === 0) return true;
772
+ const dimmed = components.filter((component) => {
773
+ const chainId = componentChainId(component);
774
+ return chainId !== void 0 && !lit.has(chainId);
775
+ });
776
+ if (dimmed.length === 0) return true;
777
+ yield (0, molstar_lib_mol_plugin_state_helpers_structure_transparency.setStructureTransparency)(plugin, dimmed, CHAIN_DIM_TRANSPARENCY, function() {
778
+ var _ref = _asyncToGenerator(function* (structure) {
779
+ return molstar_lib_mol_model_structure.Structure.toStructureElementLoci(structure);
780
+ });
781
+ return function(_x) {
782
+ return _ref.apply(this, arguments);
783
+ };
784
+ }());
785
+ return true;
786
+ });
787
+ return _dimChainsOutside.apply(this, arguments);
788
+ }
789
+ /**
790
+ * Leaves one chain lit and dims the rest: the chain whose name is pointed at,
791
+ * in the sequence panel's captions or the chain legend's rows, or the chain the
792
+ * selection covers once the pointer has gone.
793
+ *
794
+ * The chain left lit is exactly as it was - in the chain palette, in pLDDT
795
+ * bands, or in an overlay - and the others recede around it. Nothing is painted
796
+ * over the top, so what the reader is being pointed at is still the color the
797
+ * legend beside it is describing, and a selected chain reads the same as a
798
+ * hovered one rather than sprouting a highlight of its own.
799
+ *
800
+ * A hover outranks the selection while it lasts, since pointing at a chain is a
801
+ * question about that chain, and hands it back on the way out.
802
+ *
803
+ * Dimming is a state-tree commit rather than a renderer setting, so it is
804
+ * applied one at a time and coalesced: a pointer crossing the list faster than
805
+ * a commit finishes skips the chains it passed over and settles on the one it
806
+ * came to rest on, instead of working through a queue of hovers that have
807
+ * already been left.
808
+ */
809
+ function useChainHighlight({ disabled, hiddenChains, loadCount, pluginRef, selectedChains }) {
810
+ const hoveredRef = (0, react.useRef)(null);
811
+ /**
812
+ * The chains left lit on the canvas as it stands. A freshly loaded structure
813
+ * carries no dimming at all, which is what the empty key means.
814
+ */
815
+ const shownRef = (0, react.useRef)(chainsKey(/* @__PURE__ */ new Set()));
816
+ const committingRef = (0, react.useRef)(false);
817
+ /**
818
+ * Read when a commit comes round rather than closed over, so one already in
819
+ * flight settles on the current props instead of the ones it started under.
820
+ */
821
+ const propsRef = (0, react.useRef)({
822
+ disabled,
823
+ hiddenChains,
824
+ selectedChains
825
+ });
826
+ propsRef.current = {
827
+ disabled,
828
+ hiddenChains,
829
+ selectedChains
830
+ };
831
+ const litChains = (0, react.useCallback)(() => {
832
+ const { disabled: hoverOff, hiddenChains: hidden, selectedChains: selected } = propsRef.current;
833
+ const hovered = hoveredRef.current;
834
+ if (!hoverOff && hovered !== null && !hidden.has(hovered)) return new Set([hovered]);
835
+ return new Set([...selected].filter((chainId) => !hidden.has(chainId)));
836
+ }, []);
837
+ const settle = (0, react.useCallback)(_asyncToGenerator(function* () {
838
+ if (committingRef.current) return;
839
+ committingRef.current = true;
840
+ try {
841
+ while (pluginRef.current) {
842
+ const lit = litChains();
843
+ const key = chainsKey(lit);
844
+ if (key === shownRef.current) break;
845
+ if (!(yield dimChainsOutside(pluginRef.current, lit))) break;
846
+ shownRef.current = key;
847
+ }
848
+ } catch (error) {
849
+ console.error("Failed to dim chains:", error);
850
+ } finally {
851
+ committingRef.current = false;
852
+ }
853
+ }), [litChains, pluginRef]);
854
+ const highlightChain = (0, react.useCallback)((chainId) => {
855
+ hoveredRef.current = chainId;
856
+ settle();
857
+ }, [settle]);
858
+ (0, react.useEffect)(() => {
859
+ settle();
860
+ }, [
861
+ disabled,
862
+ hiddenChains,
863
+ selectedChains,
864
+ settle
865
+ ]);
866
+ (0, react.useEffect)(() => {
867
+ shownRef.current = chainsKey(/* @__PURE__ */ new Set());
868
+ settle();
869
+ }, [loadCount, settle]);
870
+ return highlightChain;
871
+ }
872
+ //#endregion
873
+ //#region src/common/chainColors.ts
874
+ /**
875
+ * Default colors for telling chains apart, in assignment order.
876
+ *
877
+ * The Okabe-Ito qualitative palette, which is built to stay distinguishable
878
+ * under every common form of color blindness - the reason to prefer it over a
879
+ * hand-picked set, since a complex is read by which chain is which and a
880
+ * viewer that collapses two of them into one color stops answering that.
881
+ *
882
+ * Its black is left out: the canvas inverts with the theme, so a chain painted
883
+ * black would vanish against the dark background. The rest carry enough
884
+ * lightness to hold up against both.
885
+ *
886
+ * Chains past the end of the palette wrap around to the start.
887
+ */
888
+ const CHAIN_COLOR_PALETTE = [
889
+ "#009E73",
890
+ "#0072B2",
891
+ "#E69F00",
892
+ "#CC79A7",
893
+ "#56B4E9",
894
+ "#D55E00",
895
+ "#F0E442"
896
+ ];
897
+ /**
898
+ * Assigns each chain a color, in the order given, with `overrides` taking
899
+ * precedence by `chainId`.
900
+ *
901
+ * Palette positions are spent on every chain whether or not it is overridden,
902
+ * so overriding one chain does not shift the colors of the others.
903
+ */
904
+ function chainColorMap(chainIds, overrides) {
905
+ const colors = /* @__PURE__ */ new Map();
906
+ chainIds.forEach((chainId, i) => {
907
+ var _overrides$chainId;
908
+ const fallback = CHAIN_COLOR_PALETTE[i % CHAIN_COLOR_PALETTE.length];
909
+ colors.set(chainId, (_overrides$chainId = overrides === null || overrides === void 0 ? void 0 : overrides[chainId]) !== null && _overrides$chainId !== void 0 ? _overrides$chainId : fallback);
910
+ });
911
+ return colors;
912
+ }
913
+ //#endregion
914
+ //#region src/core/ProteinStructureViewer/hooks/useChains.ts
915
+ /**
916
+ * Reconciles the chains found in the structure with the visibility and coloring
917
+ * the consumer asked for.
918
+ *
919
+ * Visibility is controlled when `hiddenChains` is passed and owned here when it
920
+ * is not, so the legend's toggles work out of the box without taking the
921
+ * decision away from a consumer that wants it. `onChainVisibilityChange` fires
922
+ * either way: a consumer that only wants to follow along should not have to
923
+ * take ownership to hear about it.
924
+ */
925
+ function useChains({ chainColors, chains, hiddenChains, onChainVisibilityChange }) {
926
+ const isControlled = hiddenChains !== void 0;
927
+ const [ownHidden, setOwnHidden] = (0, react.useState)([]);
928
+ const onChainVisibilityChangeRef = (0, react.useRef)(onChainVisibilityChange);
929
+ onChainVisibilityChangeRef.current = onChainVisibilityChange;
930
+ const chainIds = (0, react.useMemo)(() => chains.map((c) => c.chainId), [chains]);
931
+ /**
932
+ * Chains the structure no longer has cannot be unhidden through the legend,
933
+ * since it has no row for them, so a stale id would hide a chain that shares
934
+ * its name in the next structure. Only the uncontrolled side is pruned; a
935
+ * controlled consumer owns its own list.
936
+ */
937
+ (0, react.useEffect)(() => {
938
+ if (isControlled) return;
939
+ setOwnHidden((prev) => {
940
+ const kept = prev.filter((id) => chainIds.includes(id));
941
+ return kept.length === prev.length ? prev : kept;
942
+ });
943
+ }, [chainIds, isControlled]);
944
+ const hiddenKey = [...isControlled ? hiddenChains : ownHidden].sort().join("\0");
945
+ const hidden = (0, react.useMemo)(() => new Set(hiddenKey === "" ? [] : hiddenKey.split("\0")), [hiddenKey]);
946
+ return {
947
+ colors: (0, react.useMemo)(() => chainColorMap(chainIds, chainColors), [chainIds.map((id) => {
948
+ var _chainColors$id;
949
+ return `${id}:${(_chainColors$id = chainColors === null || chainColors === void 0 ? void 0 : chainColors[id]) !== null && _chainColors$id !== void 0 ? _chainColors$id : ""}`;
950
+ }).join("\0")]),
951
+ hidden,
952
+ toggleChain: (0, react.useCallback)((chainId) => {
953
+ var _onChainVisibilityCha;
954
+ const next = hidden.has(chainId) ? [...hidden].filter((id) => id !== chainId) : [...hidden, chainId];
955
+ const ordered = chainIds.filter((id) => next.includes(id));
956
+ if (!isControlled) setOwnHidden(ordered);
957
+ (_onChainVisibilityCha = onChainVisibilityChangeRef.current) === null || _onChainVisibilityCha === void 0 || _onChainVisibilityCha.call(onChainVisibilityChangeRef, ordered);
958
+ }, [
959
+ chainIds,
960
+ hidden,
961
+ isControlled
962
+ ])
963
+ };
964
+ }
965
+ //#endregion
966
+ //#region src/core/ProteinStructureViewer/utils/theme.ts
967
+ const themeCache = /* @__PURE__ */ new Map();
968
+ /**
969
+ * The MUI theme for a mode, built at most once. Mol*'s view components rebuild
970
+ * their `ThemeProvider` on every render, and `Theme()` is not cheap.
971
+ */
972
+ function themeForMode(mode) {
973
+ const cached = themeCache.get(mode);
974
+ if (cached) return cached;
975
+ const theme = (0, _czi_sds_components.Theme)(mode);
976
+ themeCache.set(mode, theme);
977
+ return theme;
978
+ }
979
+ const residueColorCache = /* @__PURE__ */ new Map();
980
+ function residueColorsForMode(mode) {
981
+ var _getSemanticColors, _base$fillSecondaryIn, _base$textPrimary, _base$textSecondary, _base$textTertiary;
982
+ const cached = residueColorCache.get(mode);
983
+ if (cached) return cached;
984
+ const base = (_getSemanticColors = (0, _czi_sds_components.getSemanticColors)({ theme: themeForMode(mode) })) === null || _getSemanticColors === void 0 ? void 0 : _getSemanticColors.base;
985
+ const colors = {
986
+ activeBackground: (_base$fillSecondaryIn = base === null || base === void 0 ? void 0 : base.fillSecondaryInteraction) !== null && _base$fillSecondaryIn !== void 0 ? _base$fillSecondaryIn : "transparent",
987
+ activeText: (_base$textPrimary = base === null || base === void 0 ? void 0 : base.textPrimary) !== null && _base$textPrimary !== void 0 ? _base$textPrimary : "inherit",
988
+ defaultText: (_base$textSecondary = base === null || base === void 0 ? void 0 : base.textSecondary) !== null && _base$textSecondary !== void 0 ? _base$textSecondary : "inherit",
989
+ inactiveText: (_base$textTertiary = base === null || base === void 0 ? void 0 : base.textTertiary) !== null && _base$textTertiary !== void 0 ? _base$textTertiary : "inherit"
990
+ };
991
+ residueColorCache.set(mode, colors);
992
+ return colors;
993
+ }
994
+ //#endregion
995
+ //#region src/core/ProteinStructureViewer/hooks/useSubscribe.ts
996
+ /**
997
+ * Subscribes to an observable for as long as the component is mounted, the
998
+ * hook equivalent of `PluginUIComponent`'s `subscribe`.
999
+ *
1000
+ * The handler is read through a ref so that passing a fresh closure on every
1001
+ * render - which is the normal case, since these handlers read props and state
1002
+ * - does not tear down and rebuild the subscription.
1003
+ */
1004
+ function useSubscribe(observable, handler) {
1005
+ const handlerRef = (0, react.useRef)(handler);
1006
+ handlerRef.current = handler;
1007
+ (0, react.useEffect)(() => {
1008
+ if (!observable) return;
1009
+ const subscription = observable.subscribe((value) => handlerRef.current(value));
1010
+ return () => subscription.unsubscribe();
1011
+ }, [observable]);
1012
+ }
1013
+ //#endregion
1014
+ //#region src/core/ProteinStructureViewer/hooks/useViewSetting.ts
1015
+ /**
1016
+ * One field of the settings Mol*'s own React root cannot read from context.
1017
+ *
1018
+ * The subject emits a whole new settings object on every push, so each field is
1019
+ * selected out separately: React's equality bailout then absorbs the pushes
1020
+ * that left this particular value alone, and a component only re-renders for
1021
+ * the settings it actually reads.
1022
+ */
1023
+ function useViewSetting(settings, select) {
1024
+ const [value, setValue] = (0, react.useState)(() => select(settings.value));
1025
+ useSubscribe(settings, (next) => setValue(() => select(next)));
1026
+ return value;
1027
+ }
1028
+ //#endregion
1029
+ //#region src/core/ProteinStructureViewer/hooks/useMolstarTheme.ts
1030
+ /**
1031
+ * The active theme for a component Mol* renders in its own React root.
1032
+ *
1033
+ * Those components sit outside the consumer's provider tree, so they cannot
1034
+ * read the theme from context; the mode reaches them through the settings
1035
+ * subject instead. Callers re-supply the theme to their subtree via a local
1036
+ * `ThemeProvider` so Emotion styles resolve SDS tokens correctly.
1037
+ */
1038
+ function useMolstarTheme(settings) {
1039
+ const mode = useViewSetting(settings, (s) => s.mode);
1040
+ return {
1041
+ mode,
1042
+ theme: themeForMode(mode)
1043
+ };
1044
+ }
1045
+ //#endregion
1046
+ //#region src/core/ProteinStructureViewer/components/SequenceView/constants.ts
1047
+ /** Attribute carrying a residue's sequence index, used for hit testing. */
1048
+ const SEQ_ID_ATTR = "data-seqid";
1049
+ /** Hover updates are coalesced to roughly one every three frames. */
1050
+ const HOVER_THROTTLE_MS = 3 * 16.666;
1051
+ /** How long the copy button shows its confirmation state. */
1052
+ const COPIED_FEEDBACK_MS = 2e3;
1053
+ //#endregion
1054
+ //#region src/core/ProteinStructureViewer/components/SequenceView/style.ts
1055
+ /** Height of the fade that masks residues scrolling under the panel header. */
1056
+ const SCROLL_FADE_HEIGHT = 8;
1057
+ const SequencePanel = (0, _emotion_styled.default)("div")`
1058
+ display: flex;
1059
+ flex-direction: column;
1060
+ height: 100%;
1061
+
1062
+ ${(props) => {
1063
+ var _semanticColors$base, _props$backgroundColo, _semanticColors$base2;
1064
+ const spaces = (0, _czi_sds_components.getSpaces)(props);
1065
+ const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
1066
+ return `
1067
+ padding: ${spaces === null || spaces === void 0 ? void 0 : spaces.m}px ${spaces === null || spaces === void 0 ? void 0 : spaces.m}px 0;
1068
+ border-top: 1px solid ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base = semanticColors.base) === null || _semanticColors$base === void 0 ? void 0 : _semanticColors$base.divider};
1069
+ 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};
1070
+ `;
1071
+ }}
1072
+ `;
1073
+ const EmptyState = (0, _emotion_styled.default)("div")`
1074
+ ${_czi_sds_components.fontBodyXs}
1075
+
1076
+ display: flex;
1077
+ align-items: center;
1078
+ justify-content: center;
1079
+ height: 100%;
1080
+
1081
+ ${(props) => {
1082
+ var _props$backgroundColo2, _semanticColors$base3, _semanticColors$base4;
1083
+ const spaces = (0, _czi_sds_components.getSpaces)(props);
1084
+ const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
1085
+ return `
1086
+ padding: ${spaces === null || spaces === void 0 ? void 0 : spaces.m}px;
1087
+ 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};
1088
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base4 = semanticColors.base) === null || _semanticColors$base4 === void 0 ? void 0 : _semanticColors$base4.textTertiaryOnDark};
1089
+ `;
1090
+ }}
1091
+ `;
1092
+ const PanelHeader = (0, _emotion_styled.default)("div")`
1093
+ display: flex;
1094
+ align-items: center;
1095
+
1096
+ ${(props) => {
1097
+ const spaces = (0, _czi_sds_components.getSpaces)(props);
1098
+ return `
1099
+ gap: ${spaces === null || spaces === void 0 ? void 0 : spaces.xs}px;
1100
+ `;
1101
+ }}
1102
+ `;
1103
+ const PanelTitle = (0, _emotion_styled.default)("span")`
1104
+ ${_czi_sds_components.fontHeaderM}
1105
+
1106
+ ${(props) => {
1107
+ var _semanticColors$base5;
1108
+ const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
1109
+ return `
1110
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base5 = semanticColors.base) === null || _semanticColors$base5 === void 0 ? void 0 : _semanticColors$base5.textPrimary};
1111
+ `;
1112
+ }}
1113
+ `;
1114
+ const CopyButton = (0, _emotion_styled.default)("button")`
1115
+ display: flex;
1116
+ align-items: center;
1117
+ padding: 0;
1118
+ border: none;
1119
+ background: none;
1120
+ cursor: default;
1121
+
1122
+ ${(props) => {
1123
+ var _semanticColors$base6, _semanticColors$base7;
1124
+ const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
1125
+ return `
1126
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base6 = semanticColors.base) === null || _semanticColors$base6 === void 0 ? void 0 : _semanticColors$base6.ornamentSecondary};
1127
+
1128
+ &:hover {
1129
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base7 = semanticColors.base) === null || _semanticColors$base7 === void 0 ? void 0 : _semanticColors$base7.textPrimary};
1130
+ }
1131
+ `;
1132
+ }}
1133
+ `;
1134
+ const ResidueCount = (0, _emotion_styled.default)("div")`
1135
+ display: flex;
1136
+ align-items: center;
1137
+ margin-left: auto;
1138
+
1139
+ ${(props) => {
1140
+ const spaces = (0, _czi_sds_components.getSpaces)(props);
1141
+ return `
1142
+ gap: ${spaces === null || spaces === void 0 ? void 0 : spaces.xxs}px;
1143
+ `;
1144
+ }}
1145
+ `;
1146
+ const ResidueCountLabel = (0, _emotion_styled.default)("span")`
1147
+ ${_czi_sds_components.fontBodyXs}
1148
+
1149
+ ${(props) => {
1150
+ var _semanticColors$base8;
1151
+ const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
1152
+ return `
1153
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base8 = semanticColors.base) === null || _semanticColors$base8 === void 0 ? void 0 : _semanticColors$base8.textTertiary};
1154
+ `;
1155
+ }}
1156
+ `;
1157
+ const ResidueCountValue = (0, _emotion_styled.default)("span")`
1158
+ ${_czi_sds_components.fontBodyXs}
1159
+
1160
+ ${(props) => {
1161
+ var _semanticColors$base9;
1162
+ const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
1163
+ return `
1164
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base9 = semanticColors.base) === null || _semanticColors$base9 === void 0 ? void 0 : _semanticColors$base9.textPrimary};
1165
+ `;
1166
+ }}
1167
+ `;
1168
+ /**
1169
+ * Scroll region for the residue grid. The `::before` fade masks residues as
1170
+ * they scroll up under the header, so it has to start from whatever the panel
1171
+ * is painted with or it leaves a mismatched strip.
1172
+ */
1173
+ const SequenceScroller = (0, _emotion_styled.default)("div")`
1174
+ position: relative;
1175
+ flex: 1;
1176
+ min-height: 0;
1177
+
1178
+ ${(props) => {
1179
+ var _props$backgroundColo3, _semanticColors$base10;
1180
+ const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
1181
+ return `
1182
+ &::before {
1183
+ content: "";
1184
+ position: absolute;
1185
+ inset-inline: 0;
1186
+ top: 0;
1187
+ z-index: 10;
1188
+ height: ${SCROLL_FADE_HEIGHT}px;
1189
+ pointer-events: none;
1190
+ 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);
1191
+ }
1192
+ `;
1193
+ }}
1194
+ `;
1195
+ /**
1196
+ * Residue spans are written to directly by `useResidueMarkers` rather than
1197
+ * re-rendered, so their hover and selection colors are set inline. Everything
1198
+ * here is the static styling underneath that: the monospace type, the pointer
1199
+ * affordance, and the superscript sequence numbers (which need their own color
1200
+ * rule, since they are nested inside residue spans and would otherwise inherit
1201
+ * the inline color).
1202
+ */
1203
+ const SequenceScrollArea = (0, _emotion_styled.default)("div")`
1204
+ height: 100%;
1205
+ overflow-y: auto;
1206
+
1207
+ .msp-sequence-wrapper {
1208
+ ${_czi_sds_components.fontCodeXs};
1209
+ font-size: 11px;
1210
+ line-height: 1.2;
1211
+ }
1212
+
1213
+ .msp-sequence-present {
1214
+ cursor: pointer;
1215
+ }
1216
+
1217
+ ${(props) => {
1218
+ var _semanticColors$base11;
1219
+ const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
1220
+ return `
1221
+ .msp-sequence-number {
1222
+ font-size: 8px;
1223
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base11 = semanticColors.base) === null || _semanticColors$base11 === void 0 ? void 0 : _semanticColors$base11.textTertiaryOnDark};
1224
+ }
1225
+ `;
1226
+ }}
1227
+ `;
1228
+ /**
1229
+ * Layout for one chain's residues.
1230
+ *
1231
+ * Residues are grouped into fixed-size sections, each one grid cell. The
1232
+ * equal-width auto-fill tracks and uniform gap keep the spacing between
1233
+ * sections identical on every row - including a partial last row, which keeps
1234
+ * the same column width (rather than stretching) and stacks from the leading
1235
+ * edge.
1236
+ */
1237
+ const ResidueGrid = (0, _emotion_styled.default)("div")`
1238
+ user-select: none;
1239
+ display: grid;
1240
+ grid-template-columns: repeat(
1241
+ auto-fill,
1242
+ minmax(${10}ch, 1fr)
1243
+ );
1244
+ gap: 8px;
1245
+ justify-items: start;
1246
+ padding-top: 6px;
1247
+
1248
+ /* Section wrappers, so a section's letters never split across lines. */
1249
+ & > span {
1250
+ white-space: nowrap;
1251
+ }
1252
+
1253
+ .msp-sequence-present {
1254
+ position: relative;
1255
+ display: inline-block;
1256
+ width: 1ch;
1257
+ /*
1258
+ * Reserves room above the letter for its superscript sequence number.
1259
+ * content-box clipping keeps the active fill off that gutter, so a
1260
+ * highlighted residue does not tint the number.
1261
+ */
1262
+ padding-top: 8px;
1263
+ background-clip: content-box;
1264
+ text-align: center;
1265
+ }
1266
+
1267
+ .msp-sequence-number {
1268
+ position: absolute;
1269
+ top: 0;
1270
+ line-height: 1.2;
1271
+ white-space: nowrap;
1272
+ pointer-events: none;
1273
+ }
1274
+ `;
1275
+ /**
1276
+ * The row above each chain's grid: a visibility toggle and the chain's name.
1277
+ *
1278
+ * A structure with more than one chain renders one grid per chain, and without
1279
+ * a caption between them the grids read as a single continuous protein. The
1280
+ * controls sit here rather than only in the legend because this is where a
1281
+ * reader is already looking at one chain at a time.
1282
+ */
1283
+ const ChainHeaderRow = (0, _emotion_styled.default)("div")`
1284
+ display: flex;
1285
+ align-items: center;
1286
+
1287
+ ${(props) => {
1288
+ const spaces = (0, _czi_sds_components.getSpaces)(props);
1289
+ return `
1290
+ gap: ${spaces === null || spaces === void 0 ? void 0 : spaces.xxs}px;
1291
+ margin-top: ${spaces === null || spaces === void 0 ? void 0 : spaces.xs}px;
1292
+ margin-bottom: ${spaces === null || spaces === void 0 ? void 0 : spaces.xxxs}px;
1293
+ `;
1294
+ }}
1295
+ `;
1296
+ /**
1297
+ * Shows or hides the chain's cartoon in the 3D view. Full strength even while
1298
+ * the chain is hidden, since it is the way back.
1299
+ */
1300
+ const ChainVisibilityToggle = (0, _emotion_styled.default)("button")`
1301
+ display: flex;
1302
+ align-items: center;
1303
+ padding: 0;
1304
+ border: none;
1305
+ background: none;
1306
+ cursor: pointer;
1307
+
1308
+ ${(props) => {
1309
+ var _semanticColors$base12, _semanticColors$base13;
1310
+ const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
1311
+ return `
1312
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base12 = semanticColors.base) === null || _semanticColors$base12 === void 0 ? void 0 : _semanticColors$base12.textTertiaryOnDark};
1313
+
1314
+ &:hover {
1315
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base13 = semanticColors.base) === null || _semanticColors$base13 === void 0 ? void 0 : _semanticColors$base13.textPrimary};
1316
+ }
1317
+
1318
+ svg {
1319
+ display: block;
1320
+ }
1321
+ `;
1322
+ }}
1323
+ `;
1324
+ /**
1325
+ * The chain's name, and a button because it also selects the chain: the caption
1326
+ * is already the one thing on screen that stands for a whole chain, so clicking
1327
+ * it is where a reader looks to select one. Clicking it again clears it.
1328
+ */
1329
+ const ChainLabel = (0, _emotion_styled.default)("button")`
1330
+ ${_czi_sds_components.fontBodyXs}
1331
+
1332
+ padding: 0;
1333
+ border: none;
1334
+ background: none;
1335
+ text-align: left;
1336
+
1337
+ ${(props) => {
1338
+ var _semanticColors$base14, _semanticColors$base15, _semanticColors$base16;
1339
+ const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
1340
+ 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;
1341
+ return `
1342
+ color: ${resting};
1343
+ cursor: ${props.isHidden ? "default" : "pointer"};
1344
+ font-weight: ${props.isSelected ? 600 : 500};
1345
+ opacity: ${props.isHidden ? .4 : 1};
1346
+
1347
+ &:hover {
1348
+ color: ${props.isHidden ? resting : semanticColors === null || semanticColors === void 0 || (_semanticColors$base16 = semanticColors.base) === null || _semanticColors$base16 === void 0 ? void 0 : _semanticColors$base16.textPrimary};
1349
+ }
1350
+ `;
1351
+ }}
1352
+ `;
1353
+ /**
1354
+ * Wraps a chain whose 3D cartoon is hidden. The grid stays in place and stays
1355
+ * interactive - the sequence is still the sequence - but is dimmed to match the
1356
+ * chain being absent from the structure beside it. Dimmed rather than removed
1357
+ * so the panel does not reflow every time a chain is toggled.
1358
+ */
1359
+ const HiddenChainGroup = (0, _emotion_styled.default)("div")`
1360
+ opacity: 0.4;
1361
+ `;
1362
+ //#endregion
1363
+ //#region src/core/ProteinStructureViewer/components/SequenceView/components/ChainHeader/index.tsx
1364
+ /**
1365
+ * The row above one chain's residue grid: a visibility toggle and the chain's
1366
+ * name, which selects it.
1367
+ *
1368
+ * The name is a toggle rather than a one-way switch, so the tooltip says which
1369
+ * way the next click goes - offering to select a chain that is already selected
1370
+ * would be telling the reader the wrong thing. A hidden chain cannot be
1371
+ * selected at all, so the name stops being a button then: there is no geometry
1372
+ * to focus, and the toggle beside it is what the reader wants instead.
1373
+ */
1374
+ function ChainHeader({ chainId, isHidden, isSelected, label, onHover, onSelect, onToggle }) {
1375
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ChainHeaderRow, {
1376
+ onBlur: () => onHover === null || onHover === void 0 ? void 0 : onHover(null),
1377
+ onFocus: () => onHover === null || onHover === void 0 ? void 0 : onHover(chainId),
1378
+ onMouseEnter: () => onHover === null || onHover === void 0 ? void 0 : onHover(chainId),
1379
+ onMouseLeave: () => onHover === null || onHover === void 0 ? void 0 : onHover(null),
1380
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_czi_sds_components.Tooltip, {
1381
+ arrow: true,
1382
+ placement: "top",
1383
+ title: isHidden ? `Chain ${label} is hidden` : `Click to ${isSelected ? "deselect" : "select"} Chain ${label}`,
1384
+ textAlign: "left",
1385
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ChainLabel, {
1386
+ "aria-pressed": isSelected,
1387
+ disabled: isHidden,
1388
+ isHidden,
1389
+ isSelected,
1390
+ onClick: () => onSelect === null || onSelect === void 0 ? void 0 : onSelect(chainId),
1391
+ type: "button",
1392
+ children: ["Chain ", label]
1393
+ })
1394
+ }), onToggle && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_czi_sds_components.Tooltip, {
1395
+ arrow: true,
1396
+ placement: "top",
1397
+ title: `${isHidden ? "Show" : "Hide"} Chain ${label}`,
1398
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChainVisibilityToggle, {
1399
+ "aria-label": `${isHidden ? "Show" : "Hide"} chain ${label}`,
1400
+ "aria-pressed": isHidden,
1401
+ onClick: () => onToggle(chainId),
1402
+ type: "button",
1403
+ 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 })
1404
+ })
1405
+ })]
1406
+ });
1407
+ }
1408
+ //#endregion
1409
+ //#region src/core/ProteinStructureViewer/hooks/usePlugin.ts
1410
+ /**
1411
+ * The plugin owning the React root this component renders in.
1412
+ *
1413
+ * Mol* renders its view components inside its own root and provides the plugin
1414
+ * through this context, which is what `PluginUIComponent` reads to populate
1415
+ * `this.plugin`.
1416
+ */
1417
+ function usePlugin() {
1418
+ return (0, react.useContext)(molstar_lib_mol_plugin_ui_base.PluginReactContext);
1419
+ }
1420
+ //#endregion
1421
+ //#region src/core/ProteinStructureViewer/components/SequenceView/utils/residue.ts
1422
+ /** Reads the sequence index off a residue span, or undefined if it is not one. */
1423
+ function readSeqIdx(el) {
1424
+ if (!el || !el.getAttribute) return void 0;
1425
+ return el.hasAttribute("data-seqid") ? +el.getAttribute(SEQ_ID_ATTR) : void 0;
1426
+ }
1427
+ /**
1428
+ * Nearest ancestor that scrolls vertically, or null if none. Used to scope the
1429
+ * residue tooltip's hide-on-scroll listener to the sequence's own overflow
1430
+ * container instead of a global window listener.
1431
+ */
1432
+ function getScrollParent(node) {
1433
+ var _node$parentElement;
1434
+ let el = (_node$parentElement = node === null || node === void 0 ? void 0 : node.parentElement) !== null && _node$parentElement !== void 0 ? _node$parentElement : null;
1435
+ while (el) {
1436
+ const overflowY = getComputedStyle(el).overflowY;
1437
+ if (overflowY === "auto" || overflowY === "scroll") return el;
1438
+ el = el.parentElement;
1439
+ }
1440
+ return null;
1441
+ }
1442
+ function getResidueClass(sequenceWrapper, seqIdx) {
1443
+ return sequenceWrapper.isHighlighted(seqIdx) || sequenceWrapper.isSelected(seqIdx) || sequenceWrapper.isFocused(seqIdx) ? "msp-sequence-present msp-sequence-residue-active" : "msp-sequence-present msp-sequence-residue";
1444
+ }
1445
+ /**
1446
+ * The label shown above a residue: its author sequence id for atomic units,
1447
+ * or a 1-based position for coarse ones.
1448
+ *
1449
+ * `location` is a scratch object the caller reuses across residues, since this
1450
+ * runs once per numbered residue on every render.
1451
+ */
1452
+ function getSequenceNumber(sequenceWrapper, seqIdx, location) {
1453
+ let seqNum = "";
1454
+ const loci = sequenceWrapper.getLoci(seqIdx);
1455
+ const l = molstar_lib_mol_model_structure.StructureElement.Loci.getFirstLocation(loci, location);
1456
+ if (l) {
1457
+ if (molstar_lib_mol_model_structure.Unit.isAtomic(l.unit)) {
1458
+ const seqId = molstar_lib_mol_model_structure.StructureProperties.residue.auth_seq_id(l);
1459
+ const insCode = molstar_lib_mol_model_structure.StructureProperties.residue.pdbx_PDB_ins_code(l);
1460
+ seqNum = `${seqId}${insCode ? insCode : ""}`;
1461
+ } else if (molstar_lib_mol_model_structure.Unit.isCoarse(l.unit)) seqNum = `${seqIdx + 1}`;
1462
+ }
1463
+ return seqNum;
1464
+ }
1465
+ /**
1466
+ * How often a sequence number is drawn above the residues.
1467
+ *
1468
+ * Long sequences get one every ten residues. Short ones number every residue,
1469
+ * unless their numbers run to more than one digit, which would crowd them.
1470
+ */
1471
+ function resolveSequenceNumberPeriod(sequenceWrapper, location, sequenceNumberPeriod) {
1472
+ if (sequenceNumberPeriod !== void 0) return sequenceNumberPeriod;
1473
+ if (sequenceWrapper.length > 10) return 10;
1474
+ if (getSequenceNumber(sequenceWrapper, sequenceWrapper.length - 1, location).length > 1) return 5;
1475
+ return 1;
1476
+ }
1477
+ /**
1478
+ * The grid's accessible name.
1479
+ *
1480
+ * The grid is a rendering of the structure rather than a control of its own,
1481
+ * so it is exposed as an image; that makes this label the only thing an
1482
+ * assistive technology gets, which is why it carries the residues themselves
1483
+ * rather than just how many there are. They are spaced apart so they are read
1484
+ * as individual letters instead of running together into a word.
1485
+ *
1486
+ * A complex draws one grid per chain, and the caption naming each is a visual
1487
+ * sibling rather than a label, so the chain is named here too. Without it every
1488
+ * grid announces the same name and which chain a reader is on is apparent only
1489
+ * by sight.
1490
+ */
1491
+ function buildSequenceLabel(sequenceWrapper, chainLabel) {
1492
+ const letters = [];
1493
+ for (let i = 0; i < sequenceWrapper.length; i++) letters.push(sequenceWrapper.residueLabel(i));
1494
+ const name = chainLabel ? `Chain ${chainLabel}, residue sequence, ${sequenceWrapper.length} residues` : `Residue sequence, ${sequenceWrapper.length} residues`;
1495
+ return letters.length > 0 ? `${name}: ${letters.join(" ")}` : name;
1496
+ }
1497
+ //#endregion
1498
+ //#region src/core/ProteinStructureViewer/components/SequenceView/hooks/useResidueMarkers.ts
1499
+ /**
1500
+ * Keeps the residue grid's hover and selection colors in sync with the 3D
1501
+ * view.
1502
+ *
1503
+ * The colors are written straight to the residue spans rather than rendered,
1504
+ * because a marker change would otherwise re-render a grid that can hold
1505
+ * thousands of nodes.
1506
+ */
1507
+ function useResidueMarkers({ containerRef, residueColors, sequenceWrapper }) {
1508
+ const plugin = usePlugin();
1509
+ /**
1510
+ * Residues sit at the default color until something is active. A hovered,
1511
+ * selected, or focused residue brightens and takes a translucent fill; while
1512
+ * any residue is selected, the rest dim to draw attention to the selection.
1513
+ */
1514
+ const paintMarkers = (0, react.useCallback)(() => {
1515
+ const container = containerRef.current;
1516
+ if (!container) return;
1517
+ const { activeBackground, activeText, defaultText, inactiveText } = residueColors;
1518
+ const spans = container.querySelectorAll(`[${SEQ_ID_ATTR}]`);
1519
+ let anySelected = false;
1520
+ for (let i = 0; i < sequenceWrapper.length; i++) if (sequenceWrapper.isSelected(i)) {
1521
+ anySelected = true;
1522
+ break;
1523
+ }
1524
+ const inactiveColor = anySelected ? inactiveText : defaultText;
1525
+ for (let i = 0; i < sequenceWrapper.length; i++) {
1526
+ const span = spans[i];
1527
+ if (!span) return;
1528
+ const className = getResidueClass(sequenceWrapper, i);
1529
+ if (span.className !== className) span.className = className;
1530
+ const active = sequenceWrapper.isSelected(i) || sequenceWrapper.isFocused(i) || sequenceWrapper.isHighlighted(i);
1531
+ span.style.color = active ? activeText : inactiveColor;
1532
+ span.style.backgroundColor = active ? activeBackground : "";
1533
+ }
1534
+ }, [
1535
+ containerRef,
1536
+ residueColors,
1537
+ sequenceWrapper
1538
+ ]);
1539
+ const markFocus = (0, react.useCallback)((loci) => {
1540
+ sequenceWrapper.markResidue(molstar_lib_mol_model_loci.EveryLoci, "unfocus");
1541
+ if (loci) sequenceWrapper.markResidue(loci, "focus");
1542
+ }, [sequenceWrapper]);
1543
+ const paintMarkersRef = (0, react.useRef)(paintMarkers);
1544
+ paintMarkersRef.current = paintMarkers;
1545
+ const sequenceWrapperRef = (0, react.useRef)(sequenceWrapper);
1546
+ sequenceWrapperRef.current = sequenceWrapper;
1547
+ (0, react.useEffect)(() => {
1548
+ const markProvider = (loci, action) => {
1549
+ if (sequenceWrapperRef.current.markResidue(loci.loci, action)) paintMarkersRef.current();
1550
+ };
1551
+ const { lociHighlights, lociSelects } = plugin.managers.interactivity;
1552
+ lociHighlights.addProvider(markProvider);
1553
+ lociSelects.addProvider(markProvider);
1554
+ return () => {
1555
+ lociHighlights.removeProvider(markProvider);
1556
+ lociSelects.removeProvider(markProvider);
1557
+ };
1558
+ }, [plugin]);
1559
+ useSubscribe(plugin.managers.structure.focus.behaviors.current, (focus) => {
1560
+ markFocus(focus === null || focus === void 0 ? void 0 : focus.loci);
1561
+ paintMarkers();
1562
+ });
1563
+ (0, react.useLayoutEffect)(() => {
1564
+ var _plugin$managers$stru;
1565
+ markFocus((_plugin$managers$stru = plugin.managers.structure.focus.behaviors.current.value) === null || _plugin$managers$stru === void 0 ? void 0 : _plugin$managers$stru.loci);
1566
+ paintMarkers();
1567
+ });
1568
+ }
1569
+ //#endregion
1570
+ //#region src/core/ProteinStructureViewer/utils/selection.ts
1571
+ /**
1572
+ * A stable string for a selection, for comparing one against another.
1573
+ *
1574
+ * A consumer holding the selection in state hands back a new object on every
1575
+ * render, so identity says nothing about whether the selection changed. Order
1576
+ * is normalized too: `{ residues: [2, 1] }` and `{ residues: [1, 2] }` select
1577
+ * the same residues and must not read as a change.
1578
+ */
1579
+ function selectionKey(selection) {
1580
+ var _selection$residues, _selection$chains;
1581
+ const residues = [...new Set((_selection$residues = selection.residues) !== null && _selection$residues !== void 0 ? _selection$residues : [])].sort((a, b) => a - b);
1582
+ return `${[...new Set((_selection$chains = selection.chains) !== null && _selection$chains !== void 0 ? _selection$chains : [])].sort().join(",")}|${residues.join(",")}`;
1583
+ }
1584
+ /** True when the selection names nothing at all. */
1585
+ function isEmptySelection(selection) {
1586
+ var _selection$residues2, _selection$chains2;
1587
+ if (!selection) return true;
1588
+ 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);
1589
+ }
1590
+ /**
1591
+ * The residues a selection covers, ascending and without repeats.
1592
+ *
1593
+ * Chains are expanded through `residuesByChain`, which was read off the loaded
1594
+ * structure, so a chain contributes exactly the residues it holds rather than
1595
+ * the span between its ends. A residue named directly and also carried by a
1596
+ * selected chain is counted once.
1597
+ */
1598
+ function selectionResidues(selection, residuesByChain) {
1599
+ var _selection$residues3, _selection$chains3;
1600
+ const residues = new Set((_selection$residues3 = selection.residues) !== null && _selection$residues3 !== void 0 ? _selection$residues3 : []);
1601
+ for (const chainId of (_selection$chains3 = selection.chains) !== null && _selection$chains3 !== void 0 ? _selection$chains3 : []) {
1602
+ var _residuesByChain$get;
1603
+ for (const index of (_residuesByChain$get = residuesByChain.get(chainId)) !== null && _residuesByChain$get !== void 0 ? _residuesByChain$get : []) residues.add(index);
1604
+ }
1605
+ return [...residues].sort((a, b) => a - b);
1606
+ }
1607
+ /**
1608
+ * Names a selection for the legend's readout.
1609
+ *
1610
+ * A single chain is named as one, since "Chain A" says more than the number of
1611
+ * residues it happens to hold. Anything else is reported as a count, which is
1612
+ * the only thing true of every shape a selection can take. A lone residue is
1613
+ * named by the caller instead, which reads it back off the structure to get its
1614
+ * three-letter code and the number the file gives it.
1615
+ */
1616
+ function selectionLabel(selection, residueCount) {
1617
+ var _selection$chains4, _selection$residues4;
1618
+ const chains = (_selection$chains4 = selection.chains) !== null && _selection$chains4 !== void 0 ? _selection$chains4 : [];
1619
+ if (chains.length === 1 && !((_selection$residues4 = selection.residues) === null || _selection$residues4 === void 0 ? void 0 : _selection$residues4.length)) return `Chain ${chains[0]}`;
1620
+ return `${residueCount} residues`;
1621
+ }
1622
+ /** The mean of the values `read` returns, ignoring residues without one. */
1623
+ function meanOverResidues(residues, read) {
1624
+ let sum = 0;
1625
+ let count = 0;
1626
+ for (const index of residues) {
1627
+ const value = read(index);
1628
+ if (value === null || value === void 0) continue;
1629
+ sum += value;
1630
+ count++;
1631
+ }
1632
+ return count === 0 ? null : sum / count;
1633
+ }
1634
+ //#endregion
1635
+ //#region src/core/ProteinStructureViewer/utils/residueLoci.ts
1636
+ /**
1637
+ * Every atom the predicate accepts, as geometry Mol* can focus, frame and mark,
1638
+ * or undefined when it accepts none.
1639
+ *
1640
+ * One pass over the structure rather than a pass per residue: a whole-chain
1641
+ * selection names hundreds of residues, and resolving them one at a time would
1642
+ * walk the structure once for each.
1643
+ *
1644
+ * Atoms are bucketed by the unit they belong to because element indices are
1645
+ * unit-local. That bucketing is also what lets a result span a chain break: it
1646
+ * carries an entry per unit, which is what a range across two chains needs and
1647
+ * what a single unit-local entry cannot express.
1648
+ *
1649
+ * Whole residues are collected rather than one atom each, since the caller
1650
+ * frames the camera on the loci's bounding sphere and a lone atom would zoom
1651
+ * far closer than the residue warrants.
1652
+ */
1653
+ function lociMatching(structure, matches) {
1654
+ const location = molstar_lib_mol_model_structure.StructureElement.Location.create(structure);
1655
+ const elements = [];
1656
+ for (const unit of structure.units) {
1657
+ location.unit = unit;
1658
+ const indices = [];
1659
+ for (let i = 0; i < unit.elements.length; i++) {
1660
+ location.element = unit.elements[i];
1661
+ if (matches(location)) indices.push(i);
1662
+ }
1663
+ if (indices.length > 0) elements.push({
1664
+ indices,
1665
+ unit
1666
+ });
1667
+ }
1668
+ if (elements.length === 0) return void 0;
1669
+ return molstar_lib_mol_model_structure.StructureElement.Loci(structure, elements.map(({ indices, unit }) => ({
1670
+ indices: molstar_lib_mol_data_int.OrderedSet.ofSortedArray(indices),
1671
+ unit
1672
+ })));
1673
+ }
1674
+ /**
1675
+ * Every atom of the residues a selection covers, within one structure.
1676
+ *
1677
+ * `hiddenChains` is subtracted from the result. A hidden chain is not drawn, so
1678
+ * geometry built from it would have nothing to sit on: Mol*'s focus
1679
+ * representation draws its own ball-and-stick from whatever is focused, in a
1680
+ * part of the state tree the chain's own visibility does not reach, and a
1681
+ * hidden chain left in the loci would keep showing up there after its cartoon
1682
+ * had gone.
1683
+ */
1684
+ function lociForSelectionInStructure(structure, selection, hiddenChains) {
1685
+ var _selection$residues, _selection$chains;
1686
+ if (isEmptySelection(selection)) return void 0;
1687
+ const residues = new Set((_selection$residues = selection.residues) !== null && _selection$residues !== void 0 ? _selection$residues : []);
1688
+ const chains = new Set((_selection$chains = selection.chains) !== null && _selection$chains !== void 0 ? _selection$chains : []);
1689
+ return lociMatching(structure, (location) => {
1690
+ const chainId = molstar_lib_mol_model_structure.StructureProperties.chain.auth_asym_id(location);
1691
+ if (hiddenChains === null || hiddenChains === void 0 ? void 0 : hiddenChains.has(chainId)) return false;
1692
+ return residues.has(molstar_lib_mol_model_structure.StructureProperties.residue.key(location)) || chains.has(chainId);
1693
+ });
1694
+ }
1695
+ /**
1696
+ * Every atom of the residues between two 0-based indices, inclusive.
1697
+ *
1698
+ * A contiguous span is tested by comparison rather than by expanding it into a
1699
+ * set of indices: this runs on every residue a drag crosses, and a drag can
1700
+ * span the whole structure.
1701
+ */
1702
+ function lociForResidueRange(structure, min, max) {
1703
+ return lociMatching(structure, (location) => {
1704
+ const index = molstar_lib_mol_model_structure.StructureProperties.residue.key(location);
1705
+ return index >= min && index <= max;
1706
+ });
1707
+ }
1708
+ /**
1709
+ * The same, across every structure loaded into a plugin. Results are merged
1710
+ * into the first structure that matched: a loci belongs to one structure, and
1711
+ * the residue index space already spans them as though they were one.
1712
+ */
1713
+ function lociForSelection(plugin, selection, hiddenChains) {
1714
+ for (const entry of plugin.managers.structure.hierarchy.current.structures) {
1715
+ var _entry$cell$obj;
1716
+ const structure = (_entry$cell$obj = entry.cell.obj) === null || _entry$cell$obj === void 0 ? void 0 : _entry$cell$obj.data;
1717
+ if (!structure) continue;
1718
+ const loci = lociForSelectionInStructure(structure, selection, hiddenChains);
1719
+ if (loci) return loci;
1720
+ }
1721
+ }
1722
+ //#endregion
1723
+ //#region src/core/ProteinStructureViewer/utils/residueRef.ts
1724
+ /**
1725
+ * Reads the residue a loci points at, or null when it points at nothing the
1726
+ * viewer can address.
1727
+ *
1728
+ * `index` comes from Mol*'s residue key, which counts residues in file order
1729
+ * from zero and is the same counter `injectPlddt` walks. Deriving it
1730
+ * from the residue's *number* instead would tie it to how the file happens to
1731
+ * be numbered: a crop beginning at residue 200 would index `plddt` at 199, and
1732
+ * a file numbering each chain from 1 would give two residues the same index.
1733
+ */
1734
+ function residueRefFromLoci(loci) {
1735
+ var _StructureProperties$;
1736
+ const location = molstar_lib_mol_model_structure.StructureElement.Location.create(void 0);
1737
+ const firstLoc = molstar_lib_mol_model_structure.StructureElement.Loci.getFirstLocation(loci, location);
1738
+ if (!firstLoc) return null;
1739
+ return {
1740
+ chainId: molstar_lib_mol_model_structure.StructureProperties.chain.auth_asym_id(firstLoc),
1741
+ compId: molstar_lib_mol_model_structure.StructureProperties.atom.label_comp_id(firstLoc),
1742
+ insCode: (_StructureProperties$ = molstar_lib_mol_model_structure.StructureProperties.residue.pdbx_PDB_ins_code(firstLoc)) !== null && _StructureProperties$ !== void 0 ? _StructureProperties$ : "",
1743
+ index: molstar_lib_mol_model_structure.StructureProperties.residue.key(firstLoc),
1744
+ seqId: molstar_lib_mol_model_structure.StructureProperties.residue.auth_seq_id(firstLoc)
1745
+ };
1746
+ }
1747
+ /**
1748
+ * Every residue a loci covers, as a selection the consumer can echo back.
1749
+ *
1750
+ * The counterpart to `residueRefFromLoci`, which names only the first: a drag
1751
+ * across the sequence hands Mol* a range, and reporting its first residue
1752
+ * alone would turn a range the user drew into a single-residue selection.
1753
+ *
1754
+ * Reported as residues rather than as chains even when a whole chain is
1755
+ * covered. What the user drew is a span of residues, and calling it a chain
1756
+ * because it happens to end where one does would change the selection's meaning
1757
+ * on a structure where the two coincide.
1758
+ */
1759
+ function selectionFromLoci(loci) {
1760
+ const location = molstar_lib_mol_model_structure.StructureElement.Location.create(loci.structure);
1761
+ const residues = /* @__PURE__ */ new Set();
1762
+ for (const element of loci.elements) {
1763
+ location.unit = element.unit;
1764
+ molstar_lib_mol_data_int.OrderedSet.forEach(element.indices, (i) => {
1765
+ location.element = element.unit.elements[i];
1766
+ residues.add(molstar_lib_mol_model_structure.StructureProperties.residue.key(location));
1767
+ });
1768
+ }
1769
+ return { residues: [...residues].sort((a, b) => a - b) };
1770
+ }
1771
+ /**
1772
+ * How a residue is named in the readout, e.g. `"LYS 111"`. Takes the number and
1773
+ * the insertion code from the file so that the readout and the sequence panel
1774
+ * beneath it agree on what to call a residue.
1775
+ */
1776
+ function residueLabel(residue) {
1777
+ return `${residue.compId} ${residue.seqId}${residue.insCode}`;
1778
+ }
1779
+ //#endregion
1780
+ //#region src/core/ProteinStructureViewer/components/SequenceView/utils/loci.ts
1781
+ /** The loci for a residue, or undefined when there is no residue there. */
1782
+ function getLoci(sequenceWrapper, seqIdx) {
1783
+ if (seqIdx !== void 0) {
1784
+ const loci = sequenceWrapper.getLoci(seqIdx);
1785
+ if (!molstar_lib_mol_model_structure.StructureElement.Loci.isEmpty(loci)) return loci;
1786
+ }
1787
+ }
1788
+ /**
1789
+ * Extends a loci to the range between the mouse-down anchor and the current
1790
+ * residue, so dragging across the sequence selects a contiguous span.
1791
+ *
1792
+ * The span is measured in the viewer's own residue index - position in file
1793
+ * order across the whole structure - rather than in element indices, which are
1794
+ * unit-local and so cannot describe a range reaching from one chain into the
1795
+ * next. Measuring it the way every other residue-keyed API measures it is what
1796
+ * lets a drag cross a chain break, and it means a range covers exactly the
1797
+ * residues whose indices lie between its ends.
1798
+ *
1799
+ * An endpoint that names no residue leaves the drag unextended rather than
1800
+ * guessing at where it was. An endpoint spanning several units - a chain with
1801
+ * more than one symmetry operator - is fine: each copy of a residue carries the
1802
+ * same index, so the span is the same either way and resolves back across all
1803
+ * of them.
1804
+ */
1805
+ function extendToRange(loci, anchor) {
1806
+ var _lociForResidueRange;
1807
+ const from = residueRefFromLoci(anchor);
1808
+ const to = residueRefFromLoci(loci);
1809
+ if (!from || !to) return loci;
1810
+ if (from.index === to.index) return loci;
1811
+ const min = Math.min(from.index, to.index);
1812
+ const max = Math.max(from.index, to.index);
1813
+ return (_lociForResidueRange = lociForResidueRange(loci.structure, min, max)) !== null && _lociForResidueRange !== void 0 ? _lociForResidueRange : loci;
1814
+ }
1815
+ //#endregion
1816
+ //#region src/core/ProteinStructureViewer/components/SequenceView/hooks/useHoveredResidue.ts
1817
+ /**
1818
+ * Tracks which residue the pointer is over and keeps the tooltip pinned to it.
1819
+ *
1820
+ * The tracked index also deduplicates pointer moves: the grid sees a move for
1821
+ * every pixel, but only the ones that cross into a different residue matter.
1822
+ */
1823
+ function useHoveredResidue({ containerRef, sequenceWrapper }) {
1824
+ const tooltipRef = (0, react.useRef)(null);
1825
+ /** Index of the tracked residue, or -1 when the pointer is off the grid. */
1826
+ const seqIdxRef = (0, react.useRef)(-1);
1827
+ const hide = (0, react.useCallback)(() => {
1828
+ var _tooltipRef$current;
1829
+ return (_tooltipRef$current = tooltipRef.current) === null || _tooltipRef$current === void 0 ? void 0 : _tooltipRef$current.hide();
1830
+ }, []);
1831
+ const clear = (0, react.useCallback)(() => {
1832
+ var _tooltipRef$current2;
1833
+ if (seqIdxRef.current === -1) return false;
1834
+ seqIdxRef.current = -1;
1835
+ (_tooltipRef$current2 = tooltipRef.current) === null || _tooltipRef$current2 === void 0 || _tooltipRef$current2.hide();
1836
+ return true;
1837
+ }, []);
1838
+ const moveTo = (0, react.useCallback)((seqIdx, el) => {
1839
+ var _tooltipRef$current3, _tooltipRef$current4;
1840
+ if (seqIdxRef.current === seqIdx) return false;
1841
+ seqIdxRef.current = seqIdx;
1842
+ if (seqIdx < 0) (_tooltipRef$current3 = tooltipRef.current) === null || _tooltipRef$current3 === void 0 || _tooltipRef$current3.hide();
1843
+ else (_tooltipRef$current4 = tooltipRef.current) === null || _tooltipRef$current4 === void 0 || _tooltipRef$current4.show(el, sequenceWrapper.residueLabel(seqIdx));
1844
+ return true;
1845
+ }, [sequenceWrapper]);
1846
+ (0, react.useEffect)(() => {
1847
+ const scrollTarget = getScrollParent(containerRef.current);
1848
+ if (!scrollTarget) return;
1849
+ const handleScroll = () => clear();
1850
+ scrollTarget.addEventListener("scroll", handleScroll, { passive: true });
1851
+ return () => scrollTarget.removeEventListener("scroll", handleScroll);
1852
+ }, [clear, containerRef]);
1853
+ return (0, react.useMemo)(() => ({
1854
+ clear,
1855
+ hide,
1856
+ moveTo,
1857
+ tooltipRef
1858
+ }), [
1859
+ clear,
1860
+ hide,
1861
+ moveTo
1862
+ ]);
1863
+ }
1864
+ //#endregion
1865
+ //#region src/core/ProteinStructureViewer/components/SequenceView/hooks/useLociDispatch.ts
1866
+ /**
1867
+ * Wraps a residue loci and the mouse state into a Mol* interaction event. An
1868
+ * `anchor` extends the loci to the range between it and `loci`, which is how a
1869
+ * drag across the sequence comes out as a contiguous span.
1870
+ */
1871
+ function buildEvent(loci, buttons, button, modifiers, anchor) {
1872
+ const ev = {
1873
+ button,
1874
+ buttons,
1875
+ current: molstar_lib_mol_repr_representation.Representation.Loci.Empty,
1876
+ modifiers
1877
+ };
1878
+ if (loci === void 0 || molstar_lib_mol_model_structure.StructureElement.Loci.isEmpty(loci)) return ev;
1879
+ ev.current = { loci: anchor ? extendToRange(loci, anchor) : loci };
1880
+ return ev;
1881
+ }
1882
+ /**
1883
+ * Reports the grid's hover and click to Mol*, on the same behaviors the 3D
1884
+ * view publishes to, so an interaction in either surface reaches the other.
1885
+ */
1886
+ function useLociDispatch() {
1887
+ const plugin = usePlugin();
1888
+ const hover = (0, react.useCallback)((loci, buttons, button, modifiers, anchor) => {
1889
+ plugin.behaviors.interaction.hover.next(buildEvent(loci, buttons, button, modifiers, anchor));
1890
+ }, [plugin]);
1891
+ const click = (0, react.useCallback)((loci, buttons, button, modifiers, anchor) => {
1892
+ plugin.behaviors.interaction.click.next(buildEvent(loci, buttons, button, modifiers, anchor));
1893
+ }, [plugin]);
1894
+ return (0, react.useMemo)(() => ({
1895
+ click,
1896
+ hover
1897
+ }), [click, hover]);
1898
+ }
1899
+ //#endregion
1900
+ //#region src/core/ProteinStructureViewer/components/SequenceView/hooks/useSequenceDrag.ts
1901
+ const SequenceDragContext = (0, react.createContext)({
1902
+ clearAnchor: () => void 0,
1903
+ getAnchor: () => void 0,
1904
+ setAnchor: () => void 0
1905
+ });
1906
+ const SequenceDragProvider = SequenceDragContext.Provider;
1907
+ /**
1908
+ * The anchor a drag across the sequence started from, shared by every chain's
1909
+ * grid.
1910
+ *
1911
+ * It has to live above the grids rather than inside one. A complex renders one
1912
+ * grid per chain, so a drag that begins on the target and ends on the binder
1913
+ * starts in one component and finishes in another; an anchor held by the grid
1914
+ * it started in is not there to be read by the grid it ends in, and the drag
1915
+ * would be reported as a plain click on whatever it landed on.
1916
+ *
1917
+ * Held in a ref rather than state: it changes on mouse-down and mouse-up, and
1918
+ * nothing renders from it, so re-rendering every residue in the panel twice per
1919
+ * drag would buy nothing.
1920
+ */
1921
+ function useCreateSequenceDrag() {
1922
+ const anchorRef = (0, react.useRef)(void 0);
1923
+ const getAnchor = (0, react.useCallback)(() => anchorRef.current, []);
1924
+ const setAnchor = (0, react.useCallback)((anchor) => {
1925
+ anchorRef.current = anchor;
1926
+ }, []);
1927
+ const clearAnchor = (0, react.useCallback)(() => {
1928
+ anchorRef.current = void 0;
1929
+ }, []);
1930
+ return (0, react.useMemo)(() => ({
1931
+ clearAnchor,
1932
+ getAnchor,
1933
+ setAnchor
1934
+ }), [
1935
+ clearAnchor,
1936
+ getAnchor,
1937
+ setAnchor
1938
+ ]);
1939
+ }
1940
+ function useSequenceDrag() {
1941
+ return (0, react.useContext)(SequenceDragContext);
1942
+ }
1943
+ //#endregion
1944
+ //#region src/core/ProteinStructureViewer/components/SequenceView/hooks/useResidueSelection.ts
1945
+ /**
1946
+ * Turns a press and release on the grid into a selection: one residue when
1947
+ * both land on the same one, otherwise the contiguous range dragged across -
1948
+ * which the panel-level anchor lets reach into another chain's grid.
1949
+ */
1950
+ function useResidueSelection({ sequenceWrapper }) {
1951
+ const { clearAnchor, getAnchor, setAnchor } = useSequenceDrag();
1952
+ const { click } = useLociDispatch();
1953
+ const onMouseDown = (e) => {
1954
+ e.stopPropagation();
1955
+ setAnchor(getLoci(sequenceWrapper, readSeqIdx(e.target)));
1956
+ };
1957
+ const onMouseUp = (e) => {
1958
+ e.stopPropagation();
1959
+ const anchor = getAnchor();
1960
+ if (anchor === void 0) return;
1961
+ const loci = getLoci(sequenceWrapper, readSeqIdx(e.target));
1962
+ if (loci) {
1963
+ const dragged = !molstar_lib_mol_model_structure.StructureElement.Loci.areEqual(anchor, loci);
1964
+ 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);
1965
+ }
1966
+ clearAnchor();
1967
+ };
1968
+ return {
1969
+ onMouseDown,
1970
+ onMouseUp
1971
+ };
1972
+ }
1973
+ //#endregion
1974
+ //#region src/core/ProteinStructureViewer/components/SequenceView/hooks/useResiduePointer.ts
1975
+ /**
1976
+ * Everything the grid does with a pointer: hover on move, and the drag-to-range
1977
+ * selection delegated to `useResidueSelection`.
1978
+ *
1979
+ * Hover reaches the 3D view through a throttled queue, so that sweeping the
1980
+ * pointer across the grid does not ask Mol* to re-mark on every residue
1981
+ * crossed. A drag is exempt: its hover carries the growing range, so it goes
1982
+ * out immediately to keep the preview under the pointer.
1983
+ */
1984
+ function useResiduePointer({ containerRef, sequenceWrapper }) {
1985
+ const { hover } = useLociDispatch();
1986
+ const { clear, hide, moveTo, tooltipRef } = useHoveredResidue({
1987
+ containerRef,
1988
+ sequenceWrapper
1989
+ });
1990
+ const { getAnchor } = useSequenceDrag();
1991
+ const { onMouseDown, onMouseUp } = useResidueSelection({ sequenceWrapper });
1992
+ const queueRef = (0, react.useRef)(null);
1993
+ if (queueRef.current === null) queueRef.current = new rxjs.Subject();
1994
+ const queue = queueRef.current;
1995
+ useSubscribe((0, react.useMemo)(() => queue.pipe((0, rxjs_operators.throttleTime)(HOVER_THROTTLE_MS, void 0, {
1996
+ leading: true,
1997
+ trailing: true
1998
+ })), [queue]), (e) => {
1999
+ hover(getLoci(sequenceWrapper, e.seqIdx < 0 ? void 0 : e.seqIdx), e.buttons, e.button, e.modifiers, getAnchor());
2000
+ });
2001
+ const queueHover = (e, seqIdx) => queue.next({
2002
+ button: (0, molstar_lib_mol_util_input_input_observer.getButton)(e.nativeEvent),
2003
+ buttons: (0, molstar_lib_mol_util_input_input_observer.getButtons)(e.nativeEvent),
2004
+ modifiers: (0, molstar_lib_mol_util_input_input_observer.getModifiers)(e.nativeEvent),
2005
+ seqIdx
2006
+ });
2007
+ /** Asks the 3D view to drop its hover, once, if the grid still holds one. */
2008
+ const releaseHover = (e) => {
2009
+ if (clear()) queueHover(e, -1);
2010
+ };
2011
+ const onContextMenu = (e) => {
2012
+ e.preventDefault();
2013
+ };
2014
+ const onMouseMove = (e) => {
2015
+ var _readSeqIdx;
2016
+ e.stopPropagation();
2017
+ const el = e.target;
2018
+ if (!el || !el.getAttribute) {
2019
+ releaseHover(e);
2020
+ return;
2021
+ }
2022
+ const seqIdx = (_readSeqIdx = readSeqIdx(el)) !== null && _readSeqIdx !== void 0 ? _readSeqIdx : -1;
2023
+ if (!moveTo(seqIdx, el)) return;
2024
+ const anchor = getAnchor();
2025
+ if (anchor === void 0) {
2026
+ queueHover(e, seqIdx);
2027
+ return;
2028
+ }
2029
+ 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);
2030
+ };
2031
+ /**
2032
+ * Leaving one grid is not the end of a drag: on a complex the next chain's
2033
+ * grid is a separate element, so a drag from the target into the binder
2034
+ * leaves this one on its way. The anchor is therefore left alone here and
2035
+ * dropped by the release, or by the pointer leaving the panel altogether.
2036
+ */
2037
+ const onMouseLeave = (e) => {
2038
+ e.stopPropagation();
2039
+ hide();
2040
+ releaseHover(e);
2041
+ };
2042
+ return {
2043
+ handlers: {
2044
+ onContextMenu,
2045
+ onMouseDown,
2046
+ onMouseLeave,
2047
+ onMouseMove,
2048
+ onMouseUp
2049
+ },
2050
+ tooltipRef
2051
+ };
2052
+ }
2053
+ //#endregion
2054
+ //#region src/core/ProteinStructureViewer/components/SequenceView/components/ResidueTooltip/index.tsx
2055
+ var _getSpaces$m, _getSpaces;
2056
+ /**
2057
+ * Pull the tooltip toward the residue by the SDS `m` spacing token. Spacing
2058
+ * tokens are theme-invariant, so reading from the light theme is fine; popper's
2059
+ * offset modifier needs a number, hence deriving it from the token.
2060
+ */
2061
+ const MAIN_AXIS_OFFSET = -((_getSpaces$m = (_getSpaces = (0, _czi_sds_components.getSpaces)({ theme: _czi_sds_components.defaultTheme })) === null || _getSpaces === void 0 ? void 0 : _getSpaces.m) !== null && _getSpaces$m !== void 0 ? _getSpaces$m : 12);
2062
+ /**
2063
+ * A single controlled tooltip anchored to the residue span currently under the
2064
+ * cursor. Driven imperatively through its ref so that hover updates re-render
2065
+ * only the tooltip, not the residue grid.
2066
+ */
2067
+ const ResidueTooltip = (0, react.forwardRef)((_props, ref) => {
2068
+ var _getSemanticColors;
2069
+ const [{ anchorEl, label }, setState] = (0, react.useState)({
2070
+ anchorEl: null,
2071
+ label: ""
2072
+ });
2073
+ const residueTooltipBackgroundColor = (_getSemanticColors = (0, _czi_sds_components.getSemanticColors)({ theme: (0, _mui_material.useTheme)() })) === null || _getSemanticColors === void 0 || (_getSemanticColors = _getSemanticColors.base) === null || _getSemanticColors === void 0 ? void 0 : _getSemanticColors.backgroundPrimary;
2074
+ (0, react.useImperativeHandle)(ref, () => ({
2075
+ hide: () => setState((prev) => prev.anchorEl === null ? prev : require_objectSpread2._objectSpread2(require_objectSpread2._objectSpread2({}, prev), {}, { anchorEl: null })),
2076
+ show: (nextAnchorEl, nextLabel) => setState({
2077
+ anchorEl: nextAnchorEl,
2078
+ label: nextLabel
2079
+ })
2080
+ }), []);
2081
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_czi_sds_components.Tooltip, {
2082
+ arrow: true,
2083
+ disableInteractive: true,
2084
+ open: anchorEl !== null,
2085
+ placement: "top",
2086
+ slotProps: {
2087
+ popper: {
2088
+ anchorEl,
2089
+ modifiers: [{
2090
+ name: "offset",
2091
+ options: { offset: [0, MAIN_AXIS_OFFSET] }
2092
+ }]
2093
+ },
2094
+ tooltip: { style: { backgroundColor: residueTooltipBackgroundColor } },
2095
+ arrow: { style: { color: residueTooltipBackgroundColor } }
2096
+ },
2097
+ title: label,
2098
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: {
2099
+ height: 0,
2100
+ position: "fixed",
2101
+ width: 0
2102
+ } })
2103
+ });
2104
+ });
2105
+ //#endregion
2106
+ //#region src/core/ProteinStructureViewer/components/SequenceView/components/Sequence/index.tsx
2107
+ /**
2108
+ * One residue. Not a component: the grid can hold thousands of these, and
2109
+ * giving each its own fiber costs more than it saves, since the markers that
2110
+ * actually change bypass React anyway.
2111
+ */
2112
+ function renderResidue(sequenceWrapper, seqIdx, seqNum) {
2113
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2114
+ className: getResidueClass(sequenceWrapper, seqIdx),
2115
+ "data-seqid": seqIdx,
2116
+ children: [seqNum && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2117
+ className: "msp-sequence-number",
2118
+ children: seqNum
2119
+ }), sequenceWrapper.residueLabel(seqIdx)]
2120
+ }, seqIdx);
2121
+ }
2122
+ /** Interactive residue grid, kept in sync with the 3D view's hover and selection. */
2123
+ const Sequence = ({ chainLabel, hideSequenceNumbers, residueColors, sequenceNumberPeriod, sequenceWrapper }) => {
2124
+ const containerRef = (0, react.useRef)(null);
2125
+ /** Scratch location reused by every sequence-number lookup below. */
2126
+ const locationRef = (0, react.useRef)(molstar_lib_mol_model_structure.StructureElement.Location.create(void 0));
2127
+ useResidueMarkers({
2128
+ containerRef,
2129
+ residueColors,
2130
+ sequenceWrapper
2131
+ });
2132
+ const { handlers, tooltipRef } = useResiduePointer({
2133
+ containerRef,
2134
+ sequenceWrapper
2135
+ });
2136
+ const period = resolveSequenceNumberPeriod(sequenceWrapper, locationRef.current, sequenceNumberPeriod);
2137
+ const groups = [];
2138
+ for (let start = 0; start < sequenceWrapper.length; start += 10) {
2139
+ const end = Math.min(start + 10, sequenceWrapper.length);
2140
+ const residues = [];
2141
+ for (let i = start; i < end; i++) {
2142
+ const showNum = !hideSequenceNumbers && i % period === 0;
2143
+ residues.push(renderResidue(sequenceWrapper, i, showNum ? getSequenceNumber(sequenceWrapper, i, locationRef.current) : null));
2144
+ }
2145
+ groups.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: residues }, `group-${start}`));
2146
+ }
2147
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResidueGrid, require_objectSpread2._objectSpread2(require_objectSpread2._objectSpread2({
2148
+ "aria-label": buildSequenceLabel(sequenceWrapper, chainLabel),
2149
+ className: "msp-sequence-wrapper",
2150
+ ref: containerRef,
2151
+ role: "img"
2152
+ }, handlers), {}, { children: groups })), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResidueTooltip, { ref: tooltipRef })] });
2153
+ };
2154
+ //#endregion
2155
+ //#region src/core/ProteinStructureViewer/components/SequenceView/utils/copyText.ts
2156
+ /**
2157
+ * Writes `text` to the clipboard.
2158
+ *
2159
+ * The async Clipboard API is preferred, but nested srcdoc sandboxes — MCP
2160
+ * inspectors, Claude Desktop, and similar hosts — often omit `clipboard-write`
2161
+ * from their Permissions Policy, so `navigator.clipboard.writeText` rejects
2162
+ * even on a user click. `document.execCommand("copy")` only needs that click, so
2163
+ * it covers those hosts without the consumer having to patch the bundle.
2164
+ */
2165
+ function copyText(text) {
2166
+ var _navigator$clipboard;
2167
+ if (!((_navigator$clipboard = navigator.clipboard) === null || _navigator$clipboard === void 0 ? void 0 : _navigator$clipboard.writeText)) return copyWithSelection(text);
2168
+ return navigator.clipboard.writeText(text).catch(() => copyWithSelection(text));
2169
+ }
2170
+ /**
2171
+ * Copies by selecting a transient textarea. Must run under user activation,
2172
+ * which a click still has after the Clipboard API has been refused.
2173
+ */
2174
+ function copyWithSelection(text) {
2175
+ return new Promise((resolve, reject) => {
2176
+ const area = document.createElement("textarea");
2177
+ area.value = text;
2178
+ area.setAttribute("readonly", "");
2179
+ area.setAttribute("aria-hidden", "true");
2180
+ area.style.position = "fixed";
2181
+ area.style.top = "-1000px";
2182
+ document.body.appendChild(area);
2183
+ area.select();
2184
+ area.setSelectionRange(0, text.length);
2185
+ let copied = false;
2186
+ try {
2187
+ copied = document.execCommand("copy");
2188
+ } catch (_unused) {
2189
+ copied = false;
2190
+ }
2191
+ area.remove();
2192
+ if (copied) resolve();
2193
+ else reject(/* @__PURE__ */ new Error("The host blocked clipboard access."));
2194
+ });
2195
+ }
2196
+ //#endregion
2197
+ //#region src/core/ProteinStructureViewer/components/SequenceView/utils/sequenceText.ts
2198
+ /**
2199
+ * The structure's residues as text, one section per chain separated by
2200
+ * `CHAIN_SEPARATOR`. Placeholder entries contribute nothing, since they have no
2201
+ * residues to write.
2202
+ */
2203
+ function sequenceTextFromEntries(entries) {
2204
+ return entries.filter((entry) => typeof entry.wrapper !== "string").map((entry) => {
2205
+ const wrapper = entry.wrapper;
2206
+ return Array.from({ length: wrapper.length }, (_, i) => wrapper.residueLabel(i)).join("");
2207
+ }).join("|");
2208
+ }
2209
+ //#endregion
2210
+ //#region src/core/ProteinStructureViewer/components/SequenceView/hooks/useCopySequence.ts
2211
+ /**
2212
+ * Copies the structure's residues to the clipboard, chains separated so a
2213
+ * complex does not paste as one sequence, and holds a confirmation flag for a
2214
+ * moment afterwards.
2215
+ */
2216
+ function useCopySequence(entries) {
2217
+ const [copied, setCopied] = (0, react.useState)(false);
2218
+ const timeoutRef = (0, react.useRef)(null);
2219
+ (0, react.useEffect)(() => () => {
2220
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
2221
+ }, []);
2222
+ return {
2223
+ copied,
2224
+ copySequence: (0, react.useCallback)(() => {
2225
+ copyText(sequenceTextFromEntries(entries)).then(() => {
2226
+ setCopied(true);
2227
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
2228
+ timeoutRef.current = setTimeout(() => setCopied(false), COPIED_FEEDBACK_MS);
2229
+ }).catch(() => {});
2230
+ }, [entries])
2231
+ };
2232
+ }
2233
+ //#endregion
2234
+ //#region src/core/ProteinStructureViewer/components/SequenceView/hooks/useSequenceWrappers.ts
2235
+ const EMPTY_STATE = {
2236
+ chainGroupId: -1,
2237
+ modelEntityId: "",
2238
+ operatorKey: "",
2239
+ structure: molstar_lib_mol_model_structure.Structure.Empty,
2240
+ structureOptions: {
2241
+ all: [],
2242
+ options: []
2243
+ },
2244
+ structureRef: ""
2245
+ };
2246
+ function readStructure(plugin, ref) {
2247
+ const cell = plugin.state.data.select(ref)[0];
2248
+ if (!ref || !cell || !cell.obj) return molstar_lib_mol_model_structure.Structure.Empty;
2249
+ return cell.obj.data;
2250
+ }
2251
+ /** Picks the first structure, entity, chain, and operator Mol* offers. */
2252
+ function readInitialState(plugin) {
2253
+ const structureOptions = (0, molstar_lib_mol_plugin_ui_sequence.getStructureOptions)(plugin.state.data);
2254
+ const structureRef = structureOptions.options[0][0];
2255
+ const structure = readStructure(plugin, structureRef);
2256
+ const modelEntityId = (0, molstar_lib_mol_plugin_ui_sequence.getModelEntityOptions)(structure)[0][0];
2257
+ const chainGroupId = (0, molstar_lib_mol_plugin_ui_sequence.getChainOptions)(structure, modelEntityId)[0][0];
2258
+ return {
2259
+ chainGroupId,
2260
+ modelEntityId,
2261
+ operatorKey: (0, molstar_lib_mol_plugin_ui_sequence.getOperatorOptions)(structure, modelEntityId, chainGroupId)[0][0],
2262
+ structure,
2263
+ structureOptions,
2264
+ structureRef
2265
+ };
2266
+ }
2267
+ function buildParams(state) {
2268
+ const { modelEntityId, structure, structureOptions } = state;
2269
+ const chainOptions = (0, molstar_lib_mol_plugin_ui_sequence.getChainOptions)(structure, modelEntityId);
2270
+ return {
2271
+ chain: molstar_lib_mol_util_param_definition.ParamDefinition.Select(chainOptions[0][0], chainOptions, { shortLabel: true }),
2272
+ structure: molstar_lib_mol_util_param_definition.ParamDefinition.Select(structureOptions.options[0][0], structureOptions.options, { shortLabel: true })
2273
+ };
2274
+ }
2275
+ /**
2276
+ * The file's own name for a chain group, or `""` when the structure holds no
2277
+ * unit for it. Mol* groups its sequence panel by `chainGroupId`, but every
2278
+ * chain-level prop on the viewer is keyed by `auth_asym_id`, so the two have
2279
+ * to be bridged through the structure itself.
2280
+ */
2281
+ function authAsymIdFor(structure, chainGroupId) {
2282
+ const location = molstar_lib_mol_model_structure.StructureElement.Location.create(structure);
2283
+ for (const unit of structure.units) {
2284
+ if (unit.chainGroupId !== chainGroupId) continue;
2285
+ const element = unit.elements[0];
2286
+ if (element === void 0) continue;
2287
+ molstar_lib_mol_model_structure.StructureElement.Location.set(location, structure, unit, element);
2288
+ return molstar_lib_mol_model_structure.StructureProperties.chain.auth_asym_id(location);
2289
+ }
2290
+ return "";
2291
+ }
2292
+ /**
2293
+ * One entry per polymer chain, in Mol*'s entity / chain / operator order.
2294
+ *
2295
+ * Polymer entities only. The heteroatoms on a chain are separate entities, and
2296
+ * Mol* would hand back a caption and a grid for each - a myoglobin listing
2297
+ * "Chain A" and then two more headers, both reading "Chain A_1 [auth A]", one
2298
+ * holding the heme and one the hydroxide. They are drawn in the 3D view and
2299
+ * they are not sequence, and a caption per ligand offers a chain toggle for
2300
+ * something that is not a chain.
2301
+ */
2302
+ function buildEntries(structure, selection, _params) {
2303
+ const entries = [];
2304
+ 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({
2305
+ chainId: authAsymIdFor(structure, chainGroupId),
2306
+ id: `${modelEntityId}:${chainGroupId}:${operatorKey}`,
2307
+ label: cLabel,
2308
+ wrapper: (0, molstar_lib_mol_plugin_ui_sequence.getSequenceWrapper)({
2309
+ chainGroupId,
2310
+ modelEntityId,
2311
+ operatorKey,
2312
+ structure
2313
+ }, selection)
2314
+ });
2315
+ return entries;
2316
+ }
2317
+ function countResidues(entries) {
2318
+ return entries.reduce((sum, entry) => sum + (typeof entry.wrapper === "string" ? 0 : entry.wrapper.length), 0);
2319
+ }
2320
+ /**
2321
+ * The residue wrappers for every chain of the loaded structure, resynced
2322
+ * whenever Mol* creates, updates, or removes a structure object.
2323
+ *
2324
+ * The wrappers read the live selection, so they are rebuilt on each render
2325
+ * rather than memoized; that is what keeps the grid's dimming in step with a
2326
+ * selection made in the 3D view.
2327
+ */
2328
+ function useSequenceWrappers() {
2329
+ const plugin = usePlugin();
2330
+ const [state, setState] = (0, react.useState)(EMPTY_STATE);
2331
+ const sync = (0, react.useCallback)(() => {
2332
+ if ((0, molstar_lib_mol_util.arrayEqual)((0, molstar_lib_mol_plugin_ui_sequence.getStructureOptions)(plugin.state.data).all, state.structureOptions.all)) return;
2333
+ setState(readInitialState(plugin));
2334
+ }, [plugin, state.structureOptions.all]);
2335
+ (0, react.useEffect)(() => {
2336
+ if (plugin.state.data.select(molstar_lib_mol_state_state_selection.StateSelection.Generators.rootsOfType(molstar_lib_mol_plugin_state_objects.PluginStateObject.Molecule.Structure)).length > 0) setState(readInitialState(plugin));
2337
+ }, []);
2338
+ useSubscribe(plugin.state.events.object.updated, ({ ref, obj }) => {
2339
+ if (ref === state.structureRef && obj && obj.type === molstar_lib_mol_plugin_state_objects.PluginStateObject.Molecule.Structure.type && obj.data !== state.structure) sync();
2340
+ });
2341
+ useSubscribe(plugin.state.events.object.created, ({ obj }) => {
2342
+ if (obj && obj.type === molstar_lib_mol_plugin_state_objects.PluginStateObject.Molecule.Structure.type) sync();
2343
+ });
2344
+ useSubscribe(plugin.state.events.object.removed, ({ obj }) => {
2345
+ if (obj && obj.type === molstar_lib_mol_plugin_state_objects.PluginStateObject.Molecule.Structure.type) sync();
2346
+ });
2347
+ const structure = readStructure(plugin, state.structureRef);
2348
+ if (structure === molstar_lib_mol_model_structure.Structure.Empty) return {
2349
+ entries: [],
2350
+ isEmpty: true,
2351
+ residueCount: 0
2352
+ };
2353
+ const entries = buildEntries(structure, plugin.managers.structure.selection, buildParams(state));
2354
+ return {
2355
+ entries,
2356
+ isEmpty: false,
2357
+ residueCount: countResidues(entries)
2358
+ };
2359
+ }
2360
+ //#endregion
2361
+ //#region src/core/ProteinStructureViewer/components/SequenceView/index.tsx
2362
+ /**
2363
+ * Builds the Mol* sequence panel view.
2364
+ *
2365
+ * Like the viewport, Mol* renders this inside its own React root, so the theme
2366
+ * mode and the panel's background arrive through `viewSettings` and are watched
2367
+ * for changes rather than read once. The theme is re-supplied through a local
2368
+ * `ThemeProvider` for the Emotion styles and the SDS `Tooltip` below.
2369
+ */
2370
+ function createSequenceView(viewSettings) {
2371
+ return function SequenceView() {
2372
+ const { mode, theme } = useMolstarTheme(viewSettings);
2373
+ const backgroundColor = useViewSetting(viewSettings, (s) => s.sequenceViewerBackgroundColor);
2374
+ const hiddenChains = useViewSetting(viewSettings, (s) => s.hiddenChains);
2375
+ const onChainHover = useViewSetting(viewSettings, (s) => s.onChainHover);
2376
+ const onChainSelect = useViewSetting(viewSettings, (s) => s.onChainSelect);
2377
+ const onChainToggle = useViewSetting(viewSettings, (s) => s.onChainToggle);
2378
+ const selectedChains = useViewSetting(viewSettings, (s) => s.selectedChains);
2379
+ const { entries, isEmpty, residueCount } = useSequenceWrappers();
2380
+ const { copied, copySequence } = useCopySequence(entries);
2381
+ const drag = useCreateSequenceDrag();
2382
+ if (isEmpty) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_mui_material_styles.ThemeProvider, {
2383
+ theme,
2384
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmptyState, {
2385
+ backgroundColor,
2386
+ className: "msp-sequence",
2387
+ children: "No structure available"
2388
+ })
2389
+ });
2390
+ const residueColors = residueColorsForMode(mode);
2391
+ const showChainLabels = entries.length > 1;
2392
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_mui_material_styles.ThemeProvider, {
2393
+ theme,
2394
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(SequencePanel, {
2395
+ backgroundColor,
2396
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(PanelHeader, { children: [
2397
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PanelTitle, { children: "Sequence" }),
2398
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_czi_sds_components.Tooltip, {
2399
+ arrow: true,
2400
+ placement: "top",
2401
+ title: copied ? "Copied!" : "Copy",
2402
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CopyButton, {
2403
+ "aria-label": "Copy sequence to clipboard",
2404
+ onClick: copySequence,
2405
+ type: "button",
2406
+ children: copied ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_phosphor_icons_react.CheckIcon, { size: 16 }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_phosphor_icons_react.CopyIcon, { size: 16 })
2407
+ })
2408
+ }),
2409
+ /* @__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 })] })
2410
+ ] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SequenceScroller, {
2411
+ backgroundColor,
2412
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SequenceDragProvider, {
2413
+ value: drag,
2414
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SequenceScrollArea, {
2415
+ className: "msp-sequence msp-sequence-wrapper-non-empty",
2416
+ onMouseLeave: drag.clearAnchor,
2417
+ children: entries.map((s) => {
2418
+ var _hiddenChains$has, _selectedChains$has;
2419
+ const hidden = (_hiddenChains$has = hiddenChains === null || hiddenChains === void 0 ? void 0 : hiddenChains.has(s.chainId)) !== null && _hiddenChains$has !== void 0 ? _hiddenChains$has : false;
2420
+ const grid = typeof s.wrapper === "string" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2421
+ className: "msp-sequence-wrapper",
2422
+ children: s.wrapper
2423
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Sequence, {
2424
+ chainLabel: showChainLabels ? s.label : void 0,
2425
+ residueColors,
2426
+ sequenceWrapper: s.wrapper
2427
+ });
2428
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [showChainLabels && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChainHeader, {
2429
+ chainId: s.chainId,
2430
+ isHidden: hidden,
2431
+ isSelected: (_selectedChains$has = selectedChains === null || selectedChains === void 0 ? void 0 : selectedChains.has(s.chainId)) !== null && _selectedChains$has !== void 0 ? _selectedChains$has : false,
2432
+ label: s.label,
2433
+ onHover: onChainHover,
2434
+ onSelect: onChainSelect,
2435
+ onToggle: onChainToggle
2436
+ }), hidden ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HiddenChainGroup, { children: grid }) : grid] }, s.id);
2437
+ })
2438
+ })
2439
+ })
2440
+ })]
2441
+ })
2442
+ });
2443
+ };
2444
+ }
2445
+ //#endregion
2446
+ //#region src/core/ProteinStructureViewer/utils/axes.ts
2447
+ /**
2448
+ * The orientation axes widget, as Mol* parameterizes it.
2449
+ *
2450
+ * Shared rather than declared where it is used, because two different parts of
2451
+ * Mol* take the same shape: the live canvas takes it as
2452
+ * `camera.helper.axes`, and the screenshot helper takes it as its own `axes`,
2453
+ * so an image can carry the widget or leave it out independently of the view
2454
+ * it was captured from.
2455
+ */
2456
+ const AXES_ON = {
2457
+ name: "on",
2458
+ params: {
2459
+ alpha: .51,
2460
+ colorX: 16711680,
2461
+ colorY: 32768,
2462
+ colorZ: 255,
2463
+ labelColorX: 8421504,
2464
+ labelColorY: 8421504,
2465
+ labelColorZ: 8421504,
2466
+ labelOpacity: 1,
2467
+ labelScale: .25,
2468
+ labelX: "X",
2469
+ labelY: "Y",
2470
+ labelZ: "Z",
2471
+ location: "bottom-left",
2472
+ locationOffsetX: 0,
2473
+ locationOffsetY: 0,
2474
+ originColor: 8421504,
2475
+ planeColorXY: 8421504,
2476
+ planeColorXZ: 8421504,
2477
+ planeColorYZ: 8421504,
2478
+ radiusScale: .075,
2479
+ scale: .15,
2480
+ showLabels: false,
2481
+ showPlanes: true
2482
+ }
2483
+ };
2484
+ const AXES_OFF = {
2485
+ name: "off",
2486
+ params: {}
2487
+ };
2488
+ //#endregion
2489
+ //#region src/core/ProteinStructureViewer/utils/color.ts
2490
+ /** Parses `#RRGGBB` (or `RRGGBB`) into a Mol* `Color`. */
2491
+ function parseHexColor(hex) {
2492
+ const h = hex.replace("#", "");
2493
+ 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));
2494
+ }
2495
+ /**
2496
+ * Parses a theme color into a Mol* `Color`, falling back when the token is
2497
+ * missing or is a format Mol* cannot consume (theme values are hex in practice,
2498
+ * but the palette types allow any CSS color string).
2499
+ */
2500
+ function themeColor(value, fallback) {
2501
+ const candidate = value === null || value === void 0 ? void 0 : value.trim();
2502
+ return parseHexColor(candidate !== void 0 && /^#?[0-9a-f]{6}$/i.test(candidate) ? candidate : fallback);
2503
+ }
2504
+ /**
2505
+ * Neutral fill for residues with no overlay value. Sits between the light and
2506
+ * dark backgrounds so unhighlighted geometry stays legible in both modes.
2507
+ */
2508
+ function neutralResidueColor(mode) {
2509
+ const g = mode === "dark" ? 100 : 180;
2510
+ return molstar_lib_mol_util_color.Color.fromRgb(g, g, g);
2511
+ }
2512
+ //#endregion
2513
+ //#region src/core/ProteinStructureViewer/utils/screenshot.ts
2514
+ /**
2515
+ * Each resolution as the Mol* preset that produces it: 1280x720, 1920x1080,
2516
+ * 3840x2160, 7680x4320. Named here rather than given as dimensions so the
2517
+ * aspect stays Mol*'s to choose.
2518
+ *
2519
+ * Mol*'s own `viewport` preset - the canvas at its current size - is left out
2520
+ * deliberately: it makes the image depend on how big the viewer happens to be
2521
+ * on screen, which is not something a caller asking for "high" is asking for.
2522
+ */
2523
+ const RESOLUTION_PRESETS = {
2524
+ high: "ultra-hd",
2525
+ low: "hd",
2526
+ maximum: "8k-ultra-hd",
2527
+ medium: "full-hd"
2528
+ };
2529
+ /** Painted behind the structure when a background is asked for but unreadable. */
2530
+ const FALLBACK_BACKGROUND = "#ffffff";
2531
+ /**
2532
+ * Downloads the structure as a PNG.
2533
+ *
2534
+ * Mol*'s screenshot helper renders its own pass rather than reading the canvas
2535
+ * back, so the image is drawn fresh at the requested size and carries its own
2536
+ * transparency and axes settings - none of which disturb the view on screen.
2537
+ *
2538
+ * A named background is the one exception. The helper only chooses between
2539
+ * transparent and whatever the renderer is already painting, so a color has to
2540
+ * be put on the canvas for the length of the capture and taken off afterwards.
2541
+ * `finally` is what guarantees the second half: leaving a figure's background
2542
+ * behind on the viewer would be a worse bug than a failed download.
2543
+ */
2544
+ function downloadStructureImage(_x) {
2545
+ return _downloadStructureImage.apply(this, arguments);
2546
+ }
2547
+ function _downloadStructureImage() {
2548
+ _downloadStructureImage = _asyncToGenerator(function* (plugin, options = {}) {
2549
+ const helper = plugin.helpers.viewportScreenshot;
2550
+ if (!helper) return;
2551
+ const { backgroundColor, filename, resolution = "medium", showAxes } = options;
2552
+ const transparent = backgroundColor === void 0;
2553
+ helper.behaviors.values.next(require_objectSpread2._objectSpread2(require_objectSpread2._objectSpread2({}, helper.behaviors.values.value), {}, {
2554
+ axes: showAxes ? AXES_ON : AXES_OFF,
2555
+ format: {
2556
+ name: "png",
2557
+ params: {}
2558
+ },
2559
+ resolution: {
2560
+ name: RESOLUTION_PRESETS[resolution],
2561
+ params: {}
2562
+ },
2563
+ transparent
2564
+ }));
2565
+ const canvas3d = plugin.canvas3d;
2566
+ const painted = canvas3d === null || canvas3d === void 0 ? void 0 : canvas3d.props.renderer.backgroundColor;
2567
+ const overrideBackground = !transparent && canvas3d !== void 0;
2568
+ if (overrideBackground) canvas3d === null || canvas3d === void 0 || canvas3d.setProps({ renderer: { backgroundColor: themeColor(backgroundColor, FALLBACK_BACKGROUND) } });
2569
+ try {
2570
+ yield helper.download(filename);
2571
+ } finally {
2572
+ if (overrideBackground && painted !== void 0) canvas3d === null || canvas3d === void 0 || canvas3d.setProps({ renderer: { backgroundColor: painted } });
2573
+ }
2574
+ });
2575
+ return _downloadStructureImage.apply(this, arguments);
2576
+ }
2577
+ //#endregion
2578
+ //#region src/core/ProteinStructureViewer/components/Viewport/constants.ts
2579
+ /**
2580
+ * Fraction of the bounding sphere radius within which the camera still counts
2581
+ * as sitting at the default view, absorbing easing and float error.
2582
+ */
2583
+ const DEFAULT_VIEW_TOLERANCE = .02;
2584
+ //#endregion
2585
+ //#region src/core/ProteinStructureViewer/components/Viewport/utils/camera.ts
2586
+ /** True when the camera's zoom/center differs from the default fit. */
2587
+ function isZoomedOffDefault(canvas3d) {
2588
+ const { center, radius } = canvas3d.boundingSphereVisible;
2589
+ if (radius <= 0) return false;
2590
+ const { state } = canvas3d.camera;
2591
+ const tolerance = radius * DEFAULT_VIEW_TOLERANCE;
2592
+ return Math.abs(state.radius - radius) > tolerance || molstar_lib_mol_math_linear_algebra.Vec3.distance(state.target, center) > tolerance;
2593
+ }
2594
+ //#endregion
2595
+ //#region src/core/ProteinStructureViewer/components/Viewport/hooks/useCameraReset.ts
2596
+ /**
2597
+ * Tracks whether the camera has moved off the default view, and puts it back.
2598
+ *
2599
+ * The default view is the auto-fit to the structure's bounding sphere, so
2600
+ * "moved" means either the zoom/center has drifted off that fit or a manual
2601
+ * drag has rotated it - rotation has to be tracked separately, since it leaves
2602
+ * the zoom and center at the default and the bounding-sphere check cannot see
2603
+ * it.
2604
+ */
2605
+ function useCameraReset() {
2606
+ const plugin = usePlugin();
2607
+ const [cameraChanged, setCameraChanged] = (0, react.useState)(false);
2608
+ /**
2609
+ * Mirrors the state above so the per-frame handler below can skip redundant
2610
+ * updates without re-reading state through a stale closure.
2611
+ */
2612
+ const cameraChangedRef = (0, react.useRef)(false);
2613
+ const updateCameraChanged = (0, react.useCallback)((changed) => {
2614
+ if (changed === cameraChangedRef.current) return;
2615
+ cameraChangedRef.current = changed;
2616
+ setCameraChanged(changed);
2617
+ }, []);
2618
+ /**
2619
+ * A manual gesture (rotate via drag) moves the camera off the default view.
2620
+ * Tracked separately from zoom because rotation leaves the zoom/center at
2621
+ * the default fit, so the bounding-sphere check cannot detect it.
2622
+ */
2623
+ const gesturedRef = (0, react.useRef)(false);
2624
+ /**
2625
+ * A (re)loaded structure auto-fits to the default view. Ignore camera
2626
+ * changes until that fit transition settles so the button does not flash
2627
+ * during load.
2628
+ */
2629
+ const awaitingFitRef = (0, react.useRef)(true);
2630
+ const fitStartedRef = (0, react.useRef)(false);
2631
+ /**
2632
+ * Reconciles the button visibility with the current camera. The button
2633
+ * shows when the camera differs from the default view: either the
2634
+ * zoom/center has moved off the bounding-sphere fit, or a manual gesture
2635
+ * rotated the view.
2636
+ */
2637
+ const syncCameraChanged = (0, react.useCallback)(() => {
2638
+ const canvas3d = plugin.canvas3d;
2639
+ if (!canvas3d) return;
2640
+ const inTransition = canvas3d.camera.transition.inTransition;
2641
+ if (awaitingFitRef.current) {
2642
+ if (inTransition) {
2643
+ fitStartedRef.current = true;
2644
+ return;
2645
+ }
2646
+ if (!fitStartedRef.current && isZoomedOffDefault(canvas3d)) return;
2647
+ awaitingFitRef.current = false;
2648
+ }
2649
+ if (inTransition) return;
2650
+ updateCameraChanged(gesturedRef.current || isZoomedOffDefault(canvas3d));
2651
+ }, [plugin, updateCameraChanged]);
2652
+ (0, react.useEffect)(() => {
2653
+ const subscriptions = [];
2654
+ let cameraBound = false;
2655
+ const bindCamera = () => {
2656
+ const canvas3d = plugin.canvas3d;
2657
+ if (!canvas3d || cameraBound) return;
2658
+ cameraBound = true;
2659
+ const setGestured = () => {
2660
+ gesturedRef.current = true;
2661
+ syncCameraChanged();
2662
+ };
2663
+ subscriptions.push(canvas3d.input.drag.subscribe(setGestured), canvas3d.input.wheel.subscribe(setGestured), plugin.state.data.events.object.created.subscribe(() => {
2664
+ gesturedRef.current = false;
2665
+ awaitingFitRef.current = true;
2666
+ fitStartedRef.current = false;
2667
+ updateCameraChanged(false);
2668
+ }), canvas3d.didDraw.subscribe(syncCameraChanged));
2669
+ };
2670
+ subscriptions.push(plugin.behaviors.canvas3d.initialized.subscribe(bindCamera));
2671
+ bindCamera();
2672
+ return () => subscriptions.forEach((s) => s.unsubscribe());
2673
+ }, [
2674
+ plugin,
2675
+ syncCameraChanged,
2676
+ updateCameraChanged
2677
+ ]);
2678
+ return {
2679
+ cameraChanged,
2680
+ resetCamera: (0, react.useCallback)(_asyncToGenerator(function* () {
2681
+ yield molstar_lib_mol_plugin_commands.PluginCommands.Camera.Reset(plugin, { durationMs: 250 });
2682
+ yield new Promise((resolve) => setTimeout(resolve, 250));
2683
+ yield molstar_lib_mol_plugin_commands.PluginCommands.Camera.ResetAxes(plugin, { durationMs: 250 });
2684
+ yield new Promise((resolve) => setTimeout(resolve, 250));
2685
+ gesturedRef.current = false;
2686
+ updateCameraChanged(false);
2687
+ }), [plugin, updateCameraChanged])
2688
+ };
2689
+ }
2690
+ //#endregion
2691
+ //#region src/core/ProteinStructureViewer/components/Viewport/style.ts
2692
+ /**
2693
+ * Pinned to the lower-left of the viewport, clear of the sequence panel below
2694
+ * and the axes widget in the corner.
2695
+ *
2696
+ * Anchored by its bottom edge, so the buttons stack upwards: the last one
2697
+ * stays put where a reader last saw it, and an earlier one appearing or going
2698
+ * does not move the rest. Reset comes first in the markup and so sits above
2699
+ * capture.
2700
+ */
2701
+ const ViewportButtonStack = (0, _emotion_styled.default)("div")`
2702
+ position: absolute;
2703
+ bottom: 48px;
2704
+ z-index: 10;
2705
+ display: flex;
2706
+ flex-direction: column;
2707
+
2708
+ ${(props) => {
2709
+ const spaces = (0, _czi_sds_components.getSpaces)(props);
2710
+ return `
2711
+ left: ${spaces === null || spaces === void 0 ? void 0 : spaces.m}px;
2712
+ gap: ${spaces === null || spaces === void 0 ? void 0 : spaces.xxs}px;
2713
+ `;
2714
+ }}
2715
+ `;
2716
+ //#endregion
2717
+ //#region src/core/ProteinStructureViewer/components/Viewport/index.tsx
2718
+ /**
2719
+ * Builds the Mol* viewport view.
2720
+ *
2721
+ * Mol* instantiates view components inside its own React root, so they render
2722
+ * outside the consumer's provider tree and cannot read the theme from context.
2723
+ * `viewSettings` carries the mode in instead, and is watched rather than read
2724
+ * once so that a theme change after the plugin was created still lands here;
2725
+ * the mode is re-supplied via a local `ThemeProvider` so the Emotion styles
2726
+ * below resolve SDS tokens correctly.
2727
+ */
2728
+ function createViewportView(viewSettings) {
2729
+ return function ViewportWithReset() {
2730
+ const plugin = usePlugin();
2731
+ const { theme } = useMolstarTheme(viewSettings);
2732
+ const { cameraChanged, resetCamera } = useCameraReset();
2733
+ const showAxes = useViewSetting(viewSettings, (s) => {
2734
+ var _s$showAxes;
2735
+ return (_s$showAxes = s.showAxes) !== null && _s$showAxes !== void 0 ? _s$showAxes : true;
2736
+ });
2737
+ const download = useViewSetting(viewSettings, (s) => s.download);
2738
+ /**
2739
+ * Disabled while a capture is in flight. A high resolution takes long
2740
+ * enough to click twice through, and each click renders its own pass.
2741
+ */
2742
+ const [downloading, setDownloading] = (0, react.useState)(false);
2743
+ const captureImage = (0, react.useCallback)(_asyncToGenerator(function* () {
2744
+ if (!download) return;
2745
+ setDownloading(true);
2746
+ try {
2747
+ yield downloadStructureImage(plugin, download);
2748
+ } catch (error) {
2749
+ console.error("Failed to download the structure image:", error);
2750
+ } finally {
2751
+ setDownloading(false);
2752
+ }
2753
+ }), [download, plugin]);
2754
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2755
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(molstar_lib_mol_plugin_ui_viewport.Viewport, {}),
2756
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2757
+ className: "msp-highlight-toast-wrapper",
2758
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(molstar_lib_mol_plugin_ui_toast.Toasts, {})
2759
+ }),
2760
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_mui_material_styles.ThemeProvider, {
2761
+ theme,
2762
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ViewportButtonStack, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2763
+ hidden: !showAxes || !cameraChanged,
2764
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_czi_sds_components.Tooltip, {
2765
+ arrow: true,
2766
+ placement: "right",
2767
+ title: "Reset camera",
2768
+ textAlign: "left",
2769
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_czi_sds_components.Button, {
2770
+ "aria-label": "Reset camera",
2771
+ onClick: resetCamera,
2772
+ sdsStyle: "minimal",
2773
+ sdsType: "secondary",
2774
+ size: "medium",
2775
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_phosphor_icons_react.ArrowCounterClockwiseIcon, {})
2776
+ })
2777
+ })
2778
+ }), download && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_czi_sds_components.Tooltip, {
2779
+ arrow: true,
2780
+ placement: "right",
2781
+ title: "Download image",
2782
+ textAlign: "left",
2783
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_czi_sds_components.Button, {
2784
+ "aria-label": "Download image of the structure",
2785
+ disabled: downloading,
2786
+ onClick: captureImage,
2787
+ sdsStyle: "minimal",
2788
+ sdsType: "secondary",
2789
+ size: "medium",
2790
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_phosphor_icons_react.ApertureIcon, {})
2791
+ })
2792
+ })] })
2793
+ })
2794
+ ] });
2795
+ };
2796
+ }
2797
+ //#endregion
2798
+ //#region src/core/ProteinStructureViewer/utils/cameraFocus.ts
2799
+ /**
2800
+ * Margin (angstroms) added around a residue's bounding sphere when focusing,
2801
+ * matching Mol*'s default `FocusLoci` extra radius. Used both for the framing
2802
+ * radius that sets the zoom-in distance (floored at `FOCUS_MIN_RADIUS`) and for
2803
+ * the depth-clip anchor, so the focus crop matches Mol*'s default depth slab.
2804
+ */
2805
+ const FOCUS_MIN_RADIUS = 8;
2806
+ const FOCUS_EXTRA_RADIUS = 4;
2807
+ /**
2808
+ * Never let the clip window collapse below this, so the focused residue and its
2809
+ * immediate neighborhood stay visible even at extreme zoom-in.
2810
+ */
2811
+ const MIN_CLIP_RADIUS = 4;
2812
+ /** Camera transition length (ms) when zooming in on a residue. */
2813
+ const FOCUS_DURATION_MS = 250;
2814
+ /** Clip radius has to drift this far before it is worth redrawing. */
2815
+ const CLIP_EPSILON = .5;
2816
+ /**
2817
+ * Depth-clip radius at a given camera distance. The clip scales linearly with
2818
+ * distance, `ratio` being the clip radius per unit distance (fixed at focus so
2819
+ * the clip hugs the residue there), clamped to the scene. This keeps clipping
2820
+ * tied to the zoom:
2821
+ * - at the focus zoom the clip hugs the residue, peeling the structure so the
2822
+ * selected residue reads clearly;
2823
+ * - zooming in shrinks the clip further, peeling deeper toward the residue;
2824
+ * - zooming out grows the clip, revealing more of the structure, up to the
2825
+ * whole scene (`radiusMax`) - the point at which nothing is cropped.
2826
+ */
2827
+ function clipRadiusForDistance(distance, ratio, radiusMax) {
2828
+ return Math.max(MIN_CLIP_RADIUS, Math.min(ratio * distance, radiusMax));
2829
+ }
2830
+ /**
2831
+ * Zooms the camera in on a residue. Uses Mol*'s focus framing for the camera
2832
+ * *position* (so the zoom level matches the built-in focus) but anchors the
2833
+ * clip tightly on the residue itself, and returns the clip-radius-per-distance
2834
+ * ratio used to keep clipping in sync with the zoom afterwards.
2835
+ */
2836
+ function focusResidue(plugin, loci) {
2837
+ var _plugin$canvas3d;
2838
+ const camera = (_plugin$canvas3d = plugin.canvas3d) === null || _plugin$canvas3d === void 0 ? void 0 : _plugin$canvas3d.camera;
2839
+ if (!camera) return null;
2840
+ const sphere = molstar_lib_mol_model_loci.Loci.getBoundingSphere(loci);
2841
+ if (!sphere) return null;
2842
+ const framingRadius = Math.max(sphere.radius + FOCUS_EXTRA_RADIUS, FOCUS_MIN_RADIUS);
2843
+ const snapshot = camera.getFocus(sphere.center, framingRadius);
2844
+ const focusDistance = camera.getTargetDistance(framingRadius);
2845
+ const clipRatio = (sphere.radius + FOCUS_EXTRA_RADIUS) / focusDistance;
2846
+ const radius = clipRadiusForDistance(focusDistance, clipRatio, camera.state.radiusMax);
2847
+ camera.setState(require_objectSpread2._objectSpread2(require_objectSpread2._objectSpread2({}, snapshot), {}, { radius }), FOCUS_DURATION_MS);
2848
+ return clipRatio;
2849
+ }
2850
+ /**
2851
+ * Frames the camera on a loci without cropping anything around it.
2852
+ *
2853
+ * The counterpart to `focusResidue` for something meant to be read in context
2854
+ * rather than peeled down to: the camera fits what was selected, and the depth
2855
+ * clip is left open to the whole scene, so the rest of the structure stays on
2856
+ * screen. A selected chain is shown by dimming the chains around it, and they
2857
+ * have to be there to be dimmed.
2858
+ */
2859
+ function frameLoci(plugin, loci) {
2860
+ var _plugin$canvas3d2;
2861
+ const camera = (_plugin$canvas3d2 = plugin.canvas3d) === null || _plugin$canvas3d2 === void 0 ? void 0 : _plugin$canvas3d2.camera;
2862
+ if (!camera) return;
2863
+ const sphere = molstar_lib_mol_model_loci.Loci.getBoundingSphere(loci);
2864
+ if (!sphere) return;
2865
+ const framingRadius = Math.max(sphere.radius + FOCUS_EXTRA_RADIUS, FOCUS_MIN_RADIUS);
2866
+ const snapshot = camera.getFocus(sphere.center, framingRadius);
2867
+ camera.setState(require_objectSpread2._objectSpread2(require_objectSpread2._objectSpread2({}, snapshot), {}, { radius: camera.state.radiusMax }), FOCUS_DURATION_MS);
2868
+ }
2869
+ /**
2870
+ * Keeps the depth clip in sync with the zoom while a residue is focused.
2871
+ *
2872
+ * Mol*'s trackball zoom moves the camera without touching the clip radius, so
2873
+ * this recomputes it after each draw: zooming in tightens the clip (peeling
2874
+ * toward the residue), zooming out widens it toward the whole structure. It is
2875
+ * a no-op when `getClipRatio` returns null (nothing focused) and while the
2876
+ * camera is mid-transition, so it does not fight the focus animation.
2877
+ */
2878
+ function syncClipToZoom(plugin, getClipRatio) {
2879
+ var _plugin$canvas3d3;
2880
+ const camera = (_plugin$canvas3d3 = plugin.canvas3d) === null || _plugin$canvas3d3 === void 0 ? void 0 : _plugin$canvas3d3.camera;
2881
+ const clipRatio = getClipRatio();
2882
+ if (!camera || clipRatio === null || camera.transition.inTransition) return;
2883
+ const radius = clipRadiusForDistance(molstar_lib_mol_math_linear_algebra.Vec3.distance(camera.state.position, camera.state.target), clipRatio, camera.state.radiusMax);
2884
+ if (Math.abs(camera.state.radius - radius) > CLIP_EPSILON) {
2885
+ var _plugin$canvas3d4;
2886
+ camera.setState({ radius });
2887
+ (_plugin$canvas3d4 = plugin.canvas3d) === null || _plugin$canvas3d4 === void 0 || _plugin$canvas3d4.requestDraw();
2888
+ }
2889
+ }
2890
+ //#endregion
2891
+ //#region src/core/ProteinStructureViewer/utils/chains.ts
2892
+ /**
2893
+ * The polymer of one chain, as named in the file. What each chain's cartoon is
2894
+ * built from, and so what makes a chain a thing Mol* can hide on its own.
2895
+ *
2896
+ * Paired with `chainLigandExpression`: between them they cover everything on a
2897
+ * chain the viewer draws, split by what it draws it as. The polymer
2898
+ * restriction is the one Mol*'s `polymer-cartoon` preset applied, and is what
2899
+ * keeps a heme from being handed to a representation that can only trace a
2900
+ * backbone.
2901
+ */
2902
+ function chainPolymerExpression(chainId) {
2903
+ return molstar_lib_mol_script_language_builder.MolScriptBuilder.struct.generator.atomGroups({
2904
+ "chain-test": molstar_lib_mol_script_language_builder.MolScriptBuilder.core.rel.eq([molstar_lib_mol_script_language_builder.MolScriptBuilder.ammp("auth_asym_id"), chainId]),
2905
+ "entity-test": molstar_lib_mol_script_language_builder.MolScriptBuilder.core.rel.eq([molstar_lib_mol_script_language_builder.MolScriptBuilder.ammp("entityType"), "polymer"])
2906
+ });
2907
+ }
2908
+ /**
2909
+ * Bonds worth following out of a ligand: the ones holding it to the protein.
2910
+ *
2911
+ * Mol*'s own pair, and metallic coordination is the half that matters most -
2912
+ * a heme's iron is held to its histidine by a coordination bond and not a
2913
+ * covalent one, so covalent alone would leave every metalloprotein's cofactor
2914
+ * looking dropped in rather than bound.
2915
+ */
2916
+ 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"])])]);
2917
+ /**
2918
+ * The heteroatoms of one chain - ligands, ions, glycans, lipids - along with
2919
+ * the residues holding them, which is what its ball-and-stick is built from.
2920
+ *
2921
+ * `non-polymer` is the one entity type a PDB's HETATM records land under,
2922
+ * ligands and ions alike, so there is nothing finer to test here: the
2923
+ * distinction Mol*'s presets draw between their `ligand`, `ion` and `lipid`
2924
+ * components is a residue-name lookup made to give each a slightly different
2925
+ * style, and all three are sticks. `branched` is the separate type glycans
2926
+ * arrive as.
2927
+ *
2928
+ * Water is the third type and is left out of the seed. Every solvent molecule
2929
+ * drawn as sticks buries the structure they surround, which is why Mol* draws
2930
+ * them at 60% alpha rather than plain, and none of them is what "show the
2931
+ * ligands" asks for.
2932
+ *
2933
+ * One layer of bonds out from there, taken as whole residues, is what brings
2934
+ * the binding residue along - myoglobin's His93, say. Without it the heme
2935
+ * floats unattached in the middle of a cartoon it is visibly bonded to, and
2936
+ * the bond drawn to its iron ends in mid-air. One layer and no more: the
2937
+ * residue that holds the ligand, not that residue's own neighbours.
2938
+ *
2939
+ * Intersected back with the chain, so everything a chain is drawn with belongs
2940
+ * to that chain. A ligand bonded across an interface would otherwise leave the
2941
+ * partner chain's residue in this chain's component, somewhere hiding the
2942
+ * partner chain could not reach.
2943
+ */
2944
+ function chainLigandExpression(chainId) {
2945
+ const onThisChain = molstar_lib_mol_script_language_builder.MolScriptBuilder.core.rel.eq([molstar_lib_mol_script_language_builder.MolScriptBuilder.ammp("auth_asym_id"), chainId]);
2946
+ return molstar_lib_mol_script_language_builder.MolScriptBuilder.struct.modifier.union([molstar_lib_mol_script_language_builder.MolScriptBuilder.struct.modifier.intersectBy({
2947
+ 0: molstar_lib_mol_script_language_builder.MolScriptBuilder.struct.modifier.includeConnected({
2948
+ 0: molstar_lib_mol_script_language_builder.MolScriptBuilder.struct.generator.atomGroups({
2949
+ "chain-test": onThisChain,
2950
+ "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")])
2951
+ }),
2952
+ "as-whole-residues": true,
2953
+ "bond-test": CONNECTING_BOND,
2954
+ "layer-count": 1
2955
+ }),
2956
+ by: molstar_lib_mol_script_language_builder.MolScriptBuilder.struct.generator.atomGroups({ "chain-test": onThisChain })
2957
+ })]);
2958
+ }
2959
+ /**
2960
+ * Matches how the sequence panel captions its chains, which reaches the same
2961
+ * label through Mol*'s own `getChainOptions`. Deriving it the same way is what
2962
+ * keeps a chain from going by one name in the legend and another in the panel.
2963
+ */
2964
+ const CHAIN_LABEL_OPTIONS = {
2965
+ granularity: "chain",
2966
+ hidePrefix: true,
2967
+ htmlStyling: false
2968
+ };
2969
+ /**
2970
+ * Reads the chains of a structure and the polymer residues on each.
2971
+ *
2972
+ * Residue indices are Mol*'s residue keys, counting residues in file order
2973
+ * across the whole structure, so what is reported here addresses the same
2974
+ * residues as `plddt`, `residueOverlay` and `selection`.
2975
+ *
2976
+ * Heteroatoms are drawn (see `chainLigandExpression`) but are not counted
2977
+ * here. A heme is a residue to the file and not to a reader: it is no part of
2978
+ * the sequence the panel lists, and averaging a confidence score over it would
2979
+ * divide by a residue that has none.
2980
+ *
2981
+ * Residues are gathered into a set rather than inferred from the ends: a chain
2982
+ * carrying several symmetry operators repeats each of its residues once per
2983
+ * operator, and a structure whose chains interleave leaves a chain's indices
2984
+ * non-contiguous. Neither is what a predicted complex looks like, but a range
2985
+ * would misread both.
2986
+ */
2987
+ function scanChains(structure) {
2988
+ const accumulators = /* @__PURE__ */ new Map();
2989
+ const location = molstar_lib_mol_model_structure.StructureElement.Location.create(structure);
2990
+ for (const unit of structure.units) {
2991
+ location.unit = unit;
2992
+ for (let i = 0; i < unit.elements.length; i++) {
2993
+ location.element = unit.elements[i];
2994
+ const chainId = molstar_lib_mol_model_structure.StructureProperties.chain.auth_asym_id(location);
2995
+ let chain = accumulators.get(chainId);
2996
+ if (!chain) {
2997
+ chain = {
2998
+ chainId,
2999
+ label: (0, molstar_lib_mol_theme_label.elementLabel)(location, CHAIN_LABEL_OPTIONS),
3000
+ residues: /* @__PURE__ */ new Set()
3001
+ };
3002
+ accumulators.set(chainId, chain);
3003
+ }
3004
+ if (molstar_lib_mol_model_structure.StructureProperties.entity.type(location) !== "polymer") continue;
3005
+ chain.residues.add(molstar_lib_mol_model_structure.StructureProperties.residue.key(location));
3006
+ }
3007
+ }
3008
+ const ordered = [...accumulators.values()].filter((chain) => chain.residues.size > 0).map((chain) => require_objectSpread2._objectSpread2(require_objectSpread2._objectSpread2({}, chain), {}, { residues: [...chain.residues].sort((a, b) => a - b) })).sort((a, b) => {
3009
+ var _a$residues$, _b$residues$;
3010
+ 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);
3011
+ });
3012
+ return {
3013
+ chainLabels: new Map([...accumulators.values()].map(({ chainId, label }) => [chainId, label])),
3014
+ chains: ordered.map(({ chainId, label, residues }) => {
3015
+ var _residues, _residues$;
3016
+ return {
3017
+ chainId,
3018
+ endIndex: (_residues = residues[residues.length - 1]) !== null && _residues !== void 0 ? _residues : 0,
3019
+ label,
3020
+ residueCount: residues.length,
3021
+ startIndex: (_residues$ = residues[0]) !== null && _residues$ !== void 0 ? _residues$ : 0
3022
+ };
3023
+ }),
3024
+ residuesByChain: new Map(ordered.map(({ chainId, residues }) => [chainId, residues]))
3025
+ };
3026
+ }
3027
+ /** True when the two lists name the same chains, in the same order. */
3028
+ function chainsEqual(a, b) {
3029
+ return a.length === b.length && a.every((chain, i) => {
3030
+ const other = b[i];
3031
+ return other !== void 0 && chain.chainId === other.chainId && chain.startIndex === other.startIndex && chain.endIndex === other.endIndex && chain.residueCount === other.residueCount;
3032
+ });
3033
+ }
3034
+ //#endregion
3035
+ //#region src/core/ProteinStructureViewer/utils/molstarSpec.ts
3036
+ /**
3037
+ * The spec's list-valued keys. These are appended rather than merged: Mol*
3038
+ * reads them as collections, so a consumer's entries join the viewer's instead
3039
+ * of replacing them.
3040
+ *
3041
+ * `config` is the one where order carries meaning - Mol* folds the list into a
3042
+ * Map in order, so a later entry overwrites an earlier one, and appending is
3043
+ * what lets a consumer override a setting the viewer chose.
3044
+ */
3045
+ const LIST_KEYS = [
3046
+ "actions",
3047
+ "animations",
3048
+ "behaviors",
3049
+ "config",
3050
+ "customFormats",
3051
+ "customParamEditors"
3052
+ ];
3053
+ /**
3054
+ * Whether a value should be merged into rather than replaced.
3055
+ *
3056
+ * Only object literals qualify. Arrays are collections, and the spec is full
3057
+ * of React components and Mol* class instances, all of which are values in
3058
+ * their own right - merging into one would take it apart.
3059
+ */
3060
+ function isPlainObject(value) {
3061
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
3062
+ const proto = Object.getPrototypeOf(value);
3063
+ return proto === Object.prototype || proto === null;
3064
+ }
3065
+ function deepMerge(base, override) {
3066
+ if (!isPlainObject(base) || !isPlainObject(override)) return override;
3067
+ const merged = require_objectSpread2._objectSpread2({}, base);
3068
+ for (const [key, value] of Object.entries(override)) merged[key] = key in base ? deepMerge(base[key], value) : value;
3069
+ return merged;
3070
+ }
3071
+ function withoutLists(spec) {
3072
+ const rest = require_objectSpread2._objectSpread2({}, spec);
3073
+ for (const key of LIST_KEYS) delete rest[key];
3074
+ return rest;
3075
+ }
3076
+ /**
3077
+ * Lays a consumer's Mol* spec over the viewer's own.
3078
+ *
3079
+ * The viewer builds a spec of its own - the sequence panel and viewport it
3080
+ * renders, the controls it hides, the behaviors it drops - and this is how a
3081
+ * consumer reaches the rest of Mol* without the viewer having to grow a prop
3082
+ * per setting. Anything the consumer names wins, so `canvas3d.renderer` or a
3083
+ * `PluginConfig` item can be set to something other than the default the
3084
+ * viewer picked.
3085
+ *
3086
+ * The one thing it cannot do is take something away: `behaviors` is appended
3087
+ * to, so a behavior the viewer removed - Mol*'s click-to-zoom camera - stays
3088
+ * removed, and one the viewer kept cannot be dropped.
3089
+ */
3090
+ function mergeMolstarSpec(base, override) {
3091
+ if (!override) return base;
3092
+ const merged = deepMerge(withoutLists(base), withoutLists(override));
3093
+ for (const key of LIST_KEYS) {
3094
+ var _base, _override;
3095
+ const ours = (_base = base[key]) !== null && _base !== void 0 ? _base : [];
3096
+ const theirs = (_override = override[key]) !== null && _override !== void 0 ? _override : [];
3097
+ if (ours.length > 0 || theirs.length > 0) merged[key] = [...ours, ...theirs];
3098
+ }
3099
+ return merged;
3100
+ }
3101
+ //#endregion
3102
+ //#region src/core/ProteinStructureViewer/utils/structureFormat.ts
3103
+ /** How much of the file to inspect when guessing the format. */
3104
+ const DETECT_SAMPLE = 16384;
3105
+ /**
3106
+ * Infers whether `text` is PDB or mmCIF (PDBx).
3107
+ *
3108
+ * mmCIF documents start with a `data_` block and name their coordinates
3109
+ * `_atom_site.`. PDB never does either, so either marker is enough. A leading
3110
+ * comment (`# ...`) before `data_` is still mmCIF.
3111
+ */
3112
+ function detectStructureFormat(text) {
3113
+ const sample = text.slice(0, DETECT_SAMPLE);
3114
+ if (/(?:^|\n)\s*data_/.test(sample) || /_atom_site\./.test(sample)) return "mmcif";
3115
+ return "pdb";
3116
+ }
3117
+ //#endregion
3118
+ //#region src/core/ProteinStructureViewer/utils/chainColorTheme.ts
3119
+ /** Mol* color theme name under which per-chain coloring is registered. */
3120
+ const CHAIN_COLOR_THEME_NAME = "chain-color";
3121
+ /** Painted for a chain with no color assigned, or one assigned a bad value. */
3122
+ const DEFAULT_CHAIN_COLOR_HEX = "#808080";
3123
+ const DEFAULT_CHAIN_COLOR = molstar_lib_mol_util_color.Color.fromRgb(128, 128, 128);
3124
+ /**
3125
+ * Builds a Mol* color theme that paints each chain its own color.
3126
+ *
3127
+ * This stands in for Mol*'s built-in `chain-id` theme, which picks its own
3128
+ * colors from a palette we cannot read back. Owning the assignment is what
3129
+ * lets the chain legend show a swatch that matches the structure, rather than
3130
+ * a key for colors it has to guess at.
3131
+ *
3132
+ * Created per plugin instance, and its state read live inside `color()`, for
3133
+ * the same reasons as the residue value theme beside it: several viewers can
3134
+ * color chains differently on one page, and recoloring does not rebuild the
3135
+ * structure.
3136
+ */
3137
+ function createChainColorTheme() {
3138
+ const state = {
3139
+ colors: /* @__PURE__ */ new Map(),
3140
+ version: 0
3141
+ };
3142
+ const provider = {
3143
+ category: "chain property",
3144
+ defaultValues: {},
3145
+ factory() {
3146
+ const { colors } = state;
3147
+ const parsed = /* @__PURE__ */ new Map();
3148
+ for (const [chainId, hex] of colors) parsed.set(chainId, themeColor(hex, DEFAULT_CHAIN_COLOR_HEX));
3149
+ return {
3150
+ color(location) {
3151
+ var _parsed$get;
3152
+ if (!molstar_lib_mol_model_structure.StructureElement.Location.is(location)) return DEFAULT_CHAIN_COLOR;
3153
+ const chainId = molstar_lib_mol_model_structure.StructureProperties.chain.auth_asym_id(location);
3154
+ return (_parsed$get = parsed.get(chainId)) !== null && _parsed$get !== void 0 ? _parsed$get : DEFAULT_CHAIN_COLOR;
3155
+ },
3156
+ contextHash: state.version,
3157
+ description: "Color residues by the chain they sit on",
3158
+ factory: provider.factory,
3159
+ granularity: "group",
3160
+ props: {}
3161
+ };
3162
+ },
3163
+ getParams: () => ({}),
3164
+ isApplicable: () => true,
3165
+ label: "Chain Color",
3166
+ name: CHAIN_COLOR_THEME_NAME
3167
+ };
3168
+ return {
3169
+ provider,
3170
+ setState(colors) {
3171
+ state.colors = colors;
3172
+ state.version++;
3173
+ }
3174
+ };
3175
+ }
3176
+ //#endregion
3177
+ //#region src/core/ProteinStructureViewer/utils/plddt.ts
3178
+ /** Mol* color theme name under which pLDDT coloring is registered. */
3179
+ const PLDDT_THEME_NAME = "plddt-bfactor";
3180
+ /**
3181
+ * AlphaFold's pLDDT confidence bands. Thresholds are on the 0-1 scale that
3182
+ * `plddt` values use, ascending from lowest to highest confidence.
3183
+ */
3184
+ const PLDDT_COLOR_SCALE = {
3185
+ kind: "stepped",
3186
+ stops: [
3187
+ {
3188
+ color: "#FF7C45",
3189
+ label: "0.5",
3190
+ threshold: .5
3191
+ },
3192
+ {
3193
+ color: "#FFDB11",
3194
+ label: "0.7",
3195
+ threshold: .7
3196
+ },
3197
+ {
3198
+ color: "#64CBF3",
3199
+ label: "0.9",
3200
+ threshold: .9
3201
+ },
3202
+ {
3203
+ color: "#0053D5",
3204
+ label: "1.0",
3205
+ threshold: 1
3206
+ }
3207
+ ]
3208
+ };
3209
+ const PLDDT_BANDS = PLDDT_COLOR_SCALE.kind === "stepped" ? PLDDT_COLOR_SCALE.stops : [];
3210
+ /**
3211
+ * The band colors alone, ascending from least to most confident.
3212
+ *
3213
+ * What the chain legend shows in place of a single color while pLDDT is
3214
+ * painting the structure: no one color stands for a chain then, so the swatch
3215
+ * carries the whole key instead.
3216
+ */
3217
+ const PLDDT_BAND_COLORS = PLDDT_BANDS.map((stop) => stop.color);
3218
+ const DEFAULT_PLDDT_COLOR = molstar_lib_mol_util_color.Color.fromRgb(128, 128, 128);
3219
+ /**
3220
+ * Maps a pLDDT confidence score on the 0-100 B-factor scale to its band color.
3221
+ * `PLDDT_COLOR_SCALE` thresholds are on the 0-1 scale, so the score is
3222
+ * normalized before it is sampled.
3223
+ */
3224
+ function plddtToColor(value) {
3225
+ const [r, g, b] = require_colorScales.sampleSteppedScale(PLDDT_BANDS, value / 100);
3226
+ return molstar_lib_mol_util_color.Color.fromRgb(r, g, b);
3227
+ }
3228
+ /**
3229
+ * Rewrites a PDB file's B-factor column with pLDDT scores so Mol* can read them
3230
+ * off the parsed model. B-factors occupy columns 60-66 of ATOM/HETATM lines;
3231
+ * scores arrive on a 0-1 scale and are stored on the conventional 0-100 one.
3232
+ * Residues past the end of `plddtValues` fall back to a mid-confidence 50.
3233
+ *
3234
+ * A residue is identified by chain, sequence number and insertion code. The
3235
+ * code has to be part of that: `10` and `10A` are two residues, Mol* counts
3236
+ * them as two, and treating them as one here would hand every residue after
3237
+ * them its neighbour's score.
3238
+ */
3239
+ function injectPlddtIntoPdb(pdbData, plddtValues) {
3240
+ const lines = pdbData.split("\n");
3241
+ let residueIndex = -1;
3242
+ let lastResidue = "";
3243
+ return lines.map((line) => {
3244
+ var _plddtValues$residueI;
3245
+ if (!line.startsWith("ATOM") && !line.startsWith("HETATM")) return line;
3246
+ const residue = line.substring(21, 27);
3247
+ if (residue !== lastResidue) {
3248
+ residueIndex++;
3249
+ lastResidue = residue;
3250
+ }
3251
+ const bfactorStr = (residueIndex < plddtValues.length ? ((_plddtValues$residueI = plddtValues[residueIndex]) !== null && _plddtValues$residueI !== void 0 ? _plddtValues$residueI : 0) * 100 : 50).toFixed(2).padStart(6, " ");
3252
+ const paddedLine = line.padEnd(66, " ");
3253
+ return paddedLine.substring(0, 60) + bfactorStr + paddedLine.substring(66);
3254
+ }).join("\n");
3255
+ }
3256
+ const ATOM_SITE_PREFIX = "_atom_site.";
3257
+ const B_FACTOR_COLUMN = "B_iso_or_equiv";
3258
+ function skipCifWhitespace(line, index) {
3259
+ let i = index;
3260
+ while (i < line.length && /\s/.test(line.charAt(i))) i += 1;
3261
+ return i;
3262
+ }
3263
+ function readCifToken(line, start) {
3264
+ const quote = line.charAt(start);
3265
+ if (quote === "'" || quote === "\"") {
3266
+ const end = line.indexOf(quote, start + 1);
3267
+ if (end < 0) return {
3268
+ next: line.length,
3269
+ token: line.slice(start + 1)
3270
+ };
3271
+ return {
3272
+ next: end + 1,
3273
+ token: line.slice(start + 1, end)
3274
+ };
3275
+ }
3276
+ let i = start;
3277
+ while (i < line.length && !/\s/.test(line.charAt(i))) i += 1;
3278
+ return {
3279
+ next: i,
3280
+ token: line.slice(start, i)
3281
+ };
3282
+ }
3283
+ /**
3284
+ * Splits one CIF line into tokens, honouring `'quoted'` and `"quoted"` values.
3285
+ * Atom-site rows are one line each in practice; multiline `;text;` fields are
3286
+ * not used there and are not handled.
3287
+ */
3288
+ function tokenizeCifLine(line) {
3289
+ const tokens = [];
3290
+ let i = skipCifWhitespace(line, 0);
3291
+ while (i < line.length) {
3292
+ const { next, token } = readCifToken(line, i);
3293
+ tokens.push(token);
3294
+ i = skipCifWhitespace(line, next);
3295
+ }
3296
+ return tokens;
3297
+ }
3298
+ function isCifNull(value) {
3299
+ return value === void 0 || value === "." || value === "?";
3300
+ }
3301
+ function columnIndex(columns, name) {
3302
+ return columns.indexOf(name);
3303
+ }
3304
+ /**
3305
+ * Residue identity matching `injectPlddtIntoPdb`: chain + sequence number +
3306
+ * insertion code. Prefers the `auth_*` columns Mol* reports on the parsed
3307
+ * model, falling back to `label_*` when a file omitted them. `.` / `?`
3308
+ * insertion codes count as empty, the way a blank PDB column 27 does.
3309
+ */
3310
+ function mmcifResidueKey(tokens, columns) {
3311
+ var _ref, _ref2, _ref3, _ref4;
3312
+ const authAsym = columnIndex(columns, "auth_asym_id");
3313
+ const labelAsym = columnIndex(columns, "label_asym_id");
3314
+ const authSeq = columnIndex(columns, "auth_seq_id");
3315
+ const labelSeq = columnIndex(columns, "label_seq_id");
3316
+ const ins = columnIndex(columns, "pdbx_PDB_ins_code");
3317
+ return `${(_ref = (_ref2 = authAsym >= 0 ? tokens[authAsym] : void 0) !== null && _ref2 !== void 0 ? _ref2 : labelAsym >= 0 ? tokens[labelAsym] : void 0) !== null && _ref !== void 0 ? _ref : ""}|${(_ref3 = (_ref4 = authSeq >= 0 ? tokens[authSeq] : void 0) !== null && _ref4 !== void 0 ? _ref4 : labelSeq >= 0 ? tokens[labelSeq] : void 0) !== null && _ref3 !== void 0 ? _ref3 : ""}|${isCifNull(ins >= 0 ? tokens[ins] : void 0) ? "" : tokens[ins]}`;
3318
+ }
3319
+ function isAtomSiteRow(tokens, columns) {
3320
+ const group = columnIndex(columns, "group_PDB");
3321
+ if (group < 0) return true;
3322
+ const value = tokens[group];
3323
+ return value === "ATOM" || value === "HETATM";
3324
+ }
3325
+ function plddtBFactor(residueIndex, plddtValues) {
3326
+ var _plddtValues$residueI2;
3327
+ return (residueIndex < plddtValues.length ? ((_plddtValues$residueI2 = plddtValues[residueIndex]) !== null && _plddtValues$residueI2 !== void 0 ? _plddtValues$residueI2 : 0) * 100 : 50).toFixed(2);
3328
+ }
3329
+ function isLoopStart(line) {
3330
+ return line.trim() === "loop_" || line.trim().startsWith("loop_");
3331
+ }
3332
+ function isAtomSiteColumn(line) {
3333
+ return line.trim().startsWith(ATOM_SITE_PREFIX);
3334
+ }
3335
+ function endsAtomSiteLoop(line) {
3336
+ const trimmed = line.trim();
3337
+ if (trimmed === "") return false;
3338
+ if (trimmed.startsWith("#")) return true;
3339
+ if (isLoopStart(trimmed)) return true;
3340
+ if (trimmed.startsWith("data_") || trimmed.startsWith("save_")) return true;
3341
+ return trimmed.startsWith("_") && !trimmed.startsWith(ATOM_SITE_PREFIX);
3342
+ }
3343
+ function isAtomSiteLoop(lines, loopIndex) {
3344
+ let peek = loopIndex + 1;
3345
+ while (peek < lines.length && lines[peek].trim() === "") peek += 1;
3346
+ return peek < lines.length && isAtomSiteColumn(lines[peek]);
3347
+ }
3348
+ function ensureBFactorColumn(columns, headerLines) {
3349
+ const existing = columnIndex(columns, B_FACTOR_COLUMN);
3350
+ if (existing >= 0) return existing;
3351
+ columns.push(B_FACTOR_COLUMN);
3352
+ headerLines.push(`${ATOM_SITE_PREFIX}${B_FACTOR_COLUMN}`);
3353
+ return columns.length - 1;
3354
+ }
3355
+ function rewriteAtomSiteRow(dataLine, columns, bIndex, plddtValues, walk) {
3356
+ if (dataLine.trim() === "") return dataLine;
3357
+ const tokens = tokenizeCifLine(dataLine);
3358
+ if (tokens.length === 0) return dataLine;
3359
+ while (tokens.length < columns.length) tokens.push(".");
3360
+ if (isAtomSiteRow(tokens, columns)) {
3361
+ const residue = mmcifResidueKey(tokens, columns);
3362
+ if (residue !== walk.lastResidue) {
3363
+ walk.residueIndex += 1;
3364
+ walk.lastResidue = residue;
3365
+ }
3366
+ tokens[bIndex] = plddtBFactor(walk.residueIndex, plddtValues);
3367
+ }
3368
+ return tokens.join(" ");
3369
+ }
3370
+ function rewriteAtomSiteLoop(lines, loopIndex, plddtValues, walk) {
3371
+ const rows = [lines[loopIndex]];
3372
+ let i = loopIndex + 1;
3373
+ const columns = [];
3374
+ while (i < lines.length && isAtomSiteColumn(lines[i])) {
3375
+ columns.push(lines[i].trim().slice(11));
3376
+ rows.push(lines[i]);
3377
+ i += 1;
3378
+ }
3379
+ const bIndex = ensureBFactorColumn(columns, rows);
3380
+ while (i < lines.length && !endsAtomSiteLoop(lines[i])) {
3381
+ rows.push(rewriteAtomSiteRow(lines[i], columns, bIndex, plddtValues, walk));
3382
+ i += 1;
3383
+ }
3384
+ return {
3385
+ next: i,
3386
+ rows
3387
+ };
3388
+ }
3389
+ /**
3390
+ * Rewrites an mmCIF (PDBx) file's `_atom_site.B_iso_or_equiv` column with
3391
+ * pLDDT scores so Mol* can read them off the parsed model, the same way
3392
+ * `injectPlddtIntoPdb` rewrites the PDB B-factor column. Scores arrive on a
3393
+ * 0-1 scale and are stored on the conventional 0-100 one. Residues past the
3394
+ * end of `plddtValues` fall back to a mid-confidence 50.
3395
+ *
3396
+ * If the file has no B-factor column, one is appended to the atom-site loop
3397
+ * rather than leaving the scores behind.
3398
+ */
3399
+ function injectPlddtIntoMmcif(mmcifData, plddtValues) {
3400
+ const lines = mmcifData.split("\n");
3401
+ const result = [];
3402
+ const walk = {
3403
+ lastResidue: "",
3404
+ residueIndex: -1
3405
+ };
3406
+ let i = 0;
3407
+ let rewrittenLoop = false;
3408
+ while (i < lines.length) {
3409
+ const line = lines[i];
3410
+ if (!rewrittenLoop && isLoopStart(line) && isAtomSiteLoop(lines, i)) {
3411
+ const rewritten = rewriteAtomSiteLoop(lines, i, plddtValues, walk);
3412
+ result.push(...rewritten.rows);
3413
+ i = rewritten.next;
3414
+ rewrittenLoop = true;
3415
+ continue;
3416
+ }
3417
+ result.push(line);
3418
+ i += 1;
3419
+ }
3420
+ return result.join("\n");
3421
+ }
3422
+ /**
3423
+ * Rewrites a structure file's B-factors with pLDDT scores. Dispatches to
3424
+ * `injectPlddtIntoPdb` or `injectPlddtIntoMmcif` from `format`, or from
3425
+ * detecting the format of `data` when `format` is omitted.
3426
+ */
3427
+ function injectPlddt(data, plddtValues, format = detectStructureFormat(data)) {
3428
+ return format === "mmcif" ? injectPlddtIntoMmcif(data, plddtValues) : injectPlddtIntoPdb(data, plddtValues);
3429
+ }
3430
+ /**
3431
+ * Mol* color theme that reads pLDDT scores out of the B-factor column, paired
3432
+ * with `injectPlddt`. Registered per plugin instance.
3433
+ */
3434
+ const PlddtColoring = molstar_lib_mol_model_props_common_custom_element_property.CustomElementProperty.create({
3435
+ coloring: {
3436
+ defaultColor: DEFAULT_PLDDT_COLOR,
3437
+ getColor(e) {
3438
+ return plddtToColor(e);
3439
+ }
3440
+ },
3441
+ getData(model) {
3442
+ const map = /* @__PURE__ */ new Map();
3443
+ const atomCount = model.atomicHierarchy.atoms._rowCount;
3444
+ for (let i = 0; i < atomCount; i++) map.set(i, model.atomicConformation.B_iso_or_equiv.value(i));
3445
+ return { value: map };
3446
+ },
3447
+ label: "pLDDT",
3448
+ name: PLDDT_THEME_NAME
3449
+ });
3450
+ //#endregion
3451
+ //#region src/core/ProteinStructureViewer/utils/residueValueTheme.ts
3452
+ /** Mol* color theme name under which residue value overlays are registered. */
3453
+ const RESIDUE_VALUE_THEME_NAME = "residue-value";
3454
+ /**
3455
+ * Builds a Mol* color theme that paints residues by an arbitrary per-residue
3456
+ * value.
3457
+ *
3458
+ * The theme is created per plugin instance rather than shared at module scope,
3459
+ * so several viewers can render different overlays on the same page. Its state
3460
+ * lives in the closure below and is read live inside `color()`, which is what
3461
+ * lets an overlay update recolor the structure without rebuilding it.
3462
+ */
3463
+ function createResidueValueTheme(mode) {
3464
+ const state = {
3465
+ colorScale: require_colorScales.PLASMA_COLOR_SCALE,
3466
+ max: 1,
3467
+ min: 0,
3468
+ mode,
3469
+ values: /* @__PURE__ */ new Map(),
3470
+ version: 0
3471
+ };
3472
+ const provider = {
3473
+ category: "residue property",
3474
+ defaultValues: {},
3475
+ factory() {
3476
+ const { colorScale, max, min, mode: currentMode, values } = state;
3477
+ const neutral = neutralResidueColor(currentMode);
3478
+ return {
3479
+ color(location) {
3480
+ if (!molstar_lib_mol_model_structure.StructureElement.Location.is(location)) return neutral;
3481
+ const value = values.get(molstar_lib_mol_model_structure.StructureProperties.residue.key(location));
3482
+ if (value === void 0) return neutral;
3483
+ const rgb = require_colorScales.sampleColorScale(colorScale, value, max, min);
3484
+ return rgb ? molstar_lib_mol_util_color.Color.fromRgb(rgb[0], rgb[1], rgb[2]) : neutral;
3485
+ },
3486
+ contextHash: state.version,
3487
+ description: "Color residues by a per-residue value",
3488
+ factory: provider.factory,
3489
+ granularity: "group",
3490
+ props: {}
3491
+ };
3492
+ },
3493
+ getParams: () => ({}),
3494
+ isApplicable: () => true,
3495
+ label: "Residue Value",
3496
+ name: RESIDUE_VALUE_THEME_NAME
3497
+ };
3498
+ return {
3499
+ provider,
3500
+ setState(next) {
3501
+ Object.assign(state, next);
3502
+ state.version++;
3503
+ }
3504
+ };
3505
+ }
3506
+ //#endregion
3507
+ //#region src/core/ProteinStructureViewer/hooks/useMolstarPlugin.ts
3508
+ /**
3509
+ * Theme used when neither pLDDT scores nor an overlay are supplied. Our own
3510
+ * per-chain theme rather than Mol*'s built-in `chain-id`, so the chain legend
3511
+ * can show swatches that match the structure instead of guessing at the colors
3512
+ * Mol* picked.
3513
+ */
3514
+ const FALLBACK_THEME_NAME = CHAIN_COLOR_THEME_NAME;
3515
+ /** Delay before retrying initialization while the container has no size. */
3516
+ const LAYOUT_RETRY_MS = 100;
3517
+ /** How long to wait for a frame before starting without one. */
3518
+ const FRAME_WAIT_MS = 100;
3519
+ /**
3520
+ * Yield once so the container can be laid out before it is measured.
3521
+ *
3522
+ * `requestAnimationFrame` does not fire while the document is hidden -- a
3523
+ * background tab, a collapsed pane, or a headless capture that never paints --
3524
+ * and awaiting it alone leaves the viewer parked forever on a page that has in
3525
+ * fact been laid out. Racing it against a timeout keeps the fast path on a
3526
+ * visible page and still starts on a hidden one, where the size check below is
3527
+ * what actually guards against measuring too early.
3528
+ *
3529
+ * The loser of the race is cancelled rather than left to fire: on a visible
3530
+ * page this runs once per viewer, and a grid of them would otherwise each keep
3531
+ * a stray timer alive past the frame that already resolved.
3532
+ */
3533
+ function nextFrame() {
3534
+ return new Promise((resolve) => {
3535
+ const done = () => {
3536
+ cancelAnimationFrame(frame);
3537
+ clearTimeout(timer);
3538
+ resolve();
3539
+ };
3540
+ const frame = requestAnimationFrame(done);
3541
+ const timer = setTimeout(done, FRAME_WAIT_MS);
3542
+ });
3543
+ }
3544
+ /**
3545
+ * Hover fills the geometry with a tint; selection stays outline-only. Mol*'s
3546
+ * `colorMarker` is a global toggle for the per-fragment marker tint, so it is
3547
+ * scoped to hover via the per-state strengths below.
3548
+ */
3549
+ const HIGHLIGHT_STRENGTH = .2;
3550
+ const SELECT_STRENGTH = 0;
3551
+ /**
3552
+ * Turns the screenshot controls off, on the versions of Mol* that have them.
3553
+ *
3554
+ * `ShowScreenshotControls` arrived in Mol* 5 and this package's peer range
3555
+ * reaches back to 4, so the key is read through an index rather than named
3556
+ * directly: naming it would not compile against 4, and passing the resulting
3557
+ * `undefined` would ask Mol* to set a config item it has no name for. On 4
3558
+ * there are no screenshot controls to hide in the first place.
3559
+ */
3560
+ const SCREENSHOT_CONTROLS_CONFIG = (() => {
3561
+ const item = molstar_lib_mol_plugin_config.PluginConfig.Viewport.ShowScreenshotControls;
3562
+ return item ? [[item, false]] : [];
3563
+ })();
3564
+ function setAxes(plugin, enabled) {
3565
+ if (!plugin.canvas3d) return;
3566
+ try {
3567
+ plugin.canvas3d.setProps({ camera: { helper: { axes: enabled ? AXES_ON : AXES_OFF } } });
3568
+ } catch (e) {
3569
+ console.warn("Could not set axes:", e);
3570
+ }
3571
+ }
3572
+ function createViewer(_x) {
3573
+ return _createViewer.apply(this, arguments);
3574
+ }
3575
+ function _createViewer() {
3576
+ _createViewer = _asyncToGenerator(function* ({ backgroundColor, chainColorTheme, edgeColor, highlightColor, molstarSpec, residueValueTheme, root, showAxes, showSequenceViewer, viewSettings }) {
3577
+ var _spec$config;
3578
+ const spec = (0, molstar_lib_mol_plugin_ui_spec.DefaultPluginUISpec)();
3579
+ const behaviors = spec.behaviors.filter((b) => b.transformer !== molstar_lib_mol_plugin_behavior.PluginBehaviors.Camera.FocusLoci);
3580
+ const plugin = yield (0, molstar_lib_mol_plugin_ui.createPluginUI)({
3581
+ render: molstar_lib_mol_plugin_ui_react18.renderReact18,
3582
+ spec: mergeMolstarSpec(require_objectSpread2._objectSpread2(require_objectSpread2._objectSpread2({}, spec), {}, {
3583
+ behaviors,
3584
+ canvas3d: {
3585
+ camera: { helper: { axes: showAxes ? AXES_ON : AXES_OFF } },
3586
+ marking: {
3587
+ highlightEdgeColor: edgeColor,
3588
+ selectEdgeColor: edgeColor
3589
+ },
3590
+ renderer: {
3591
+ backgroundColor,
3592
+ colorMarker: true,
3593
+ highlightColor,
3594
+ highlightStrength: HIGHLIGHT_STRENGTH,
3595
+ selectStrength: SELECT_STRENGTH
3596
+ }
3597
+ },
3598
+ components: {
3599
+ controls: {
3600
+ bottom: "none",
3601
+ left: "none",
3602
+ right: "none"
3603
+ },
3604
+ remoteState: "none",
3605
+ sequenceViewer: { view: createSequenceView(viewSettings) },
3606
+ viewport: { view: createViewportView(viewSettings) }
3607
+ },
3608
+ config: [
3609
+ ...(_spec$config = spec.config) !== null && _spec$config !== void 0 ? _spec$config : [],
3610
+ [molstar_lib_mol_plugin_config.PluginConfig.Viewport.ShowExpand, false],
3611
+ [molstar_lib_mol_plugin_config.PluginConfig.Viewport.ShowControls, false],
3612
+ [molstar_lib_mol_plugin_config.PluginConfig.Viewport.ShowSettings, false],
3613
+ [molstar_lib_mol_plugin_config.PluginConfig.Viewport.ShowSelectionMode, false],
3614
+ [molstar_lib_mol_plugin_config.PluginConfig.Viewport.ShowAnimation, false],
3615
+ [molstar_lib_mol_plugin_config.PluginConfig.Viewport.ShowTrajectoryControls, false],
3616
+ ...SCREENSHOT_CONTROLS_CONFIG
3617
+ ],
3618
+ layout: { initial: {
3619
+ controlsDisplay: "reactive",
3620
+ isExpanded: false,
3621
+ regionState: {
3622
+ bottom: "hidden",
3623
+ left: "hidden",
3624
+ right: "hidden",
3625
+ top: showSequenceViewer ? "full" : "hidden"
3626
+ }
3627
+ } }
3628
+ }), molstarSpec),
3629
+ target: root
3630
+ });
3631
+ const registry = plugin.representation.structure.themes.colorThemeRegistry;
3632
+ if (PlddtColoring.colorThemeProvider) registry.add(PlddtColoring.colorThemeProvider);
3633
+ registry.add(residueValueTheme.provider);
3634
+ registry.add(chainColorTheme.provider);
3635
+ for (const p of [...plugin.managers.lociLabels.providers]) plugin.managers.lociLabels.removeProvider(p);
3636
+ plugin.managers.structure.focus.behaviors.current.subscribe((entry) => {
3637
+ const select = plugin.managers.interactivity.lociSelects;
3638
+ if (entry === null || entry === void 0 ? void 0 : entry.loci) select.selectOnly({ loci: entry.loci });
3639
+ else select.deselectAll();
3640
+ });
3641
+ return plugin;
3642
+ });
3643
+ return _createViewer.apply(this, arguments);
3644
+ }
3645
+ /**
3646
+ * The representation parameters Mol*'s own presets compute, read from the same
3647
+ * manager state they read. Ported rather than hardcoded so a chain drawn here
3648
+ * looks exactly like the one the preset used to draw. Mol* hands the same set
3649
+ * to its cartoons and its ball-and-sticks, and so does this.
3650
+ */
3651
+ function representationTypeParams(plugin) {
3652
+ const { hydrogens, ignoreLight, visualQuality } = plugin.managers.structure.component.state.options;
3653
+ return {
3654
+ ignoreHydrogens: hydrogens !== "all",
3655
+ ignoreHydrogensVariant: hydrogens === "only-polar" ? "non-polar" : "all",
3656
+ ignoreLight,
3657
+ quality: visualQuality
3658
+ };
3659
+ }
3660
+ /**
3661
+ * The two halves of a chain the viewer draws. Each names the component its
3662
+ * half is built under and tags the representation drawn over it, and it is
3663
+ * that tag `applyColorTheme` reads back to tell the two apart.
3664
+ */
3665
+ const POLYMER_PART = "polymer";
3666
+ const LIGAND_PART = "ligand";
3667
+ /**
3668
+ * True for the representation drawing a chain's heteroatoms.
3669
+ *
3670
+ * Read off the tag put on the representation when it was built, rather than
3671
+ * off the component's key. Mol* does not store the key as it was given: it
3672
+ * files components under `structure-component-<key>`, so a test against the
3673
+ * key as written here silently matches nothing, and every ligand quietly takes
3674
+ * the structure-wide theme instead. The tag survives untouched.
3675
+ */
3676
+ function isLigandRepresentation(representation) {
3677
+ var _representation$cell$;
3678
+ return ((_representation$cell$ = representation.cell.transform.tags) === null || _representation$cell$ === void 0 ? void 0 : _representation$cell$.includes(LIGAND_PART)) === true;
3679
+ }
3680
+ /**
3681
+ * How heteroatoms are colored: the theme Mol* puts on a ball-and-stick when
3682
+ * left to itself, with the parameters it chooses too.
3683
+ *
3684
+ * Named rather than left blank only so that it survives `applyColorTheme`,
3685
+ * which would otherwise repaint the ligands along with everything else. The
3686
+ * structure-wide theme is the wrong answer for them twice over: a heme reads
3687
+ * as a heme because its iron is orange and its nitrogens are blue, which one
3688
+ * flat color throws away, and pLDDT and residue overlays have no value for a
3689
+ * HETATM to be colored by at all.
3690
+ */
3691
+ const ELEMENT_THEME = "element-symbol";
3692
+ /**
3693
+ * Draws one chain: its polymer as a cartoon, its ligands and ions as
3694
+ * ball-and-stick.
3695
+ *
3696
+ * Two components rather than one, because a component is what a representation
3697
+ * attaches to and neither representation suits both halves - a cartoon has no
3698
+ * backbone to trace through a heme, and a ball-and-stick over the whole chain
3699
+ * would draw every protein atom as a sphere.
3700
+ *
3701
+ * Hands back the refs of whichever components the chain turned out to have,
3702
+ * which is what `setSubtreeVisibility` is later pointed at. Grouping them
3703
+ * under the chain is what makes hiding a chain take its ligands with it.
3704
+ */
3705
+ function buildChainComponents(_x5, _x6, _x7, _x8) {
3706
+ return _buildChainComponents.apply(this, arguments);
3707
+ }
3708
+ function _buildChainComponents() {
3709
+ _buildChainComponents = _asyncToGenerator(function* (plugin, structure, chain, typeParams) {
3710
+ const builders = plugin.builders.structure;
3711
+ const refs = [];
3712
+ const addPart = function() {
3713
+ var _ref = _asyncToGenerator(function* (part, expression, props) {
3714
+ const component = yield builders.tryCreateComponentFromExpression(structure, expression, `${part}-${chain.chainId}`, { label: chain.label });
3715
+ if (!component) return;
3716
+ yield builders.representation.addRepresentation(component, props, { tag: part });
3717
+ refs.push(component.ref);
3718
+ });
3719
+ return function addPart(_x2, _x3, _x4) {
3720
+ return _ref.apply(this, arguments);
3721
+ };
3722
+ }();
3723
+ yield addPart(POLYMER_PART, chainPolymerExpression(chain.chainId), {
3724
+ type: "cartoon",
3725
+ typeParams
3726
+ });
3727
+ yield addPart(LIGAND_PART, chainLigandExpression(chain.chainId), {
3728
+ color: ELEMENT_THEME,
3729
+ type: "ball-and-stick",
3730
+ typeParams
3731
+ });
3732
+ return refs;
3733
+ });
3734
+ return _buildChainComponents.apply(this, arguments);
3735
+ }
3736
+ const NOTHING_LOADED = {
3737
+ chains: [],
3738
+ componentRefs: /* @__PURE__ */ new Map(),
3739
+ residuesByChain: /* @__PURE__ */ new Map()
3740
+ };
3741
+ /**
3742
+ * Parses the structure and draws each chain: a cartoon over its polymer, and
3743
+ * ball-and-stick over its ligands and ions.
3744
+ *
3745
+ * Mol*'s `default` hierarchy preset would be shorter, but it groups every
3746
+ * polymer chain into a single component, and a component is the unit Mol* can
3747
+ * hide. Building them per chain is what makes a chain individually hideable;
3748
+ * the refs handed back are what `setSubtreeVisibility` is later pointed at.
3749
+ *
3750
+ * The wrapper detects PDB vs mmCIF from the text; this just forwards that
3751
+ * guess to Mol*.
3752
+ */
3753
+ function loadStructure(_x9, _x10, _x11, _x12) {
3754
+ return _loadStructure.apply(this, arguments);
3755
+ }
3756
+ function _loadStructure() {
3757
+ _loadStructure = _asyncToGenerator(function* (plugin, structureText, usePlddtColoring, showAxes) {
3758
+ try {
3759
+ var _plugin$canvas3d;
3760
+ yield plugin.clear();
3761
+ const data = yield plugin.builders.data.rawData({
3762
+ data: structureText,
3763
+ label: "Structure"
3764
+ });
3765
+ const trajectory = yield plugin.builders.structure.parseTrajectory(data, detectStructureFormat(structureText));
3766
+ const model = yield plugin.builders.structure.createModel(trajectory);
3767
+ const structure = yield plugin.builders.structure.createStructure(model, {
3768
+ name: "model",
3769
+ params: {}
3770
+ });
3771
+ const data3d = structure.data;
3772
+ if (!data3d) return NOTHING_LOADED;
3773
+ const { chainLabels, chains, residuesByChain } = scanChains(data3d);
3774
+ const typeParams = representationTypeParams(plugin);
3775
+ const componentRefs = /* @__PURE__ */ new Map();
3776
+ for (const [chainId, label] of chainLabels) {
3777
+ const refs = yield buildChainComponents(plugin, structure, {
3778
+ chainId,
3779
+ label
3780
+ }, typeParams);
3781
+ if (refs.length > 0) componentRefs.set(chainId, refs);
3782
+ }
3783
+ if (usePlddtColoring) yield applyColorTheme(plugin, PLDDT_THEME_NAME);
3784
+ (_plugin$canvas3d = plugin.canvas3d) === null || _plugin$canvas3d === void 0 || _plugin$canvas3d.requestCameraReset();
3785
+ setAxes(plugin, showAxes);
3786
+ return {
3787
+ chains,
3788
+ componentRefs,
3789
+ residuesByChain
3790
+ };
3791
+ } catch (error) {
3792
+ console.error("Failed to load structure:", error);
3793
+ return NOTHING_LOADED;
3794
+ }
3795
+ });
3796
+ return _loadStructure.apply(this, arguments);
3797
+ }
3798
+ /**
3799
+ * Shows or hides everything each chain is drawn with, cartoon and sticks
3800
+ * alike. Every component under the chain, so hiding it takes its ligands with
3801
+ * it rather than leaving a heme floating where its protein used to be.
3802
+ *
3803
+ * The whole subtree of each, not just the component cell: visibility is
3804
+ * per-cell, and it is the representation underneath that actually draws.
3805
+ *
3806
+ * Set outright rather than through the component manager's `toggleVisibility`,
3807
+ * which flips whatever it finds: the hidden set is derived from props on every
3808
+ * pass, so it has to be applied as a statement of what should be true and not
3809
+ * as a change to what already is.
3810
+ */
3811
+ function applyChainVisibility(plugin, componentRefs, hidden) {
3812
+ 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));
3813
+ }
3814
+ /**
3815
+ * Recolors every loaded structure's representations with the named theme,
3816
+ * leaving the heteroatoms on `ELEMENT_THEME`.
3817
+ *
3818
+ * The per-representation form of `updateRepresentationsTheme`, so the two can
3819
+ * be told apart in one pass; handing it a filtered list instead would leave
3820
+ * the ligands on whichever theme happened to be current when they were built.
3821
+ */
3822
+ function applyColorTheme(_x13, _x14) {
3823
+ return _applyColorTheme.apply(this, arguments);
3824
+ }
3825
+ function _applyColorTheme() {
3826
+ _applyColorTheme = _asyncToGenerator(function* (plugin, colorTheme) {
3827
+ yield plugin.dataTransaction(_asyncToGenerator(function* () {
3828
+ 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 }));
3829
+ }));
3830
+ });
3831
+ return _applyColorTheme.apply(this, arguments);
3832
+ }
3833
+ /**
3834
+ * Owns the Mol* plugin for one viewer: creates it, loads the structure, wires
3835
+ * interaction callbacks, and disposes of it on unmount.
3836
+ *
3837
+ * The plugin is built once and then mutated in place. Creating it is expensive
3838
+ * and destroys the camera, so prop changes that Mol* can absorb (background,
3839
+ * axes, coloring, new structure) are pushed in through the effects below and in
3840
+ * the sibling hooks rather than by rebuilding.
3841
+ */
3842
+ function useMolstarPlugin({ backgroundColor, chainColors, containerRef, download, edgeColor, hasPlddt, hiddenChains, highlightColor, mode, molstarSpec, onChainHover, onChainSelect, onChainToggle, onResidueClick, onResidueHover, onSelectionChange, onSelectionClear, selectedChains, sequenceViewerBackgroundColor, showAxes, showSequenceViewer, structure }) {
3843
+ const pluginRef = (0, react.useRef)(null);
3844
+ const residueValueThemeRef = (0, react.useRef)(null);
3845
+ const chainColorThemeRef = (0, react.useRef)(null);
3846
+ const currentStructureRef = (0, react.useRef)(null);
3847
+ const [isReady, setIsReady] = (0, react.useState)(false);
3848
+ const [chains, setChains] = (0, react.useState)([]);
3849
+ const [loadCount, setLoadCount] = (0, react.useState)(0);
3850
+ /**
3851
+ * Where each chain's components live in the state tree, which is what the
3852
+ * visibility effect points `setSubtreeVisibility` at. Rewritten by every
3853
+ * load, since the refs do not survive `plugin.clear()`.
3854
+ */
3855
+ const componentRefsRef = (0, react.useRef)(/* @__PURE__ */ new Map());
3856
+ /**
3857
+ * Which residues sit on each chain, for the readout to average a score over
3858
+ * a whole-chain selection. A ref rather than state: nothing renders from it
3859
+ * directly, and it is rewritten in step with `chains`, which does.
3860
+ */
3861
+ const residuesByChainRef = (0, react.useRef)(/* @__PURE__ */ new Map());
3862
+ /**
3863
+ * The visibility and coloring to apply as soon as a structure exists. Read
3864
+ * through refs because both arrive as fresh objects on every render, and a
3865
+ * load has to be able to consult them without the load effect depending on
3866
+ * them - a new Set each render would reload the structure on every pass.
3867
+ */
3868
+ const hiddenChainsRef = (0, react.useRef)(hiddenChains);
3869
+ hiddenChainsRef.current = hiddenChains;
3870
+ const chainColorsRef = (0, react.useRef)(chainColors);
3871
+ chainColorsRef.current = chainColors;
3872
+ /**
3873
+ * Carries the theme-dependent props into the views Mol* renders in its own
3874
+ * React root. They cannot read them from context, and rebuilding the plugin
3875
+ * would throw away the camera, so the values are pushed to them instead.
3876
+ */
3877
+ const viewSettingsRef = (0, react.useRef)(null);
3878
+ if (viewSettingsRef.current === null) viewSettingsRef.current = new rxjs.BehaviorSubject({
3879
+ mode,
3880
+ sequenceViewerBackgroundColor,
3881
+ showAxes
3882
+ });
3883
+ const viewSettings = viewSettingsRef.current;
3884
+ /**
3885
+ * Clip-radius-per-camera-distance ratio recorded at the last residue focus,
3886
+ * or null when no residue is focused. Anchors the adaptive depth clipping.
3887
+ */
3888
+ const clipRatioRef = (0, react.useRef)(null);
3889
+ const setClipRatio = (0, react.useCallback)((ratio) => {
3890
+ clipRatioRef.current = ratio;
3891
+ }, []);
3892
+ /**
3893
+ * Takes up what a load produced: the component refs the chain-keyed props
3894
+ * address, the visibility they were already asking for, and the chain list
3895
+ * the consumer is told about.
3896
+ *
3897
+ * Chains are published only when they actually differ, so reloading the same
3898
+ * structure - a theme switch that happens to reload, a parent handing back an
3899
+ * equal PDB string - does not hand every consumer a new array to react to.
3900
+ */
3901
+ const adoptLoadedStructure = (0, react.useCallback)((plugin, loaded) => {
3902
+ componentRefsRef.current = loaded.componentRefs;
3903
+ residuesByChainRef.current = loaded.residuesByChain;
3904
+ applyChainVisibility(plugin, loaded.componentRefs, hiddenChainsRef.current);
3905
+ setChains((prev) => chainsEqual(prev, loaded.chains) ? prev : loaded.chains);
3906
+ setLoadCount((count) => count + 1);
3907
+ }, []);
3908
+ const onResidueClickRef = (0, react.useRef)(onResidueClick);
3909
+ onResidueClickRef.current = onResidueClick;
3910
+ const onSelectionClearRef = (0, react.useRef)(onSelectionClear);
3911
+ onSelectionClearRef.current = onSelectionClear;
3912
+ const onSelectionChangeRef = (0, react.useRef)(onSelectionChange);
3913
+ onSelectionChangeRef.current = onSelectionChange;
3914
+ const onResidueHoverRef = (0, react.useRef)(onResidueHover);
3915
+ onResidueHoverRef.current = onResidueHover;
3916
+ /** Last residue reported to `onResidueHover`, to suppress repeats. */
3917
+ const lastHoverRef = (0, react.useRef)(null);
3918
+ const initialPropsRef = (0, react.useRef)({
3919
+ backgroundColor,
3920
+ edgeColor,
3921
+ hasPlddt,
3922
+ highlightColor,
3923
+ mode,
3924
+ molstarSpec,
3925
+ showAxes,
3926
+ showSequenceViewer,
3927
+ structure
3928
+ });
3929
+ initialPropsRef.current = {
3930
+ backgroundColor,
3931
+ edgeColor,
3932
+ hasPlddt,
3933
+ highlightColor,
3934
+ mode,
3935
+ molstarSpec,
3936
+ showAxes,
3937
+ showSequenceViewer,
3938
+ structure
3939
+ };
3940
+ /**
3941
+ * The consumer's `canvas3d` overrides, keyed by content. Unlike the rest of
3942
+ * the spec these are re-applied when they change, and they commonly arrive
3943
+ * as an object written inline - so the effect below turns on what they say
3944
+ * rather than on the identity of the object saying it.
3945
+ */
3946
+ const canvas3dOverrideKey = (() => {
3947
+ try {
3948
+ var _molstarSpec$canvas3d;
3949
+ return JSON.stringify((_molstarSpec$canvas3d = molstarSpec === null || molstarSpec === void 0 ? void 0 : molstarSpec.canvas3d) !== null && _molstarSpec$canvas3d !== void 0 ? _molstarSpec$canvas3d : null);
3950
+ } catch (_unused) {
3951
+ return String(Date.now());
3952
+ }
3953
+ })();
3954
+ (0, react.useEffect)(() => {
3955
+ const container = containerRef.current;
3956
+ if (!container) return;
3957
+ let cancelled = false;
3958
+ let clipSubscription;
3959
+ const init = function() {
3960
+ var _ref2 = _asyncToGenerator(function* () {
3961
+ yield nextFrame();
3962
+ if (cancelled) return;
3963
+ const rect = container.getBoundingClientRect();
3964
+ if (rect.width === 0 || rect.height === 0) {
3965
+ setTimeout(init, LAYOUT_RETRY_MS);
3966
+ return;
3967
+ }
3968
+ const initial = initialPropsRef.current;
3969
+ try {
3970
+ var _plugin$canvas3d2;
3971
+ const residueValueTheme = createResidueValueTheme(initial.mode);
3972
+ const chainColorTheme = createChainColorTheme();
3973
+ const plugin = yield createViewer({
3974
+ backgroundColor: initial.backgroundColor,
3975
+ chainColorTheme,
3976
+ edgeColor: initial.edgeColor,
3977
+ highlightColor: initial.highlightColor,
3978
+ molstarSpec: initial.molstarSpec,
3979
+ residueValueTheme,
3980
+ root: container,
3981
+ showAxes: initial.showAxes,
3982
+ showSequenceViewer: initial.showSequenceViewer,
3983
+ viewSettings
3984
+ });
3985
+ if (cancelled) {
3986
+ plugin.dispose();
3987
+ return;
3988
+ }
3989
+ pluginRef.current = plugin;
3990
+ residueValueThemeRef.current = residueValueTheme;
3991
+ chainColorThemeRef.current = chainColorTheme;
3992
+ const latest = initialPropsRef.current;
3993
+ const loaded = yield loadStructure(plugin, latest.structure, latest.hasPlddt, latest.showAxes);
3994
+ currentStructureRef.current = latest.structure;
3995
+ adoptLoadedStructure(plugin, loaded);
3996
+ /**
3997
+ * Mol*'s click behavior is a BehaviorSubject, so subscribing replays
3998
+ * whatever it is currently holding - which, before anything has been
3999
+ * clicked, is an empty click. Acting on that would read as the user
4000
+ * clicking empty space and clear a selection the consumer had set
4001
+ * before the viewer was even interactive.
4002
+ *
4003
+ * Only the replay is skipped, not every empty click: clicking away
4004
+ * from the structure is how a selection is meant to be cleared.
4005
+ */
4006
+ let clickReplayConsumed = false;
4007
+ plugin.behaviors.interaction.click.subscribe((e) => {
4008
+ var _onResidueClickRef$cu, _onSelectionChangeRef;
4009
+ if (!clickReplayConsumed) {
4010
+ clickReplayConsumed = true;
4011
+ if (molstar_lib_mol_repr_representation.Representation.Loci.isEmpty(e.current)) return;
4012
+ }
4013
+ if (molstar_lib_mol_repr_representation.Representation.Loci.isEmpty(e.current)) {
4014
+ var _onSelectionClearRef$;
4015
+ (_onSelectionClearRef$ = onSelectionClearRef.current) === null || _onSelectionClearRef$ === void 0 || _onSelectionClearRef$.call(onSelectionClearRef);
4016
+ return;
4017
+ }
4018
+ const loci = e.current.loci;
4019
+ if (!molstar_lib_mol_model_structure.StructureElement.Loci.is(loci)) return;
4020
+ const residue = residueRefFromLoci(loci);
4021
+ if (!residue) return;
4022
+ (_onResidueClickRef$cu = onResidueClickRef.current) === null || _onResidueClickRef$cu === void 0 || _onResidueClickRef$cu.call(onResidueClickRef, residue);
4023
+ (_onSelectionChangeRef = onSelectionChangeRef.current) === null || _onSelectionChangeRef === void 0 || _onSelectionChangeRef.call(onSelectionChangeRef, selectionFromLoci(loci));
4024
+ });
4025
+ plugin.behaviors.interaction.hover.subscribe((e) => {
4026
+ var _lastHoverRef$current, _onResidueHoverRef$cu;
4027
+ const loci = e.current.loci;
4028
+ const residue = molstar_lib_mol_model_structure.StructureElement.Loci.is(loci) ? residueRefFromLoci(loci) : null;
4029
+ if ((residue === null || residue === void 0 ? void 0 : residue.index) === ((_lastHoverRef$current = lastHoverRef.current) === null || _lastHoverRef$current === void 0 ? void 0 : _lastHoverRef$current.index)) return;
4030
+ lastHoverRef.current = residue;
4031
+ (_onResidueHoverRef$cu = onResidueHoverRef.current) === null || _onResidueHoverRef$cu === void 0 || _onResidueHoverRef$cu.call(onResidueHoverRef, residue);
4032
+ });
4033
+ clipSubscription = (_plugin$canvas3d2 = plugin.canvas3d) === null || _plugin$canvas3d2 === void 0 ? void 0 : _plugin$canvas3d2.didDraw.subscribe(() => {
4034
+ syncClipToZoom(plugin, () => clipRatioRef.current);
4035
+ });
4036
+ if (!cancelled) setIsReady(true);
4037
+ } catch (error) {
4038
+ console.error("Failed to initialize Mol* viewer:", error);
4039
+ }
4040
+ });
4041
+ return function init() {
4042
+ return _ref2.apply(this, arguments);
4043
+ };
4044
+ }();
4045
+ init();
4046
+ return () => {
4047
+ var _pluginRef$current;
4048
+ cancelled = true;
4049
+ clipSubscription === null || clipSubscription === void 0 || clipSubscription.unsubscribe();
4050
+ (_pluginRef$current = pluginRef.current) === null || _pluginRef$current === void 0 || _pluginRef$current.dispose();
4051
+ pluginRef.current = null;
4052
+ residueValueThemeRef.current = null;
4053
+ chainColorThemeRef.current = null;
4054
+ componentRefsRef.current = /* @__PURE__ */ new Map();
4055
+ residuesByChainRef.current = /* @__PURE__ */ new Map();
4056
+ currentStructureRef.current = null;
4057
+ clipRatioRef.current = null;
4058
+ setIsReady(false);
4059
+ };
4060
+ }, []);
4061
+ (0, react.useEffect)(() => {
4062
+ var _pluginRef$current2, _initialPropsRef$curr;
4063
+ const canvas3d = (_pluginRef$current2 = pluginRef.current) === null || _pluginRef$current2 === void 0 ? void 0 : _pluginRef$current2.canvas3d;
4064
+ if (!canvas3d || !isReady) return;
4065
+ canvas3d.setProps({
4066
+ marking: {
4067
+ highlightEdgeColor: edgeColor,
4068
+ selectEdgeColor: edgeColor
4069
+ },
4070
+ renderer: {
4071
+ backgroundColor,
4072
+ highlightColor
4073
+ }
4074
+ });
4075
+ const override = (_initialPropsRef$curr = initialPropsRef.current.molstarSpec) === null || _initialPropsRef$curr === void 0 ? void 0 : _initialPropsRef$curr.canvas3d;
4076
+ if (override) canvas3d.setProps(override);
4077
+ }, [
4078
+ backgroundColor,
4079
+ canvas3dOverrideKey,
4080
+ edgeColor,
4081
+ highlightColor,
4082
+ isReady
4083
+ ]);
4084
+ (0, react.useEffect)(() => {
4085
+ viewSettings.next({
4086
+ download,
4087
+ hiddenChains,
4088
+ mode,
4089
+ onChainHover,
4090
+ onChainSelect,
4091
+ onChainToggle,
4092
+ selectedChains,
4093
+ sequenceViewerBackgroundColor,
4094
+ showAxes
4095
+ });
4096
+ }, [
4097
+ download,
4098
+ hiddenChains,
4099
+ mode,
4100
+ onChainHover,
4101
+ onChainSelect,
4102
+ onChainToggle,
4103
+ selectedChains,
4104
+ sequenceViewerBackgroundColor,
4105
+ showAxes,
4106
+ viewSettings
4107
+ ]);
4108
+ (0, react.useEffect)(() => {
4109
+ if (!isReady || !pluginRef.current) return;
4110
+ setAxes(pluginRef.current, showAxes);
4111
+ }, [showAxes, isReady]);
4112
+ (0, react.useEffect)(() => {
4113
+ const plugin = pluginRef.current;
4114
+ if (!plugin || !isReady) return;
4115
+ if (structure === currentStructureRef.current) return;
4116
+ currentStructureRef.current = structure;
4117
+ clipRatioRef.current = null;
4118
+ lastHoverRef.current = null;
4119
+ loadStructure(plugin, structure, hasPlddt, showAxes).then((loaded) => {
4120
+ if (pluginRef.current !== plugin) return;
4121
+ adoptLoadedStructure(plugin, loaded);
4122
+ });
4123
+ }, [
4124
+ structure,
4125
+ hasPlddt,
4126
+ isReady,
4127
+ adoptLoadedStructure
4128
+ ]);
4129
+ (0, react.useEffect)(() => {
4130
+ const plugin = pluginRef.current;
4131
+ if (!plugin || !isReady) return;
4132
+ applyChainVisibility(plugin, componentRefsRef.current, hiddenChains);
4133
+ }, [hiddenChains, isReady]);
4134
+ return {
4135
+ chainColorThemeRef,
4136
+ chains,
4137
+ isReady,
4138
+ loadCount,
4139
+ pluginRef,
4140
+ residuesByChainRef,
4141
+ residueValueThemeRef,
4142
+ setClipRatio
4143
+ };
4144
+ }
4145
+ //#endregion
4146
+ //#region src/core/ProteinStructureViewer/utils/focusRepresentation.ts
4147
+ /**
4148
+ * What Mol* draws around a focused selection, as ball-and-stick over the
4149
+ * cartoon: the selection itself, the residues near it, and the non-covalent
4150
+ * contacts between the two. All three stay on; what changes is how far "near"
4151
+ * reaches.
4152
+ */
4153
+ const FOCUS_COMPONENTS = [
4154
+ "target",
4155
+ "surroundings",
4156
+ "interactions"
4157
+ ];
4158
+ /** Mol*'s own default, in angstroms. */
4159
+ const NEIGHBOUR_RADIUS = 5;
4160
+ /**
4161
+ * Mol* builds the surroundings as `includeSurroundings(target, radius)` with
4162
+ * whole residues, so a radius of zero resolves to the residues of the target
4163
+ * and nothing else.
4164
+ */
4165
+ const OWN_RESIDUES_ONLY = 0;
4166
+ /**
4167
+ * Chooses how far the focus shell reaches around a selection.
4168
+ *
4169
+ * A residue, or a short range, is a site: the 5A shell around it is the point
4170
+ * of looking at it, and Mol*'s default earns its keep. A whole chain is a
4171
+ * structural unit instead, and its neighbours are the chain bound to it - so
4172
+ * on a complex the shell reaches straight across the interface and draws the
4173
+ * partner's contact face in atoms, which reads as the partner being selected
4174
+ * too.
4175
+ *
4176
+ * Taking the radius to zero confines the shell to the selection rather than
4177
+ * turning it off: the surroundings and the interactions are still drawn, over
4178
+ * the selected chain's own residues, so a chain shows its own atoms and
4179
+ * contacts and no others'.
4180
+ */
4181
+ function setFocusNeighbourhood(_x, _x2) {
4182
+ return _setFocusNeighbourhood.apply(this, arguments);
4183
+ }
4184
+ function _setFocusNeighbourhood() {
4185
+ _setFocusNeighbourhood = _asyncToGenerator(function* (plugin, includeNeighbours) {
4186
+ const expandRadius = includeNeighbours ? NEIGHBOUR_RADIUS : OWN_RESIDUES_ONLY;
4187
+ yield plugin.state.updateBehavior(molstar_lib_mol_plugin_behavior_dynamic_selection_structure_focus_representation.StructureFocusRepresentation, (params) => {
4188
+ params.components = FOCUS_COMPONENTS;
4189
+ params.expandRadius = expandRadius;
4190
+ });
4191
+ });
4192
+ return _setFocusNeighbourhood.apply(this, arguments);
4193
+ }
4194
+ //#endregion
4195
+ //#region src/core/ProteinStructureViewer/hooks/useSelectionFocus.ts
4196
+ /**
4197
+ * Points the camera at a loci and marks it.
4198
+ *
4199
+ * A click has already focused what the consumer echoes back by the time it
4200
+ * arrives, so the focus is only set when it is not already what is marked;
4201
+ * setting it again would restate the selection and repaint the sequence panel
4202
+ * for nothing.
4203
+ */
4204
+ function focusLoci(plugin, loci, setClipRatio) {
4205
+ const focused = plugin.managers.structure.focus.behaviors.current.value;
4206
+ if (!focused || !molstar_lib_mol_model_structure.StructureElement.Loci.areEqual(focused.loci, loci)) plugin.managers.structure.focus.setFromLoci(loci);
4207
+ setClipRatio(focusResidue(plugin, loci));
4208
+ }
4209
+ /**
4210
+ * Drops the focus, and the depth clipping anchored to it.
4211
+ *
4212
+ * `resetCamera` separates the two ways this happens. The consumer clearing the
4213
+ * selection means there is nothing to look at, so the view goes back to the
4214
+ * default. A selection that merely resolved to nothing visible - the chain it
4215
+ * covers having been hidden - still stands, so throwing the camera back would
4216
+ * be answering a question nobody asked.
4217
+ */
4218
+ function clearFocus(plugin, setClipRatio, resetCamera) {
4219
+ var _plugin$canvas3d;
4220
+ setClipRatio(null);
4221
+ plugin.managers.structure.focus.clear();
4222
+ if (resetCamera) (_plugin$canvas3d = plugin.canvas3d) === null || _plugin$canvas3d === void 0 || _plugin$canvas3d.requestCameraReset();
4223
+ }
4224
+ /**
4225
+ * Focuses a loci, resizing the focus shell first when this selection wants a
4226
+ * different reach than the last one did.
4227
+ *
4228
+ * The resize is asynchronous and the focus has to follow it - the shell is
4229
+ * sized before focusing so it is not drawn at the old reach and rebuilt - so
4230
+ * this hands back a teardown that abandons the focus if the effect is torn
4231
+ * down while the resize is still in flight.
4232
+ *
4233
+ * `currentShell` is the reach already configured, carried in a ref so that two
4234
+ * selections of the same kind do not each restate it.
4235
+ */
4236
+ function focusWithShell(plugin, loci, includeNeighbours, currentShell, pluginRef, setClipRatio) {
4237
+ if (currentShell.current === includeNeighbours) {
4238
+ focusLoci(plugin, loci, setClipRatio);
4239
+ return;
4240
+ }
4241
+ let cancelled = false;
4242
+ currentShell.current = includeNeighbours;
4243
+ setFocusNeighbourhood(plugin, includeNeighbours).then(() => {
4244
+ if (cancelled || pluginRef.current !== plugin) return;
4245
+ focusLoci(plugin, loci, setClipRatio);
4246
+ });
4247
+ return () => {
4248
+ cancelled = true;
4249
+ };
4250
+ }
4251
+ /**
4252
+ * Makes `selection` the one thing that decides where the camera sits.
4253
+ *
4254
+ * Setting the prop frames whatever it covers; clearing it drops the focus and
4255
+ * zooms back out. The camera frames the selection's bounding sphere, so a range
4256
+ * or a whole chain is fitted rather than approached as a point - one residue is
4257
+ * just the smallest case of that.
4258
+ *
4259
+ * Residues are also marked, which is what draws Mol*'s ball-and-stick and
4260
+ * outline over them. Whole chains are not: those are shown by dimming the
4261
+ * chains around them instead, in `useChainHighlight`.
4262
+ *
4263
+ * The click subscription only reports the selection it was given, so a click
4264
+ * moves the camera only once the consumer accepts it and echoes it back -
4265
+ * which is what the prop being controlled is supposed to mean. Clicking still
4266
+ * marks immediately, through Mol*'s own focus behavior, so the structure
4267
+ * responds to a click whether or not the consumer wires the prop up.
4268
+ */
4269
+ function useSelectionFocus({ hiddenChains, isReady, pluginRef, selection, setClipRatio }) {
4270
+ var _selection$chains, _selection$residues;
4271
+ /**
4272
+ * What this hook last pointed the camera at, as a key rather than the object
4273
+ * itself: a consumer echoing a selection back builds a new object each time,
4274
+ * and comparing identity would refocus on every render.
4275
+ *
4276
+ * Tracked here rather than compared against the previous prop so that a
4277
+ * selection made before the plugin was ready is still applied once it is,
4278
+ * and so that a plugin rebuilt underneath us - which comes up focused on
4279
+ * nothing - is not assumed to still be holding the old one.
4280
+ */
4281
+ const focusedRef = (0, react.useRef)(null);
4282
+ /**
4283
+ * Whether the focus shell currently reaches beyond the selection, so the
4284
+ * behavior is only updated when the answer actually changes rather than on
4285
+ * every selection.
4286
+ */
4287
+ const neighbourhoodRef = (0, react.useRef)(null);
4288
+ /**
4289
+ * What the camera should be pointed at, as a string. Hiding a chain changes
4290
+ * what there is to focus, so it belongs in here alongside the selection
4291
+ * itself - and comparing strings rather than objects is what keeps a consumer
4292
+ * echoing an equal selection back from refocusing on every render.
4293
+ */
4294
+ const focusKey = selection ? `${selectionKey(selection)}#${[...hiddenChains].sort().join(",")}` : null;
4295
+ /**
4296
+ * Whether the selection names chains and nothing else, which is the shape
4297
+ * shown by dimming the chains around it rather than by focusing it.
4298
+ */
4299
+ const chainsOnly = Boolean((selection === null || selection === void 0 || (_selection$chains = selection.chains) === null || _selection$chains === void 0 ? void 0 : _selection$chains.length) && !((_selection$residues = selection.residues) === null || _selection$residues === void 0 ? void 0 : _selection$residues.length));
4300
+ (0, react.useEffect)(() => {
4301
+ var _chosen$chains;
4302
+ const plugin = pluginRef.current;
4303
+ if (!plugin || !isReady) {
4304
+ focusedRef.current = null;
4305
+ neighbourhoodRef.current = null;
4306
+ return;
4307
+ }
4308
+ if (focusKey === focusedRef.current) return;
4309
+ focusedRef.current = focusKey;
4310
+ if (focusKey === null) {
4311
+ clearFocus(plugin, setClipRatio, true);
4312
+ return;
4313
+ }
4314
+ const chosen = selection;
4315
+ const loci = lociForSelection(plugin, chosen, hiddenChains);
4316
+ if (!loci) {
4317
+ clearFocus(plugin, setClipRatio, false);
4318
+ return;
4319
+ }
4320
+ if (chainsOnly) {
4321
+ plugin.managers.structure.focus.clear();
4322
+ frameLoci(plugin, loci);
4323
+ setClipRatio(null);
4324
+ return;
4325
+ }
4326
+ return focusWithShell(plugin, loci, !((_chosen$chains = chosen.chains) === null || _chosen$chains === void 0 ? void 0 : _chosen$chains.length), neighbourhoodRef, pluginRef, setClipRatio);
4327
+ }, [
4328
+ focusKey,
4329
+ isReady,
4330
+ setClipRatio
4331
+ ]);
4332
+ }
4333
+ //#endregion
4334
+ //#region src/core/ProteinStructureViewer/hooks/useSelectionReadout.ts
4335
+ /**
4336
+ * Tracks the residue under the pointer, deduplicated, and passes the event on
4337
+ * to the consumer.
4338
+ *
4339
+ * Mol* emits hover events continuously, so a residue the pointer is resting on
4340
+ * would otherwise cost a state update per event. The index alone does not
4341
+ * identify one: loading a new structure into the same plugin renumbers from
4342
+ * zero, so the residue now at an index is not the one that was there before.
4343
+ * Comparing the label as well is enough, because the readout this drives is
4344
+ * built from the label and the index and from nothing else.
4345
+ */
4346
+ function useResidueHoverState(onResidueHover) {
4347
+ const [hoveredResidue, setHoveredResidue] = (0, react.useState)(null);
4348
+ return {
4349
+ handleResidueHover: (0, react.useCallback)((residue) => {
4350
+ setHoveredResidue((prev) => {
4351
+ if (residue === null) return prev === null ? prev : null;
4352
+ const label = residueLabel(residue);
4353
+ return prev !== null && prev.index === residue.index && prev.label === label ? prev : {
4354
+ index: residue.index,
4355
+ label
4356
+ };
4357
+ });
4358
+ onResidueHover === null || onResidueHover === void 0 || onResidueHover(residue);
4359
+ }, [onResidueHover]),
4360
+ hoveredResidue
4361
+ };
4362
+ }
4363
+ /**
4364
+ * Derives the legend's readouts from what is hovered and what is selected.
4365
+ *
4366
+ * The viewer already knows everything they need - the label, the pLDDT, the
4367
+ * overlay value - so they are worked out here rather than asked of the
4368
+ * consumer. A readout covering several residues reports the mean of each,
4369
+ * which is the only reading that survives a range or a whole chain.
4370
+ */
4371
+ function useSelectionReadout({ hoveredResidue, isReady, plddt, pluginRef, residueOverlay, residuesByChainRef, selection }) {
4372
+ const [resolved, setResolved] = (0, react.useState)(null);
4373
+ /**
4374
+ * What the selection covers has to be read off the structure, since a
4375
+ * selection can be made without a click ever naming anything: a lone residue
4376
+ * is named the way the click path names it, so the two readouts cannot
4377
+ * disagree, and a chain is expanded to the residues it actually holds rather
4378
+ * than to the span between its ends.
4379
+ */
4380
+ (0, react.useEffect)(() => {
4381
+ var _residuesByChainRef$c;
4382
+ const plugin = pluginRef.current;
4383
+ if (!plugin || !isReady || isEmptySelection(selection)) {
4384
+ setResolved(null);
4385
+ return;
4386
+ }
4387
+ const chosen = selection;
4388
+ const residues = selectionResidues(chosen, (_residuesByChainRef$c = residuesByChainRef.current) !== null && _residuesByChainRef$c !== void 0 ? _residuesByChainRef$c : /* @__PURE__ */ new Map());
4389
+ if (residues.length === 0) {
4390
+ setResolved(null);
4391
+ return;
4392
+ }
4393
+ if (residues.length > 1) {
4394
+ setResolved({
4395
+ label: selectionLabel(chosen, residues.length),
4396
+ residues
4397
+ });
4398
+ return;
4399
+ }
4400
+ const loci = lociForSelection(plugin, chosen);
4401
+ const residue = loci && residueRefFromLoci(loci);
4402
+ setResolved(residue ? {
4403
+ label: residueLabel(residue),
4404
+ residues
4405
+ } : null);
4406
+ }, [isReady, selection && selectionKey(selection)]);
4407
+ const buildReadout = (0, react.useCallback)((label, residues) => ({
4408
+ label,
4409
+ plddt: meanOverResidues(residues, (i) => plddt === null || plddt === void 0 ? void 0 : plddt[i]),
4410
+ value: meanOverResidues(residues, (i) => residueOverlay === null || residueOverlay === void 0 ? void 0 : residueOverlay.values.get(i)),
4411
+ residueCount: residues.length
4412
+ }), [plddt, residueOverlay]);
4413
+ return {
4414
+ hoveredReadout: (0, react.useMemo)(() => hoveredResidue === null ? null : buildReadout(hoveredResidue.label, [hoveredResidue.index]), [hoveredResidue, buildReadout]),
4415
+ selectedReadout: (0, react.useMemo)(() => resolved === null ? null : buildReadout(resolved.label, resolved.residues), [resolved, buildReadout])
4416
+ };
4417
+ }
4418
+ //#endregion
4419
+ //#region src/core/ProteinStructureViewer/hooks/useStructureColoring.ts
4420
+ /**
4421
+ * Decides what is coloring the structure and keeps that theme's data current.
4422
+ *
4423
+ * Three themes compete for the same representations, in a fixed order: a
4424
+ * residue value overlay, then pLDDT scores, then per-chain colors. Only one
4425
+ * can be applied, so the precedence lives here rather than being spread across
4426
+ * the hooks that own each theme's data - and the legend resolves what to
4427
+ * describe the same way, so the two cannot disagree about what is on screen.
4428
+ *
4429
+ * Recoloring happens in place: the camera is left where it is, so selecting or
4430
+ * switching an overlay does not zoom back out to the default view.
4431
+ */
4432
+ function useStructureColoring({ chainColorThemeRef, chainColors, hasPlddt, isReady, mode, overlay, pluginRef, residueValueThemeRef }) {
4433
+ (0, react.useEffect)(() => {
4434
+ const plugin = pluginRef.current;
4435
+ const residueTheme = residueValueThemeRef.current;
4436
+ const chainTheme = chainColorThemeRef.current;
4437
+ if (!plugin || !residueTheme || !chainTheme || !isReady) return;
4438
+ chainTheme.setState(chainColors);
4439
+ if (overlay) {
4440
+ var _overlay$colorScale, _overlay$min;
4441
+ residueTheme.setState({
4442
+ colorScale: (_overlay$colorScale = overlay.colorScale) !== null && _overlay$colorScale !== void 0 ? _overlay$colorScale : require_colorScales.PLASMA_COLOR_SCALE,
4443
+ max: overlay.max,
4444
+ min: (_overlay$min = overlay.min) !== null && _overlay$min !== void 0 ? _overlay$min : 0,
4445
+ mode,
4446
+ values: overlay.values
4447
+ });
4448
+ applyColorTheme(plugin, RESIDUE_VALUE_THEME_NAME);
4449
+ return;
4450
+ }
4451
+ residueTheme.setState({
4452
+ mode,
4453
+ values: /* @__PURE__ */ new Map()
4454
+ });
4455
+ applyColorTheme(plugin, hasPlddt ? PLDDT_THEME_NAME : FALLBACK_THEME_NAME);
4456
+ }, [
4457
+ overlay,
4458
+ hasPlddt,
4459
+ isReady,
4460
+ mode,
4461
+ chainColors
4462
+ ]);
4463
+ }
4464
+ //#endregion
4465
+ //#region src/core/ProteinStructureViewer/ProteinStructureViewer.types.ts
4466
+ /** Pixel dimensions each resolution produces, for documentation and labels. */
4467
+ const DOWNLOAD_RESOLUTIONS = {
4468
+ high: "3840x2160",
4469
+ low: "1280x720",
4470
+ maximum: "7680x4320",
4471
+ medium: "1920x1080"
4472
+ };
4473
+ //#endregion
4474
+ //#region src/core/ProteinStructureViewer/index.tsx
4475
+ const _excluded = [
4476
+ "backgroundColor",
4477
+ "chainColors",
4478
+ "disableChainHighlightOnHover",
4479
+ "download",
4480
+ "hiddenChains",
4481
+ "onChainVisibilityChange",
4482
+ "onChainsChange",
4483
+ "molstarSpec",
4484
+ "onResidueClick",
4485
+ "onResidueHover",
4486
+ "onSelectionChange",
4487
+ "structure",
4488
+ "plddt",
4489
+ "residueOverlay",
4490
+ "selection",
4491
+ "sequenceViewerBackgroundColor",
4492
+ "showAxes",
4493
+ "showChainLegend",
4494
+ "showLegend",
4495
+ "showSequenceViewer",
4496
+ "stats"
4497
+ ];
4498
+ /** Fallbacks for theme tokens Mol* needs as concrete hex colors. */
4499
+ const FALLBACK_EDGE_COLOR = "#6c6c6c";
4500
+ const FALLBACK_HIGHLIGHT_COLOR = "#1b1b1b";
4501
+ /**
4502
+ * Keyed by mode: unlike the two above, the canvas has to invert with it. Only
4503
+ * reached when the viewer renders outside an SDS theme; inside one the
4504
+ * `base.backgroundPrimary` token wins.
4505
+ */
4506
+ const FALLBACK_BACKGROUND_COLOR = {
4507
+ dark: "#000000",
4508
+ light: "#ffffff"
4509
+ };
4510
+ /** Caption shown beneath the color key when no overlay is active. */
4511
+ const PLDDT_SCALE_LABEL = "pLDDT";
4512
+ /** Caption used when an overlay does not name itself. */
4513
+ const DEFAULT_OVERLAY_LABEL = "Value";
4514
+ /** Readout slot label used when an overlay does not name one. */
4515
+ const DEFAULT_READOUT_LABEL = "Value";
4516
+ /**
4517
+ * Chooses what the legend describes, following whatever is actually coloring
4518
+ * the structure: the overlay when one is set, the pLDDT bands when scores were
4519
+ * supplied, and nothing at all otherwise - Mol* falls back to chain coloring
4520
+ * there, which no per-residue scale describes, so a key would be labelling
4521
+ * colors that are not on screen.
4522
+ */
4523
+ function resolveScaleProps(overlay, hasPlddt) {
4524
+ if (overlay) {
4525
+ var _overlay$colorScale, _overlay$label, _overlay$min, _overlay$readoutLabel;
4526
+ return {
4527
+ scale: (_overlay$colorScale = overlay.colorScale) !== null && _overlay$colorScale !== void 0 ? _overlay$colorScale : require_colorScales.PLASMA_COLOR_SCALE,
4528
+ scaleLabel: (_overlay$label = overlay.label) !== null && _overlay$label !== void 0 ? _overlay$label : DEFAULT_OVERLAY_LABEL,
4529
+ scaleMax: overlay.max,
4530
+ scaleMin: (_overlay$min = overlay.min) !== null && _overlay$min !== void 0 ? _overlay$min : 0,
4531
+ scaleTooltip: overlay.tooltip,
4532
+ scaleTooltipProps: overlay.tooltipProps,
4533
+ valueLabel: (_overlay$readoutLabel = overlay.readoutLabel) !== null && _overlay$readoutLabel !== void 0 ? _overlay$readoutLabel : DEFAULT_READOUT_LABEL
4534
+ };
4535
+ }
4536
+ if (hasPlddt) return {
4537
+ scale: PLDDT_COLOR_SCALE,
4538
+ scaleLabel: PLDDT_SCALE_LABEL,
4539
+ scaleMax: null,
4540
+ scaleTooltip: void 0,
4541
+ scaleTooltipProps: void 0,
4542
+ valueLabel: void 0
4543
+ };
4544
+ return {
4545
+ scale: null,
4546
+ scaleLabel: void 0,
4547
+ scaleMax: null,
4548
+ scaleTooltip: void 0,
4549
+ scaleTooltipProps: void 0,
4550
+ valueLabel: void 0
4551
+ };
4552
+ }
4553
+ /**
4554
+ * Interactive 3D protein structure viewer built on Mol*, with a sequence panel,
4555
+ * pLDDT confidence coloring, per-residue value overlays, and a stats legend.
4556
+ *
4557
+ * The viewer is controlled for selection: `selectedResidue` drives the camera
4558
+ * (clicking a residue zooms in on it, clearing the selection zooms back out),
4559
+ * while hover state is owned internally so the legend can show a live readout
4560
+ * without the consumer round-tripping every pointer move.
4561
+ */
4562
+ const ProteinStructureViewer = (0, react.forwardRef)((props, ref) => {
4563
+ var _selection$chains;
4564
+ const { backgroundColor, chainColors: chainColorOverrides, disableChainHighlightOnHover = false, download, hiddenChains: hiddenChainsProp, onChainVisibilityChange, onChainsChange, molstarSpec, onResidueClick, onResidueHover, onSelectionChange, structure, plddt, residueOverlay, selection = null, sequenceViewerBackgroundColor, showAxes = true, showChainLegend = true, showLegend = true, showSequenceViewer = true, stats } = props, rest = require_objectSpread2._objectWithoutProperties(props, _excluded);
4565
+ const theme = (0, _mui_material_styles.useTheme)();
4566
+ const mode = (0, _czi_sds_components.getMode)({ theme });
4567
+ const semanticColors = (0, _czi_sds_components.getSemanticColors)({ theme });
4568
+ const pluginMountRef = (0, react.useRef)(null);
4569
+ const hasPlddt = Boolean(plddt && plddt.length > 0);
4570
+ const processedStructure = (0, react.useMemo)(() => hasPlddt ? injectPlddt(structure, plddt) : structure, [
4571
+ structure,
4572
+ plddt,
4573
+ hasPlddt
4574
+ ]);
4575
+ const bgColor = (0, react.useMemo)(() => {
4576
+ var _semanticColors$base;
4577
+ 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]);
4578
+ }, [
4579
+ backgroundColor,
4580
+ mode,
4581
+ semanticColors
4582
+ ]);
4583
+ const edgeColor = (0, react.useMemo)(() => {
4584
+ var _semanticColors$base2;
4585
+ return themeColor(semanticColors === null || semanticColors === void 0 || (_semanticColors$base2 = semanticColors.base) === null || _semanticColors$base2 === void 0 ? void 0 : _semanticColors$base2.borderPrimary, FALLBACK_EDGE_COLOR);
4586
+ }, [semanticColors]);
4587
+ const highlightColor = (0, react.useMemo)(() => {
4588
+ var _semanticColors$base3;
4589
+ return themeColor(semanticColors === null || semanticColors === void 0 || (_semanticColors$base3 = semanticColors.base) === null || _semanticColors$base3 === void 0 ? void 0 : _semanticColors$base3.fillSelected, FALLBACK_HIGHLIGHT_COLOR);
4590
+ }, [semanticColors]);
4591
+ const { handleResidueHover, hoveredResidue } = useResidueHoverState(onResidueHover);
4592
+ const handleSelectionClear = (0, react.useCallback)(() => {
4593
+ onSelectionChange === null || onSelectionChange === void 0 || onSelectionChange(null);
4594
+ }, [onSelectionChange]);
4595
+ /**
4596
+ * Chains the selection covers whole, which is what makes a chain caption a
4597
+ * toggle rather than a one-way switch. Keyed on its contents because the
4598
+ * set is pushed into the views Mol* renders, where a fresh object every
4599
+ * render would be a fresh push every render.
4600
+ */
4601
+ const selectedChainsKey = [...(_selection$chains = selection === null || selection === void 0 ? void 0 : selection.chains) !== null && _selection$chains !== void 0 ? _selection$chains : []].sort().join("\0");
4602
+ const selectedChains = (0, react.useMemo)(() => new Set(selectedChainsKey === "" ? [] : selectedChainsKey.split("\0")), [selectedChainsKey]);
4603
+ /**
4604
+ * Clicking a chain selects it; clicking the selected chain again clears it.
4605
+ *
4606
+ * A whole chain is reported as the chain it is, not as the hundreds of
4607
+ * residue indices it stands for, so echoing it back costs nothing. Only a
4608
+ * selection that is exactly this one chain toggles off - with a residue or
4609
+ * another chain also selected, the click is narrowing to this chain rather
4610
+ * than undoing itself.
4611
+ */
4612
+ const handleChainSelect = (0, react.useCallback)((chainId) => {
4613
+ var _selection$chains2, _selection$residues;
4614
+ 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);
4615
+ onSelectionChange === null || onSelectionChange === void 0 || onSelectionChange(isOnlyThisChain ? null : { chains: [chainId] });
4616
+ }, [onSelectionChange, selection]);
4617
+ /**
4618
+ * Dims every other chain while a chain's name is pointed at, in the legend
4619
+ * or in the sequence panel's captions, and leaves the selected chain lit
4620
+ * once the pointer has gone.
4621
+ *
4622
+ * Reached through a ref because the two ends need each other: dimming
4623
+ * other chains takes the plugin, which the hook below creates, and that
4624
+ * hook has to be handed the callback to give the captions it renders. This
4625
+ * one is stable and forwards to whatever the highlight hook installs once
4626
+ * there is a plugin for it to talk to.
4627
+ */
4628
+ const highlightChainRef = (0, react.useRef)(() => void 0);
4629
+ const highlightChain = (0, react.useCallback)((chainId) => {
4630
+ highlightChainRef.current(chainId);
4631
+ }, []);
4632
+ const [chains, setChains] = (0, react.useState)([]);
4633
+ const { colors: chainColors, hidden: hiddenChains, toggleChain } = useChains({
4634
+ chainColors: chainColorOverrides,
4635
+ chains,
4636
+ hiddenChains: hiddenChainsProp,
4637
+ onChainVisibilityChange
4638
+ });
4639
+ const { chainColorThemeRef, chains: loadedChains, isReady, loadCount, pluginRef, residuesByChainRef, residueValueThemeRef, setClipRatio } = useMolstarPlugin({
4640
+ backgroundColor: bgColor,
4641
+ chainColors,
4642
+ containerRef: pluginMountRef,
4643
+ download,
4644
+ edgeColor,
4645
+ hasPlddt,
4646
+ hiddenChains,
4647
+ highlightColor,
4648
+ mode,
4649
+ molstarSpec,
4650
+ onChainHover: highlightChain,
4651
+ onChainSelect: handleChainSelect,
4652
+ onChainToggle: toggleChain,
4653
+ onResidueClick,
4654
+ onResidueHover: handleResidueHover,
4655
+ onSelectionChange,
4656
+ onSelectionClear: handleSelectionClear,
4657
+ selectedChains,
4658
+ sequenceViewerBackgroundColor,
4659
+ showAxes,
4660
+ showSequenceViewer,
4661
+ structure: processedStructure
4662
+ });
4663
+ highlightChainRef.current = useChainHighlight({
4664
+ disabled: disableChainHighlightOnHover,
4665
+ hiddenChains,
4666
+ loadCount,
4667
+ pluginRef,
4668
+ selectedChains
4669
+ });
4670
+ (0, react.useEffect)(() => {
4671
+ setChains(loadedChains);
4672
+ }, [loadedChains]);
4673
+ const onChainsChangeRef = (0, react.useRef)(onChainsChange);
4674
+ onChainsChangeRef.current = onChainsChange;
4675
+ (0, react.useEffect)(() => {
4676
+ var _onChainsChangeRef$cu;
4677
+ (_onChainsChangeRef$cu = onChainsChangeRef.current) === null || _onChainsChangeRef$cu === void 0 || _onChainsChangeRef$cu.call(onChainsChangeRef, loadedChains);
4678
+ }, [loadedChains]);
4679
+ useStructureColoring({
4680
+ chainColorThemeRef,
4681
+ chainColors,
4682
+ hasPlddt,
4683
+ isReady,
4684
+ mode,
4685
+ overlay: residueOverlay,
4686
+ pluginRef,
4687
+ residueValueThemeRef
4688
+ });
4689
+ useSelectionFocus({
4690
+ hiddenChains,
4691
+ isReady,
4692
+ pluginRef,
4693
+ selection,
4694
+ setClipRatio
4695
+ });
4696
+ const { hoveredReadout, selectedReadout } = useSelectionReadout({
4697
+ hoveredResidue,
4698
+ isReady,
4699
+ plddt,
4700
+ pluginRef,
4701
+ residueOverlay,
4702
+ residuesByChainRef,
4703
+ selection
4704
+ });
4705
+ const scaleProps = (0, react.useMemo)(() => resolveScaleProps(residueOverlay, hasPlddt), [residueOverlay, hasPlddt]);
4706
+ /**
4707
+ * What a chain's swatch can show follows whatever is painting the
4708
+ * structure, in the same order the color key resolves in.
4709
+ *
4710
+ * Chain coloring gives each chain one color of its own. pLDDT gives every
4711
+ * chain the same four bands, which says they are colored by confidence
4712
+ * rather than telling them apart - so the swatch carries the whole key,
4713
+ * quartered. An overlay is continuous and has no discrete colors to put in
4714
+ * a swatch at all, so the rows keep their labels and toggles and the color
4715
+ * key beside them does the describing.
4716
+ */
4717
+ const chainColoringActive = !residueOverlay && !hasPlddt;
4718
+ const plddtColoringActive = !residueOverlay && hasPlddt;
4719
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ViewerRoot, require_objectSpread2._objectSpread2(require_objectSpread2._objectSpread2({
4720
+ ref,
4721
+ showSequenceViewer
4722
+ }, rest), {}, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PluginMount, { ref: pluginMountRef }), showLegend && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StructureLegend, require_objectSpread2._objectSpread2({
4723
+ chainBandColors: plddtColoringActive ? PLDDT_BAND_COLORS : void 0,
4724
+ chainColors: chainColoringActive ? chainColors : void 0,
4725
+ chains: showChainLegend ? chains : [],
4726
+ hiddenChains,
4727
+ hoveredResidue: hoveredReadout,
4728
+ onChainHover: highlightChain,
4729
+ onChainSelect: handleChainSelect,
4730
+ onChainToggle: toggleChain,
4731
+ selectedChains,
4732
+ selectedResidue: selectedReadout,
4733
+ showSequenceViewer,
4734
+ stats: stats !== null && stats !== void 0 ? stats : []
4735
+ }, scaleProps))] }));
4736
+ });
4737
+ var ProteinStructureViewer_default = (0, react.memo)(ProteinStructureViewer);
4738
+ //#endregion
4739
+ Object.defineProperty(exports, "DOWNLOAD_RESOLUTIONS", {
4740
+ enumerable: true,
4741
+ get: function() {
4742
+ return DOWNLOAD_RESOLUTIONS;
4743
+ }
4744
+ });
4745
+ Object.defineProperty(exports, "PLDDT_COLOR_SCALE", {
4746
+ enumerable: true,
4747
+ get: function() {
4748
+ return PLDDT_COLOR_SCALE;
4749
+ }
4750
+ });
4751
+ Object.defineProperty(exports, "ProteinStructureViewer_default", {
4752
+ enumerable: true,
4753
+ get: function() {
4754
+ return ProteinStructureViewer_default;
4755
+ }
4756
+ });
4757
+ Object.defineProperty(exports, "detectStructureFormat", {
4758
+ enumerable: true,
4759
+ get: function() {
4760
+ return detectStructureFormat;
4761
+ }
4762
+ });
4763
+ Object.defineProperty(exports, "injectPlddt", {
4764
+ enumerable: true,
4765
+ get: function() {
4766
+ return injectPlddt;
4767
+ }
4768
+ });
4769
+ Object.defineProperty(exports, "injectPlddtIntoMmcif", {
4770
+ enumerable: true,
4771
+ get: function() {
4772
+ return injectPlddtIntoMmcif;
4773
+ }
4774
+ });
4775
+ Object.defineProperty(exports, "injectPlddtIntoPdb", {
4776
+ enumerable: true,
4777
+ get: function() {
4778
+ return injectPlddtIntoPdb;
4779
+ }
4780
+ });