react-cheminfo 0.39.0 → 0.40.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 (104) hide show
  1. package/bin/check-deploy.mjs +4 -2
  2. package/lib/deploy/core/checkDeploy.d.ts +3 -1
  3. package/lib/deploy/core/checkDeploy.d.ts.map +1 -1
  4. package/lib/deploy/core/checkDeploy.js +8 -2
  5. package/lib/deploy/core/checkDeploy.js.map +1 -1
  6. package/lib/deploy/core/index.d.ts +1 -0
  7. package/lib/deploy/core/index.d.ts.map +1 -1
  8. package/lib/deploy/core/index.js +1 -0
  9. package/lib/deploy/core/index.js.map +1 -1
  10. package/lib/deploy/core/publishedImage.d.ts +30 -0
  11. package/lib/deploy/core/publishedImage.d.ts.map +1 -0
  12. package/lib/deploy/core/publishedImage.js +96 -0
  13. package/lib/deploy/core/publishedImage.js.map +1 -0
  14. package/lib/deploy/core/types.d.ts +1 -1
  15. package/lib/deploy/core/types.d.ts.map +1 -1
  16. package/lib/deploy/core/types.js.map +1 -1
  17. package/lib/molecule3d/ui/Molecule3DToolbar.d.ts +9 -7
  18. package/lib/molecule3d/ui/Molecule3DToolbar.d.ts.map +1 -1
  19. package/lib/molecule3d/ui/Molecule3DToolbar.js +13 -8
  20. package/lib/molecule3d/ui/Molecule3DToolbar.js.map +1 -1
  21. package/lib/molecule3d/ui/MoleculeCanvas3D.js +1 -1
  22. package/lib/molecule3d/ui/MoleculeCanvas3D.js.map +1 -1
  23. package/lib/molecule3d/ui/camera.d.ts +0 -11
  24. package/lib/molecule3d/ui/camera.d.ts.map +1 -1
  25. package/lib/molecule3d/ui/camera.js +6 -80
  26. package/lib/molecule3d/ui/camera.js.map +1 -1
  27. package/lib/molecule3d/ui/exportMoleculeImage.d.ts +6 -2
  28. package/lib/molecule3d/ui/exportMoleculeImage.d.ts.map +1 -1
  29. package/lib/molecule3d/ui/exportMoleculeImage.js.map +1 -1
  30. package/lib/molecule3d/ui/index.d.ts +9 -0
  31. package/lib/molecule3d/ui/index.d.ts.map +1 -1
  32. package/lib/molecule3d/ui/index.js +5 -0
  33. package/lib/molecule3d/ui/index.js.map +1 -1
  34. package/lib/molecule3d/ui/measurements.d.ts +22 -2
  35. package/lib/molecule3d/ui/measurements.d.ts.map +1 -1
  36. package/lib/molecule3d/ui/measurements.js +25 -3
  37. package/lib/molecule3d/ui/measurements.js.map +1 -1
  38. package/lib/molecule3d/ui/useImageExport.d.ts +2 -2
  39. package/lib/molecule3d/ui/useImageExport.d.ts.map +1 -1
  40. package/lib/molecule3d/ui/useImageExport.js.map +1 -1
  41. package/lib/molecule3d/ui/viewer.d.ts.map +1 -1
  42. package/lib/molecule3d/ui/viewer.js +16 -49
  43. package/lib/molecule3d/ui/viewer.js.map +1 -1
  44. package/lib/molstar/core/camera.d.ts +37 -0
  45. package/lib/molstar/core/camera.d.ts.map +1 -0
  46. package/lib/molstar/core/camera.js +49 -0
  47. package/lib/molstar/core/camera.js.map +1 -0
  48. package/lib/molstar/core/cameraPose.d.ts +54 -0
  49. package/lib/molstar/core/cameraPose.d.ts.map +1 -0
  50. package/lib/molstar/core/cameraPose.js +78 -0
  51. package/lib/molstar/core/cameraPose.js.map +1 -0
  52. package/lib/molstar/core/hover.d.ts +49 -0
  53. package/lib/molstar/core/hover.d.ts.map +1 -0
  54. package/lib/molstar/core/hover.js +91 -0
  55. package/lib/molstar/core/hover.js.map +1 -0
  56. package/lib/molstar/core/index.d.ts +7 -0
  57. package/lib/molstar/core/index.d.ts.map +1 -0
  58. package/lib/molstar/core/index.js +5 -0
  59. package/lib/molstar/core/index.js.map +1 -0
  60. package/lib/molstar/core/plugin.d.ts +80 -0
  61. package/lib/molstar/core/plugin.d.ts.map +1 -0
  62. package/lib/molstar/core/plugin.js +125 -0
  63. package/lib/molstar/core/plugin.js.map +1 -0
  64. package/lib/molstar.d.ts +18 -0
  65. package/lib/molstar.d.ts.map +1 -0
  66. package/lib/molstar.js +17 -0
  67. package/lib/molstar.js.map +1 -0
  68. package/lib/share/ui/SharePreview.d.ts.map +1 -1
  69. package/lib/share/ui/SharePreview.js +25 -0
  70. package/lib/share/ui/SharePreview.js.map +1 -1
  71. package/lib/share/ui/useShareRegionReports.d.ts.map +1 -1
  72. package/lib/share/ui/useShareRegionReports.js +41 -12
  73. package/lib/share/ui/useShareRegionReports.js.map +1 -1
  74. package/lib/structure/ui/ConformerTable.d.ts +8 -0
  75. package/lib/structure/ui/ConformerTable.d.ts.map +1 -1
  76. package/lib/structure/ui/ConformerTable.js +24 -4
  77. package/lib/structure/ui/ConformerTable.js.map +1 -1
  78. package/package.json +2 -1
  79. package/src/deploy/core/checkDeploy.ts +14 -2
  80. package/src/deploy/core/index.ts +5 -0
  81. package/src/deploy/core/publishedImage.ts +110 -0
  82. package/src/deploy/core/types.ts +3 -1
  83. package/src/molecule3d/ui/Molecule3DToolbar.tsx +23 -16
  84. package/src/molecule3d/ui/MoleculeCanvas3D.tsx +1 -1
  85. package/src/molecule3d/ui/camera.ts +18 -93
  86. package/src/molecule3d/ui/exportMoleculeImage.ts +6 -2
  87. package/src/molecule3d/ui/index.ts +12 -0
  88. package/src/molecule3d/ui/measurements.ts +42 -3
  89. package/src/molecule3d/ui/useImageExport.ts +2 -2
  90. package/src/molecule3d/ui/viewer.ts +21 -51
  91. package/src/molstar/core/camera.ts +61 -0
  92. package/src/molstar/core/cameraPose.ts +126 -0
  93. package/src/molstar/core/hover.ts +109 -0
  94. package/src/molstar/core/index.ts +13 -0
  95. package/src/molstar/core/plugin.ts +159 -0
  96. package/src/molstar.ts +22 -0
  97. package/src/share/ui/SharePreview.tsx +31 -0
  98. package/src/share/ui/useShareRegionReports.ts +39 -15
  99. package/src/structure/ui/ConformerTable.tsx +38 -4
  100. package/lib/molecule3d/ui/viewerSpec.d.ts +0 -17
  101. package/lib/molecule3d/ui/viewerSpec.d.ts.map +0 -1
  102. package/lib/molecule3d/ui/viewerSpec.js +0 -34
  103. package/lib/molecule3d/ui/viewerSpec.js.map +0 -1
  104. package/src/molecule3d/ui/viewerSpec.ts +0 -38
