react-cheminfo 0.39.1 → 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 (96) 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/structure/ui/ConformerTable.d.ts +8 -0
  69. package/lib/structure/ui/ConformerTable.d.ts.map +1 -1
  70. package/lib/structure/ui/ConformerTable.js +24 -4
  71. package/lib/structure/ui/ConformerTable.js.map +1 -1
  72. package/package.json +2 -1
  73. package/src/deploy/core/checkDeploy.ts +14 -2
  74. package/src/deploy/core/index.ts +5 -0
  75. package/src/deploy/core/publishedImage.ts +110 -0
  76. package/src/deploy/core/types.ts +3 -1
  77. package/src/molecule3d/ui/Molecule3DToolbar.tsx +23 -16
  78. package/src/molecule3d/ui/MoleculeCanvas3D.tsx +1 -1
  79. package/src/molecule3d/ui/camera.ts +18 -93
  80. package/src/molecule3d/ui/exportMoleculeImage.ts +6 -2
  81. package/src/molecule3d/ui/index.ts +12 -0
  82. package/src/molecule3d/ui/measurements.ts +42 -3
  83. package/src/molecule3d/ui/useImageExport.ts +2 -2
  84. package/src/molecule3d/ui/viewer.ts +21 -51
  85. package/src/molstar/core/camera.ts +61 -0
  86. package/src/molstar/core/cameraPose.ts +126 -0
  87. package/src/molstar/core/hover.ts +109 -0
  88. package/src/molstar/core/index.ts +13 -0
  89. package/src/molstar/core/plugin.ts +159 -0
  90. package/src/molstar.ts +22 -0
  91. package/src/structure/ui/ConformerTable.tsx +38 -4
  92. package/lib/molecule3d/ui/viewerSpec.d.ts +0 -17
  93. package/lib/molecule3d/ui/viewerSpec.d.ts.map +0 -1
  94. package/lib/molecule3d/ui/viewerSpec.js +0 -34
  95. package/lib/molecule3d/ui/viewerSpec.js.map +0 -1
  96. package/src/molecule3d/ui/viewerSpec.ts +0 -38
