Browse Source

Merge branch 'master' into model-conf-fields

Alexander Rose 2 năm trước cách đây
mục cha
commit
e0ea9a2855

+ 2 - 0
CHANGELOG.md

@@ -18,8 +18,10 @@ Note that since we don't clearly distinguish between a public and private interf
 - Remove `JSX` reference from `loci-labels.ts`
 - Fix overpaint/transparency/substance smoothing not updated when geometry changes
 - Fix camera project/unproject when using offset viewport
+- Add support for loading all blocks from a mmcif file as a trajectory
 - Add `Frustum3D` and `Plane3D` math primitives
 - Include `occupancy` and `B_iso_or_equiv` when creating `Conformation` from `Model`
+- Remove LazyImports (introduced in v3.31.1)
 
 ## [v3.32.0] - 2023-03-20
 

+ 6 - 2
src/examples/image-renderer/index.ts

@@ -12,13 +12,16 @@
 import { ArgumentParser } from 'argparse';
 import fs from 'fs';
 import path from 'path';
+import gl from 'gl';
+import pngjs from 'pngjs';
+import jpegjs from 'jpeg-js';
 
 import { Download, ParseCif } from '../../mol-plugin-state/transforms/data';
 import { ModelFromTrajectory, StructureComponent, StructureFromModel, TrajectoryFromMmCif } from '../../mol-plugin-state/transforms/model';
 import { StructureRepresentation3D } from '../../mol-plugin-state/transforms/representation';
 import { HeadlessPluginContext } from '../../mol-plugin/headless-plugin-context';
 import { DefaultPluginSpec } from '../../mol-plugin/spec';
-import { STYLIZED_POSTPROCESSING } from '../../mol-plugin/util/headless-screenshot';
+import { ExternalModules, STYLIZED_POSTPROCESSING } from '../../mol-plugin/util/headless-screenshot';
 import { setFSModule } from '../../mol-util/data-source';
 
 
