react-cheminfo 0.18.0 → 0.20.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/lib/about/core/about.d.ts +15 -4
- package/lib/about/core/about.d.ts.map +1 -1
- package/lib/about/core/about.js +8 -1
- package/lib/about/core/about.js.map +1 -1
- package/lib/about/core/index.d.ts +1 -0
- package/lib/about/core/index.d.ts.map +1 -1
- package/lib/about/core/index.js +1 -0
- package/lib/about/core/index.js.map +1 -1
- package/lib/about/core/repository.d.ts +14 -0
- package/lib/about/core/repository.d.ts.map +1 -0
- package/lib/about/core/repository.js +21 -0
- package/lib/about/core/repository.js.map +1 -0
- package/lib/about/ui/AboutBuild.d.ts +5 -4
- package/lib/about/ui/AboutBuild.d.ts.map +1 -1
- package/lib/about/ui/AboutBuild.js +10 -10
- package/lib/about/ui/AboutBuild.js.map +1 -1
- package/lib/about/ui/AboutPage.d.ts.map +1 -1
- package/lib/about/ui/AboutPage.js +7 -8
- package/lib/about/ui/AboutPage.js.map +1 -1
- package/lib/about/ui/AboutVersion.d.ts +26 -0
- package/lib/about/ui/AboutVersion.d.ts.map +1 -0
- package/lib/about/ui/AboutVersion.js +54 -0
- package/lib/about/ui/AboutVersion.js.map +1 -0
- package/lib/about/ui/index.d.ts +2 -0
- package/lib/about/ui/index.d.ts.map +1 -1
- package/lib/about/ui/index.js +1 -0
- package/lib/about/ui/index.js.map +1 -1
- package/lib/build/core/buildInfo.d.ts +44 -0
- package/lib/build/core/buildInfo.d.ts.map +1 -1
- package/lib/build/core/buildInfo.js +63 -0
- package/lib/build/core/buildInfo.js.map +1 -1
- package/lib/build/core/index.d.ts +1 -1
- package/lib/build/core/index.d.ts.map +1 -1
- package/lib/build/core/index.js +1 -1
- package/lib/build/core/index.js.map +1 -1
- package/lib/build/vite/buildInfo.d.ts.map +1 -1
- package/lib/build/vite/buildInfo.js +2 -2
- package/lib/build/vite/buildInfo.js.map +1 -1
- package/lib/ecosystem/core/sites.d.ts.map +1 -1
- package/lib/ecosystem/core/sites.js +12 -0
- package/lib/ecosystem/core/sites.js.map +1 -1
- package/lib/ecosystem/core/types.d.ts +8 -0
- package/lib/ecosystem/core/types.d.ts.map +1 -1
- package/lib/molecule3d/core/camera.d.ts +89 -0
- package/lib/molecule3d/core/camera.d.ts.map +1 -0
- package/lib/molecule3d/core/camera.js +172 -0
- package/lib/molecule3d/core/camera.js.map +1 -0
- package/lib/molecule3d/core/index.d.ts +2 -0
- package/lib/molecule3d/core/index.d.ts.map +1 -1
- package/lib/molecule3d/core/index.js +1 -0
- package/lib/molecule3d/core/index.js.map +1 -1
- package/lib/molecule3d/ui/MoleculeCanvas3D.d.ts.map +1 -1
- package/lib/molecule3d/ui/MoleculeCanvas3D.js +14 -37
- package/lib/molecule3d/ui/MoleculeCanvas3D.js.map +1 -1
- package/lib/molecule3d/ui/camera.d.ts +35 -2
- package/lib/molecule3d/ui/camera.d.ts.map +1 -1
- package/lib/molecule3d/ui/camera.js +113 -4
- package/lib/molecule3d/ui/camera.js.map +1 -1
- package/lib/molecule3d/ui/captureScene.d.ts +14 -0
- package/lib/molecule3d/ui/captureScene.d.ts.map +1 -0
- package/lib/molecule3d/ui/captureScene.js +24 -0
- package/lib/molecule3d/ui/captureScene.js.map +1 -0
- package/lib/molecule3d/ui/createViewer.d.ts +18 -0
- package/lib/molecule3d/ui/createViewer.d.ts.map +1 -0
- package/lib/molecule3d/ui/createViewer.js +19 -0
- package/lib/molecule3d/ui/createViewer.js.map +1 -0
- package/lib/molecule3d/ui/moleculeViewer3DProps.d.ts +21 -0
- package/lib/molecule3d/ui/moleculeViewer3DProps.d.ts.map +1 -1
- package/lib/molecule3d/ui/useMoleculeScene.d.ts +47 -0
- package/lib/molecule3d/ui/useMoleculeScene.d.ts.map +1 -0
- package/lib/molecule3d/ui/useMoleculeScene.js +82 -0
- package/lib/molecule3d/ui/useMoleculeScene.js.map +1 -0
- package/lib/molecule3d/ui/useSharedCamera.d.ts +52 -0
- package/lib/molecule3d/ui/useSharedCamera.d.ts.map +1 -0
- package/lib/molecule3d/ui/useSharedCamera.js +82 -0
- package/lib/molecule3d/ui/useSharedCamera.js.map +1 -0
- package/lib/molecule3d/ui/viewer.d.ts +21 -15
- package/lib/molecule3d/ui/viewer.d.ts.map +1 -1
- package/lib/molecule3d/ui/viewer.js +41 -47
- package/lib/molecule3d/ui/viewer.js.map +1 -1
- package/lib/molecule3d/ui/viewerSpec.d.ts +17 -0
- package/lib/molecule3d/ui/viewerSpec.d.ts.map +1 -0
- package/lib/molecule3d/ui/viewerSpec.js +34 -0
- package/lib/molecule3d/ui/viewerSpec.js.map +1 -0
- package/package.json +1 -1
- package/src/about/core/about.ts +25 -5
- package/src/about/core/index.ts +1 -0
- package/src/about/core/repository.ts +22 -0
- package/src/about/ui/AboutBuild.tsx +13 -20
- package/src/about/ui/AboutPage.tsx +42 -28
- package/src/about/ui/AboutVersion.tsx +95 -0
- package/src/about/ui/index.ts +2 -0
- package/src/build/core/buildInfo.ts +67 -0
- package/src/build/core/index.ts +9 -1
- package/src/build/vite/buildInfo.ts +2 -2
- package/src/ecosystem/core/sites.ts +12 -0
- package/src/ecosystem/core/types.ts +8 -0
- package/src/molecule3d/core/camera.ts +212 -0
- package/src/molecule3d/core/index.ts +14 -0
- package/src/molecule3d/ui/MoleculeCanvas3D.tsx +14 -43
- package/src/molecule3d/ui/camera.ts +128 -7
- package/src/molecule3d/ui/captureScene.ts +31 -0
- package/src/molecule3d/ui/createViewer.ts +24 -0
- package/src/molecule3d/ui/moleculeViewer3DProps.ts +21 -0
- package/src/molecule3d/ui/useMoleculeScene.ts +141 -0
- package/src/molecule3d/ui/useSharedCamera.ts +121 -0
- package/src/molecule3d/ui/viewer.ts +45 -50
- package/src/molecule3d/ui/viewerSpec.ts +38 -0
package/src/about/ui/index.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export type { AboutBuildProps } from './AboutBuild.tsx';
|
|
2
2
|
export { AboutBuild } from './AboutBuild.tsx';
|
|
3
|
+
export type { AboutVersionProps } from './AboutVersion.tsx';
|
|
4
|
+
export { AboutVersion } from './AboutVersion.tsx';
|
|
3
5
|
export type { AboutCitationsProps } from './AboutCitations.tsx';
|
|
4
6
|
export { AboutCitations } from './AboutCitations.tsx';
|
|
5
7
|
export type { AboutPageProps } from './AboutPage.tsx';
|
|
@@ -25,6 +25,72 @@ export interface BuildInfo {
|
|
|
25
25
|
reactCheminfo?: string;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* The version a build reports when the repository has never been released.
|
|
30
|
+
*
|
|
31
|
+
* release-please writes the version into the root `package.json`, so a site
|
|
32
|
+
* whose first release has not happened yet is built from the `0.0.0` it was
|
|
33
|
+
* scaffolded with. That is not a version anybody can look up, so nothing shows
|
|
34
|
+
* it: a page saying `0.0.0` reads as a bug rather than as a release.
|
|
35
|
+
*/
|
|
36
|
+
export const UNRELEASED_VERSION = '0.0.0';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The version to show a reader, if there is one.
|
|
40
|
+
* @param build - What the build published about itself, or `undefined`.
|
|
41
|
+
* @returns The released version, or `undefined` when the build published none
|
|
42
|
+
* or has never been released.
|
|
43
|
+
*/
|
|
44
|
+
export function releasedVersion(
|
|
45
|
+
build: BuildInfo | undefined,
|
|
46
|
+
): string | undefined {
|
|
47
|
+
const version = build?.version;
|
|
48
|
+
if (version === undefined || version === UNRELEASED_VERSION) return undefined;
|
|
49
|
+
return version;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* What the build is called on the page: the release when there is one, and the
|
|
54
|
+
* commit when there is not.
|
|
55
|
+
*
|
|
56
|
+
* A site that has never been released still has to be identifiable in a
|
|
57
|
+
* report, and `0.0.0` identifies nothing while the commit it was built from
|
|
58
|
+
* identifies exactly one build.
|
|
59
|
+
* @param build - What the build published about itself, or `undefined`.
|
|
60
|
+
* @returns The label, or `undefined` when the build published neither.
|
|
61
|
+
*/
|
|
62
|
+
export function buildLabel(build: BuildInfo | undefined): string | undefined {
|
|
63
|
+
const version = releasedVersion(build);
|
|
64
|
+
if (version !== undefined) return version;
|
|
65
|
+
const commit = build?.commit;
|
|
66
|
+
return commit === undefined ? undefined : shortCommit(commit);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* When the build was made, as it is read on the badge: the day and the minute.
|
|
71
|
+
*
|
|
72
|
+
* A build a reader is asked to quote is worth little without a date, and two
|
|
73
|
+
* builds of the same day are told apart by the minute. The seconds are left to
|
|
74
|
+
* the hover, where the whole record is.
|
|
75
|
+
* @param builtAt - The ISO instant the build wrote.
|
|
76
|
+
* @returns The instant, in UTC, e.g. `2026-09-16 09:41 UTC`.
|
|
77
|
+
*/
|
|
78
|
+
export function buildStamp(builtAt: string): string {
|
|
79
|
+
return `${builtAt.slice(0, MINUTE_LENGTH).replace('T', ' ')} UTC`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The whole record in one line, for the hover of a badge that shows one part
|
|
84
|
+
* of it.
|
|
85
|
+
* @param build - What the build published about itself.
|
|
86
|
+
* @returns e.g. `Built 2026-09-16 09:41:07 UTC from commit a1b2c3d`.
|
|
87
|
+
*/
|
|
88
|
+
export function buildSummary(build: BuildInfo): string {
|
|
89
|
+
const built = `Built ${formatBuiltAt(build.builtAt)}`;
|
|
90
|
+
if (build.commit === undefined) return built;
|
|
91
|
+
return `${built} from commit ${shortCommit(build.commit)}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
28
94
|
/**
|
|
29
95
|
* The commit as it is written for a reader.
|
|
30
96
|
* @param commit - The full hash.
|
|
@@ -45,4 +111,5 @@ export function formatBuiltAt(builtAt: string): string {
|
|
|
45
111
|
}
|
|
46
112
|
|
|
47
113
|
const SHORT_COMMIT_LENGTH = 7;
|
|
114
|
+
const MINUTE_LENGTH = 16;
|
|
48
115
|
const SECOND_LENGTH = 19;
|
package/src/build/core/index.ts
CHANGED
|
@@ -1,2 +1,10 @@
|
|
|
1
1
|
export type { BuildInfo } from './buildInfo.ts';
|
|
2
|
-
export {
|
|
2
|
+
export {
|
|
3
|
+
UNRELEASED_VERSION,
|
|
4
|
+
buildLabel,
|
|
5
|
+
buildStamp,
|
|
6
|
+
buildSummary,
|
|
7
|
+
formatBuiltAt,
|
|
8
|
+
releasedVersion,
|
|
9
|
+
shortCommit,
|
|
10
|
+
} from './buildInfo.ts';
|
|
@@ -15,6 +15,7 @@ import { dirname, join, resolve } from 'node:path';
|
|
|
15
15
|
import type { Plugin } from 'vite';
|
|
16
16
|
|
|
17
17
|
import type { BuildInfo } from '../core/buildInfo.ts';
|
|
18
|
+
import { UNRELEASED_VERSION } from '../core/buildInfo.ts';
|
|
18
19
|
|
|
19
20
|
import { commitFromEnvironment, readGitHead } from './gitHead.ts';
|
|
20
21
|
|
|
@@ -92,7 +93,7 @@ export function resolveBuildInfo(root: string): BuildInfo {
|
|
|
92
93
|
const repository = findRepositoryRoot(root);
|
|
93
94
|
|
|
94
95
|
return {
|
|
95
|
-
version: readVersion(repository) ?? readVersion(root) ??
|
|
96
|
+
version: readVersion(repository) ?? readVersion(root) ?? UNRELEASED_VERSION,
|
|
96
97
|
builtAt: buildInstant(),
|
|
97
98
|
commit: readGitHead(repository) ?? commitFromEnvironment(),
|
|
98
99
|
reactCheminfo: readOwnVersion(),
|
|
@@ -174,5 +175,4 @@ function buildInstant(): string {
|
|
|
174
175
|
|
|
175
176
|
const RESOLVED_MODULE = `\0${BUILD_INFO_MODULE}`;
|
|
176
177
|
const OWN_NAME = 'react-cheminfo';
|
|
177
|
-
const UNRELEASED = '0.0.0';
|
|
178
178
|
const SECOND_LENGTH = 19;
|
|
@@ -62,6 +62,7 @@ export const ECOSYSTEM_SITES: readonly EcosystemSite[] = [
|
|
|
62
62
|
name: { lead: 'openbabel', alt: 'cheminfo', dot: true },
|
|
63
63
|
host: 'openbabel.cheminfo.org',
|
|
64
64
|
repository: 'https://github.com/cheminfo/openbabel.cheminfo.org',
|
|
65
|
+
publicRepository: true,
|
|
65
66
|
tagline: 'Convert a structure between any pair of chemical file formats.',
|
|
66
67
|
group: 'structures',
|
|
67
68
|
brand: '#0f766e',
|
|
@@ -84,6 +85,7 @@ export const ECOSYSTEM_SITES: readonly EcosystemSite[] = [
|
|
|
84
85
|
name: { lead: 'dbe', alt: 'cheminfo', dot: true },
|
|
85
86
|
host: 'dbe.cheminfo.org',
|
|
86
87
|
repository: 'https://github.com/cheminfo/dbe.cheminfo.org',
|
|
88
|
+
publicRepository: true,
|
|
87
89
|
tagline: 'Count rings and double bonds from a formula or a structure.',
|
|
88
90
|
group: 'basics',
|
|
89
91
|
brand: '#63123f',
|
|
@@ -97,6 +99,7 @@ export const ECOSYSTEM_SITES: readonly EcosystemSite[] = [
|
|
|
97
99
|
name: { lead: 'NMR', alt: 'ium' },
|
|
98
100
|
host: 'www.nmrium.org',
|
|
99
101
|
repository: 'https://github.com/cheminfo/nmrium',
|
|
102
|
+
publicRepository: true,
|
|
100
103
|
tagline: 'Process and assign NMR spectra in the browser.',
|
|
101
104
|
group: 'spectra',
|
|
102
105
|
brand: '#ea580c',
|
|
@@ -133,6 +136,7 @@ export const ECOSYSTEM_SITES: readonly EcosystemSite[] = [
|
|
|
133
136
|
name: { lead: 'surge', alt: 'cheminfo', dot: true },
|
|
134
137
|
host: 'surge.cheminfo.org',
|
|
135
138
|
repository: 'https://github.com/cheminfo/surge.cheminfo.org',
|
|
139
|
+
publicRepository: true,
|
|
136
140
|
tagline: 'Every constitutional isomer of a molecular formula.',
|
|
137
141
|
group: 'structures',
|
|
138
142
|
brand: '#4338ca',
|
|
@@ -144,6 +148,7 @@ export const ECOSYSTEM_SITES: readonly EcosystemSite[] = [
|
|
|
144
148
|
name: { lead: 'tex', alt: 'cheminfo', dot: true },
|
|
145
149
|
host: 'tex.cheminfo.org',
|
|
146
150
|
repository: 'https://github.com/cheminfo/tex.cheminfo.org',
|
|
151
|
+
publicRepository: true,
|
|
147
152
|
tagline: 'LaTeX formulas rendered to SVG or PNG from a URL.',
|
|
148
153
|
group: 'computing',
|
|
149
154
|
brand: '#a21caf',
|
|
@@ -166,6 +171,7 @@ export const ECOSYSTEM_SITES: readonly EcosystemSite[] = [
|
|
|
166
171
|
name: { lead: 'regexp', alt: 'cheminfo', dot: true },
|
|
167
172
|
host: 'regexp.cheminfo.org',
|
|
168
173
|
repository: 'https://github.com/cheminfo/regexp.cheminfo.org',
|
|
174
|
+
publicRepository: true,
|
|
169
175
|
tagline: 'Learn regular expressions in a live playground.',
|
|
170
176
|
group: 'computing',
|
|
171
177
|
brand: '#1e3a8a',
|
|
@@ -177,6 +183,7 @@ export const ECOSYSTEM_SITES: readonly EcosystemSite[] = [
|
|
|
177
183
|
name: { lead: 'pdb', alt: 'cheminfo', dot: true },
|
|
178
184
|
host: 'pdb.cheminfo.org',
|
|
179
185
|
repository: 'https://github.com/cheminfo/pdb.cheminfo.org',
|
|
186
|
+
publicRepository: true,
|
|
180
187
|
tagline: 'A fast look at any Protein Data Bank entry.',
|
|
181
188
|
group: 'research',
|
|
182
189
|
brand: '#2563eb',
|
|
@@ -188,6 +195,7 @@ export const ECOSYSTEM_SITES: readonly EcosystemSite[] = [
|
|
|
188
195
|
name: { lead: 'elucidation', alt: 'cheminfo', dot: true },
|
|
189
196
|
host: 'elucidation.cheminfo.org',
|
|
190
197
|
repository: 'https://github.com/cheminfo/elucidation.cheminfo.org',
|
|
198
|
+
publicRepository: true,
|
|
191
199
|
tagline: 'A structure from a 1H NMR spectrum and a formula.',
|
|
192
200
|
group: 'spectra',
|
|
193
201
|
brand: '#7e22ce',
|
|
@@ -210,6 +218,7 @@ export const ECOSYSTEM_SITES: readonly EcosystemSite[] = [
|
|
|
210
218
|
name: { lead: 'Poly', alt: 'Carp' },
|
|
211
219
|
host: 'polycarp.cheminfo.org',
|
|
212
220
|
repository: 'https://github.com/cheminfo/polycarp.cheminfo.org',
|
|
221
|
+
publicRepository: true,
|
|
213
222
|
tagline: 'Predict the microstructure of a radical copolymerisation.',
|
|
214
223
|
group: 'research',
|
|
215
224
|
brand: '#701a75',
|
|
@@ -243,6 +252,7 @@ export const ECOSYSTEM_SITES: readonly EcosystemSite[] = [
|
|
|
243
252
|
name: { lead: 'database', alt: 'cheminfo', dot: true },
|
|
244
253
|
host: 'database.cheminfo.org',
|
|
245
254
|
repository: 'https://github.com/cheminfo/database.cheminfo.org',
|
|
255
|
+
publicRepository: true,
|
|
246
256
|
tagline: 'Query a real chemical dataset in SQL and in Mango.',
|
|
247
257
|
group: 'research',
|
|
248
258
|
brand: '#7c2d12',
|
|
@@ -254,6 +264,7 @@ export const ECOSYSTEM_SITES: readonly EcosystemSite[] = [
|
|
|
254
264
|
name: { lead: 'Symme', alt: 'Try' },
|
|
255
265
|
host: 'symmetry.cheminfo.org',
|
|
256
266
|
repository: 'https://github.com/cheminfo/symmetry.cheminfo.org',
|
|
267
|
+
publicRepository: true,
|
|
257
268
|
tagline: 'Find the symmetry of a molecule, a crystal or a pattern.',
|
|
258
269
|
group: 'basics',
|
|
259
270
|
brand: '#9f1239',
|
|
@@ -267,6 +278,7 @@ export const ECOSYSTEM_SITES: readonly EcosystemSite[] = [
|
|
|
267
278
|
name: { lead: 'osiris', alt: 'cheminfo', dot: true },
|
|
268
279
|
host: 'osiris.cheminfo.org',
|
|
269
280
|
repository: 'https://github.com/cheminfo/osiris.cheminfo.org',
|
|
281
|
+
publicRepository: true,
|
|
270
282
|
tagline: 'Predicted toxicity risks and properties, one molecule or many.',
|
|
271
283
|
group: 'research',
|
|
272
284
|
brand: '#854d0e',
|
|
@@ -60,6 +60,14 @@ export interface EcosystemSite {
|
|
|
60
60
|
host: string;
|
|
61
61
|
/** Where the sources live, and what a Source link opens. */
|
|
62
62
|
repository: string;
|
|
63
|
+
/**
|
|
64
|
+
* Whether a visitor can open that repository. A private one is named
|
|
65
|
+
* nowhere: no licence-and-source section, no link from the version, no issue
|
|
66
|
+
* tracker a reader cannot reach. It is stated rather than assumed, so a site
|
|
67
|
+
* whose record forgets it advertises nothing rather than a dead link.
|
|
68
|
+
* @default false
|
|
69
|
+
*/
|
|
70
|
+
publicRepository?: boolean;
|
|
63
71
|
/** One line on what the site does. */
|
|
64
72
|
tagline: string;
|
|
65
73
|
/**
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the camera stands, in a form a link can carry.
|
|
3
|
+
*
|
|
4
|
+
* A position in ångström would not survive the trip: the molecule a link names
|
|
5
|
+
* is built again on the other side, and a distance that frames a benzene leaves
|
|
6
|
+
* a peptide off screen. So the camera is stored against the scene rather than
|
|
7
|
+
* against the coordinates — a rotation from the front view, a zoom against the
|
|
8
|
+
* distance that frames the whole model, and a target as an offset from the
|
|
9
|
+
* scene centre in bounding-sphere radii. All three read the same whatever the
|
|
10
|
+
* molecule and whatever the size of the canvas showing it.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { clamp } from '../../format/core/clamp.ts';
|
|
14
|
+
import { roundTo } from '../../format/core/roundTo.ts';
|
|
15
|
+
// Deep import on purpose: the codec is the one way a link carries a camera, and
|
|
16
|
+
// `share/core` knows nothing of molecules, so nothing is pulled in by it.
|
|
17
|
+
import type { ShareParamCodec } from '../../share/core/params.ts';
|
|
18
|
+
|
|
19
|
+
/** A rotation, as a unit quaternion `[x, y, z, w]`. */
|
|
20
|
+
export type Molecule3DRotation = readonly [number, number, number, number];
|
|
21
|
+
|
|
22
|
+
/** A point or a direction, `[x, y, z]`. */
|
|
23
|
+
export type Molecule3DVector = readonly [number, number, number];
|
|
24
|
+
|
|
25
|
+
/** Where the camera stands, against the scene rather than its coordinates. */
|
|
26
|
+
export interface Molecule3DCamera {
|
|
27
|
+
/**
|
|
28
|
+
* Rotation of the camera from the front view — looking down -z with y up —
|
|
29
|
+
* as a unit quaternion.
|
|
30
|
+
*/
|
|
31
|
+
rotation: Molecule3DRotation;
|
|
32
|
+
/**
|
|
33
|
+
* How close the camera is. `1` frames the whole model, as the reset button
|
|
34
|
+
* does; `2` is twice as close, so the model is drawn twice as large.
|
|
35
|
+
*/
|
|
36
|
+
zoom: number;
|
|
37
|
+
/**
|
|
38
|
+
* Where the camera looks, as an offset from the centre of the model, in
|
|
39
|
+
* bounding-sphere radii. `[0, 0, 0]` looks at the centre.
|
|
40
|
+
*/
|
|
41
|
+
offset: Molecule3DVector;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Looking at the centre of the model from the front, framing all of it. */
|
|
45
|
+
export const DEFAULT_MOLECULE_3D_CAMERA: Molecule3DCamera = {
|
|
46
|
+
rotation: [0, 0, 0, 1],
|
|
47
|
+
zoom: 1,
|
|
48
|
+
offset: [0, 0, 0],
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** The closest and furthest a link may ask the camera to stand. */
|
|
52
|
+
export const MOLECULE_3D_ZOOM_RANGE = { minimum: 0.01, maximum: 100 } as const;
|
|
53
|
+
|
|
54
|
+
/** Furthest a link may push the target from the centre, in radii. */
|
|
55
|
+
const MAX_OFFSET = 100;
|
|
56
|
+
|
|
57
|
+
/** Decimals kept of a quaternion component: 0.0001 is under a hundredth of a degree. */
|
|
58
|
+
const ROTATION_DECIMALS = 4;
|
|
59
|
+
|
|
60
|
+
/** Decimals kept of the zoom and of the target offset. */
|
|
61
|
+
const POSITION_DECIMALS = 3;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Write a camera as the text a link carries.
|
|
65
|
+
*
|
|
66
|
+
* The four components of the rotation come first, then the zoom, then the
|
|
67
|
+
* target offset — which is left out entirely while the camera looks at the
|
|
68
|
+
* centre of the model, as it does until someone pans.
|
|
69
|
+
* @param camera - The camera to write.
|
|
70
|
+
* @returns Comma-separated numbers: five, or eight when the camera is panned.
|
|
71
|
+
*/
|
|
72
|
+
export function formatMolecule3DCamera(camera: Molecule3DCamera): string {
|
|
73
|
+
const { rotation, zoom, offset } = normalizeMolecule3DCamera(camera);
|
|
74
|
+
const parts: string[] = [];
|
|
75
|
+
for (let index = 0; index < 4; index++) {
|
|
76
|
+
parts.push(String(roundTo(rotation[index] ?? 0, ROTATION_DECIMALS)));
|
|
77
|
+
}
|
|
78
|
+
parts.push(String(roundTo(zoom, POSITION_DECIMALS)));
|
|
79
|
+
if (offset[0] !== 0 || offset[1] !== 0 || offset[2] !== 0) {
|
|
80
|
+
for (const value of offset) {
|
|
81
|
+
parts.push(String(roundTo(value, POSITION_DECIMALS)));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return parts.join(',');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Read a camera out of the text a link carries.
|
|
89
|
+
*
|
|
90
|
+
* A rotation that is not a rotation — four zeros, a stray word — is what makes
|
|
91
|
+
* the whole value unusable; everything else is brought back into range rather
|
|
92
|
+
* than rejected, so a link written against an older range still opens.
|
|
93
|
+
* @param raw - The text of the parameter.
|
|
94
|
+
* @returns The camera, or `null` when the text does not describe one.
|
|
95
|
+
*/
|
|
96
|
+
export function parseMolecule3DCamera(raw: string): Molecule3DCamera | null {
|
|
97
|
+
const parts = raw.split(',');
|
|
98
|
+
if (parts.length !== 5 && parts.length !== 8) return null;
|
|
99
|
+
const numbers: number[] = [];
|
|
100
|
+
for (const part of parts) {
|
|
101
|
+
const value = Number(part.trim());
|
|
102
|
+
if (!Number.isFinite(value)) return null;
|
|
103
|
+
numbers.push(value);
|
|
104
|
+
}
|
|
105
|
+
const rotation = normalizeRotation([
|
|
106
|
+
numbers[0] ?? 0,
|
|
107
|
+
numbers[1] ?? 0,
|
|
108
|
+
numbers[2] ?? 0,
|
|
109
|
+
numbers[3] ?? 1,
|
|
110
|
+
]);
|
|
111
|
+
if (rotation === null) return null;
|
|
112
|
+
return {
|
|
113
|
+
rotation,
|
|
114
|
+
zoom: clampZoom(numbers[4] ?? 1),
|
|
115
|
+
offset: [
|
|
116
|
+
clampOffset(numbers[5] ?? 0),
|
|
117
|
+
clampOffset(numbers[6] ?? 0),
|
|
118
|
+
clampOffset(numbers[7] ?? 0),
|
|
119
|
+
],
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* A camera with its rotation made a unit quaternion and its numbers in range.
|
|
125
|
+
* @param camera - The camera to repair.
|
|
126
|
+
* @returns A camera safe to hand the viewer; the default when the rotation is
|
|
127
|
+
* degenerate.
|
|
128
|
+
*/
|
|
129
|
+
export function normalizeMolecule3DCamera(
|
|
130
|
+
camera: Molecule3DCamera,
|
|
131
|
+
): Molecule3DCamera {
|
|
132
|
+
const rotation = normalizeRotation(camera.rotation);
|
|
133
|
+
if (rotation === null) return DEFAULT_MOLECULE_3D_CAMERA;
|
|
134
|
+
return {
|
|
135
|
+
rotation,
|
|
136
|
+
zoom: clampZoom(camera.zoom),
|
|
137
|
+
offset: [
|
|
138
|
+
clampOffset(camera.offset[0]),
|
|
139
|
+
clampOffset(camera.offset[1]),
|
|
140
|
+
clampOffset(camera.offset[2]),
|
|
141
|
+
],
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Whether two cameras are the same once written into a link.
|
|
147
|
+
*
|
|
148
|
+
* What a link carries is rounded, so two cameras a drag apart by less than the
|
|
149
|
+
* rounding are one camera as far as the address is concerned — which is what
|
|
150
|
+
* keeps a settling animation from rewriting it a dozen times.
|
|
151
|
+
* @param first - One camera, or `null` for none.
|
|
152
|
+
* @param second - The other.
|
|
153
|
+
* @returns True when a link would carry the same text for both.
|
|
154
|
+
*/
|
|
155
|
+
export function sameMolecule3DCamera(
|
|
156
|
+
first: Molecule3DCamera | null,
|
|
157
|
+
second: Molecule3DCamera | null,
|
|
158
|
+
): boolean {
|
|
159
|
+
if (first === null || second === null) return first === second;
|
|
160
|
+
return formatMolecule3DCamera(first) === formatMolecule3DCamera(second);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* The codec a site mirrors the camera in the address with.
|
|
165
|
+
*
|
|
166
|
+
* The default framing is written as nothing at all, so the address of a page
|
|
167
|
+
* nobody has turned yet stays a plain link.
|
|
168
|
+
* @returns The codec, whose value is `null` while the camera is where a reset
|
|
169
|
+
* would leave it.
|
|
170
|
+
*/
|
|
171
|
+
export function molecule3DCameraParam(): ShareParamCodec<Molecule3DCamera | null> {
|
|
172
|
+
return {
|
|
173
|
+
parse(raw) {
|
|
174
|
+
return raw === null ? null : parseMolecule3DCamera(raw);
|
|
175
|
+
},
|
|
176
|
+
serialize(value) {
|
|
177
|
+
if (value === null) return null;
|
|
178
|
+
const text = formatMolecule3DCamera(value);
|
|
179
|
+
return text === formatMolecule3DCamera(DEFAULT_MOLECULE_3D_CAMERA)
|
|
180
|
+
? null
|
|
181
|
+
: text;
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function normalizeRotation(
|
|
187
|
+
rotation: Molecule3DRotation,
|
|
188
|
+
): Molecule3DRotation | null {
|
|
189
|
+
let squared = 0;
|
|
190
|
+
for (let index = 0; index < 4; index++) {
|
|
191
|
+
const value = rotation[index] ?? 0;
|
|
192
|
+
if (!Number.isFinite(value)) return null;
|
|
193
|
+
squared += value * value;
|
|
194
|
+
}
|
|
195
|
+
if (squared <= 0) return null;
|
|
196
|
+
const scale = 1 / Math.sqrt(squared);
|
|
197
|
+
return [
|
|
198
|
+
rotation[0] * scale,
|
|
199
|
+
rotation[1] * scale,
|
|
200
|
+
rotation[2] * scale,
|
|
201
|
+
rotation[3] * scale,
|
|
202
|
+
];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function clampZoom(zoom: number): number {
|
|
206
|
+
const { minimum, maximum } = MOLECULE_3D_ZOOM_RANGE;
|
|
207
|
+
return clamp(zoom, minimum, maximum, 1);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function clampOffset(value: number): number {
|
|
211
|
+
return clamp(value, -MAX_OFFSET, MAX_OFFSET, 0);
|
|
212
|
+
}
|
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
Molecule3DCamera,
|
|
3
|
+
Molecule3DRotation,
|
|
4
|
+
Molecule3DVector,
|
|
5
|
+
} from './camera.ts';
|
|
6
|
+
export {
|
|
7
|
+
DEFAULT_MOLECULE_3D_CAMERA,
|
|
8
|
+
MOLECULE_3D_ZOOM_RANGE,
|
|
9
|
+
formatMolecule3DCamera,
|
|
10
|
+
molecule3DCameraParam,
|
|
11
|
+
normalizeMolecule3DCamera,
|
|
12
|
+
parseMolecule3DCamera,
|
|
13
|
+
sameMolecule3DCamera,
|
|
14
|
+
} from './camera.ts';
|
|
1
15
|
export type { ImageSize } from './exportImage.ts';
|
|
2
16
|
export { dataUriBytes, rasterSvgMarkup } from './exportImage.ts';
|
|
3
17
|
export type { Molecule3DGesture, Molecule3DGestureKey } from './gestures.ts';
|
|
@@ -6,10 +6,9 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { CSSProperties, ReactElement } from 'react';
|
|
9
|
-
import {
|
|
9
|
+
import { useEffect, useRef, useState } from 'react';
|
|
10
10
|
|
|
11
11
|
import type { Measurement, MeasurementKind } from '../core/measurement.ts';
|
|
12
|
-
import type { Molecule3DFile } from '../core/settings.ts';
|
|
13
12
|
import {
|
|
14
13
|
normalizeMolecule3DSettings,
|
|
15
14
|
resolveMolecule3DTools,
|
|
@@ -19,13 +18,13 @@ import { Molecule3DExport } from './Molecule3DExport.tsx';
|
|
|
19
18
|
import { Molecule3DOptions } from './Molecule3DOptions.tsx';
|
|
20
19
|
import { Molecule3DToolbar } from './Molecule3DToolbar.tsx';
|
|
21
20
|
import { DEFAULT_SPIN_SPEED } from './camera.ts';
|
|
22
|
-
import {
|
|
21
|
+
import { createMolecule3DViewer } from './createViewer.ts';
|
|
23
22
|
import type { MoleculeViewer3DProps } from './moleculeViewer3DProps.ts';
|
|
24
23
|
import { useControlledState } from './useControlledState.ts';
|
|
25
24
|
import { useImageExport } from './useImageExport.ts';
|
|
25
|
+
import { useMoleculeScene } from './useMoleculeScene.ts';
|
|
26
26
|
import { usePolarSurfaceArea } from './usePolarSurfaceArea.ts';
|
|
27
27
|
import type { Molecule3DViewer } from './viewer.ts';
|
|
28
|
-
import { createMolecule3DViewer } from './viewer.ts';
|
|
29
28
|
|
|
30
29
|
/** Props of {@link MoleculeCanvas3D}. */
|
|
31
30
|
export interface MoleculeCanvas3DProps extends Omit<
|
|
@@ -84,11 +83,6 @@ export function MoleculeCanvas3D(props: MoleculeCanvas3DProps): ReactElement {
|
|
|
84
83
|
// effect keyed on it runs again, in order, after the viewer exists.
|
|
85
84
|
const [container, setContainer] = useState<HTMLDivElement | null>(null);
|
|
86
85
|
const viewerRef = useRef<Molecule3DViewer | null>(null);
|
|
87
|
-
const framedRef = useRef<{
|
|
88
|
-
molfile: Molecule3DFile;
|
|
89
|
-
viewer: Molecule3DViewer;
|
|
90
|
-
} | null>(null);
|
|
91
|
-
|
|
92
86
|
// The viewer is created once, so what its callbacks read is kept fresh here.
|
|
93
87
|
const latest = useRef({
|
|
94
88
|
measurements,
|
|
@@ -126,37 +120,6 @@ export function MoleculeCanvas3D(props: MoleculeCanvas3DProps): ReactElement {
|
|
|
126
120
|
};
|
|
127
121
|
}, [container]);
|
|
128
122
|
|
|
129
|
-
// One animation frame of coalescing, so dragging a slider costs one rebuild.
|
|
130
|
-
useEffect(() => {
|
|
131
|
-
const viewer = viewerRef.current;
|
|
132
|
-
if (container === null || viewer === null) return;
|
|
133
|
-
let cancelled = false;
|
|
134
|
-
const frame = requestAnimationFrame(() => {
|
|
135
|
-
// Framed on a new molecule or a new viewer, never on a restyle: a slider
|
|
136
|
-
// that snapped the camera back would undo the reader's orientation.
|
|
137
|
-
const framed = framedRef.current;
|
|
138
|
-
const isNew =
|
|
139
|
-
molfile !== null &&
|
|
140
|
-
(framed?.molfile !== molfile || framed.viewer !== viewer);
|
|
141
|
-
framedRef.current = molfile === null ? null : { molfile, viewer };
|
|
142
|
-
const frameCamera = isNew ? frameNewMolecule : 'none';
|
|
143
|
-
void drawScene(viewer, molfile, settings, measurements, frameCamera)
|
|
144
|
-
.then(() => {
|
|
145
|
-
if (!cancelled) latest.current.onFailureChange(null);
|
|
146
|
-
})
|
|
147
|
-
.catch((error: unknown) => {
|
|
148
|
-
if (cancelled) return;
|
|
149
|
-
latest.current.onFailureChange(
|
|
150
|
-
error instanceof Error ? error.message : String(error),
|
|
151
|
-
);
|
|
152
|
-
});
|
|
153
|
-
});
|
|
154
|
-
return () => {
|
|
155
|
-
cancelled = true;
|
|
156
|
-
cancelAnimationFrame(frame);
|
|
157
|
-
};
|
|
158
|
-
}, [container, molfile, frameNewMolecule, settings, measurements]);
|
|
159
|
-
|
|
160
123
|
useEffect(() => {
|
|
161
124
|
void viewerRef.current?.setSpin(spinning, spinSpeed);
|
|
162
125
|
}, [container, spinning, spinSpeed]);
|
|
@@ -165,9 +128,17 @@ export function MoleculeCanvas3D(props: MoleculeCanvas3DProps): ReactElement {
|
|
|
165
128
|
void viewerRef.current?.setMeasureKind(measureKind);
|
|
166
129
|
}, [container, measureKind]);
|
|
167
130
|
|
|
168
|
-
const resetView =
|
|
169
|
-
|
|
170
|
-
|
|
131
|
+
const { resetView } = useMoleculeScene({
|
|
132
|
+
container,
|
|
133
|
+
viewerRef,
|
|
134
|
+
molfile,
|
|
135
|
+
frameNewMolecule,
|
|
136
|
+
settings,
|
|
137
|
+
measurements,
|
|
138
|
+
spinning,
|
|
139
|
+
onFailureChange,
|
|
140
|
+
camera: props,
|
|
141
|
+
});
|
|
171
142
|
|
|
172
143
|
const { canvasSize, exportImage } = useImageExport(
|
|
173
144
|
container,
|