@@ -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';
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Lifecycle of one headless molstar canvas: created now, disposed whenever,
3
+ * every call queued behind initialisation.
4
+ *
5
+ * The constructor is **synchronous** on purpose. React 19 runs an effect, its
6
+ * cleanup and the effect again on every mount in development, so an `await`ed
7
+ * constructor hands the cleanup nothing to dispose and leaks a WebGL context
8
+ * per mount — browsers drop the oldest after about sixteen, and the viewer
9
+ * silently goes blank. Returning the handle immediately means `dispose()` can
10
+ * always be called, even before initialisation has finished; the work is queued
11
+ * behind `ready`.
12
+ *
13
+ * molstar's own UI is never mounted: every control on our sites is ours.
14
+ */
15
+
16
+ import { PluginViewModel } from 'molstar/lib/extensions/plugin/view-model.js';
17
+ import type { PluginContext } from 'molstar/lib/mol-plugin/context.js';
18
+ import type { PluginSpec } from 'molstar/lib/mol-plugin/spec.js';
19
+ // Lowercased on import: it is a factory, not a constructor.
20
+ import { DefaultPluginSpec as defaultPluginSpec } from 'molstar/lib/mol-plugin/spec.js';
21
+ import { Color } from 'molstar/lib/mol-util/color/color.js';
22
+
23
+ /** Settings fixed for the life of a plugin. */
24
+ export interface MolstarPluginOptions {
25
+ /**
26
+ * Scene background, as `#rrggbb`. A WebGL clear colour, so it cannot be a CSS
27
+ * custom property.
28
+ * @default '#ffffff'
29
+ */
30
+ background?: string;
31
+ /**
32
+ * How long molstar animates a reframe it decided on itself; 0 jumps.
33
+ *
34
+ * Replacing a scene commits several times — the old drawing is deleted, the
35
+ * new one added — and molstar glides the camera on each, so the model appears
36
+ * to drift into place. Reframing is right; animating it between two unrelated
37
+ * scenes is not.
38
+ * @default molstar's own 250 ms
39
+ */
40
+ cameraResetDurationMilliseconds?: number;
41
+ /**
42
+ * The last word on the spec, applied over everything above: a site that needs
43
+ * a behaviour dropped, a marking colour or a renderer setting of its own
44
+ * passes it here rather than mounting its own view model.
45
+ */
46
+ spec?: (spec: PluginSpec) => PluginSpec;
47
+ }
48
+
49
+ /** One molstar canvas, and the queue everything drawn on it goes through. */
50
+ export class MolstarPlugin {
51
+ readonly #model: PluginViewModel;
52
+ #disposed = false;
53
+
54
+ /** Resolves once the canvas exists; every call awaits it internally. */
55
+ readonly ready: Promise<void>;
56
+
57
+ /**
58
+ * Mount a canvas in `container` and start initialising it.
59
+ * @param container - An element with `position: relative`; molstar inserts
60
+ * its own canvas into it.
61
+ * @param options - See {@link MolstarPluginOptions}.
62
+ */
63
+ constructor(container: HTMLElement, options: MolstarPluginOptions = {}) {
64
+ const {
65
+ background = '#ffffff', // tokens-ok: a WebGL clear colour
66
+ cameraResetDurationMilliseconds,
67
+ spec: refine,
68
+ } = options;
69
+ const base = defaultPluginSpec();
70
+ const spec: PluginSpec = {
71
+ ...base,
72
+ canvas3d: {
73
+ ...base.canvas3d,
74
+ renderer: { backgroundColor: Color.fromHexStyle(background) },
75
+ // A scene is read from its own shape, never from the world axes.
76
+ camera: { helper: { axes: { name: 'off', params: {} } } },
77
+ ...(cameraResetDurationMilliseconds === undefined
78
+ ? {}
79
+ : { cameraResetDurationMs: cameraResetDurationMilliseconds }),
80
+ },
81
+ };
82
+ this.#model = new PluginViewModel({
83
+ spec: refine === undefined ? spec : refine(spec),
84
+ });
85
+ this.#model.mount(container);
86
+ this.ready = this.#model.initialized;
87
+ }
88
+
89
+ /** Whether {@link dispose} has been called. */
90
+ get disposed(): boolean {
91
+ return this.#disposed;
92
+ }
93
+
94
+ /**
95
+ * Wait for initialisation, then run `action` on the plugin.
96
+ * @param action - What to do with the molstar context.
97
+ * @returns What `action` returned, or `undefined` once the plugin has been
98
+ * disposed — before the call or while `action` was still running. An
99
+ * initialisation failure, and anything `action` throws while the plugin is
100
+ * alive, still reach the caller.
101
+ */
102
+ async run<Result>(
103
+ action: (plugin: PluginContext) => Result | Promise<Result>,
104
+ ): Promise<Result | undefined> {
105
+ if (this.#disposed) return undefined;
106
+ await this.ready;
107
+ if (this.#disposed) return undefined;
108
+ try {
109
+ return await action(this.#model.plugin);
110
+ } catch (error) {
111
+ if (this.#disposed) return undefined;
112
+ throw error;
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Start a subscription once the plugin exists.
118
+ * @param start - Subscribes, and returns its own unsubscribe function.
119
+ * @returns A function that stops the subscription, or stops it from ever
120
+ * starting; safe to call at any time.
121
+ */
122
+ subscribe(start: (plugin: PluginContext) => () => void): () => void {
123
+ let stop: (() => void) | null = null;
124
+ let cancelled = false;
125
+ void this.run((plugin) => {
126
+ if (cancelled) return;
127
+ stop = start(plugin);
128
+ });
129
+ return () => {
130
+ cancelled = true;
131
+ stop?.();
132
+ stop = null;
133
+ };
134
+ }
135
+
136
+ /** Re-read the container's size. Call from a `ResizeObserver`. */
137
+ handleResize(): void {
138
+ if (this.#disposed) return;
139
+ this.#model.plugin.handleResize();
140
+ }
141
+
142
+ /**
143
+ * Tear the canvas down and release its WebGL context. Idempotent, and safe to
144
+ * call before initialisation has finished.
145
+ */
146
+ dispose(): void {
147
+ if (this.#disposed) return;
148
+ this.#disposed = true;
149
+ // `mount` creates the canvas synchronously, so the context exists even when
150
+ // initialisation went on to fail; releasing it is what stops the browser
151
+ // dropping an older viewer's. A rejected `ready` must not escape here
152
+ // either — nobody is left to handle it.
153
+ void this.ready
154
+ .catch(() => undefined)
155
+ .then(() => {
156
+ this.#model.plugin.dispose();
157
+ });
158
+ }
159
+ }
package/src/molstar.ts ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The molstar layer under the 3D views: the plugin lifecycle, the camera, the
3
+ * hover labels, the measurements and the picture of a scene.
4
+ *
5
+ * Three of our sites mount a headless molstar canvas — the molecule viewer
6
+ * here, lcao's orbitals and symmetry's crystals — and each of them had its own
7
+ * copy of the lifecycle, the spec and the spin. This is that one copy; what
8
+ * stays in a site is the scene it draws.
9
+ *
10
+ * **Importing this pulls molstar in statically.** A site that only wants
11
+ * `<MoleculeViewer3D>` imports `react-cheminfo/molecule3d`, whose canvas is
12
+ * lazy.
13
+ */
14
+
15
+ export * from './molstar/core/index.ts';
16
+ export { captureScene } from './molecule3d/ui/captureScene.ts';
17
+ export type { MoleculeStructureSource } from './molecule3d/ui/measurements.ts';
18
+ export {
19
+ atomReferenceOf,
20
+ clearMeasurements,
21
+ renderMeasurements,
22
+ } from './molecule3d/ui/measurements.ts';
@@ -78,6 +78,14 @@ export interface ConformerTableProps {
78
78
  * @default undefined — every column keeps its own
79
79
  */
80
80
  columnLabels?: Partial<Record<ConformerColumn, string>>;
81
+ /**
82
+ * Whether the energies on screen are expected to be replaced — a force
83
+ * field's numbers while a better method is still working on the set. They
84
+ * are then drawn muted, so a reader can tell at a glance which of the two
85
+ * rankings they are looking at.
86
+ * @default false
87
+ */
88
+ provisional?: boolean;
81
89
  }
82
90
 
83
91
  /**
@@ -102,6 +110,7 @@ export function ConformerTable(
102
110
  label = 'Conformers, most stable first',
103
111
  rowName = String,
104
112
  columnLabels,
113
+ provisional = false,
105
114
  } = props;
106
115
 
107
116
  const bodyRef = useRef<HTMLTableSectionElement>(null);
@@ -208,7 +217,7 @@ export function ConformerTable(
208
217
  <ClickToCopy
209
218
  key={column}
210
219
  as="td"
211
- style={cellStyle(column)}
220
+ style={cellStyle(column, provisional)}
212
221
  value={content}
213
222
  label={COPY_LABELS[column]}
214
223
  testId={CELL_TEST_IDS[column]}
@@ -218,7 +227,7 @@ export function ConformerTable(
218
227
  ) : (
219
228
  <td
220
229
  key={column}
221
- style={cellStyle(column)}
230
+ style={cellStyle(column, provisional)}
222
231
  data-testid={CELL_TEST_IDS[column]}
223
232
  >
224
233
  {content}
@@ -360,8 +369,24 @@ function headerStyle(column: ConformerColumn): CSSProperties | undefined {
360
369
  return NUMERIC_CONFORMER_COLUMNS.has(column) ? numericStyle : undefined;
361
370
  }
362
371
 
363
- function cellStyle(column: ConformerColumn): CSSProperties | undefined {
364
- return NUMERIC_CONFORMER_COLUMNS.has(column) ? numericStyle : undefined;
372
+ /**
373
+ * A cell's own style: numbers are right-aligned and tabular, and every cell a
374
+ * better method will restate is muted while it works. The rank is not one of
375
+ * them — it names the row rather than measuring it, and it is what a reader
376
+ * follows as the order changes under them.
377
+ * @param column - The column the cell is in.
378
+ * @param provisional - Whether the energies are still expected to change.
379
+ * @returns The cell's inline style, or `undefined` when it needs none.
380
+ */
381
+ function cellStyle(
382
+ column: ConformerColumn,
383
+ provisional: boolean,
384
+ ): CSSProperties | undefined {
385
+ const numeric = NUMERIC_CONFORMER_COLUMNS.has(column);
386
+ if (!provisional || column === 'id') {
387
+ return numeric ? numericStyle : undefined;
388
+ }
389
+ return numeric ? provisionalNumericStyle : provisionalStyle;
365
390
  }
366
391
 
367
392
  const tableStyle = { width: '100%' } as const satisfies CSSProperties;
@@ -371,6 +396,15 @@ const numericStyle = {
371
396
  fontVariantNumeric: 'tabular-nums',
372
397
  } as const satisfies CSSProperties;
373
398
 
399
+ const provisionalStyle = {
400
+ color: 'var(--text-muted)',
401
+ } as const satisfies CSSProperties;
402
+
403
+ const provisionalNumericStyle = {
404
+ ...numericStyle,
405
+ ...provisionalStyle,
406
+ } as const satisfies CSSProperties;
407
+
374
408
  /**
375
409
  * The selected row is tinted with the site's own accent, so the table belongs
376
410
  * to whichever site draws it; a row is a pointer only where a click does
@@ -1,17 +0,0 @@
1
- /**
2
- * How the molecule viewer configures molstar: the plugin spec it mounts with.
3
- *
4
- * molstar's own UI is not mounted — every control is the component's — and the
5
- * axes helper is off, since a molecule is read from its own shape rather than
6
- * from the world axes.
7
- */
8
- import { PluginViewModel } from 'molstar/lib/extensions/plugin/view-model.js';
9
- /**
10
- * Build a view model for one canvas and mount it.
11
- * @param container - A positioned element; molstar inserts its canvas into it.
12
- * @param background - Scene background as `#rrggbb`; a WebGL clear colour, so
13
- * it cannot be a CSS custom property.
14
- * @returns The mounted view model, still initialising.
15
- */
16
- export declare function mountMolecule3DPlugin(container: HTMLElement, background: string): PluginViewModel;
17
- //# sourceMappingURL=viewerSpec.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"viewerSpec.d.ts","sourceRoot":"","sources":["../../../src/molecule3d/ui/viewerSpec.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,6CAA6C,CAAC;AAK9E;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,SAAS,EAAE,WAAW,EACtB,UAAU,EAAE,MAAM,GACjB,eAAe,CAcjB"}
@@ -1,34 +0,0 @@
1
- /**
2
- * How the molecule viewer configures molstar: the plugin spec it mounts with.
3
- *
4
- * molstar's own UI is not mounted — every control is the component's — and the
5
- * axes helper is off, since a molecule is read from its own shape rather than
6
- * from the world axes.
7
- */
8
- import { PluginViewModel } from 'molstar/lib/extensions/plugin/view-model.js';
9
- // Lowercased on import: it is a factory, not a constructor.
10
- import { DefaultPluginSpec as defaultPluginSpec } from 'molstar/lib/mol-plugin/spec.js';
11
- import { Color } from 'molstar/lib/mol-util/color/color.js';
12
- /**
13
- * Build a view model for one canvas and mount it.
14
- * @param container - A positioned element; molstar inserts its canvas into it.
15
- * @param background - Scene background as `#rrggbb`; a WebGL clear colour, so
16
- * it cannot be a CSS custom property.
17
- * @returns The mounted view model, still initialising.
18
- */
19
- export function mountMolecule3DPlugin(container, background) {
20
- const spec = defaultPluginSpec();
21
- const model = new PluginViewModel({
22
- spec: {
23
- ...spec,
24
- canvas3d: {
25
- ...spec.canvas3d,
26
- renderer: { backgroundColor: Color.fromHexStyle(background) },
27
- camera: { helper: { axes: { name: 'off', params: {} } } },
28
- },
29
- },
30
- });
31
- model.mount(container);
32
- return model;
33
- }
34
- //# sourceMappingURL=viewerSpec.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"viewerSpec.js","sourceRoot":"","sources":["../../../src/molecule3d/ui/viewerSpec.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,6CAA6C,CAAC;AAC9E,4DAA4D;AAC5D,OAAO,EAAE,iBAAiB,IAAI,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACxF,OAAO,EAAE,KAAK,EAAE,MAAM,qCAAqC,CAAC;AAE5D;;;;;;GAMG;AACH,MAAM,UAAU,qBAAqB,CACnC,SAAsB,EACtB,UAAkB;IAElB,MAAM,IAAI,GAAG,iBAAiB,EAAE,CAAC;IACjC,MAAM,KAAK,GAAG,IAAI,eAAe,CAAC;QAChC,IAAI,EAAE;YACJ,GAAG,IAAI;YACP,QAAQ,EAAE;gBACR,GAAG,IAAI,CAAC,QAAQ;gBAChB,QAAQ,EAAE,EAAE,eAAe,EAAE,KAAK,CAAC,YAAY,CAAC,UAAU,CAAC,EAAE;gBAC7D,MAAM,EAAE,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE;aAC1D;SACF;KACF,CAAC,CAAC;IACH,KAAK,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;IACvB,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -1,38 +0,0 @@
1
- /**
2
- * How the molecule viewer configures molstar: the plugin spec it mounts with.
3
- *
4
- * molstar's own UI is not mounted — every control is the component's — and the
5
- * axes helper is off, since a molecule is read from its own shape rather than
6
- * from the world axes.
7
- */
8
-
9
- import { PluginViewModel } from 'molstar/lib/extensions/plugin/view-model.js';
10
- // Lowercased on import: it is a factory, not a constructor.
11
- import { DefaultPluginSpec as defaultPluginSpec } from 'molstar/lib/mol-plugin/spec.js';
12
- import { Color } from 'molstar/lib/mol-util/color/color.js';
13
-
14
- /**
15
- * Build a view model for one canvas and mount it.
16
- * @param container - A positioned element; molstar inserts its canvas into it.
17
- * @param background - Scene background as `#rrggbb`; a WebGL clear colour, so
18
- * it cannot be a CSS custom property.
19
- * @returns The mounted view model, still initialising.
20
- */
21
- export function mountMolecule3DPlugin(
22
- container: HTMLElement,
23
- background: string,
24
- ): PluginViewModel {
25
- const spec = defaultPluginSpec();
26
- const model = new PluginViewModel({
27
- spec: {
28
- ...spec,
29
- canvas3d: {
30
- ...spec.canvas3d,
31
- renderer: { backgroundColor: Color.fromHexStyle(background) },
32
- camera: { helper: { axes: { name: 'off', params: {} } } },
33
- },
34
- },
35
- });
36
- model.mount(container);
37
- return model;
38
- }