@@ -45,7 +48,8 @@ async function main() {
     console.log('Outputs:', args.outDirectory);
 
     // Create a headless plugin
-    const plugin = new HeadlessPluginContext(DefaultPluginSpec(), { width: 800, height: 800 });
+    const externalModules: ExternalModules = { gl, pngjs, 'jpeg-js': jpegjs };
+    const plugin = new HeadlessPluginContext(externalModules, DefaultPluginSpec(), { width: 800, height: 800 });
     await plugin.init();
 
     // Download and visualize data in the plugin

+ 26 - 9
src/mol-plugin-state/transforms/model.ts

@@ -272,25 +272,42 @@ const TrajectoryFromMmCif = PluginStateTransform.BuiltIn({
     params(a) {
         if (!a) {
             return {
-                blockHeader: PD.Optional(PD.Text(void 0, { description: 'Header of the block to parse. If none is specifed, the 1st data block in the file is used.' }))
+                loadAllBlocks: PD.Optional(PD.Boolean(false, { description: 'If True, ignore Block Header parameter and parse all datablocks into a single trajectory.' })),
+                blockHeader: PD.Optional(PD.Text(void 0, { description: 'Header of the block to parse. If none is specifed, the 1st data block in the file is used.', hideIf: p => p.loadAllBlocks === true })),
             };
         }
         const { blocks } = a.data;
         return {
-            blockHeader: PD.Optional(PD.Select(blocks[0] && blocks[0].header, blocks.map(b => [b.header, b.header] as [string, string]), { description: 'Header of the block to parse' }))
+            loadAllBlocks: PD.Optional(PD.Boolean(false, { description: 'If True, ignore Block Header parameter and parse all data blocks into a single trajectory.' })),
+            blockHeader: PD.Optional(PD.Select(blocks[0] && blocks[0].header, blocks.map(b => [b.header, b.header] as [string, string]), { description: 'Header of the block to parse', hideIf: p => p.loadAllBlocks === true })),
         };
     }
 })({
     isApplicable: a => a.data.blocks.length > 0,
     apply({ a, params }) {
         return Task.create('Parse mmCIF', async ctx => {
-            const header = params.blockHeader || a.data.blocks[0].header;
-            const block = a.data.blocks.find(b => b.header === header);
-            if (!block) throw new Error(`Data block '${[header]}' not found.`);
-            const models = await trajectoryFromMmCIF(block).runInContext(ctx);
-            if (models.frameCount === 0) throw new Error('No models found.');
-            const props = trajectoryProps(models);
-            return new SO.Molecule.Trajectory(models, props);
+            let trajectory: Trajectory;
+            if (params.loadAllBlocks) {
+                const models: Model[] = [];
+                for (const block of a.data.blocks) {
+                    if (ctx.shouldUpdate) {
+                        await ctx.update(`Parsing ${block.header}...`);
+                    }
+                    const t = await trajectoryFromMmCIF(block).runInContext(ctx);
+                    for (let i = 0; i < t.frameCount; i++) {
+                        models.push(await Task.resolveInContext(t.getFrameAtIndex(i), ctx));
+                    }
+                }
+                trajectory = new ArrayTrajectory(models);
+            } else {
+                const header = params.blockHeader || a.data.blocks[0].header;
+                const block = a.data.blocks.find(b => b.header === header);
+                if (!block) throw new Error(`Data block '${[header]}' not found.`);
+                trajectory = await trajectoryFromMmCIF(block).runInContext(ctx);
+            }
+            if (trajectory.frameCount === 0) throw new Error('No models found.');
+            const props = trajectoryProps(trajectory);
+            return new SO.Molecule.Trajectory(trajectory, props);
         });
     }
 });

+ 28 - 6
src/mol-plugin/headless-plugin-context.ts

@@ -5,33 +5,55 @@
  */
 
 import fs from 'fs';
+import { type PNG } from 'pngjs'; // Only import type here, the actual import must be provided by the caller
+import { type BufferRet as JpegBufferRet } from 'jpeg-js'; // Only import type here, the actual import must be provided by the caller
+
 import { Canvas3D } from '../mol-canvas3d/canvas3d';
 import { PostprocessingProps } from '../mol-canvas3d/passes/postprocessing';
 import { PluginContext } from './context';
 import { PluginSpec } from './spec';
-import { HeadlessScreenshotHelper, HeadlessScreenshotHelperOptions } from './util/headless-screenshot';
+import { HeadlessScreenshotHelper, HeadlessScreenshotHelperOptions, ExternalModules, RawImageData } from './util/headless-screenshot';
 
 
 /** PluginContext that can be used in Node.js (without DOM) */
 export class HeadlessPluginContext extends PluginContext {
     renderer: HeadlessScreenshotHelper;
 
-    constructor(spec: PluginSpec, canvasSize: { width: number, height: number } = { width: 640, height: 480 }, rendererOptions?: HeadlessScreenshotHelperOptions) {
+    /** External modules (`gl` and optionally `pngjs` and `jpeg-js`) must be provided to the constructor (this is to avoid Mol* being dependent on these packages which are only used here) */
+    constructor(externalModules: ExternalModules, spec: PluginSpec, canvasSize: { width: number, height: number } = { width: 640, height: 480 }, rendererOptions?: HeadlessScreenshotHelperOptions) {
         super(spec);
-        this.renderer = new HeadlessScreenshotHelper(canvasSize, undefined, rendererOptions);
+        this.renderer = new HeadlessScreenshotHelper(externalModules, canvasSize, undefined, rendererOptions);
         (this.canvas3d as Canvas3D) = this.renderer.canvas3d;
     }
 
-    /** Render the current plugin state to a PNG or JPEG file */
+    /** Render the current plugin state and save to a PNG or JPEG file */
     async saveImage(outPath: string, imageSize?: { width: number, height: number }, props?: Partial<PostprocessingProps>, format?: 'png' | 'jpeg', jpegQuality = 90) {
         this.canvas3d!.commit(true);
         return await this.renderer.saveImage(outPath, imageSize, props, format, jpegQuality);
     }
 
+    /** Render the current plugin state and return as raw image data */
+    async getImageRaw(imageSize?: { width: number, height: number }, props?: Partial<PostprocessingProps>): Promise<RawImageData> {
+        this.canvas3d!.commit(true);
+        return await this.renderer.getImageRaw(imageSize, props);
+    }
+
+    /** Render the current plugin state and return as a PNG object */
+    async getImagePng(imageSize?: { width: number, height: number }, props?: Partial<PostprocessingProps>): Promise<PNG> {
+        this.canvas3d!.commit(true);
+        return await this.renderer.getImagePng(imageSize, props);
+    }
+
+    /** Render the current plugin state and return as a JPEG object */
+    async getImageJpeg(imageSize?: { width: number, height: number }, props?: Partial<PostprocessingProps>, jpegQuality: number = 90): Promise<JpegBufferRet> {
+        this.canvas3d!.commit(true);
+        return await this.renderer.getImageJpeg(imageSize, props);
+    }
+
     /** Get the current plugin state */
-    getStateSnapshot() {
+    async getStateSnapshot() {
         this.canvas3d!.commit(true);
-        return this.managers.snapshot.getStateSnapshot({ params: {} });
+        return await this.managers.snapshot.getStateSnapshot({ params: {} });
     }
 
     /** Save the current plugin state to a MOLJ file */

+ 16 - 12
src/mol-plugin/util/headless-screenshot.ts

@@ -10,8 +10,8 @@
 
 import fs from 'fs';
 import path from 'path';
-import { type BufferRet as JpegBufferRet } from 'jpeg-js'; // Only import type here, the actual import is done by LazyImports
-import { type PNG } from 'pngjs'; // Only import type here, the actual import is done by LazyImports
+import { type BufferRet as JpegBufferRet } from 'jpeg-js'; // Only import type here, the actual import must be provided by the caller
+import { type PNG } from 'pngjs'; // Only import type here, the actual import must be provided by the caller
 
 import { Canvas3D, Canvas3DContext, Canvas3DProps, DefaultCanvas3DParams } from '../../mol-canvas3d/canvas3d';
 import { ImagePass, ImageProps } from '../../mol-canvas3d/passes/image';
@@ -22,16 +22,14 @@ import { AssetManager } from '../../mol-util/assets';
 import { ColorNames } from '../../mol-util/color/names';
 import { PixelData } from '../../mol-util/image';
 import { InputObserver } from '../../mol-util/input/input-observer';
-import { LazyImports } from '../../mol-util/lazy-imports';
 import { ParamDefinition } from '../../mol-util/param-definition';
 
 
-const lazyImports = LazyImports.create('gl', 'jpeg-js', 'pngjs') as {
+export interface ExternalModules {
     'gl': typeof import('gl'),
-    'jpeg-js': typeof import('jpeg-js'),
-    'pngjs': typeof import('pngjs'),
-};
-
+    'jpeg-js'?: typeof import('jpeg-js'),
+    'pngjs'?: typeof import('pngjs'),
+}
 
 export type HeadlessScreenshotHelperOptions = {
     webgl?: WebGLContextAttributes,
@@ -51,11 +49,11 @@ export class HeadlessScreenshotHelper {
     readonly canvas3d: Canvas3D;
     readonly imagePass: ImagePass;
 
-    constructor(readonly canvasSize: { width: number, height: number }, canvas3d?: Canvas3D, options?: HeadlessScreenshotHelperOptions) {
+    constructor(readonly externalModules: ExternalModules, readonly canvasSize: { width: number, height: number }, canvas3d?: Canvas3D, options?: HeadlessScreenshotHelperOptions) {
         if (canvas3d) {
             this.canvas3d = canvas3d;
         } else {
-            const glContext = lazyImports.gl(this.canvasSize.width, this.canvasSize.height, options?.webgl ?? defaultWebGLAttributes());
+            const glContext = this.externalModules.gl(this.canvasSize.width, this.canvasSize.height, options?.webgl ?? defaultWebGLAttributes());
             const webgl = createContext(glContext);
             const input = InputObserver.create();
             const attribs = { ...Canvas3DContext.DefaultAttribs };
@@ -93,14 +91,20 @@ export class HeadlessScreenshotHelper {
 
     async getImagePng(imageSize?: { width: number, height: number }, postprocessing?: Partial<PostprocessingProps>): Promise<PNG> {
         const imageData = await this.getImageRaw(imageSize, postprocessing);
-        const generatedPng = new lazyImports.pngjs.PNG({ width: imageData.width, height: imageData.height });
+        if (!this.externalModules.pngjs) {
+            throw new Error("External module 'pngjs' was not provided. If you want to use getImagePng, you must import 'pngjs' and provide it to the HeadlessPluginContext/HeadlessScreenshotHelper constructor.");
+        }
+        const generatedPng = new this.externalModules.pngjs.PNG({ width: imageData.width, height: imageData.height });
         generatedPng.data = Buffer.from(imageData.data.buffer);
         return generatedPng;
     }
 
     async getImageJpeg(imageSize?: { width: number, height: number }, postprocessing?: Partial<PostprocessingProps>, jpegQuality: number = 90): Promise<JpegBufferRet> {
         const imageData = await this.getImageRaw(imageSize, postprocessing);
-        const generatedJpeg = lazyImports['jpeg-js'].encode(imageData, jpegQuality);
+        if (!this.externalModules['jpeg-js']) {
+            throw new Error("External module 'jpeg-js' was not provided. If you want to use getImageJpeg, you must import 'jpeg-js' and provide it to the HeadlessPluginContext/HeadlessScreenshotHelper constructor.");
+        }
+        const generatedJpeg = this.externalModules['jpeg-js'].encode(imageData, jpegQuality);
         return generatedJpeg;
     }
 

+ 0 - 47
src/mol-util/lazy-imports.ts

@@ -1,47 +0,0 @@
-/**
- * Copyright (c) 2019-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Adam Midlik <midlik@gmail.com>
- *
- * Manage dependencies which are not listed in `package.json` for performance reasons.
- */
-
-
-const _loadedExtraPackages: { [dep: string]: any } = {};
-
-/** Define imports that only get imported when first needed.
- * Example usage:
- * ```
- * const lazyImports = LazyImports.create('gl', 'jpeg-js', 'pngjs') as {
- *     'gl': typeof import('gl'),
- *     'jpeg-js': typeof import('jpeg-js'),
- *     'pngjs': typeof import('pngjs'),
- * };
- * ...
- * lazyImports.pngjs.blablabla("I'm being imported now");
- * lazyImports.pngjs.blablabla("I'm cached :D");
- * ```
- */
-export class LazyImports {
-    static create<U extends string>(...packages: U[]): { [dep in U]: any } {
-        return new LazyImports(packages) as any;
-    }
-    private constructor(private packages: string[]) {
-        for (const p of packages) {
-            Object.defineProperty(this, p, {
-                get: () => this.getPackage(p),
-            });
-        }
-    }
-    private getPackage(packageName: string) {
-        if (!_loadedExtraPackages[packageName]) {
-            try {
-                _loadedExtraPackages[packageName] = require(packageName);
-            } catch {
-                const message = `Package '${packageName}' is not installed. (Some packages are not listed in the 'molstar' package dependencies for performance reasons. If you're seeing this error, you'll probably need them. If your project depends on 'molstar', add these to your dependencies: ${this.packages.join(', ')}. If you're running 'molstar' directly, run this: npm install --no-save ${this.packages.join(' ')})`;
-                throw new Error(message);
-            }
-        }
-        return _loadedExtraPackages[packageName];
-    }
-}