@@ -10,10 +10,13 @@
10
10
  * measurement.
11
11
  */
12
12
 
13
- import { OrderedSet } from 'molstar/lib/mol-data/int.js';
13
+ import { OrderedSet, SortedArray } from 'molstar/lib/mol-data/int.js';
14
14
  import type { Loci } from 'molstar/lib/mol-model/loci.js';
15
15
  import type { UnitIndex } from 'molstar/lib/mol-model/structure/structure/element/util.js';
16
- import type { Structure } from 'molstar/lib/mol-model/structure.js';
16
+ import type {
17
+ ElementIndex,
18
+ Structure,
19
+ } from 'molstar/lib/mol-model/structure.js';
17
20
  import { StructureElement } from 'molstar/lib/mol-model/structure.js';
18
21
  import type { PluginContext } from 'molstar/lib/mol-plugin/context.js';
19
22
  import { MeasurementGroupTag } from 'molstar/lib/mol-plugin-state/manager/structure/measurement.js';
@@ -31,19 +34,32 @@ import { moleculeStructure } from './renderMolecule.ts';
31
34
  // Lowercased: it is a factory, not a constructor.
32
35
  const elementLoci = StructureElement.Loci;
33
36
 
37
+ /**
38
+ * Where the atoms a measurement addresses are found in the plugin's state.
39
+ *
40
+ * A site that mounts a molstar plugin of its own builds its scene under refs of
41
+ * its own, so it says here which structure the clicks are read against; the
42
+ * default is the one `<MoleculeViewer3D>` draws.
43
+ */
44
+ export type MoleculeStructureSource = (
45
+ plugin: PluginContext,
46
+ ) => Structure | undefined;
47
+
34
48
  /**
35
49
  * Draw exactly `measurements` over the current molecule. An atom the structure
36
50
  * does not have skips its measurement rather than failing the scene.
37
51
  * @param plugin - The molstar context.
38
52
  * @param measurements - Everything to show, replacing what is shown.
53
+ * @param structureOf - See {@link MoleculeStructureSource}.
39
54
  * @returns Nothing; resolves once every label is on screen.
40
55
  */
