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.
- package/bin/check-deploy.mjs +4 -2
- package/lib/deploy/core/checkDeploy.d.ts +3 -1
- package/lib/deploy/core/checkDeploy.d.ts.map +1 -1
- package/lib/deploy/core/checkDeploy.js +8 -2
- package/lib/deploy/core/checkDeploy.js.map +1 -1
- package/lib/deploy/core/index.d.ts +1 -0
- package/lib/deploy/core/index.d.ts.map +1 -1
- package/lib/deploy/core/index.js +1 -0
- package/lib/deploy/core/index.js.map +1 -1
- package/lib/deploy/core/publishedImage.d.ts +30 -0
- package/lib/deploy/core/publishedImage.d.ts.map +1 -0
- package/lib/deploy/core/publishedImage.js +96 -0
- package/lib/deploy/core/publishedImage.js.map +1 -0
- package/lib/deploy/core/types.d.ts +1 -1
- package/lib/deploy/core/types.d.ts.map +1 -1
- package/lib/deploy/core/types.js.map +1 -1
- package/lib/molecule3d/ui/Molecule3DToolbar.d.ts +9 -7
- package/lib/molecule3d/ui/Molecule3DToolbar.d.ts.map +1 -1
- package/lib/molecule3d/ui/Molecule3DToolbar.js +13 -8
- package/lib/molecule3d/ui/Molecule3DToolbar.js.map +1 -1
- package/lib/molecule3d/ui/MoleculeCanvas3D.js +1 -1
- package/lib/molecule3d/ui/MoleculeCanvas3D.js.map +1 -1
- package/lib/molecule3d/ui/camera.d.ts +0 -11
- package/lib/molecule3d/ui/camera.d.ts.map +1 -1
- package/lib/molecule3d/ui/camera.js +6 -80
- package/lib/molecule3d/ui/camera.js.map +1 -1
- package/lib/molecule3d/ui/exportMoleculeImage.d.ts +6 -2
- package/lib/molecule3d/ui/exportMoleculeImage.d.ts.map +1 -1
- package/lib/molecule3d/ui/exportMoleculeImage.js.map +1 -1
- package/lib/molecule3d/ui/index.d.ts +9 -0
- package/lib/molecule3d/ui/index.d.ts.map +1 -1
- package/lib/molecule3d/ui/index.js +5 -0
- package/lib/molecule3d/ui/index.js.map +1 -1
- package/lib/molecule3d/ui/measurements.d.ts +22 -2
- package/lib/molecule3d/ui/measurements.d.ts.map +1 -1
- package/lib/molecule3d/ui/measurements.js +25 -3
- package/lib/molecule3d/ui/measurements.js.map +1 -1
- package/lib/molecule3d/ui/useImageExport.d.ts +2 -2
- package/lib/molecule3d/ui/useImageExport.d.ts.map +1 -1
- package/lib/molecule3d/ui/useImageExport.js.map +1 -1
- package/lib/molecule3d/ui/viewer.d.ts.map +1 -1
- package/lib/molecule3d/ui/viewer.js +16 -49
- package/lib/molecule3d/ui/viewer.js.map +1 -1
- package/lib/molstar/core/camera.d.ts +37 -0
- package/lib/molstar/core/camera.d.ts.map +1 -0
- package/lib/molstar/core/camera.js +49 -0
- package/lib/molstar/core/camera.js.map +1 -0
- package/lib/molstar/core/cameraPose.d.ts +54 -0
- package/lib/molstar/core/cameraPose.d.ts.map +1 -0
- package/lib/molstar/core/cameraPose.js +78 -0
- package/lib/molstar/core/cameraPose.js.map +1 -0
- package/lib/molstar/core/hover.d.ts +49 -0
- package/lib/molstar/core/hover.d.ts.map +1 -0
- package/lib/molstar/core/hover.js +91 -0
- package/lib/molstar/core/hover.js.map +1 -0
- package/lib/molstar/core/index.d.ts +7 -0
- package/lib/molstar/core/index.d.ts.map +1 -0
- package/lib/molstar/core/index.js +5 -0
- package/lib/molstar/core/index.js.map +1 -0
- package/lib/molstar/core/plugin.d.ts +80 -0
- package/lib/molstar/core/plugin.d.ts.map +1 -0
- package/lib/molstar/core/plugin.js +125 -0
- package/lib/molstar/core/plugin.js.map +1 -0
- package/lib/molstar.d.ts +18 -0
- package/lib/molstar.d.ts.map +1 -0
- package/lib/molstar.js +17 -0
- package/lib/molstar.js.map +1 -0
- package/lib/structure/ui/ConformerTable.d.ts +8 -0
- package/lib/structure/ui/ConformerTable.d.ts.map +1 -1
- package/lib/structure/ui/ConformerTable.js +24 -4
- package/lib/structure/ui/ConformerTable.js.map +1 -1
- package/package.json +2 -1
- package/src/deploy/core/checkDeploy.ts +14 -2
- package/src/deploy/core/index.ts +5 -0
- package/src/deploy/core/publishedImage.ts +110 -0
- package/src/deploy/core/types.ts +3 -1
- package/src/molecule3d/ui/Molecule3DToolbar.tsx +23 -16
- package/src/molecule3d/ui/MoleculeCanvas3D.tsx +1 -1
- package/src/molecule3d/ui/camera.ts +18 -93
- package/src/molecule3d/ui/exportMoleculeImage.ts +6 -2
- package/src/molecule3d/ui/index.ts +12 -0
- package/src/molecule3d/ui/measurements.ts +42 -3
- package/src/molecule3d/ui/useImageExport.ts +2 -2
- package/src/molecule3d/ui/viewer.ts +21 -51
- package/src/molstar/core/camera.ts +61 -0
- package/src/molstar/core/cameraPose.ts +126 -0
- package/src/molstar/core/hover.ts +109 -0
- package/src/molstar/core/index.ts +13 -0
- package/src/molstar/core/plugin.ts +159 -0
- package/src/molstar.ts +22 -0
- package/src/structure/ui/ConformerTable.tsx +38 -4
- package/lib/molecule3d/ui/viewerSpec.d.ts +0 -17
- package/lib/molecule3d/ui/viewerSpec.d.ts.map +0 -1
- package/lib/molecule3d/ui/viewerSpec.js +0 -34
- package/lib/molecule3d/ui/viewerSpec.js.map +0 -1
- 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(' ', ' ')
|
|
106
|
+
.replaceAll('&', '&')
|
|
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
|
-
|
|
364
|
-
|
|
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
|
-
}
|