jbrowse-plugin-protein3d 0.4.13 → 0.5.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 (53) hide show
  1. package/dist/LaunchProteinView/components/FoldseekActionMenu.js +15 -14
  2. package/dist/LaunchProteinView/components/ProteinViewActions.js +27 -15
  3. package/dist/LaunchProteinView/components/UserProvidedStructure.js +1 -2
  4. package/dist/LaunchProteinView/hooks/useSafeLaunch.d.ts +9 -0
  5. package/dist/LaunchProteinView/hooks/useSafeLaunch.js +15 -0
  6. package/dist/LaunchProteinView/utils/launchViewUtils.d.ts +9 -11
  7. package/dist/LaunchProteinView/utils/launchViewUtils.js +6 -8
  8. package/dist/LaunchProteinView/utils/sideBySide.d.ts +5 -0
  9. package/dist/LaunchProteinView/utils/sideBySide.js +9 -0
  10. package/dist/LaunchProteinViewExtensionPoint/index.js +7 -4
  11. package/dist/ProteinView/applyLociInteractivity.d.ts +23 -17
  12. package/dist/ProteinView/applyLociInteractivity.js +33 -61
  13. package/dist/ProteinView/components/FeatureBar.d.ts +1 -1
  14. package/dist/ProteinView/components/FeatureBar.js +36 -34
  15. package/dist/ProteinView/components/FeatureTypeLabel.js +5 -9
  16. package/dist/ProteinView/components/ProteinFeatureTrack.js +7 -15
  17. package/dist/ProteinView/components/ProteinViewHeader.js +9 -1
  18. package/dist/ProteinView/components/ResidueValueTrack.js +26 -15
  19. package/dist/ProteinView/hooks/useProteinFeatureTrackData.d.ts +1 -0
  20. package/dist/ProteinView/hooks/useProteinFeatureTrackData.js +3 -2
  21. package/dist/ProteinView/model.d.ts +12 -0
  22. package/dist/ProteinView/structureModel.d.ts +14 -5
  23. package/dist/ProteinView/structureModel.js +69 -92
  24. package/dist/ProteinView/subscribeMolstarInteraction.d.ts +8 -0
  25. package/dist/ProteinView/subscribeMolstarInteraction.js +1 -1
  26. package/dist/ProteinView/util.d.ts +0 -5
  27. package/dist/ProteinView/util.js +0 -11
  28. package/dist/jbrowse-plugin-protein3d.umd.production.min.js +15 -15
  29. package/dist/jbrowse-plugin-protein3d.umd.production.min.js.map +4 -4
  30. package/dist/version.d.ts +1 -1
  31. package/dist/version.js +1 -1
  32. package/package.json +1 -1
  33. package/src/LaunchProteinView/components/FoldseekActionMenu.tsx +22 -17
  34. package/src/LaunchProteinView/components/ProteinViewActions.tsx +32 -17
  35. package/src/LaunchProteinView/components/UserProvidedStructure.tsx +1 -6
  36. package/src/LaunchProteinView/hooks/useSafeLaunch.ts +17 -0
  37. package/src/LaunchProteinView/utils/launchViewUtils.ts +30 -29
  38. package/src/LaunchProteinView/utils/sideBySide.ts +14 -0
  39. package/src/LaunchProteinViewExtensionPoint/index.ts +8 -9
  40. package/src/ProteinView/applyLociInteractivity.ts +62 -114
  41. package/src/ProteinView/components/FeatureBar.tsx +40 -44
  42. package/src/ProteinView/components/FeatureTypeLabel.tsx +6 -11
  43. package/src/ProteinView/components/ProteinFeatureTrack.tsx +5 -17
  44. package/src/ProteinView/components/ProteinViewHeader.tsx +15 -0
  45. package/src/ProteinView/components/ResidueValueTrack.tsx +40 -23
  46. package/src/ProteinView/hooks/useProteinFeatureTrackData.ts +6 -2
  47. package/src/ProteinView/structureModel.ts +90 -108
  48. package/src/ProteinView/subscribeMolstarInteraction.ts +9 -1
  49. package/src/ProteinView/util.ts +0 -25
  50. package/src/version.ts +1 -1
  51. package/dist/ProteinView/highlightResidueRange.d.ts +0 -14
  52. package/dist/ProteinView/highlightResidueRange.js +0 -19
  53. package/src/ProteinView/highlightResidueRange.ts +0 -44
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const version = "0.4.13";
1
+ export declare const version = "0.5.0";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const version = '0.4.13';
1
+ export const version = '0.5.0';
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.4.13",
2
+ "version": "0.5.0",
3
3
  "name": "jbrowse-plugin-protein3d",