41
56
  export async function renderMeasurements(
42
57
  plugin: PluginContext,
43
58
  measurements: readonly Measurement[],
59
+ structureOf: MoleculeStructureSource = moleculeStructure,
44
60
  ): Promise<void> {
45
61
  await clearMeasurements(plugin);
46
- const structure = moleculeStructure(plugin);
62
+ const structure = structureOf(plugin);
47
63
  if (structure === undefined) return;
48
64
  const manager = plugin.managers.structure.measurement;
49
65
  for (const measurement of measurements) {
@@ -143,6 +159,29 @@ export class MeasurementPicker {
143
159
  }
144
160
  }
145
161
 
162
+ /**
163
+ * Address one atom of a structure the way a {@link Measurement} does.
164
+ *
165
+ * A site that builds its own scene numbers atoms by their position in the file
166
+ * it drew; this is how that number becomes a reference a measurement keeps.
167
+ * @param structure - The structure on screen.
168
+ * @param element - The atom's index in the model, i.e. its position in the file.
169
+ * @returns The reference, or `undefined` when the structure has no such atom.
170
+ */
171
+ export function atomReferenceOf(
172
+ structure: Structure,
173
+ element: ElementIndex,
174
+ ): AtomReference | undefined {
175
+ const units = structure.units;
176
+ for (let unit = 0; unit < units.length; unit++) {
177
+ const elements = units[unit]?.elements;
178
+ if (elements === undefined) continue;
179
+ const index = SortedArray.indexOf(elements, element);
180
+ if (index !== -1) return { unit, element: index };
181
+ }
182
+ return undefined;
183
+ }
184
+
146
185
  function atomReference(
147
186
  structure: Structure,
148
187
  loci: Loci,
@@ -4,8 +4,8 @@ import { useCallback } from 'react';
4
4
  import type { FigureFormat } from '../../download/core/downloadFigure.ts';
5
5
  import type { FigurePixels } from '../../download/core/figureScale.ts';
6
6
 
7
+ import type { SceneCapture } from './exportMoleculeImage.ts';
7
8
  import { exportMoleculeImage } from './exportMoleculeImage.ts';
8
- import type { Molecule3DViewer } from './viewer.ts';
9
9
 
10
10
  /** What {@link useImageExport} hands the export panel. */
11
11
  export interface ImageExport {
@@ -24,7 +24,7 @@ export interface ImageExport {
24
24
  */
25
25
  export function useImageExport(
26
26
  container: HTMLDivElement | null,
27
- viewerRef: RefObject<Molecule3DViewer | null>,
27
+ viewerRef: RefObject<SceneCapture | null>,
28
28
  fileName: string,
29
29
  ): ImageExport {
30
30
  const canvasSize = useCallback((): FigurePixels | null => {
@@ -8,22 +8,20 @@
8
8
  * is the component's.
9
9
  */
10
10
 
11
- import type { PluginViewModel } from 'molstar/lib/extensions/plugin/view-model.js';
12
11
  import type { PluginContext } from 'molstar/lib/mol-plugin/context.js';
13
12
 
13
+ import {
14
+ DEFAULT_CAMERA_DURATION,
15
+ DEFAULT_SPIN_SPEED,
16
+ MolstarPlugin,
17
+ setSpin,
18
+ } from '../../molstar/core/index.ts';
14
19
  import type { Molecule3DCamera } from '../core/camera.ts';
15
20
  import type { ImageSize } from '../core/exportImage.ts';
16
21
  import type { Measurement, MeasurementKind } from '../core/measurement.ts';
17
22
  import type { Molecule3DFile } from '../core/settings.ts';
18
23
 
19
- import {
20
- DEFAULT_CAMERA_DURATION,
21
- DEFAULT_SPIN_SPEED,
22
- applyCamera,
23
- resetCamera,
24
- setSpin,
25
- watchCamera,
26
- } from './camera.ts';
24
+ import { applyCamera, resetCamera, watchCamera } from './camera.ts';
27
25
  import { captureScene } from './captureScene.ts';
28
26
  import {
29
27
  MeasurementPicker,
@@ -32,7 +30,6 @@ import {
32
30
  } from './measurements.ts';
33
31
  import { clearMolecule, renderMolecule } from './renderMolecule.ts';
34
32
  import { clearSurface, renderSurface } from './renderSurface.ts';
35
- import { mountMolecule3DPlugin } from './viewerSpec.ts';
36
33
  import type {
37
34
  Molecule3DViewerOptions,
38
35
  MoleculeStyle,
@@ -44,9 +41,8 @@ import type {
44
41
  * method resolves to nothing once `dispose` has been called.
45
42
  */
46
43
  export class Molecule3DViewer {
47
- readonly #model: PluginViewModel;
44
+ readonly #plugin: MolstarPlugin;
48
45
  #picker: MeasurementPicker | undefined;
49
- #disposed = false;
50
46
 
51
47
  /** Resolves once the canvas exists; every method awaits it internally. */
52
48
  readonly ready: Promise<void>;
@@ -57,17 +53,15 @@ export class Molecule3DViewer {
57
53
  * @param options - See {@link Molecule3DViewerOptions}.
58
54
  */
59
55
  constructor(container: HTMLElement, options: Molecule3DViewerOptions = {}) {
60
- const {
61
- background = '#ffffff', // tokens-ok: a WebGL clear colour
62
- onMeasure = ignore,
63
- } = options;
64
- this.#model = mountMolecule3DPlugin(container, background);
65
- this.ready = this.#model.initialized;
56
+ const { background, onMeasure = ignore } = options;
57
+ this.#plugin = new MolstarPlugin(container, { background });
58
+ this.ready = this.#plugin.ready;
66
59
  // Registered before any `#run`, so the picker exists by the time one runs.
67
- this.ready.then(() => {
68
- if (this.#disposed) return;
69
- this.#picker = new MeasurementPicker(this.#model.plugin, onMeasure);
70
- }, ignore);
60
+ void this.#plugin
61
+ .run((plugin) => {
62
+ this.#picker = new MeasurementPicker(plugin, onMeasure);
63
+ })
64
+ .catch(ignore);
71
65
  }
72
66
 
73
67
  /**
@@ -177,16 +171,7 @@ export class Molecule3DViewer {
177
171
  * the canvas exists.
178
172
  */
179
173
  watchCamera(listener: (camera: Molecule3DCamera | null) => void): () => void {
180
- let stop: (() => void) | null = null;
181
- let cancelled = false;
182
- void this.#run((plugin) => {
183
- if (!cancelled) stop = watchCamera(plugin, listener);
184
- });
185
- return () => {
186
- cancelled = true;
187
- stop?.();
188
- stop = null;
189
- };
174
+ return this.#plugin.subscribe((plugin) => watchCamera(plugin, listener));
190
175
  }
191
176
 
192
177
  /**
@@ -203,34 +188,19 @@ export class Molecule3DViewer {
203
188
 
204
189
  /** Re-read the container's size. Call from a `ResizeObserver`. */
205
190
  handleResize(): void {
206
- if (this.#disposed) return;
207
- this.#model.plugin.handleResize();
191
+ this.#plugin.handleResize();
208
192
  }
209
193
 
210
194
  /** Tear the viewer down and release its WebGL context. Idempotent. */
211
195
  dispose(): void {
212
- if (this.#disposed) return;
213
- this.#disposed = true;
214
196
  this.#picker?.dispose();
215
- void this.ready
216
- .catch(() => undefined)
217
- .then(() => {
218
- this.#model.plugin.dispose();
219
- });
197
+ this.#plugin.dispose();
220
198
  }
221
199
 
222
- async #run<Result>(
200
+ #run<Result>(
223
201
  action: (plugin: PluginContext) => Result | Promise<Result>,
224
202
  ): Promise<Result | undefined> {
225
- if (this.#disposed) return undefined;
226
- await this.ready;
227
- if (this.#disposed) return undefined;
228
- try {
229
- return await action(this.#model.plugin);
230
- } catch (error) {
231
- if (this.#disposed) return undefined;
232
- throw error;
233
- }
203
+ return this.#plugin.run(action);
234
204
  }
235
205
  }
236
206
 
@@ -0,0 +1,61 @@
1
+ /**
2
+ * The camera moves every molstar scene of ours needs, and the one measurement
3
+ * they all take: how big a sphere has to be framed for a handful of atoms to
4
+ * be legible.
5
+ */
6
+
7
+ import { Vec3 } from 'molstar/lib/mol-math/linear-algebra.js';
8
+ import type { PluginContext } from 'molstar/lib/mol-plugin/context.js';
9
+
10
+ /** Transition length used when the caller does not pick one, milliseconds. */
11
+ export const DEFAULT_CAMERA_DURATION = 250;
12
+
13
+ /** Turn rate used when the caller does not pick one, in molstar's spin unit. */
14
+ export const DEFAULT_SPIN_SPEED = 1 / 3;
15
+
16
+ /**
17
+ * Fraction of the bounding sphere kept as breathing room around the scene.
18
+ *
19
+ * `camera.reset()` frames with molstar's own margin, which suits a protein
20
+ * filling a wide viewport and leaves a small molecule a speck in the middle:
21
+ * measured coverage was 20% of the pixels. Framing the visible bounding sphere
22
+ * directly, with a small margin, is what makes a single water molecule legible.
23
+ */
24
+ export const FRAMING_MARGIN = 0.08;
25
+
26
+ /** Smallest radius framed, ångström, so one atom is not a close-up. */
27
+ export const MINIMUM_FRAMING_RADIUS = 0.5;
28
+
29
+ /** Axis the automatic spin turns about: screen up. */
30
+ const SPIN_AXIS = Vec3.create(0, 1, 0);
31
+
32
+ /**
33
+ * The radius a reset frames, which is the length every stored camera is
34
+ * measured against — so reading a camera and applying it are the same move in
35
+ * opposite directions.
36
+ * @param radius - Radius of the scene's visible bounding sphere.
37
+ * @returns The framed radius.
38
+ */
39
+ export function framedRadius(radius: number): number {
40
+ return Math.max(radius * (1 + FRAMING_MARGIN), MINIMUM_FRAMING_RADIUS);
41
+ }
42
+
43
+ /**
44
+ * Turn the automatic spin on or off. It moves the camera, never the object.
45
+ * @param plugin - The molstar context.
46
+ * @param spinning - Whether the scene should keep turning.
47
+ * @param speed - Turn rate, in molstar's own spin unit.
48
+ */
49
+ export function setSpin(
50
+ plugin: PluginContext,
51
+ spinning: boolean,
52
+ speed = DEFAULT_SPIN_SPEED,
53
+ ): void {
54
+ plugin.canvas3d?.setProps({
55
+ trackball: {
56
+ animate: spinning
57
+ ? { name: 'spin', params: { speed, axis: SPIN_AXIS } }
58
+ : { name: 'off', params: {} },
59
+ },
60
+ });
61
+ }
@@ -0,0 +1,126 @@
1
+ /**
2
+ * The two directions between molstar's camera and the camera a shared link
3
+ * carries.
4
+ *
5
+ * A link stores the camera against what is drawn rather than against ångström —
6
+ * a rotation from the front view, a zoom against the distance that frames the
7
+ * scene, and a target offset in its own radii — so a view opens the same on any
8
+ * screen. Which sphere is the yardstick is the caller's to decide: a site whose
9
+ * scene grows after the model arrives measures against the model alone, or one
10
+ * view would read as two zooms.
11
+ */
12
+
13
+ import { Quat, Vec3 } from 'molstar/lib/mol-math/linear-algebra.js';
14
+
15
+ import type { Molecule3DCamera } from '../../molecule3d/core/camera.ts';
16
+ import { normalizeMolecule3DCamera } from '../../molecule3d/core/camera.ts';
17
+
18
+ /** Where the camera is and where it looks, in world coordinates. */
19
+ export interface CameraPose {
20
+ position: Vec3;
21
+ target: Vec3;
22
+ up: Vec3;
23
+ }
24
+
25
+ /** The sphere a stored camera is measured against. */
26
+ export interface CameraReference {
27
+ center: Vec3;
28
+ /** Radius, ångström, framing margin included. */
29
+ radius: number;
30
+ }
31
+
32
+ /** How small an offset, in radii, is written as none at all. */
33
+ export interface PoseOptions {
34
+ /**
35
+ * Target offsets below this, in radii, are read as zero. The framing centres
36
+ * on the scene a hair off the atoms' own centre, and that hair would
37
+ * otherwise be written into every link as `,0,0,0`.
38
+ * @default 0
39
+ */
40
+ snapOffset?: number;
41
+ }
42
+
43
+ /** The camera's own axes, which a stored rotation turns into world ones. */
44
+ const CAMERA_UP = Vec3.create(0, 1, 0);
45
+ const CAMERA_BACK = Vec3.create(0, 0, 1);
46
+
47
+ /**
48
+ * Measure a pose against what is drawn.
49
+ * @param pose - Where the camera is.
50
+ * @param reference - The sphere to measure against.
51
+ * @param framingDistance - How far from the target the camera stands to frame
52
+ * the reference sphere; the length `zoom: 1` means.
53
+ * @param options - See {@link PoseOptions}.
54
+ * @returns The camera, or `null` for a degenerate pose.
55
+ */
56
+ export function poseToCamera(
57
+ pose: CameraPose,
58
+ reference: CameraReference,
59
+ framingDistance: number,
60
+ options: PoseOptions = {},
61
+ ): Molecule3DCamera | null {
62
+ const { snapOffset = 0 } = options;
63
+ const { position, target, up } = pose;
64
+ const back = Vec3.sub(Vec3.zero(), position, target);
65
+ const distance = Vec3.magnitude(back);
66
+ if (distance <= 0) return null;
67
+ Vec3.scale(back, back, 1 / distance);
68
+ // The trackball keeps up and back close to square, never exactly so.
69
+ const trueUp = Vec3.scaleAndAdd(Vec3.zero(), up, back, -Vec3.dot(up, back));
70
+ if (Vec3.magnitude(trueUp) <= 0) return null;
71
+ Vec3.normalize(trueUp, trueUp);
72
+ const right = Vec3.cross(Vec3.zero(), trueUp, back);
73
+ const rotation = Quat.fromBasis(Quat.identity(), right, trueUp, back);
74
+ const offset = Vec3.sub(Vec3.zero(), target, reference.center);
75
+ Vec3.scale(offset, offset, 1 / reference.radius);
76
+ return normalizeMolecule3DCamera({
77
+ rotation: [
78
+ rotation[0] ?? 0,
79
+ rotation[1] ?? 0,
80
+ rotation[2] ?? 0,
81
+ rotation[3] ?? 1,
82
+ ],
83
+ zoom: framingDistance / distance,
84
+ offset: [
85
+ snap(offset[0], snapOffset),
86
+ snap(offset[1], snapOffset),
87
+ snap(offset[2], snapOffset),
88
+ ],
89
+ });
90
+ }
91
+
92
+ /**
93
+ * The pose a stored camera stands at: {@link poseToCamera} the other way round.
94
+ * @param camera - The stored camera.
95
+ * @param reference - The sphere it was measured against.
96
+ * @param framingDistance - See {@link poseToCamera}.
97
+ * @returns The pose.
98
+ */
99
+ export function cameraToPose(
100
+ camera: Molecule3DCamera,
101
+ reference: CameraReference,
102
+ framingDistance: number,
103
+ ): CameraPose {
104
+ const { rotation, zoom, offset } = normalizeMolecule3DCamera(camera);
105
+ const turn = Quat.create(rotation[0], rotation[1], rotation[2], rotation[3]);
106
+ const up = Vec3.transformQuat(Vec3.zero(), CAMERA_UP, turn);
107
+ const back = Vec3.transformQuat(Vec3.zero(), CAMERA_BACK, turn);
108
+ const shift = Vec3.create(offset[0], offset[1], offset[2]);
109
+ const target = Vec3.scaleAndAdd(
110
+ Vec3.zero(),
111
+ reference.center,
112
+ shift,
113
+ reference.radius,
114
+ );
115
+ const position = Vec3.scaleAndAdd(
116
+ Vec3.zero(),
117
+ target,
118
+ back,
119
+ framingDistance / zoom,
120
+ );
121
+ return { position, target, up };
122
+ }
123
+
124
+ function snap(value: number | undefined, threshold: number): number {
125
+ return value === undefined || Math.abs(value) < threshold ? 0 : value;
126
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * What the pointer rests on, as a line of text.
3
+ *
4
+ * Every drawing our sites make carries a label already — `C1 sp³ σ to H5 (+)`,
5
+ * `C3 along [111]` — but molstar's own UI is not mounted, so nothing shows
6
+ * them. This turns the plugin's hover behaviour into that one string, which the
7
+ * canvas renders itself.
8
+ */
9
+
10
+ import type { Loci } from 'molstar/lib/mol-model/loci.js';
11
+ import { isEmptyLoci, isEveryLoci } from 'molstar/lib/mol-model/loci.js';
12
+ import {
13
+ Bond,
14
+ StructureElement,
15
+ StructureProperties,
16
+ } from 'molstar/lib/mol-model/structure.js';
17
+ import type { PluginContext } from 'molstar/lib/mol-plugin/context.js';
18
+ import { lociLabel } from 'molstar/lib/mol-theme/label.js';
19
+
20
+ /**
21
+ * Report whatever the pointer rests on.
22
+ * @param plugin - The molstar context.
23
+ * @param listener - Called with the loci, or `null` when the pointer is over
24
+ * nothing. The pointer over the background reports an empty loci, whose label
25
+ * is the word "Nothing" — a sentence, not an absence — so it arrives as `null`.
26
+ * @returns The unsubscribe function.
27
+ */
28
+ export function subscribeHover(
29
+ plugin: PluginContext,
30
+ listener: (loci: Loci | null) => void,
31
+ ): () => void {
32
+ const subscription = plugin.behaviors.interaction.hover.subscribe((event) => {
33
+ const { loci } = event.current;
34
+ listener(isEmptyLoci(loci) || isEveryLoci(loci) ? null : loci);
35
+ });
36
+ return () => {
37
+ subscription.unsubscribe();
38
+ };
39
+ }
40
+
41
+ /**
42
+ * The one line a loci is worth, in molstar's own words.
43
+ * @param loci - What the pointer is over.
44
+ * @returns The label, stripped of the markup molstar writes into it, or `null`
45
+ * when there is nothing to say.
46
+ */
47
+ export function lociText(loci: Loci): string | null {
48
+ if (isEmptyLoci(loci) || isEveryLoci(loci)) return null;
49
+ const label = stripMarkup(lociLabel(loci, { granularity: 'element' }));
50
+ return label === '' ? null : label;
51
+ }
52
+
53
+ /**
54
+ * The atoms a loci holds, in words a reader wants.
55
+ *
56
+ * molstar names one after the row it parsed — `xyz | Model 0 | Instance 1_555 |
57
+ * A | MOL 1 | O [idx 1]` — which is the address of a line in a file we wrote to
58
+ * hand it the atoms, and says nothing a student wants. The element and which
59
+ * atom of the scene it is do: the two hydrogens of water can then be told apart.
60
+ * @param loci - What the pointer rests on: an atom, or the two ends of a bond.
61
+ * @returns `O 1`, `Si 4 — O 8`, or `null` when the loci holds no atom.
62
+ */
63
+ export function atomText(loci: Loci): string | null {
64
+ const atoms = StructureElement.Loci.is(loci)
65
+ ? loci
66
+ : Bond.isLoci(loci)
67
+ ? Bond.toStructureElementLoci(loci)
68
+ : null;
69
+ if (atoms === null) return null;
70
+ const names: string[] = [];
71
+ StructureElement.Loci.forEachLocation(atoms, (location) => {
72
+ names.push(
73
+ atomName(
74
+ String(StructureProperties.atom.type_symbol(location)),
75
+ StructureProperties.atom.sourceIndex(location),
76
+ ),
77
+ );
78
+ });
79
+ return names.length === 0 ? null : names.join(' — ');
80
+ }
81
+
82
+ /**
83
+ * What a site calls one atom of its scene.
84
+ *
85
+ * molstar upper-cases an element symbol on its way in, so the atom it hands
86
+ * back from an `Si` it was given is an `SI`, which is not how anybody writes
87
+ * silicon. The number is which atom of the scene it is, counting from one.
88
+ * @param element - Its element symbol, in any case.
89
+ * @param index - Its place in the file the viewer was handed, from zero.
90
+ * @returns `O 1`, `Si 4`.
91
+ */
92
+ export function atomName(element: string, index: number): string {
93
+ const symbol =
94
+ element.charAt(0).toUpperCase() + element.slice(1).toLowerCase();
95
+ return `${symbol} ${index + 1}`;
96
+ }
97
+
98
+ /**
99
+ * Molstar's label providers return HTML, and a readout is plain text.
100
+ * @param label
101
+ */
102
+ function stripMarkup(label: string): string {
103
+ return label
104
+ .replaceAll(/<[^>]*>/g, ' ')
105
+ .replaceAll('&nbsp;', ' ')
106
+ .replaceAll('&amp;', '&')
107
+ .replaceAll(/\s+/g, ' ')
108
+ .trim();
109
+ }
@@ -0,0 +1,13 @@
1
+ export type { CameraPose, CameraReference, PoseOptions } from './cameraPose.ts';
2
+ export { cameraToPose, poseToCamera } from './cameraPose.ts';
3
+ export {
4
+ DEFAULT_CAMERA_DURATION,
5
+ DEFAULT_SPIN_SPEED,
6
+ FRAMING_MARGIN,
7
+ MINIMUM_FRAMING_RADIUS,
8
+ framedRadius,
9
+ setSpin,
10
+ } from './camera.ts';
11
+ export { atomName, atomText, lociText, subscribeHover } from './hover.ts';
12
+ export type { MolstarPluginOptions } from './plugin.ts';
13
+ export { MolstarPlugin } from './plugin.ts';