@e-infra/react-molstar-wrapper 0.0.8 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,6 +5,52 @@
5
5
 
6
6
  A React wrapper for the Mol* molecular visualization library, providing seamless integration of molecular structure visualization capabilities into React applications.
7
7
 
8
+ ## Table of Contents
9
+
10
+ - [Features](#features)
11
+ - [Overview](#overview)
12
+ - [Installation](#installation)
13
+ - [Usage](#usage)
14
+ - [React](#react)
15
+ - [Next.js Integration](#nextjs-integration)
16
+ - [Important Notice](#important-notice)
17
+ - [Props](#props)
18
+ - [Data Source (Required)](#data-source-required)
19
+ - [Configuration](#configuration)
20
+ - [Animation Controls](#animation-controls)
21
+ - [ViewerRef Methods](#viewerref-methods)
22
+ - [Protein Type](#protein-type)
23
+ - [Examples](#examples)
24
+ - [Basic Example](#basic-example)
25
+ - [Example with Multiple Proteins and Custom Styling](#example-with-multiple-proteins-and-custom-styling)
26
+ - [Example with Ref Methods](#example-with-ref-methods)
27
+ - [Example with Rock Animation](#example-with-rock-animation)
28
+ - [Example with Local File](#example-with-local-file)
29
+ - [Example with Custom Model Source URLs](#example-with-custom-model-source-urls)
30
+ - [Advanced Usage](#advanced-usage)
31
+ - [Using Precomputed MVS Data](#using-precomputed-mvs-data)
32
+ - [Combining Multiple Features](#combining-multiple-features)
33
+ - [Documentation](#documentation)
34
+ - [License](#license)
35
+
36
+ ## Features
37
+
38
+ - **Molecular Visualization** - Display 3D molecular structures using the Mol* visualization engine
39
+ - **React Component** - Simple React component with minimal setup
40
+ - **Multiple Data Sources** - Load proteins from UniProt IDs or local files (PDB, CIF formats)
41
+ - **Customizable Appearance** - Configure background colors, labels, and UI presets (minimal, standard, expanded)
42
+ - **Animations** - Support for spin and rock animations with configurable speeds
43
+ - **Domain Highlighting** - Focus on specific protein domains using chopping data
44
+ - **Transform Controls** - Update protein superposition with custom rotation and translation
45
+ - **Imperative API** - Access viewer methods via forwarded ref for programmatic control
46
+ - **Next.js Compatible** - Works with Next.js using dynamic imports
47
+ - **Plugin Lifecycle Management** - Shared plugin instance with reference counting and automatic garbage collection
48
+ - **Multiple Representations** - Choose from cartoon, ball-and-stick, spacefill, line, surface, or backbone representations
49
+ - **Custom Model Sources** - Configure custom URLs for remote model retrieval
50
+ - **Precomputed MVS Support** - Load precomputed Mol* View State data directly
51
+ - **Styling Support** - Apply custom CSS classes and heights to the viewer container
52
+ - **Responsive Design** - Viewer fills available height by default, with optional explicit sizing
53
+
8
54
  ## Overview
9
55
 
10
56
  This package provides a lightweight React wrapper around the [Mol*](https://molstar.org/) molecular visualization library, enabling developers to easily integrate 3D molecular structure visualization into their React applications.
@@ -52,37 +98,416 @@ const Viewer = dynamic(
52
98
 
53
99
  This approach prevents errors related to `document` not being defined during server-side rendering, as Mol* expects the DOM to be available during initialization.
54
100
 
55
- ### Notice
101
+ ### Important Notice
102
+
103
+ > [!IMPORTANT]
104
+ > It is important to include library styles as well! Otherwise loader and error view will be broken.
105
+ > ```typescript
106
+ > import "react-molstar-wrapper/style.css";
107
+ > ```
108
+
109
+ ## Props
110
+
111
+ The `Viewer` component accepts the following props:
112
+
113
+ ### Data Source (Required)
114
+
115
+ Exactly one of these must be provided:
116
+
117
+ | Prop | Type | Description |
118
+ |------|------|-------------|
119
+ | `proteins` | `Protein[]` | Array of protein objects to visualize. When provided, the component will call `createMVS` to compute the view state. |
120
+ | `mvs` | `MVSData` | Precomputed MVS (Mol* View State) data. If provided, the viewer loads this directly without calling `createMVS`. |
121
+
122
+ ### Configuration
123
+
124
+ | Prop | Type | Default | Description |
125
+ |------|------|---------|-------------|
126
+ | `modelSourceUrls` | `Partial<ModelSourceUrls>` | `undefined` | Optional lookup mapping used by `createMVS` to resolve model source URLs for remote model retrieval when `proteins` is used. Format: `{ uniProtId: (id: string) => string }`. |
127
+ | `initialUI` | `"minimal" \| "standard" \| "expanded"` | `"standard"` | Which initial UI preset to use for the embedded plugin. Controls the visibility of control chrome. |
128
+ | `bgColor` | `ColorHEX` | `"#ffffff"` | Background color for the viewer (any valid CSS hex color). |
129
+ | `labels` | `boolean` | `true` | Whether to show labels in the viewer. |
130
+ | `height` | `number` | `undefined` | Optional explicit height (in pixels) for the outer wrapper. If omitted, the wrapper will fill available height (`100%`). |
131
+ | `className` | `string` | `undefined` | Optional CSS class to apply to the outer wrapper. |
132
+
133
+ ### Animation Controls
134
+
135
+ At most one of these may be provided:
136
+
137
+ | Prop | Type | Default | Description |
138
+ |------|------|---------|-------------|
139
+ | `spin` | `boolean` | `false` | Whether to enable continuous spin animation. Mutually exclusive with `rock`. |
140
+ | `rock` | `boolean` | `false` | Whether to enable rock animation (back-and-forth). Mutually exclusive with `spin`. |
141
+ | `spinSpeed` | `number` | `0.05` | Speed multiplier for the spin animation. |
142
+ | `rockSpeed` | `number` | `0.2` | Speed multiplier for the rock animation. |
143
+
144
+ ## ViewerRef Methods
145
+
146
+ The component forwards a ref exposing the following async methods:
147
+
148
+ ### `highlight(proteinIndex: number, label: string): Promise<void>`
149
+
150
+ Focuses/highlights a domain within the specified protein by matching the label against the protein's chopping data. The domain's first range (start/end) is used to focus the view.
151
+
152
+ - **Parameters:**
153
+ - `proteinIndex` - Index of the protein in the `proteins` array
154
+ - `label` - Label of the domain to highlight (must match a label in the protein's `chopping` data)
155
+
156
+ - **Behavior:** No-op if plugin, proteins, or matching domain is not available.
157
+
158
+ ### `reset(): Promise<void>`
159
+
160
+ Resets the plugin's view to its default/original pose.
161
+
162
+ ### `updateSuperposition(proteinIndex: number, translation?, rotation?): Promise<void>`
163
+
164
+ Updates the transform for a loaded structure (protein) without reloading the entire scene.
165
+
166
+ - **Parameters:**
167
+ - `proteinIndex` - Index of the protein to update
168
+ - `translation` - Optional `[x, y, z]` tuple for translation
169
+ - `rotation` - Optional 3x3 matrix represented as `[[r11,r12,r13],[r21,r22,r23],[r31,r32,r33]]`
170
+
171
+ - **Behavior:** This method relies on the Mol* plugin API to update transforms in-place.
172
+
173
+ ## Protein Type
174
+
175
+ The `Protein` type represents a protein structure to visualize:
176
+
177
+ ```typescript
178
+ type Protein = {
179
+ // Exactly one of these must be provided
180
+ uniProtId?: string; // UniProt ID for remote fetching
181
+ file?: File; // Local file to load
182
+
183
+ // Optional properties
184
+ chain?: string; // Chain identifier
185
+ superposition?: {
186
+ rotation: Matrix3D; // 3x3 rotation matrix
187
+ translation: Vector3D; // [x, y, z] translation vector
188
+ };
189
+ chopping?: Chopping[]; // Domain definitions for highlighting
190
+ representation?: "cartoon" | "ball_and_stick" | "spacefill" | "line" | "surface" | "backbone";
191
+ };
192
+ ```
193
+
194
+ ### Chopping Type
195
+
196
+ ```typescript
197
+ type Chopping = {
198
+ label: string; // Domain label for identification
199
+ showLabel?: boolean; // Whether to show the label in the viewer
200
+ ranges: {
201
+ start: number; // Residue start position
202
+ end: number; // Residue end position
203
+ }[];
204
+ }[];
205
+ ```
206
+
207
+ ## Examples
208
+
209
+ ### Basic Example
210
+
211
+ ```typescript
212
+ import "react-molstar-wrapper/style.css";
213
+ import Viewer from "react-molstar-wrapper";
214
+ import type { Protein } from "react-molstar-wrapper";
215
+
216
+ const proteins: Protein[] = [
217
+ {
218
+ uniProtId: "P12345",
219
+ },
220
+ ];
221
+
222
+ function App() {
223
+ return (
224
+ <Viewer
225
+ proteins={proteins}
226
+ spin={true}
227
+ />
228
+ );
229
+ }
230
+ ```
231
+
232
+ ### Example with Multiple Proteins and Custom Styling
233
+
234
+ ```typescript
235
+ import "react-molstar-wrapper/style.css";
236
+ import Viewer from "react-molstar-wrapper";
237
+ import type { Protein } from "react-molstar-wrapper";
238
+
239
+ const proteins: Protein[] = [
240
+ {
241
+ uniProtId: "P12345",
242
+ representation: "cartoon",
243
+ chopping: [
244
+ {
245
+ label: "Domain A",
246
+ ranges: [{ start: 1, end: 100 }],
247
+ },
248
+ {
249
+ label: "Domain B",
250
+ ranges: [{ start: 101, end: 200 }],
251
+ },
252
+ ],
253
+ },
254
+ {
255
+ uniProtId: "P67890",
256
+ representation: "ball_and_stick",
257
+ superposition: {
258
+ rotation: [
259
+ [1, 0, 0],
260
+ [0, 1, 0],
261
+ [0, 0, 1],
262
+ ],
263
+ translation: [10, 0, 0],
264
+ },
265
+ },
266
+ ];
267
+
268
+ function App() {
269
+ return (
270
+ <Viewer
271
+ proteins={proteins}
272
+ initialUI="minimal"
273
+ bgColor="#1a1a2e"
274
+ labels={true}
275
+ height={600}
276
+ className="my-viewer"
277
+ />
278
+ );
279
+ }
280
+ ```
56
281
 
57
- It is important to include library styles as well! Otherwise loader and error view will be broken.
282
+ ### Example with Ref Methods
58
283
 
59
284
  ```typescript
60
285
  import "react-molstar-wrapper/style.css";
286
+ import Viewer, { type ViewerRef } from "react-molstar-wrapper";
287
+ import type { Protein } from "react-molstar-wrapper";
288
+ import { useRef } from "react";
289
+
290
+ const proteins: Protein[] = [
291
+ {
292
+ uniProtId: "P12345",
293
+ chopping: [
294
+ {
295
+ label: "Active Site",
296
+ ranges: [{ start: 50, end: 75 }],
297
+ },
298
+ ],
299
+ },
300
+ ];
301
+
302
+ function App() {
303
+ const viewerRef = useRef<ViewerRef | null>(null);
304
+
305
+ const handleHighlight = async () => {
306
+ await viewerRef.current?.highlight(0, "Active Site");
307
+ };
308
+
309
+ const handleReset = async () => {
310
+ await viewerRef.current?.reset();
311
+ };
312
+
313
+ const handleUpdateTransform = async () => {
314
+ await viewerRef.current?.updateSuperposition(
315
+ 0,
316
+ [5, 0, 0],
317
+ [
318
+ [1, 0, 0],
319
+ [0, 1, 0],
320
+ [0, 0, 1],
321
+ ]
322
+ );
323
+ };
324
+
325
+ return (
326
+ <div>
327
+ <div style={{ marginBottom: "10px" }}>
328
+ <button onClick={handleHighlight}>Highlight Active Site</button>
329
+ <button onClick={handleReset}>Reset View</button>
330
+ <button onClick={handleUpdateTransform}>Update Transform</button>
331
+ </div>
332
+ <Viewer
333
+ ref={viewerRef}
334
+ proteins={proteins}
335
+ bgColor="#ffffff"
336
+ height={500}
337
+ />
338
+ </div>
339
+ );
340
+ }
61
341
  ```
62
342
 
63
- ### Example
343
+ ### Example with Rock Animation
64
344
 
65
345
  ```typescript
346
+ import "react-molstar-wrapper/style.css";
347
+ import Viewer from "react-molstar-wrapper";
348
+ import type { Protein } from "react-molstar-wrapper";
349
+
350
+ const proteins: Protein[] = [
351
+ {
352
+ uniProtId: "P12345",
353
+ },
354
+ ];
355
+
356
+ function App() {
357
+ return (
358
+ <Viewer
359
+ proteins={proteins}
360
+ rock={true}
361
+ rockSpeed={0.3}
362
+ bgColor="#f0f0f0"
363
+ />
364
+ );
365
+ }
366
+ ```
367
+
368
+ ### Example with Local File
66
369
 
370
+ ```typescript
371
+ import "react-molstar-wrapper/style.css";
372
+ import Viewer from "react-molstar-wrapper";
373
+ import type { Protein } from "react-molstar-wrapper";
374
+ import { useState } from "react";
375
+
376
+ function App() {
377
+ const [protein, setProtein] = useState<Protein | undefined>();
378
+
379
+ const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
380
+ const file = event.target.files?.[0];
381
+ if (file) {
382
+ setProtein({ file });
383
+ }
384
+ };
385
+
386
+ return (
387
+ <div>
388
+ <input type="file" onChange={handleFileChange} accept=".pdb,.cif" />
389
+ {protein && (
390
+ <Viewer
391
+ proteins={[protein]}
392
+ height={500}
393
+ />
394
+ )}
395
+ </div>
396
+ );
397
+ }
398
+ ```
399
+
400
+ ### Example with Custom Model Source URLs
401
+
402
+ ```typescript
67
403
  import "react-molstar-wrapper/style.css";
68
404
  import Viewer from "react-molstar-wrapper";
69
405
  import type { Protein } from "react-molstar-wrapper";
70
406
 
71
407
  const proteins: Protein[] = [
72
408
  {
73
- uniProtId: "P12345";
409
+ uniProtId: "P12345",
74
410
  },
75
411
  ];
76
412
 
77
- <Viewer
78
- proteins={proteins}
79
- spin={true}
80
- />
413
+ const modelSourceUrls = {
414
+ uniProtId: (id: string) => `https://api.example.com/protein/${id}`,
415
+ };
416
+
417
+ function App() {
418
+ return (
419
+ <Viewer
420
+ proteins={proteins}
421
+ modelSourceUrls={modelSourceUrls}
422
+ bgColor="#ffffff"
423
+ />
424
+ );
425
+ }
81
426
  ```
82
427
 
83
428
  ## Advanced Usage
84
429
 
85
- (TODO)
430
+ ### Using Precomputed MVS Data
431
+
432
+ If you have precomputed MVS (Mol* View State) data, you can pass it directly to the viewer:
433
+
434
+ ```typescript
435
+ import "react-molstar-wrapper/style.css";
436
+ import Viewer from "react-molstar-wrapper";
437
+ import type { MVSData } from "molstar/lib/extensions/mvs/mvs-data.d.ts";
438
+
439
+ const mvsData: MVSData = {
440
+ // Your precomputed MVS data
441
+ };
442
+
443
+ function App() {
444
+ return (
445
+ <Viewer
446
+ mvs={mvsData}
447
+ bgColor="#ffffff"
448
+ />
449
+ );
450
+ }
451
+ ```
452
+
453
+ ### Combining Multiple Features
454
+
455
+ ```typescript
456
+ import "react-molstar-wrapper/style.css";
457
+ import Viewer, { type ViewerRef } from "react-molstar-wrapper";
458
+ import type { Protein } from "react-molstar-wrapper";
459
+ import { useRef, useEffect } from "react";
460
+
461
+ const proteins: Protein[] = [
462
+ {
463
+ uniProtId: "P12345",
464
+ representation: "cartoon",
465
+ chopping: [
466
+ { label: "N-terminal", ranges: [{ start: 1, end: 50 }] },
467
+ { label: "Core", ranges: [{ start: 51, end: 150 }] },
468
+ { label: "C-terminal", ranges: [{ start: 151, end: 200 }] },
469
+ ],
470
+ },
471
+ {
472
+ uniProtId: "P67890",
473
+ representation: "surface",
474
+ superposition: {
475
+ rotation: [
476
+ [0.9, -0.1, 0],
477
+ [0.1, 0.9, 0],
478
+ [0, 0, 1],
479
+ ],
480
+ translation: [15, 0, 0],
481
+ },
482
+ },
483
+ ];
484
+
485
+ function App() {
486
+ const viewerRef = useRef<ViewerRef | null>(null);
487
+
488
+ // Auto-highlight a domain after viewer loads
489
+ useEffect(() => {
490
+ const timer = setTimeout(async () => {
491
+ await viewerRef.current?.highlight(0, "Core");
492
+ }, 2000);
493
+
494
+ return () => clearTimeout(timer);
495
+ }, []);
496
+
497
+ return (
498
+ <Viewer
499
+ ref={viewerRef}
500
+ proteins={proteins}
501
+ initialUI="expanded"
502
+ bgColor="#0d1117"
503
+ labels={true}
504
+ spin={false}
505
+ height={700}
506
+ className="custom-viewer"
507
+ />
508
+ );
509
+ }
510
+ ```
86
511
 
87
512
  ## Documentation
88
513
 
@@ -92,3 +517,7 @@ const proteins: Protein[] = [
92
517
  ## License
93
518
 
94
519
  See [LICENSE.md](LICENSE.md) for details.
520
+
521
+ ## Acknowledgments
522
+
523
+ This library is built on top of [Mol*](https://molstar.org/), an open-source molecular visualization toolkit.
package/dist/index.cjs.js CHANGED
@@ -478,6 +478,8 @@ function ErrorView() {
478
478
  class Plugin {
479
479
  plugin;
480
480
  objectUrls;
481
+ labelsVisible = true;
482
+ storedLabels = [];
481
483
  constructor(plugin) {
482
484
  this.plugin = plugin;
483
485
  this.objectUrls = /* @__PURE__ */ new Set();
@@ -557,6 +559,60 @@ class Plugin {
557
559
  renderer: { backgroundColor: color.Color.fromHexString(hexString) }
558
560
  });
559
561
  }
562
+ /**
563
+ * Set the visibility of labels by removing or adding them to the state tree.
564
+ * This approach is more reliable than trying to modify the isHidden property.
565
+ */
566
+ async setLabelsVisibility(visible) {
567
+ if (this.labelsVisible === visible) {
568
+ return;
569
+ }
570
+ try {
571
+ const state = this.plugin.state.data;
572
+ if (visible) {
573
+ if (this.storedLabels.length > 0) {
574
+ const update = state.build();
575
+ for (const storedLabel of this.storedLabels) {
576
+ const parentCell = state.cells.get(storedLabel.parentRef);
577
+ if (!parentCell) continue;
578
+ update.to(storedLabel.parentRef).apply(
579
+ storedLabel.transform.transformer,
580
+ storedLabel.transform.params
581
+ );
582
+ }
583
+ await this.plugin.runTask(state.updateTree(update));
584
+ }
585
+ this.labelsVisible = true;
586
+ } else {
587
+ const allObjects = state.selectQ((q) => q.root.subtree());
588
+ const labels = allObjects.filter((cell) => {
589
+ if (!cell.obj) return false;
590
+ const label = cell.obj.label || "";
591
+ const description = cell.obj.description || "";
592
+ return label === "MVS Custom Label" || description === "MVS Custom Label";
593
+ });
594
+ this.storedLabels = [];
595
+ if (labels.length > 0) {
596
+ const update = state.build();
597
+ for (const labelCell of labels) {
598
+ const parentRef = labelCell.transform.parent;
599
+ this.storedLabels.push({
600
+ parentRef,
601
+ transform: {
602
+ transformer: labelCell.transform.transformer,
603
+ params: labelCell.transform.params
604
+ }
605
+ });
606
+ update.delete(labelCell.transform.ref);
607
+ }
608
+ await this.plugin.runTask(state.updateTree(update));
609
+ }
610
+ this.labelsVisible = false;
611
+ }
612
+ } catch (e) {
613
+ console.error("Error setting label visibility:", e);
614
+ }
615
+ }
560
616
  setAnimation(type, speed) {
561
617
  if (type === "off") {
562
618
  this.plugin.canvas3d?.setProps({
@@ -584,15 +640,15 @@ class Plugin {
584
640
  }
585
641
  });
586
642
  }
587
- async focusOnDomain(domainStart, domainEnd) {
643
+ async focusOnDomain(domainStart, domainEnd, proteinIndex = 0) {
588
644
  const state = this.plugin.state.data;
589
645
  const structures = state.selectQ(
590
646
  (q) => q.rootsOfType(objects.PluginStateObject.Molecule.Structure)
591
647
  );
592
- if (structures.length === 0) {
648
+ if (structures.length === 0 || proteinIndex >= structures.length) {
593
649
  return;
594
650
  }
595
- const structureCell = structures[0];
651
+ const structureCell = structures[proteinIndex];
596
652
  if (!structureCell?.obj) {
597
653
  return;
598
654
  }
@@ -773,7 +829,7 @@ function normalizeChoppingData(chopping) {
773
829
  }
774
830
  return chopping.map((entry) => ({
775
831
  label: entry.label,
776
- showLabel: entry.showLabel,
832
+ showLabel: entry.showLabel ?? true,
777
833
  ranges: entry.ranges.map((range) => ({
778
834
  start: Math.min(range.start, range.end),
779
835
  end: Math.max(range.start, range.end)
@@ -1109,7 +1165,8 @@ const Viewer = require$$0.forwardRef(function Viewer2({
1109
1165
  rock = false,
1110
1166
  rockSpeed = 0.2,
1111
1167
  height,
1112
- className
1168
+ className,
1169
+ labels = true
1113
1170
  }, ref) {
1114
1171
  const containerRef = require$$0.useRef(null);
1115
1172
  const pluginRef = require$$0.useRef(null);
@@ -1132,7 +1189,7 @@ const Viewer = require$$0.forwardRef(function Viewer2({
1132
1189
  const start = domain?.ranges[0]?.start;
1133
1190
  const end = domain?.ranges[0]?.end;
1134
1191
  if (start !== void 0 && end !== void 0) {
1135
- await pluginRef.current.focusOnDomain(start, end);
1192
+ await pluginRef.current.focusOnDomain(start, end, proteinIndex);
1136
1193
  }
1137
1194
  },
1138
1195
  reset: async () => {
@@ -1229,6 +1286,14 @@ const Viewer = require$$0.forwardRef(function Viewer2({
1229
1286
  pluginRef.current.setBackgroundColor(bgColor);
1230
1287
  }
1231
1288
  }, [state, bgColor]);
1289
+ require$$0.useEffect(() => {
1290
+ if (state !== "success" || !pluginRef.current) {
1291
+ return;
1292
+ }
1293
+ pluginRef.current.setLabelsVisibility(labels).catch((error) => {
1294
+ console.error("Error setting label visibility:", error);
1295
+ });
1296
+ }, [state, labels]);
1232
1297
  const styles = {
1233
1298
  height: height ? `${height}px` : "100%",
1234
1299
  position: "relative"