Skip to content

Commit 66af20a

Browse files
authored
Merge pull request #13 from molstar/strucmotif
Add APIs to support interactive 3D visualization required by the Advanced Search UI
2 parents 3525424 + 42c3eee commit 66af20a

9 files changed

Lines changed: 750 additions & 142 deletions

File tree

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,23 @@
22

33
[Semantic Versioning](https://semver.org/)
44

5+
## [2.14.0] - Unreleased
6+
### General
7+
- Default assembly preset
8+
- More configuration options for controls in the layout and viewport
9+
10+
### API
11+
- Subscribe to a structural selection-related event in the plugin
12+
- Update the interactivity settings in the plugin's interactivity manager
13+
- Retrieve the list of assembly IDs for a default structure model
14+
- Retrieve asym and author chain IDs for a default structure model
15+
- Set the current structure view based on the provided assembly ID
16+
- Add custom labels to specified targets within the current structure
17+
- Focus on a specific residue within the current structure
18+
- Set ball and stick representation for a specific target
19+
- Determine the unknown assemblyId for a set of targets
20+
- Toggle visibility of structure component groups
21+
522
## [2.13.0] - 2025-09-29
623
### General
724
- Dependency update: Mol* to v5.0.0

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@rcsb/rcsb-molstar",
3-
"version": "2.13.0",
3+
"version": "2.14.0-dev.6",
44
"description": "RCSB PDB apps and props based on Mol*.",
55
"homepage": "https://github.com/molstar/rcsb-molstar#readme",
66
"repository": {

src/viewer/helpers/model.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ export class ModelLoader {
4141
params?: P
4242
): Promise<S | ReturnType<typeof RcsbPreset.apply> | undefined> {
4343
const trajectory = await this.plugin.builders.structure.parseTrajectory(data, format);
44+
if (!trajectory) {
45+
throw new Error('Trajectory data is unavailable or invalid');
46+
}
4447
if (reprProvider) {
4548
return this.plugin.builders.structure.hierarchy.applyPreset(trajectory, reprProvider, params);
4649
} else {

src/viewer/helpers/preset.ts

Lines changed: 32 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import {
3535
import {
3636
createGlyGenSelectionExpressions,
3737
createSelectionExpressions,
38-
normalizeTargets,
38+
normalizeTarget,
3939
SelectionExpression,
4040
Target,
4141
targetToLoci,
@@ -48,6 +48,7 @@ import {
4848
} from 'molstar/lib/extensions/assembly-symmetry/prop';
4949
import { Task } from 'molstar/lib/mol-task';
5050
import { QualityAssessment } from 'molstar/lib/extensions/model-archive/quality-assessment/prop';
51+
import { getAssemblyIdsFromModel, firstMatchingAssemblyId } from './viewer';
5152

5253
type BaseProps = {
5354
assemblyId?: string
@@ -126,8 +127,12 @@ export type GlyGenProps = {
126127
glycosylation: Target[],
127128
} & BaseProps
128129

130+
export type DefaultAssembly = {
131+
kind: 'default-assembly'
132+
} & BaseProps;
133+
129134
export type PresetProps = ValidationProps | StandardProps | SymmetryProps | FeatureProps | DensityProps | AlignmentProps |
130-
MembraneProps | FeatureDensityProps | MotifProps | NakbProps | GlyGenProps | EmptyProps;
135+
MembraneProps | FeatureDensityProps | MotifProps | NakbProps | GlyGenProps | EmptyProps | DefaultAssembly;
131136

132137
const RcsbParams = () => ({
133138
preset: PD.Value<PresetProps>({ kind: 'standard', assemblyId: '' }, { isHidden: true })
@@ -150,17 +155,27 @@ export const RcsbPreset = TrajectoryHierarchyPresetProvider({
150155
// jump through some hoops to determine the unknown assemblyId of query selections
151156
if (p.kind === 'motif') determineAssemblyId(trajectory, p);
152157

153-
const structureParams: RootStructureDefinition.Params = { name: 'model', params: {} };
154-
if (p.assemblyId && p.assemblyId !== '' && p.assemblyId !== '0') {
155-
Object.assign(structureParams, {
156-
name: 'assembly',
157-
params: { id: p.assemblyId }
158-
} as RootStructureDefinition.Params);
159-
}
160-
161158
const model = await builder.createModel(trajectory, modelParams);
162159
const modelProperties = await builder.insertModelProperties(model);
163160

161+
const structureParams: RootStructureDefinition.Params = { name: 'model', params: {} };
162+
if (p.kind === 'default-assembly') {
163+
const assemblyIds = getAssemblyIdsFromModel(model.cell?.obj?.data);
164+
if (assemblyIds.length > 0) {
165+
Object.assign(structureParams, {
166+
name: 'assembly',
167+
params: { id: assemblyIds[0] }
168+
} as RootStructureDefinition.Params);
169+
}
170+
} else {
171+
if (p.assemblyId && p.assemblyId !== '' && p.assemblyId !== '0') {
172+
Object.assign(structureParams, {
173+
name: 'assembly',
174+
params: { id: p.assemblyId }
175+
} as RootStructureDefinition.Params);
176+
}
177+
}
178+
164179
let structure: StructureObject | undefined = undefined;
165180
let structureProperties: StructureObject | undefined = undefined;
166181
let unitcell: StateObjectSelector | undefined = undefined;
@@ -232,7 +247,7 @@ export const RcsbPreset = TrajectoryHierarchyPresetProvider({
232247
} else if (p.kind === 'motif' && structure?.obj) {
233248
// let's force ASM_1 for motifs (as we use this contract in the rest of the stack)
234249
// TODO should ASM_1 be the default, seems like we'd run into problems when selecting ligands that are e.g. ambiguous with asym_id & seq_id alone?
235-
const targets = normalizeTargets(p.targets, structure!.obj.data);
250+
const targets = p.targets.map(t => normalizeTarget(t, structure!.obj!.data));
236251
let selectionExpressions = createSelectionExpressions(p.label || model.data!.entryId, targets);
237252
const globalExpressions = createSelectionExpressions(p.label || model.data!.entryId);
238253
selectionExpressions = selectionExpressions.concat(globalExpressions.map(e => { return { ...e, alpha: 0.21 }; }));
@@ -359,62 +374,15 @@ function determineAssemblyId(traj: any, p: MotifProps) {
359374
// nothing to do if assembly is known
360375
if (p.assemblyId && p.assemblyId !== '' && p.assemblyId !== '0') return;
361376

362-
function equals(expr: string, val: string): boolean {
363-
const list = parseOperatorList(expr);
364-
const split = val.split('x');
365-
let matches = 0;
366-
for (let i = 0, il = Math.min(list.length, split.length); i < il; i++) {
367-
if (list[i].indexOf(split[i]) !== -1) matches++;
368-
}
369-
return matches === split.length;
370-
}
371-
372-
function parseOperatorList(value: string): string[][] {
373-
// '(X0)(1-5)' becomes [['X0'], ['1', '2', '3', '4', '5']]
374-
// kudos to Glen van Ginkel.
375-
376-
const oeRegex = /\(?([^()]+)\)?]*/g, groups: string[] = [], ret: string[][] = [];
377-
378-
let g: any;
379-
while (g = oeRegex.exec(value)) groups[groups.length] = g[1];
380-
381-
groups.forEach(g => {
382-
const group: string[] = [];
383-
g.split(',').forEach(e => {
384-
const dashIndex = e.indexOf('-');
385-
if (dashIndex > 0) {
386-
const from = parseInt(e.substring(0, dashIndex)), to = parseInt(e.substring(dashIndex + 1));
387-
for (let i = from; i <= to; i++) group[group.length] = i.toString();
388-
} else {
389-
group[group.length] = e.trim();
390-
}
391-
});
392-
ret[ret.length] = group;
393-
});
394-
395-
return ret;
396-
}
397-
398377
// set of provided [structOperId, labelAsymId] combinations
399378
const ids = p.targets.map(t => [t.structOperId || '1', t.labelAsymId!]).filter((x, i, a) => a.indexOf(x) === i);
400-
401379
try {
402-
// find first assembly that contains all requested structOperIds - if multiple, the first will be returned
403380
const pdbx_struct_assembly_gen = traj.obj.data.representative.sourceData.data.frame.categories.pdbx_struct_assembly_gen;
404-
if (pdbx_struct_assembly_gen) {
405-
const assembly_id = pdbx_struct_assembly_gen.getField('assembly_id');
406-
const oper_expression = pdbx_struct_assembly_gen.getField('oper_expression');
407-
const asym_id_list = pdbx_struct_assembly_gen.getField('asym_id_list');
408-
409-
for (let i = 0, il = pdbx_struct_assembly_gen.rowCount; i < il; i++) {
410-
if (ids.some(val => !equals(oper_expression.str(i), val[0]) || asym_id_list.str(i).indexOf(val[1]) === -1)) continue;
411-
412-
Object.assign(p, { assemblyId: assembly_id.str(i) });
413-
return;
414-
}
415-
} else {
416-
// this happens e.g. for AlphaFold structures
417-
console.warn(`Source file is missing 'pdbx_struct_assembly_gen' category`);
381+
// find first assembly that contains all requested structOperIds - if multiple, the first will be returned
382+
const assemblyId = firstMatchingAssemblyId(pdbx_struct_assembly_gen, ids);
383+
if (assemblyId) {
384+
Object.assign(p, { assemblyId: assemblyId });
385+
return;
418386
}
419387
} catch (error) {
420388
console.warn(error);
@@ -466,3 +434,4 @@ async function initVolumeStreaming(plugin: PluginContext, structure: StructureOb
466434
volume: false
467435
});
468436
}
437+

0 commit comments

Comments
 (0)