react-cheminfo 0.13.0 → 0.15.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 (99) hide show
  1. package/README.md +79 -21
  2. package/lib/about/core/about.d.ts +13 -7
  3. package/lib/about/core/about.d.ts.map +1 -1
  4. package/lib/about/core/about.js +4 -2
  5. package/lib/about/core/about.js.map +1 -1
  6. package/lib/about/ui/AboutBuild.d.ts +20 -0
  7. package/lib/about/ui/AboutBuild.d.ts.map +1 -0
  8. package/lib/about/ui/AboutBuild.js +38 -0
  9. package/lib/about/ui/AboutBuild.js.map +1 -0
  10. package/lib/about/ui/AboutPage.d.ts +7 -0
  11. package/lib/about/ui/AboutPage.d.ts.map +1 -1
  12. package/lib/about/ui/AboutPage.js +5 -2
  13. package/lib/about/ui/AboutPage.js.map +1 -1
  14. package/lib/about/ui/index.d.ts +2 -0
  15. package/lib/about/ui/index.d.ts.map +1 -1
  16. package/lib/about/ui/index.js +1 -0
  17. package/lib/about/ui/index.js.map +1 -1
  18. package/lib/build/core/buildInfo.d.ts +39 -0
  19. package/lib/build/core/buildInfo.d.ts.map +1 -0
  20. package/lib/build/core/buildInfo.js +20 -0
  21. package/lib/build/core/buildInfo.js.map +1 -0
  22. package/lib/build/core/current.d.ts +12 -0
  23. package/lib/build/core/current.d.ts.map +1 -0
  24. package/lib/build/core/current.js +11 -0
  25. package/lib/build/core/current.js.map +1 -0
  26. package/lib/build/core/index.d.ts +3 -0
  27. package/lib/build/core/index.d.ts.map +1 -0
  28. package/lib/build/core/index.js +2 -0
  29. package/lib/build/core/index.js.map +1 -0
  30. package/lib/build/vite/buildInfo.d.ts +57 -0
  31. package/lib/build/vite/buildInfo.d.ts.map +1 -0
  32. package/lib/build/vite/buildInfo.js +151 -0
  33. package/lib/build/vite/buildInfo.js.map +1 -0
  34. package/lib/build/vite/gitHead.d.ts +29 -0
  35. package/lib/build/vite/gitHead.d.ts.map +1 -0
  36. package/lib/build/vite/gitHead.js +102 -0
  37. package/lib/build/vite/gitHead.js.map +1 -0
  38. package/lib/build/vite/index.d.ts +4 -0
  39. package/lib/build/vite/index.d.ts.map +1 -0
  40. package/lib/build/vite/index.js +3 -0
  41. package/lib/build/vite/index.js.map +1 -0
  42. package/lib/build-info.d.ts +3 -0
  43. package/lib/build-info.d.ts.map +1 -0
  44. package/lib/build-info.js +2 -0
  45. package/lib/build-info.js.map +1 -0
  46. package/lib/core.d.ts +1 -0
  47. package/lib/core.d.ts.map +1 -1
  48. package/lib/core.js +1 -0
  49. package/lib/core.js.map +1 -1
  50. package/lib/credits/core/credits.d.ts +18 -0
  51. package/lib/credits/core/credits.d.ts.map +1 -1
  52. package/lib/credits/core/credits.js +21 -0
  53. package/lib/credits/core/credits.js.map +1 -1
  54. package/lib/ecosystem/ui/Wordmark.d.ts +14 -3
  55. package/lib/ecosystem/ui/Wordmark.d.ts.map +1 -1
  56. package/lib/ecosystem/ui/Wordmark.js +8 -4
  57. package/lib/ecosystem/ui/Wordmark.js.map +1 -1
  58. package/lib/molecule3d/ui/MoleculeCanvas3D.d.ts.map +1 -1
  59. package/lib/molecule3d/ui/MoleculeCanvas3D.js +4 -3
  60. package/lib/molecule3d/ui/MoleculeCanvas3D.js.map +1 -1
  61. package/lib/molecule3d/ui/camera.d.ts +3 -1
  62. package/lib/molecule3d/ui/camera.d.ts.map +1 -1
  63. package/lib/molecule3d/ui/camera.js +10 -2
  64. package/lib/molecule3d/ui/camera.js.map +1 -1
  65. package/lib/molecule3d/ui/drawScene.d.ts +4 -2
  66. package/lib/molecule3d/ui/drawScene.d.ts.map +1 -1
  67. package/lib/molecule3d/ui/drawScene.js +6 -2
  68. package/lib/molecule3d/ui/drawScene.js.map +1 -1
  69. package/lib/molecule3d/ui/moleculeViewer3DProps.d.ts +8 -0
  70. package/lib/molecule3d/ui/moleculeViewer3DProps.d.ts.map +1 -1
  71. package/lib/molecule3d/ui/viewer.d.ts +2 -1
  72. package/lib/molecule3d/ui/viewer.d.ts.map +1 -1
  73. package/lib/molecule3d/ui/viewer.js +3 -2
  74. package/lib/molecule3d/ui/viewer.js.map +1 -1
  75. package/lib/vite.d.ts +1 -0
  76. package/lib/vite.d.ts.map +1 -1
  77. package/lib/vite.js +1 -0
  78. package/lib/vite.js.map +1 -1
  79. package/package.json +2 -1
  80. package/src/about/core/about.ts +18 -9
  81. package/src/about/ui/AboutBuild.tsx +83 -0
  82. package/src/about/ui/AboutPage.tsx +15 -7
  83. package/src/about/ui/index.ts +2 -0
  84. package/src/build/core/buildInfo.ts +48 -0
  85. package/src/build/core/current.ts +12 -0
  86. package/src/build/core/index.ts +2 -0
  87. package/src/build/vite/buildInfo.ts +178 -0
  88. package/src/build/vite/gitHead.ts +114 -0
  89. package/src/build/vite/index.ts +8 -0
  90. package/src/build-info.ts +2 -0
  91. package/src/core.ts +1 -0
  92. package/src/credits/core/credits.ts +21 -0
  93. package/src/ecosystem/ui/Wordmark.tsx +22 -7
  94. package/src/molecule3d/ui/MoleculeCanvas3D.tsx +4 -2
  95. package/src/molecule3d/ui/camera.ts +13 -3
  96. package/src/molecule3d/ui/drawScene.ts +6 -3
  97. package/src/molecule3d/ui/moleculeViewer3DProps.ts +8 -0
  98. package/src/molecule3d/ui/viewer.ts +6 -2
  99. package/src/vite.ts +1 -0
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Tell a site which build of itself it is running.
3
+ *
4
+ * Every site of the family carries an About page, and the one thing that page
5
+ * cannot state from a hand-written record is the release it is serving. So the
6
+ * build states it: the plugin resolves the version, the day and the commit
7
+ * once, hands them to the page through a virtual module, and writes the same
8
+ * record to `build-info.json` so the whole family can be asked what it is
9
+ * running with one request each.
10
+ */
11
+
12
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
13
+ import { dirname, join, resolve } from 'node:path';
14
+
15
+ import type { Plugin } from 'vite';
16
+
17
+ import type { BuildInfo } from '../core/buildInfo.ts';
18
+
19
+ import { commitFromEnvironment, readGitHead } from './gitHead.ts';
20
+
21
+ /** The module a site imports its build record from. */
22
+ export const BUILD_INFO_MODULE = 'react-cheminfo/build-info';
23
+
24
+ /** What the build writes about itself. */
25
+ export interface BuildInfoOptions {
26
+ /**
27
+ * Where the record is written in the build, so a deployment can be asked
28
+ * what it is running. `false` writes none.
29
+ * @default 'build-info.json'
30
+ */
31
+ fileName?: string | false;
32
+ }
33
+
34
+ /**
35
+ * Resolve the build's own version, instant and commit, and publish them.
36
+ *
37
+ * The site reads them from the module the plugin fills in:
38
+ *
39
+ * ```ts
40
+ * import { BUILD_INFO } from 'react-cheminfo/build-info';
41
+ * ```
42
+ *
43
+ * That module exists on disk and exports `undefined`, so a Playwright spec or
44
+ * a unit test importing the same About record in plain Node still resolves it —
45
+ * a virtual specifier does not, and the whole suite dies on the import.
46
+ * @param options - See {@link BuildInfoOptions}.
47
+ * @returns The Vite plugin.
48
+ */
49
+ export function cheminfoBuildInfo(options: BuildInfoOptions = {}): Plugin {
50
+ const { fileName = 'build-info.json' } = options;
51
+ let root = process.cwd();
52
+ let outDir = 'dist';
53
+ let info: BuildInfo = resolveBuildInfo(root);
54
+
55
+ return {
56
+ name: 'cheminfo-build-info',
57
+ // Vite's own resolver runs first and would hand back the module on disk,
58
+ // which exports `undefined`; and the dependency optimizer pre-bundles a
59
+ // bare specifier without asking a plugin at all.
60
+ enforce: 'pre',
61
+ config() {
62
+ return { optimizeDeps: { exclude: [BUILD_INFO_MODULE] } };
63
+ },
64
+ configResolved(config) {
65
+ root = config.root;
66
+ outDir = config.build.outDir;
67
+ info = resolveBuildInfo(root);
68
+ },
69
+ resolveId(id) {
70
+ return id === BUILD_INFO_MODULE ? RESOLVED_MODULE : undefined;
71
+ },
72
+ load(id) {
73
+ if (id !== RESOLVED_MODULE) return undefined;
74
+ return `export const BUILD_INFO = ${JSON.stringify(info)};\n`;
75
+ },
76
+ closeBundle() {
77
+ if (fileName === false) return;
78
+ const target = resolve(root, outDir, fileName);
79
+ mkdirSync(dirname(target), { recursive: true });
80
+ writeFileSync(target, `${JSON.stringify(info, null, 2)}\n`);
81
+ },
82
+ };
83
+ }
84
+
85
+ /**
86
+ * What the build knows about itself, read off the checkout it runs in.
87
+ * @param root - The project Vite is building, which may sit under the
88
+ * repository root as `frontend` does.
89
+ * @returns The record, with whatever it could not resolve left out.
90
+ */
91
+ export function resolveBuildInfo(root: string): BuildInfo {
92
+ const repository = findRepositoryRoot(root);
93
+
94
+ return {
95
+ version: readVersion(repository) ?? readVersion(root) ?? UNRELEASED,
96
+ builtAt: buildInstant(),
97
+ commit: readGitHead(repository) ?? commitFromEnvironment(),
98
+ reactCheminfo: readOwnVersion(),
99
+ };
100
+ }
101
+
102
+ /**
103
+ * The topmost directory of the run of ancestors that all hold a `package.json`.
104
+ *
105
+ * It is the repository root, and it is where the released version lives:
106
+ * release-please bumps the root `package.json`, while the `frontend` workspace
107
+ * of a site that has one keeps the `0.0.0` it was scaffolded with.
108
+ * @param start - Where to start climbing.
109
+ * @returns The highest such directory, or `start` when it holds none itself.
110
+ */
111
+ export function findRepositoryRoot(start: string): string {
112
+ let directory = resolve(start);
113
+ let highest = directory;
114
+
115
+ for (;;) {
116
+ if (readPackage(directory) !== undefined) highest = directory;
117
+ const parent = dirname(directory);
118
+ // The run has ended: a directory with no package.json is above the project,
119
+ // and so is everything above it.
120
+ if (parent === directory || readPackage(parent) === undefined) {
121
+ return highest;
122
+ }
123
+ directory = parent;
124
+ }
125
+ }
126
+
127
+ function readVersion(directory: string): string | undefined {
128
+ const version = readPackage(directory)?.version;
129
+ return typeof version === 'string' && version.length > 0
130
+ ? version
131
+ : undefined;
132
+ }
133
+
134
+ /**
135
+ * The version of this package, so a site can be asked which chrome it runs.
136
+ * @returns The version, or `undefined` when the manifest is out of reach.
137
+ */
138
+ function readOwnVersion(): string | undefined {
139
+ let directory = import.meta.dirname;
140
+
141
+ for (;;) {
142
+ const manifest = readPackage(directory);
143
+ if (manifest?.name === OWN_NAME && typeof manifest.version === 'string') {
144
+ return manifest.version;
145
+ }
146
+ const parent = dirname(directory);
147
+ if (parent === directory) return undefined;
148
+ directory = parent;
149
+ }
150
+ }
151
+
152
+ function readPackage(
153
+ directory: string,
154
+ ): { name?: unknown; version?: unknown } | undefined {
155
+ try {
156
+ return JSON.parse(
157
+ readFileSync(join(directory, 'package.json'), 'utf8'),
158
+ ) as Record<string, unknown>;
159
+ } catch {
160
+ return undefined;
161
+ }
162
+ }
163
+
164
+ /**
165
+ * When the build ran, honouring `SOURCE_DATE_EPOCH` so a reproducible build
166
+ * stays reproducible.
167
+ * @returns The instant, to the second, as `2026-09-16T09:41:07Z`.
168
+ */
169
+ function buildInstant(): string {
170
+ const epoch = Number(process.env.SOURCE_DATE_EPOCH);
171
+ const when = Number.isFinite(epoch) && epoch > 0 ? epoch * 1000 : Date.now();
172
+ return `${new Date(when).toISOString().slice(0, SECOND_LENGTH)}Z`;
173
+ }
174
+
175
+ const RESOLVED_MODULE = `\0${BUILD_INFO_MODULE}`;
176
+ const OWN_NAME = 'react-cheminfo';
177
+ const UNRELEASED = '0.0.0';
178
+ const SECOND_LENGTH = 19;
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Read the commit a build is made from, without running git.
3
+ *
4
+ * The image build cannot run git: `.dockerignore` keeps `.git` out of the
5
+ * context and `node:alpine` ships no git binary, while the shared
6
+ * `docker-image` workflow passes no build argument a commit could arrive in.
7
+ * What a site does instead is let three paths back into the context —
8
+ * `.git/HEAD`, `.git/refs` and `.git/packed-refs`, a few kilobytes — and those
9
+ * three are enough to resolve the hash by reading files.
10
+ */
11
+
12
+ import { readFileSync } from 'node:fs';
13
+ import { dirname, isAbsolute, join, resolve } from 'node:path';
14
+
15
+ /**
16
+ * The commit `HEAD` names, read from the repository above a directory.
17
+ * @param start - Where to start looking; every ancestor is tried in turn.
18
+ * @returns The full hash, or `undefined` when the repository is out of reach —
19
+ * which is what a build sees when the three paths were not let back in.
20
+ */
21
+ export function readGitHead(start: string): string | undefined {
22
+ const gitDir = findGitDir(start);
23
+ if (gitDir === undefined) return undefined;
24
+
25
+ const head = readText(join(gitDir, 'HEAD'));
26
+ if (head === undefined) return undefined;
27
+
28
+ // A checkout made by `actions/checkout` is detached, so `HEAD` is the hash
29
+ // itself and neither `refs` nor `packed-refs` is read at all.
30
+ if (!head.startsWith('ref:')) return asCommit(head);
31
+
32
+ const ref = head.slice('ref:'.length).trim();
33
+ const loose = readText(join(gitDir, ref));
34
+ return (
35
+ asCommit(loose) ??
36
+ commitInPackedRefs(readText(join(gitDir, 'packed-refs')), ref)
37
+ );
38
+ }
39
+
40
+ /**
41
+ * The commit named by the environment, for a build that cannot see `.git`.
42
+ * @returns The hash, or `undefined` when no variable names one.
43
+ */
44
+ export function commitFromEnvironment(): string | undefined {
45
+ return (
46
+ asCommit(process.env.GITHUB_SHA) ??
47
+ asCommit(process.env.SOURCE_COMMIT) ??
48
+ asCommit(process.env.GIT_COMMIT)
49
+ );
50
+ }
51
+
52
+ /**
53
+ * The `.git` directory governing a path.
54
+ * @param start - Where to start looking.
55
+ * @returns Its absolute path, or `undefined` when there is none above `start`.
56
+ */
57
+ export function findGitDir(start: string): string | undefined {
58
+ let directory = resolve(start);
59
+
60
+ for (;;) {
61
+ const candidate = join(directory, '.git');
62
+ const text = readText(candidate);
63
+ // A worktree and a submodule write a file naming the real directory; a
64
+ // plain repository has the directory itself, which reads as `undefined`.
65
+ if (text?.startsWith('gitdir:')) {
66
+ const target = text.slice('gitdir:'.length).trim();
67
+ return isAbsolute(target) ? target : resolve(directory, target);
68
+ }
69
+ if (readText(join(candidate, 'HEAD')) !== undefined) return candidate;
70
+
71
+ const parent = dirname(directory);
72
+ if (parent === directory) return undefined;
73
+ directory = parent;
74
+ }
75
+ }
76
+
77
+ /**
78
+ * The hash a packed `refs` file gives a ref.
79
+ * @param packed - The contents of `packed-refs`, when there is one.
80
+ * @param ref - The ref to look up, e.g. `refs/heads/main`.
81
+ * @returns The hash, or `undefined` when the file does not carry the ref.
82
+ */
83
+ function commitInPackedRefs(
84
+ packed: string | undefined,
85
+ ref: string,
86
+ ): string | undefined {
87
+ if (packed === undefined) return undefined;
88
+
89
+ for (const line of packed.split('\n')) {
90
+ // `#` is the header and `^` the object a tag points at: neither is a ref.
91
+ if (line.startsWith('#') || line.startsWith('^')) continue;
92
+ const space = line.indexOf(' ');
93
+ if (space === -1 || line.slice(space + 1).trim() !== ref) continue;
94
+ return asCommit(line.slice(0, space));
95
+ }
96
+
97
+ return undefined;
98
+ }
99
+
100
+ function asCommit(value: string | undefined): string | undefined {
101
+ const text = value?.trim();
102
+ return text !== undefined && COMMIT.test(text) ? text : undefined;
103
+ }
104
+
105
+ function readText(path: string): string | undefined {
106
+ try {
107
+ return readFileSync(path, 'utf8').trim();
108
+ } catch {
109
+ return undefined;
110
+ }
111
+ }
112
+
113
+ // Forty hexadecimal characters, or sixty-four in a repository on sha256.
114
+ const COMMIT = /^[\da-f]{40}(?:[\da-f]{24})?$/;
@@ -0,0 +1,8 @@
1
+ export type { BuildInfoOptions } from './buildInfo.ts';
2
+ export {
3
+ BUILD_INFO_MODULE,
4
+ cheminfoBuildInfo,
5
+ findRepositoryRoot,
6
+ resolveBuildInfo,
7
+ } from './buildInfo.ts';
8
+ export { commitFromEnvironment, findGitDir, readGitHead } from './gitHead.ts';
@@ -0,0 +1,2 @@
1
+ export type { BuildInfo } from './build/core/buildInfo.ts';
2
+ export { BUILD_INFO } from './build/core/current.ts';
package/src/core.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './about/core/index.ts';
2
+ export * from './build/core/index.ts';
2
3
  export * from './chart/core/index.ts';