4
4
  "repository": {
5
5
  "type": "git",
@@ -1,10 +1,11 @@
1
1
  import React, { useState } from 'react'
2
2
 
3
3
  import { ErrorMessage } from '@jbrowse/core/ui'
4
+ import { isSessionWithAddTracks } from '@jbrowse/core/util'
4
5
  import { Button, Menu, MenuItem } from '@mui/material'
5
6
 
7
+ import { useSafeLaunch } from '../hooks/useSafeLaunch'
6
8
  import { caCoordsToPdb, hasValidCaCoords } from '../utils/caCoordsToPdb'
7
- import { safeLaunch } from '../utils/launchHelpers'
8
9
  import {
9
10
  getConfidenceUrlFromTarget,
10
11
  getUniprotIdFromAlphaFoldTarget,
@@ -41,7 +42,6 @@ export default function FoldseekActionMenu({
41
42
  onClose: () => void
42
43
  }) {
43
44
  const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null)
44
- const [launchError, setLaunchError] = useState<unknown>()
45
45
  const open = Boolean(anchorEl)
46
46
 
47
47
  const uniprotId = getUniprotIdFromAlphaFoldTarget(hit.target)
@@ -54,12 +54,9 @@ export default function FoldseekActionMenu({
54
54
  setAnchorEl(null)
55
55
  }
56
56
 
57
- const baseParams = { session, view, feature, selectedTranscript, uniprotId }
57
+ const { runLaunch, launchError } = useSafeLaunch(onClose, handleMenuClose)
58
58
 
59
- const runLaunch = (fn: () => void | Promise<void>) => () => {
60
- handleMenuClose()
61
- void safeLaunch(fn, onClose, setLaunchError)
62
- }
59
+ const baseParams = { session, view, feature, selectedTranscript, uniprotId }
63
60
 
64
61
  const handleLaunch3D = runLaunch(() => {
65
62
  // Use tCa coordinates to generate PDB data if no URL is available
@@ -75,22 +72,19 @@ export default function FoldseekActionMenu({
75
72
  })
76
73
  })
77
74
 
78
- const handleLaunch1D = runLaunch(async () => {
79
- await launch1DProteinView({
80
- ...baseParams,
81
- confidenceUrl: getConfidenceUrlFromTarget(hit.target),
82
- })
83
- })
84
-
85
75
  const handleLaunchMSA = runLaunch(() => {
86
76
  launchMsaView(baseParams)
87
77
  })
88
78
 
89
- const canLoad = hit.structureUrl ?? hasValidCaCoords(hit.tCa, hit.tSeq)
79
+ const canLoad = !!hit.structureUrl || hasValidCaCoords(hit.tCa, hit.tSeq)
90
80
  if (!canLoad) {
91
81
  return <span>-</span>
92
82
  }
93
83
 
84
+ // 1D launch needs an add-tracks session and a uniprotId; narrowing both here
85
+ // gates the menu item and types its handler from a single condition.
86
+ const addTracksSession = isSessionWithAddTracks(session) ? session : undefined
87
+
94
88
  return (
95
89
  <>
96
90
  {launchError ? <ErrorMessage error={launchError} /> : null}
@@ -99,8 +93,19 @@ export default function FoldseekActionMenu({
99
93
  </Button>
100
94
  <Menu anchorEl={anchorEl} open={open} onClose={handleMenuClose}>
101
95
  <MenuItem onClick={handleLaunch3D}>Launch 3D protein view</MenuItem>
102
- {uniprotId ? (
103
- <MenuItem onClick={handleLaunch1D}>
96
+ {addTracksSession && uniprotId ? (
97
+ <MenuItem
98
+ onClick={runLaunch(() =>
99
+ launch1DProteinView({
100
+ session: addTracksSession,
101
+ view,
102
+ feature,
103
+ selectedTranscript,
104
+ uniprotId,
105
+ confidenceUrl: getConfidenceUrlFromTarget(hit.target),
106
+ }),
107
+ )}
108
+ >
104
109
  Launch 1D protein annotation view
105
110
  </MenuItem>
106
111
  ) : null}
@@ -1,6 +1,7 @@
1
1
  import React, { useState } from 'react'
2
2
 
3
3
  import { ErrorMessage } from '@jbrowse/core/ui'
4
+ import { isSessionWithAddTracks } from '@jbrowse/core/util'
4
5
  import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'
5
6
  import SettingsIcon from '@mui/icons-material/Settings'
6
7
  import { Button, ButtonGroup, IconButton, Tooltip, Typography } from '@mui/material'
@@ -8,7 +9,8 @@ import { Button, ButtonGroup, IconButton, Tooltip, Typography } from '@mui/mater
8
9
  import LaunchOptionsDialog from './LaunchOptionsDialog'
9
10
  import LaunchSettingsDialog from './LaunchSettingsDialog'
10
11
  import SequenceMismatchNotice from './SequenceMismatchNotice'
11
- import { getLaunchMissingReasons, safeLaunch } from '../utils/launchHelpers'
12
+ import { useSafeLaunch } from '../hooks/useSafeLaunch'
13
+ import { getLaunchMissingReasons } from '../utils/launchHelpers'
12
14
  import {
13
15
  hasMsaViewPlugin,
14
16
  launch1DProteinView,
@@ -61,7 +63,6 @@ export default function ProteinViewActions({
61
63
  }: ProteinViewActionsProps) {
62
64
  const [dialogOpen, setDialogOpen] = useState(false)
63
65
  const [settingsOpen, setSettingsOpen] = useState(false)
64
- const [launchError, setLaunchError] = useState<unknown>()
65
66
  // Disable launch while loading — SWR's keepPreviousData would otherwise let
66
67
  // a user click Launch on stale results (wrong UniProt ID) during a refetch.
67
68
  const canLaunch =
@@ -82,6 +83,8 @@ export default function ProteinViewActions({
82
83
  setDialogOpen(false)
83
84
  }
84
85
 
86
+ const { runLaunch, launchError } = useSafeLaunch(handleClose, closeMenu)
87
+
85
88
  const baseParams = {
86
89
  session,
87
90
  view,
@@ -96,19 +99,10 @@ export default function ProteinViewActions({
96
99
  alignmentAlgorithm,
97
100
  }
98
101
 
99
- const runLaunch = (fn: () => void | Promise<void>) => () => {
100
- closeMenu()
101
- void safeLaunch(fn, handleClose, setLaunchError)
102
- }
103
-
104
102
  const handleLaunch3DView = runLaunch(() => {
105
103
  launch3DProteinView(launch3DParams)
106
104
  })
107
105
 
108
- const handleLaunch1DView = runLaunch(async () => {
109
- await launch1DProteinView({ ...baseParams, confidenceUrl })
110
- })
111
-
112
106
  const handleLaunchMsa = runLaunch(() => {
113
107
  launchMsaView(baseParams)
114
108
  })
@@ -117,6 +111,13 @@ export default function ProteinViewActions({
117
111
  launch3DProteinViewWithMsa(launch3DParams)
118
112
  })
119
113
 
114
+ // The 1D annotation view needs an add-tracks session and a known uniprotId.
115
+ // Narrowing here is the single source of truth: the option only exists when
116
+ // both hold, and its handler is type-checked against those narrowed values —
117
+ // so a 1D launch that can't work is unrepresentable rather than a silent
118
+ // no-op.
119
+ const addTracksSession = isSessionWithAddTracks(session) ? session : undefined
120
+
120
121
  const launchOptions = [
121
122
  {
122
123
  key: '3d',
@@ -125,12 +126,26 @@ export default function ProteinViewActions({
125
126
  'View protein structure with genome-to-structure coordinate mapping',
126
127
  onClick: handleLaunch3DView,
127
128
  },
128
- {
129
- key: '1d',
130
- title: 'Launch 1D protein annotation view',
131
- description: 'View protein features and annotations as a linear track',
132
- onClick: handleLaunch1DView,
133
- },
129
+ ...(addTracksSession && uniprotId
130
+ ? [
131
+ {
132
+ key: '1d',
133
+ title: 'Launch 1D protein annotation view',
134
+ description:
135
+ 'View protein features and annotations as a linear track',
136
+ onClick: runLaunch(() =>
137
+ launch1DProteinView({
138
+ session: addTracksSession,
139
+ view,
140
+ feature,
141
+ selectedTranscript,
142
+ uniprotId,
143
+ confidenceUrl,
144
+ }),
145
+ ),
146
+ },
147
+ ]
148
+ : []),
134
149
  ...(hasMsaViewPlugin()
135
150
  ? [
136
151
  {
@@ -13,11 +13,7 @@ import ExternalLink from '../../components/ExternalLink'
13
13
  import useStructureFileSequence from '../hooks/useStructureFileSequence'
14
14
  import useTranscriptIsoformSelection from '../hooks/useTranscriptIsoformSelection'
15
15
  import { launch3DProteinView } from '../utils/launchViewUtils'
16
- import {
17
- getGeneDisplayName,
18
- getTranscriptDisplayName,
19
- stripStopCodon,
20
- } from '../utils/util'
16
+ import { stripStopCodon } from '../utils/util'
21
17
 
22
18
  import type { AlignmentAlgorithm } from '../../ProteinView/types'
23
19
  import type { AbstractSessionModel, Feature } from '@jbrowse/core/util'
@@ -117,7 +113,6 @@ const UserProvidedStructure = observer(function UserProvidedStructure({
117
113
  data: structureData,
118
114
  userProvidedTranscriptSequence: protein.seq,
119
115
  alignmentAlgorithm,
120
- displayName: `Protein view ${getGeneDisplayName(feature)} - ${getTranscriptDisplayName(selectedTranscript)}`,
121
116
  })
122
117
  handleClose()
123
118
  } catch (e) {
@@ -0,0 +1,17 @@
1
+ import { useState } from 'react'
2
+
3
+ import { safeLaunch } from '../utils/launchHelpers'
4
+
5
+ /**
6
+ * Shared launch-button wiring for the action components: holds the launch
7
+ * error state and returns a `runLaunch` factory that closes any open menu,
8
+ * runs the launch via safeLaunch, and surfaces failures inline.
9
+ */
10
+ export function useSafeLaunch(onSuccess: () => void, onBeforeLaunch?: () => void) {
11
+ const [launchError, setLaunchError] = useState<unknown>()
12
+ const runLaunch = (fn: () => void | Promise<void>) => () => {
13
+ onBeforeLaunch?.()
14
+ void safeLaunch(fn, onSuccess, setLaunchError)
15
+ }
16
+ return { runLaunch, launchError }
17
+ }
@@ -1,16 +1,18 @@
1
- import { isSessionWithAddTracks } from '@jbrowse/core/util'
2
-
3
1
  declare global {
4
2
  interface Window {
5
3
  JBrowsePluginMsaView?: unknown
6
4
  }
7
5
  }
8
6
 
9
- import { getLaunchSideBySide, launchViewSideBySide } from './sideBySide'
7
+ import { maybeLaunchSideBySide } from './sideBySide'
10
8
  import { getGeneDisplayName, getTranscriptDisplayName } from './util'
11
9
  import { launchProteinAnnotationView } from '../components/launchProteinAnnotationView'
12
10
 
13
- import type { AbstractSessionModel, Feature } from '@jbrowse/core/util'
11
+ import type {
12
+ AbstractSessionModel,
13
+ Feature,
14
+ SessionWithAddTracks,
15
+ } from '@jbrowse/core/util'
14
16
  import type { LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view'
15
17
 
16
18
  export const ALPHAFOLD_VERSION = 'v6'
@@ -84,6 +86,14 @@ interface LaunchViewParams {
84
86
  uniprotId?: string
85
87
  }
86
88
 
89
+ interface Launch3DExtraParams {
90
+ url?: string
91
+ data?: string
92
+ userProvidedTranscriptSequence?: string
93
+ alignmentAlgorithm?: string
94
+ displayName?: string
95
+ }
96
+
87
97
  export function formatViewName(
88
98
  prefix: string,
89
99
  feature: Feature,
@@ -115,17 +125,13 @@ export function launch3DProteinView({
115
125
  displayName,
116
126
  connectedMsaViewId,
117
127
  sideBySide,
118
- }: LaunchViewParams & {
119
- url?: string
120
- data?: string
121
- userProvidedTranscriptSequence?: string
122
- alignmentAlgorithm?: string
123
- displayName?: string
124
- connectedMsaViewId?: string
125
- // explicit override; when undefined the launch-dialog localStorage preference
126
- // decides (left genome | right protein)
127
- sideBySide?: boolean
128
- }) {
128
+ }: LaunchViewParams &
129
+ Launch3DExtraParams & {
130
+ connectedMsaViewId?: string
131
+ // explicit override; when undefined the launch-dialog localStorage
132
+ // preference decides (left genome | right protein)
133
+ sideBySide?: boolean
134
+ }) {
129
135
  const snap = {
130
136
  type: 'ProteinView',
131
137
  alignmentAlgorithm,
@@ -144,12 +150,14 @@ export function launch3DProteinView({
144
150
  formatViewName('Protein view', feature, selectedTranscript, uniprotId),
145
151
  }
146
152
  const proteinView = session.addView('ProteinView', snap)
147
- if (sideBySide ?? getLaunchSideBySide()) {
148
- launchViewSideBySide(session, proteinView.id)
149
- }
153
+ maybeLaunchSideBySide(session, proteinView.id, sideBySide)
150
154
  return proteinView
151
155
  }
152
156
 
157
+ // The 1D annotation view adds temporary tracks/assemblies, so it requires a
158
+ // SessionWithAddTracks and a known uniprotId. Demanding both in the signature
159
+ // forces callers to narrow up front — there's no silent no-op when a wide
160
+ // session or missing id slips through.
153
161
  export async function launch1DProteinView({
154
162
  session,
155
163
  view,
@@ -157,12 +165,11 @@ export async function launch1DProteinView({
157
165
  selectedTranscript,
158
166
  uniprotId,
159
167
  confidenceUrl,
160
- }: LaunchViewParams & {
168
+ }: Omit<LaunchViewParams, 'session' | 'uniprotId'> & {
169
+ session: SessionWithAddTracks
170
+ uniprotId: string
161
171
  confidenceUrl?: string
162
172
  }) {
163
- if (!uniprotId || !isSessionWithAddTracks(session)) {
164
- return
165
- }
166
173
  await launchProteinAnnotationView({
167
174
  session,
168
175
  selectedTranscript,
@@ -209,13 +216,7 @@ export function hasMsaViewPlugin() {
209
216
  }
210
217
 
211
218
  export function launch3DProteinViewWithMsa(
212
- params: LaunchViewParams & {
213
- url?: string
214
- data?: string
215
- userProvidedTranscriptSequence?: string
216
- alignmentAlgorithm?: string
217
- displayName?: string
218
- },
219
+ params: LaunchViewParams & Launch3DExtraParams,
219
220
  ) {
220
221
  const { uniprotId } = params
221
222
  if (!uniprotId) {
@@ -53,3 +53,17 @@ export function launchViewSideBySide(
53
53
  session.setUseWorkspaces(true)
54
54
  }
55
55
  }
56
+
57
+ /**
58
+ * Apply the side-by-side split honoring an explicit override, falling back to
59
+ * the launch-dialog localStorage preference when undefined.
60
+ */
61
+ export function maybeLaunchSideBySide(
62
+ session: AbstractSessionModel,
63
+ viewId: string,
64
+ sideBySide?: boolean,
65
+ ) {
66
+ if (sideBySide ?? getLaunchSideBySide()) {
67
+ launchViewSideBySide(session, viewId)
68
+ }
69
+ }
@@ -1,8 +1,5 @@
1
1
  import { type ConnectedViewSpec, resolveShortLaunch } from './resolveShortLaunch'
2
- import {
3
- getLaunchSideBySide,
4
- launchViewSideBySide,
5
- } from '../LaunchProteinView/utils/sideBySide'
2
+ import { maybeLaunchSideBySide } from '../LaunchProteinView/utils/sideBySide'
6
3
 
7
4
  import type PluginManager from '@jbrowse/core/PluginManager'
8
5
  import type { AbstractSessionModel } from '@jbrowse/core/util'
@@ -76,9 +73,11 @@ export default function LaunchProteinViewExtensionPointF(
76
73
 
77
74
  const finalUrl = url ?? resolved?.url
78
75
  if (!finalUrl) {
79
- throw new Error(
80
- 'No url or uniprotId provided when launching protein view',
81
- )
76
+ const message =
77
+ 'No url or uniprotId provided when launching protein view'
78
+ console.error(message)
79
+ session.notify(`Could not launch protein view: ${message}`, 'error')
80
+ return
82
81
  }
83
82
 
84
83
  // A session spec launches each view independently with an auto-generated
@@ -119,8 +118,8 @@ export default function LaunchProteinViewExtensionPointF(
119
118
  ],
120
119
  })
121
120
 
122
- if (ownsConnectedView && (sideBySide ?? getLaunchSideBySide())) {
123
- launchViewSideBySide(session, proteinView.id)
121
+ if (ownsConnectedView) {
122
+ maybeLaunchSideBySide(session, proteinView.id, sideBySide)
124
123
  }
125
124
  },
126
125
  )
@@ -1,133 +1,81 @@
1
1
  import loadMolstar from './loadMolstar'
2
- import { getMolstarStructureSelection } from './util'
3
2
 
4
- import type {
5
- Structure,
6
- StructureElement,
7
- } from 'molstar/lib/mol-model/structure'
3
+ import type { Structure } from 'molstar/lib/mol-model/structure'
8
4
  import type { PluginContext } from 'molstar/lib/mol-plugin/context'
5
+ import type { MolScriptBuilder } from 'molstar/lib/mol-script/language/builder'
6
+ import type { Expression } from 'molstar/lib/mol-script/language/expression'
9
7
 
10
- type InteractivityMode = 'highlight' | 'select' | 'clear'
8
+ type ResidueTest = (Q: typeof MolScriptBuilder) => Expression
11
9
 
12
- function clearLoci(plugin: PluginContext) {
13
- plugin.managers.interactivity.lociHighlights.clearHighlights()
14
- plugin.managers.interactivity.lociSelects.deselectAll()
15
- }
10
+ /**
11
+ * Which residues a highlight/selection should cover, in the plugin's native
12
+ * 0-based structure-sequence coordinates (see coordinates.ts). `range` is the
13
+ * half-open span [start, end); `list` is an explicit set of positions. The one
14
+ * conversion to molstar's 1-based inclusive label_seq_id happens in specToTest
15
+ * below — the single boundary where structure positions cross into molstar.
16
+ */
17
+ export type ResidueSpec =
18
+ | { kind: 'range'; start: number; end: number }
19
+ | { kind: 'list'; residues: number[] }
16
20
 
17
- function applyLoci(
18
- plugin: PluginContext,
19
- loci: StructureElement.Loci,
20
- mode: 'highlight' | 'select',
21
- ) {
22
- if (mode === 'highlight') {
23
- plugin.managers.interactivity.lociHighlights.clearHighlights()
24
- plugin.managers.interactivity.lociHighlights.highlight({ loci })
25
- } else {
26
- plugin.managers.interactivity.lociSelects.deselectAll()
27
- plugin.managers.interactivity.lociSelects.select({ loci })
28
- }
29
- }
21
+ const seqId = (Q: typeof MolScriptBuilder) =>
22
+ Q.struct.atomProperty.macromolecular.label_seq_id()
30
23
 
31
- export async function applyLociInteractivityMultiple({
32
- structure,
33
- residues,
34
- plugin,
35
- mode,
36
- }: {
37
- structure: Structure
38
- residues: number[]
39
- plugin: PluginContext
40
- mode: InteractivityMode
41
- }) {
42
- if (mode === 'clear' || residues.length === 0) {
43
- clearLoci(plugin)
44
- return
45
- }
46
-
47
- const { StructureSelection, Script } = await loadMolstar()
24
+ const specToTest = (spec: ResidueSpec): ResidueTest =>
25
+ spec.kind === 'range'
26
+ ? Q =>
27
+ Q.core.logic.and([
28
+ Q.core.rel.gre([seqId(Q), spec.start + 1]),
29
+ Q.core.rel.lte([seqId(Q), spec.end]),
30
+ ])
31
+ : Q =>
32
+ Q.core.logic.or(
33
+ spec.residues.map(pos => Q.core.rel.eq([seqId(Q), pos + 1])),
34
+ )
48
35
 
49
- const sel = Script.getStructureSelection(
50
- Q =>
51
- Q.struct.generator.atomGroups({
52
- 'residue-test': Q.core.logic.or(
53
- residues.map(residue =>
54
- Q.core.rel.eq([
55
- Q.struct.atomProperty.macromolecular.label_seq_id(),
56
- residue,
57
- ]),
58
- ),
59
- ),
60
- 'group-by': Q.struct.atomProperty.macromolecular.residueKey(),
61
- }),
62
- structure,
63
- )
36
+ const isActive = (spec: ResidueSpec | undefined): spec is ResidueSpec =>
37
+ spec !== undefined &&
38
+ (spec.kind === 'range' ? spec.end > spec.start : spec.residues.length > 0)
64
39
 
65
- const loci = StructureSelection.toLociWithSourceUnits(sel)
66
- applyLoci(plugin, loci, mode)
67
- }
68
-
69
- export async function applyLociInteractivity({
40
+ /**
41
+ * Reconcile one interactivity channel (hover-`highlight` or click-`select`) to
42
+ * the desired residue spec. Passing `undefined` (or an empty `list`) clears the
43
+ * channel, so callers describe the target state declaratively rather than
44
+ * juggling clear/apply calls.
45
+ */
46
+ export async function setMolstarLoci({
70
47
  structure,
71
- startResidue,
72
- endResidue,
73
48
  plugin,
74
- mode,
49
+ channel,
50
+ spec,
75
51
  }: {
76
52
  structure: Structure
77
- startResidue: number
78
- endResidue: number
79
53
  plugin: PluginContext
80
- mode: InteractivityMode
54
+ channel: 'highlight' | 'select'
55
+ spec: ResidueSpec | undefined
81
56
  }) {
82
- if (mode === 'clear') {
83
- clearLoci(plugin)
84
- return
57
+ const { lociHighlights, lociSelects } = plugin.managers.interactivity
58
+ if (channel === 'highlight') {
59
+ lociHighlights.clearHighlights()
60
+ } else {
61
+ lociSelects.deselectAll()
85
62
  }
86
63
 
87
- const { StructureSelection, Script } = await loadMolstar()
88
- const sel = Script.getStructureSelection(
89
- Q =>
90
- Q.struct.generator.atomGroups({
91
- 'residue-test': Q.core.logic.and([
92
- Q.core.rel.gre([
93
- Q.struct.atomProperty.macromolecular.label_seq_id(),
94
- startResidue,
95
- ]),
96
- Q.core.rel.lte([
97
- Q.struct.atomProperty.macromolecular.label_seq_id(),
98
- endResidue,
99
- ]),
100
- ]),
101
- 'group-by': Q.struct.atomProperty.macromolecular.residueKey(),
102
- }),
103
- structure,
104
- )
105
-
106
- const loci = StructureSelection.toLociWithSourceUnits(sel)
107
- applyLoci(plugin, loci, mode)
108
- }
109
-
110
- export async function applyLociInteractivitySingle({
111
- structure,
112
- selectedResidue,
113
- plugin,
114
- mode,
115
- }: {
116
- structure: Structure
117
- selectedResidue: number
118
- plugin: PluginContext
119
- mode: InteractivityMode
120
- }) {
121
- if (mode === 'clear') {
122
- clearLoci(plugin)
123
- return
64
+ if (isActive(spec)) {
65
+ const { StructureSelection, Script } = await loadMolstar()
66
+ const sel = Script.getStructureSelection(
67
+ Q =>
68
+ Q.struct.generator.atomGroups({
69
+ 'residue-test': specToTest(spec)(Q),
70
+ 'group-by': Q.struct.atomProperty.macromolecular.residueKey(),
71
+ }),
72
+ structure,
73
+ )
74
+ const loci = StructureSelection.toLociWithSourceUnits(sel)
75
+ if (channel === 'highlight') {
76
+ lociHighlights.highlight({ loci })
77
+ } else {
78
+ lociSelects.select({ loci })
79
+ }
124
80
  }
125
-
126
- const { StructureSelection } = await loadMolstar()
127
- const sel = await getMolstarStructureSelection({
128
- structure,
129
- selectedResidue: selectedResidue + 1,
130
- })
131
- const loci = StructureSelection.toLociWithSourceUnits(sel)
132
- applyLoci(plugin, loci, mode)
133
81
  }