3
4
  export * from './citation/core/index.ts';
4
5
  export * from './clipboard/core/index.ts';
@@ -194,6 +194,27 @@ export const CREDITS = [
194
194
  description: 'the dragging that arranges things into an order.',
195
195
  license: 'MIT',
196
196
  },
197
+ {
198
+ id: 'react-roi',
199
+ name: 'react-roi',
200
+ href: 'https://github.com/zakodium-oss/react-roi',
201
+ description: 'the box a region of an image is drawn with.',
202
+ license: 'MIT',
203
+ },
204
+ {
205
+ id: 'file-collection',
206
+ name: 'file-collection',
207
+ href: 'https://github.com/cheminfo/file-collection',
208
+ description: 'the reader that takes dropped files, folders and archives.',
209
+ license: 'MIT',
210
+ },
211
+ {
212
+ id: 'zip-js',
213
+ name: 'zip.js',
214
+ href: 'https://github.com/gildas-lormeau/zip.js',
215
+ description: 'the ZIP archives that are read and written in the browser.',
216
+ license: 'BSD-3-Clause',
217
+ },
197
218
  {
198
219
  id: 'preact-signals',
199
220
  name: 'Preact Signals',
@@ -3,12 +3,22 @@ import type { CSSProperties, ReactElement } from 'react';
3
3
  import { joinClassNames } from '../../shared/ui/joinClassNames.ts';
4
4
  import { siteById } from '../core/lookup.ts';
5
5
  import { siteNameColors } from '../core/nameColors.ts';
6
- import type { SiteId } from '../core/sites.ts';
6
+ import type { EcosystemSite, SiteId } from '../core/sites.ts';
7
7
 
8
8
  /** What a site's written name needs. */
9
9
  export interface WordmarkProps {
10
- /** The site whose name is written. */
11
- siteId: SiteId;
10
+ /**
11
+ * The site whose name is written, passed rather than named — for a site that
12
+ * is deliberately not one of `ECOSYSTEM_SITES`. One of `site` and `siteId` is
13
+ * required.
14
+ * @default undefined
15
+ */
16
+ site?: EcosystemSite;
17
+ /**
18
+ * The same site, named rather than passed, which is what a header knows.
19
+ * @default undefined
20
+ */
21
+ siteId?: SiteId;
12
22
  /**
13
23
  * Size of the name, in pixels. The weight comes from the surrounding
14
24
  * context, so the same wordmark suits a header bar and a heading.
@@ -32,12 +42,17 @@ export interface WordmarkProps {
32
42
  * its address.
33
43
  * @param props - The site, the size of the name, and extra class names.
34
44
  * @returns The name, as one inline element that never wraps mid-address.
45
+ * @throws {Error} When neither `site` nor `siteId` is given.
35
46
  */
36
47
  export function Wordmark(props: WordmarkProps): ReactElement {
37
- const { siteId, size = 17, className } = props;
38
- const site = siteById(siteId);
39
- const { lead, alt, dot } = site.name;
40
- const colors = siteNameColors(site);
48
+ const { site, siteId, size = 17, className } = props;
49
+ const written = site ?? (siteId === undefined ? undefined : siteById(siteId));
50
+ if (written === undefined) {
51
+ throw new Error('Wordmark needs one of its `site` and `siteId` props');
52
+ }
53
+
54
+ const { lead, alt, dot } = written.name;
55
+ const colors = siteNameColors(written);
41
56
 
42
57
  return (
43
58
  <span
@@ -44,6 +44,7 @@ export interface MoleculeCanvas3DProps extends Omit<
44
44
  export function MoleculeCanvas3D(props: MoleculeCanvas3DProps): ReactElement {
45
45
  const {
46
46
  molfile,
47
+ frameNewMolecule = 'keep',
47
48
  tools: toolsProp,
48
49
  settings: settingsProp,
49
50
  defaultSettings,
@@ -134,10 +135,11 @@ export function MoleculeCanvas3D(props: MoleculeCanvas3DProps): ReactElement {
134
135
  // Framed on a new molecule or a new viewer, never on a restyle: a slider
135
136
  // that snapped the camera back would undo the reader's orientation.
136
137
  const framed = framedRef.current;
137
- const frameCamera =
138
+ const isNew =
138
139
  molfile !== null &&
139
140
  (framed?.molfile !== molfile || framed.viewer !== viewer);
140
141
  framedRef.current = molfile === null ? null : { molfile, viewer };
142
+ const frameCamera = isNew ? frameNewMolecule : 'none';
141
143
  void drawScene(viewer, molfile, settings, measurements, frameCamera)
142
144
  .then(() => {
143
145
  if (!cancelled) latest.current.onFailureChange(null);
@@ -153,7 +155,7 @@ export function MoleculeCanvas3D(props: MoleculeCanvas3DProps): ReactElement {
153
155
  cancelled = true;
154
156
  cancelAnimationFrame(frame);
155
157
  };
156
- }, [container, molfile, settings, measurements]);
158
+ }, [container, molfile, frameNewMolecule, settings, measurements]);
157
159
 
158
160
  useEffect(() => {
159
161
  void viewerRef.current?.setSpin(spinning, spinSpeed);
@@ -28,6 +28,10 @@ const SPIN_AXIS = Vec3.create(0, 1, 0);
28
28
  /** Smallest radius framed, in ångström, so a single atom is not a close-up. */
29
29
  const MIN_FRAMING_RADIUS = 0.5;
30
30
 
31
+ /** Screen up and viewing direction when the model is seen as its file lays it. */
32
+ const FRONT_UP = Vec3.create(0, 1, 0);
33
+ const FRONT_DIRECTION = Vec3.create(0, 0, -1);
34
+
31
35
  /**
32
36
  * Frame everything currently in the scene.
33
37
  *
@@ -38,19 +42,25 @@ const MIN_FRAMING_RADIUS = 0.5;
38
42
  * after that commit.
39
43
  * @param plugin - The molstar context.
40
44
  * @param durationMilliseconds - Transition length. Pass 0 for an instant jump.
45
+ * @param fromFront - Look down -z with y up, instead of keeping the current
46
+ * direction.
41
47
  */
42
48
  export function resetCamera(
43
49
  plugin: PluginContext,
44
50
  durationMilliseconds = DEFAULT_CAMERA_DURATION,
51
+ fromFront = false,
45
52
  ): void {
46
53
  plugin.canvas3d?.requestCameraReset({
47
54
  durationMs: durationMilliseconds,
48
55
  snapshot: (scene, camera) => {
49
56
  const { center, radius } = scene.boundingSphereVisible;
50
- return camera.getFocus(
51
- center,
52
- Math.max(radius * (1 + FRAMING_MARGIN), MIN_FRAMING_RADIUS),
57
+ const framed = Math.max(
58
+ radius * (1 + FRAMING_MARGIN),
59
+ MIN_FRAMING_RADIUS,
53
60
  );
61
+ return fromFront
62
+ ? camera.getFocus(center, framed, FRONT_UP, FRONT_DIRECTION)
63
+ : camera.getFocus(center, framed);
54
64
  },
55
65
  });
56
66
  }
@@ -11,7 +11,9 @@ import type { Molecule3DViewer } from './viewer.ts';
11
11
  * @param molfile - What to draw, or `null` to empty the scene.
12
12
  * @param settings - How to draw it.
13
13
  * @param measurements - What to draw over it.
14
- * @param frameCamera - Whether to frame the camera once everything is there.
14
+ * @param frameCamera - How to frame the camera once everything is there:
15
+ * `keep` glides to it along the current direction, `front` jumps to it looking
16
+ * down -z, `none` leaves the camera alone.
15
17
  * @returns Nothing; resolves once the scene is complete.
16
18
  */
17
19
  export async function drawScene(
@@ -19,7 +21,7 @@ export async function drawScene(
19
21
  molfile: Molecule3DFile | null,
20
22
  settings: Molecule3DSettings,
21
23
  measurements: readonly Measurement[],
22
- frameCamera: boolean,
24
+ frameCamera: 'none' | 'keep' | 'front',
23
25
  ): Promise<void> {
24
26
  if (molfile === null) {
25
27
  await viewer.hideMolecule();
@@ -39,5 +41,6 @@ export async function drawScene(
39
41
  });
40
42
  }
41
43
  await viewer.showMeasurements(measurements);
42
- if (frameCamera) await viewer.resetCamera();
44
+ if (frameCamera === 'keep') await viewer.resetCamera();
45
+ if (frameCamera === 'front') await viewer.resetCamera(0, true);
43
46
  }
@@ -18,6 +18,14 @@ import type {
18
18
  export interface MoleculeViewer3DProps {
19
19
  /** The molecule to draw, with 3D coordinates, or `null` for none. */
20
20
  molfile: Molecule3DFile | null;
21
+ /**
22
+ * How the camera meets a new molecule. `keep` glides to it along the current
23
+ * direction, which suits conformers of one molecule; `front` jumps to it
24
+ * looking down -z with y up, so unrelated molecules, each laid out in its
25
+ * file the way it reads best, never swing in from the previous view.
26
+ * @default 'keep'
27
+ */
28
+ frameNewMolecule?: 'keep' | 'front';
21
29
  /**
22
30
  * Which buttons the toolbar over the canvas shows; tools not named stay on.
23
31
  * @default every tool
@@ -182,11 +182,15 @@ export class Molecule3DViewer {
182
182
  /**
183
183
  * Frame everything on screen.
184
184
  * @param durationMilliseconds - Transition length; 0 jumps.
185
+ * @param fromFront - Look down -z with y up instead of keeping the direction.
185
186
  * @returns Nothing; resolves once the move has been ordered.
186
187
  */
187
- resetCamera(durationMilliseconds = DEFAULT_CAMERA_DURATION): Promise<void> {
188
+ resetCamera(
189
+ durationMilliseconds = DEFAULT_CAMERA_DURATION,
190
+ fromFront = false,
191
+ ): Promise<void> {
188
192
  return this.#run((plugin) => {
189
- resetCamera(plugin, durationMilliseconds);
193
+ resetCamera(plugin, durationMilliseconds, fromFront);
190
194
  });
191
195
  }
192
196
 
package/src/vite.ts CHANGED
@@ -1,2 +1,3 @@
1
+ export * from './build/vite/index.ts';
1
2
  export * from './seo/vite/index.ts';
2
3
  export * from './slides/vite/index.ts';