Browse Source

Merge branch 'master' of https://github.com/molstar/molstar

dsehnal 4 years ago
parent
commit
18023d7f26

+ 3 - 1
src/apps/viewer/index.ts

@@ -10,6 +10,7 @@ import { CellPack } from '../../extensions/cellpack';
 import { DnatcoConfalPyramids } from '../../extensions/dnatco';
 import { G3DFormat, G3dProvider } from '../../extensions/g3d/format';
 import { Mp4Export } from '../../extensions/mp4-export';
+import { GeometryExport } from '../../extensions/geo-export';
 import { PDBeStructureQualityReport } from '../../extensions/pdbe';
 import { RCSBAssemblySymmetry, RCSBValidationReport } from '../../extensions/rcsb';
 import { DownloadStructure, PdbDownloadProvider } from '../../mol-plugin-state/actions/structure';
@@ -55,7 +56,8 @@ const Extensions = {
     'rcsb-validation-report': PluginSpec.Behavior(RCSBValidationReport),
     'anvil-membrane-orientation': PluginSpec.Behavior(ANVILMembraneOrientation),
     'g3d': PluginSpec.Behavior(G3DFormat),
-    'mp4-export': PluginSpec.Behavior(Mp4Export)
+    'mp4-export': PluginSpec.Behavior(Mp4Export),
+    'geo-export': PluginSpec.Behavior(GeometryExport)
 };
 
 const DefaultViewerOptions = {

+ 69 - 0
src/extensions/geo-export/controls.ts

@@ -0,0 +1,69 @@
+/**
+ * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
+ */
+
+import { PluginComponent } from '../../mol-plugin-state/component';
+import { PluginContext } from '../../mol-plugin/context';
+import { Task } from '../../mol-task';
+import { ObjExporter } from './export';
+import { PluginStateObject } from '../../mol-plugin-state/objects';
+import { StateSelection } from '../../mol-state';
+import { SetUtils } from '../../mol-util/set';
+import { zip } from '../../mol-util/zip/zip';
+
+export class GeometryControls extends PluginComponent {
+    getFilename() {
+        const models = this.plugin.state.data.select(StateSelection.Generators.rootsOfType(PluginStateObject.Molecule.Model)).map(s => s.obj!.data);
+        const uniqueIds = new Set<string>();
+        models.forEach(m => uniqueIds.add(m.entryId.toUpperCase()));
+        const idString = SetUtils.toArray(uniqueIds).join('-');
+        return `${idString || 'molstar-model'}`;
+    }
+
+    exportObj() {
+        const task = Task.create('Export OBJ', async ctx => {
+            try {
+                const renderObjects = this.plugin.canvas3d?.getRenderObjects()!;
+
+                const filename = this.getFilename();
+                const objExporter = new ObjExporter(filename);
+                for (let i = 0, il = renderObjects.length; i < il; ++i) {
+                    await ctx.update({ message: `Exporting object ${i}/${il}` });
+                    await objExporter.add(renderObjects[i], ctx);
+                }
+                const { obj, mtl } = objExporter.getData();
+
+                const asciiWrite = (data: Uint8Array, str: string) => {
+                    for (let i = 0, il = str.length; i < il; ++i) {
+                        data[i] = str.charCodeAt(i);
+                    }
+                };
+                const objData = new Uint8Array(obj.length);
+                asciiWrite(objData, obj);
+                const mtlData = new Uint8Array(mtl.length);
+                asciiWrite(mtlData, mtl);
+
+                const zipDataObj = {
+                    [filename + '.obj']: objData,
+                    [filename + '.mtl']: mtlData
+                };
+                const zipData = await zip(ctx, zipDataObj);
+                return {
+                    zipData,
+                    filename: filename + '.zip'
+                };
+            } catch (e) {
+                this.plugin.log.error('' + e);
+                throw e;
+            }
+        });
+
+        return this.plugin.runTask(task, { useOverlay: true });
+    }
+
+    constructor(private plugin: PluginContext) {
+        super();
+    }
+}

+ 321 - 0
src/extensions/geo-export/export.ts

@@ -0,0 +1,321 @@
+/**
+ * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
+ */
+
+import { GraphicsRenderObject } from '../../mol-gl/render-object';
+import { MeshValues } from '../../mol-gl/renderable/mesh';
+import { LinesValues } from '../../mol-gl/renderable/lines';
+import { PointsValues } from '../../mol-gl/renderable/points';
+import { SpheresValues } from '../../mol-gl/renderable/spheres';
+import { CylindersValues } from '../../mol-gl/renderable/cylinders';
+import { BaseValues, SizeValues } from '../../mol-gl/renderable/schema';
+import { TextureImage } from '../../mol-gl/renderable/util';
+import { MeshBuilder } from '../../mol-geo/geometry/mesh/mesh-builder';
+import { addSphere } from '../../mol-geo/geometry/mesh/builder/sphere';
+import { addCylinder } from '../../mol-geo/geometry/mesh/builder/cylinder';
+import { Vec3, Mat3, Mat4 } from '../../mol-math/linear-algebra';
+import { RuntimeContext } from '../../mol-task';
+import { StringBuilder } from '../../mol-util';
+import { Color } from '../../mol-util/color/color';
+import { decodeFloatRGB } from '../../mol-util/float-packing';
+
+// avoiding namespace lookup improved performance in Chrome (Aug 2020)
+const v3fromArray = Vec3.fromArray;
+const v3transformMat4 = Vec3.transformMat4;
+const v3transformMat3 = Vec3.transformMat3;
+const mat3directionTransform = Mat3.directionTransform;
+
+type RenderObjectExportData = {
+    [k: string]: string | Uint8Array | undefined
+}
+
+interface RenderObjectExporter<D extends RenderObjectExportData> {
+    add(renderObject: GraphicsRenderObject, ctx: RuntimeContext): Promise<void> | undefined
+    getData(): D
+}
+
+// http://paulbourke.net/dataformats/obj/
+// http://paulbourke.net/dataformats/mtl/
+
+export type ObjData = {
+    obj: string
+    mtl: string
+}
+
+export class ObjExporter implements RenderObjectExporter<ObjData> {
+    private obj = StringBuilder.create();
+    private mtl = StringBuilder.create();
+    private vertexOffset = 0;
+    private currentColor: Color | undefined;
+    private currentAlpha: number | undefined;
+    private materialSet = new Set<string>();
+
+    private static getSizeFromTexture(tSize: TextureImage<Uint8Array>, i: number): number {
+        const r = tSize.array[i * 3];
+        const g = tSize.array[i * 3 + 1];
+        const b = tSize.array[i * 3 + 2];
+        return decodeFloatRGB(r, g, b);
+    }
+
+    private static getSize(values: BaseValues & SizeValues, instanceIndex: number, group: number): number {
+        const tSize = values.tSize.ref.value;
+        let size = 0;
+        switch (values.dSizeType.ref.value) {
+            case 'uniform':
+                size = values.uSize.ref.value;
+                break;
+            case 'instance':
+                size = ObjExporter.getSizeFromTexture(tSize, instanceIndex) / 100;
+                break;
+            case 'group':
+                size = ObjExporter.getSizeFromTexture(tSize, group) / 100;
+                break;
+            case 'groupInstance':
+                const groupCount = values.uGroupCount.ref.value;
+                size = ObjExporter.getSizeFromTexture(tSize, instanceIndex * groupCount + group) / 100;
+                break;
+        }
+        return size * values.uSizeFactor.ref.value;
+    }
+
+    private updateMaterial(color: Color, alpha: number) {
+        if (this.currentColor === color && this.currentAlpha === alpha) return;
+
+        this.currentColor = color;
+        this.currentAlpha = alpha;
+        const material = Color.toHexString(color) + alpha;
+        StringBuilder.writeSafe(this.obj, `usemtl ${material}`);
+        StringBuilder.newline(this.obj);
+        if (!this.materialSet.has(material)) {
+            this.materialSet.add(material);
+            const [r, g, b] = Color.toRgbNormalized(color);
+            const mtl = this.mtl;
+            StringBuilder.writeSafe(mtl, `newmtl ${material}\n`);
+            StringBuilder.writeSafe(mtl, 'illum 2\n'); // illumination model
+            StringBuilder.writeSafe(mtl, 'Ns 163\n'); // specular exponent
+            StringBuilder.writeSafe(mtl, 'Ni 0.001\n'); // optical density a.k.a. index of refraction
+            StringBuilder.writeSafe(mtl, 'Ka 0 0 0\n'); // ambient reflectivity
+            StringBuilder.writeSafe(mtl, 'Kd '); // diffuse reflectivity
+            StringBuilder.writeFloat(mtl, r, 1000);
+            StringBuilder.whitespace1(mtl);
+            StringBuilder.writeFloat(mtl, g, 1000);
+            StringBuilder.whitespace1(mtl);
+            StringBuilder.writeFloat(mtl, b, 1000);
+            StringBuilder.newline(mtl);
+            StringBuilder.writeSafe(mtl, 'Ks 0.25 0.25 0.25\n'); // specular reflectivity
+            StringBuilder.writeSafe(mtl, 'd '); // dissolve
+            StringBuilder.writeFloat(mtl, alpha, 1000);
+            StringBuilder.newline(mtl);
+        }
+    }
+
+    private async addMeshWithColors(vertices: Float32Array, normals: Float32Array, indices: Uint32Array, groups: Float32Array, vertexCount: number, drawCount: number, values: BaseValues, instanceIndex: number, ctx: RuntimeContext) {
+        const obj = this.obj;
+        const t = Mat4();
+        const n = Mat3();
+        const tmpV = Vec3();
+
+        const colorType = values.dColorType.ref.value;
+        const tColor = values.tColor.ref.value.array;
+        const uAlpha = values.uAlpha.ref.value;
+        const aTransform = values.aTransform.ref.value;
+
+        Mat4.fromArray(t, aTransform, instanceIndex * 16);
+        mat3directionTransform(n, t);
+
+        const currentProgress = (vertexCount * 2 + drawCount) * instanceIndex;
+        await ctx.update({ isIndeterminate: false, current: currentProgress, max: (vertexCount * 2 + drawCount) * values.uInstanceCount.ref.value });
+
+        // position
+        for (let i = 0; i < vertexCount; ++i) {
+            if (i % 1000 === 0 && ctx.shouldUpdate) await ctx.update({ current: currentProgress + i });
+            v3transformMat4(tmpV, v3fromArray(tmpV, vertices, i * 3), t);
+            StringBuilder.writeSafe(obj, 'v ');
+            StringBuilder.writeFloat(obj, tmpV[0], 1000);
+            StringBuilder.whitespace1(obj);
+            StringBuilder.writeFloat(obj, tmpV[1], 1000);
+            StringBuilder.whitespace1(obj);
+            StringBuilder.writeFloat(obj, tmpV[2], 1000);
+            StringBuilder.newline(obj);
+        }
+
+        // normal
+        for (let i = 0; i < vertexCount; ++i) {
+            if (i % 1000 === 0 && ctx.shouldUpdate) await ctx.update({ current: currentProgress + vertexCount + i });
+            v3transformMat3(tmpV, v3fromArray(tmpV, normals, i * 3), n);
+            StringBuilder.writeSafe(obj, 'vn ');
+            StringBuilder.writeFloat(obj, tmpV[0], 100);
+            StringBuilder.whitespace1(obj);
+            StringBuilder.writeFloat(obj, tmpV[1], 100);
+            StringBuilder.whitespace1(obj);
+            StringBuilder.writeFloat(obj, tmpV[2], 100);
+            StringBuilder.newline(obj);
+        }
+
+        // face
+        for (let i = 0; i < drawCount; i += 3) {
+            if (i % 3000 === 0 && ctx.shouldUpdate) await ctx.update({ current: currentProgress + vertexCount * 2 + i });
+            let color: Color;
+            switch (colorType) {
+                case 'uniform':
+                    color = Color.fromNormalizedArray(values.uColor.ref.value, 0);
+                    break;
+                case 'instance':
+                    color = Color.fromArray(tColor, instanceIndex * 3);
+                    break;
+                case 'group':
+                    color = Color.fromArray(tColor, groups[indices[i]] * 3);
+                    break;
+                case 'groupInstance':
+                    const groupCount = values.uGroupCount.ref.value;
+                    const group = groups[indices[i]];
+                    color = Color.fromArray(tColor, (instanceIndex * groupCount + group) * 3);
+                    break;
+                case 'vertex':
+                    color = Color.fromArray(tColor, i * 3);
+                    break;
+                case 'vertexInstance':
+                    color = Color.fromArray(tColor, (instanceIndex * drawCount + i) * 3);
+                    break;
+                default: throw new Error('Unsupported color type.');
+            }
+            this.updateMaterial(color, uAlpha);
+
+            const v1 = this.vertexOffset + indices[i] + 1;
+            const v2 = this.vertexOffset + indices[i + 1] + 1;
+            const v3 = this.vertexOffset + indices[i + 2] + 1;
+            StringBuilder.writeSafe(obj, 'f ');
+            StringBuilder.writeInteger(obj, v1);
+            StringBuilder.writeSafe(obj, '//');
+            StringBuilder.writeIntegerAndSpace(obj, v1);
+            StringBuilder.writeInteger(obj, v2);
+            StringBuilder.writeSafe(obj, '//');
+            StringBuilder.writeIntegerAndSpace(obj, v2);
+            StringBuilder.writeInteger(obj, v3);
+            StringBuilder.writeSafe(obj, '//');
+            StringBuilder.writeInteger(obj, v3);
+            StringBuilder.newline(obj);
+        }
+
+        this.vertexOffset += vertexCount;
+    }
+
+    private async addMesh(values: MeshValues, ctx: RuntimeContext) {
+        const aPosition = values.aPosition.ref.value;
+        const aNormal = values.aNormal.ref.value;
+        const elements = values.elements.ref.value;
+        const aGroup = values.aGroup.ref.value;
+        const instanceCount = values.instanceCount.ref.value;
+        const vertexCount = values.uVertexCount.ref.value;
+        const drawCount = values.drawCount.ref.value;
+
+        for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
+            await this.addMeshWithColors(aPosition, aNormal, elements, aGroup, vertexCount, drawCount, values, instanceIndex, ctx);
+        }
+    }
+
+    private async addLines(values: LinesValues, ctx: RuntimeContext) {
+        // TODO
+    }
+
+    private async addPoints(values: PointsValues, ctx: RuntimeContext) {
+        // TODO
+    }
+
+    private async addSpheres(values: SpheresValues, ctx: RuntimeContext) {
+        const center = Vec3();
+
+        const aPosition = values.aPosition.ref.value;
+        const aGroup = values.aGroup.ref.value;
+        const instanceCount = values.instanceCount.ref.value;
+        const vertexCount = values.uVertexCount.ref.value;
+
+        for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
+            const state = MeshBuilder.createState(512, 256);
+
+            for (let i = 0; i < vertexCount; i += 4) {
+                v3fromArray(center, aPosition, i * 3);
+
+                const group = aGroup[i];
+                const radius = ObjExporter.getSize(values, instanceIndex, group);
+                state.currentGroup = group;
+                addSphere(state, center, radius, 2);
+            }
+
+            const mesh = MeshBuilder.getMesh(state);
+            const vertices = mesh.vertexBuffer.ref.value;
+            const normals = mesh.normalBuffer.ref.value;
+            const indices = mesh.indexBuffer.ref.value;
+            const groups = mesh.groupBuffer.ref.value;
+            await this.addMeshWithColors(vertices, normals, indices, groups, vertices.length / 3, indices.length, values, instanceIndex, ctx);
+        }
+    }
+
+    private async addCylinders(values: CylindersValues, ctx: RuntimeContext) {
+        const start = Vec3();
+        const end = Vec3();
+
+        const aStart = values.aStart.ref.value;
+        const aEnd = values.aEnd.ref.value;
+        const aScale = values.aScale.ref.value;
+        const aCap = values.aCap.ref.value;
+        const aGroup = values.aGroup.ref.value;
+        const instanceCount = values.instanceCount.ref.value;
+        const vertexCount = values.uVertexCount.ref.value;
+
+        for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
+            const state = MeshBuilder.createState(512, 256);
+
+            for (let i = 0; i < vertexCount; i += 6) {
+                v3fromArray(start, aStart, i * 3);
+                v3fromArray(end, aEnd, i * 3);
+
+                const group = aGroup[i];
+                const radius = ObjExporter.getSize(values, instanceIndex, group) * aScale[i];
+                const cap = aCap[i];
+                const topCap = cap === 1 || cap === 3;
+                const bottomCap = cap >= 2;
+                const cylinderProps = { radiusTop: radius, radiusBottom: radius, topCap, bottomCap, radialSegments: 32 };
+                state.currentGroup = aGroup[i];
+                addCylinder(state, start, end, 1, cylinderProps);
+            }
+
+            const mesh = MeshBuilder.getMesh(state);
+            const vertices = mesh.vertexBuffer.ref.value;
+            const normals = mesh.normalBuffer.ref.value;
+            const indices = mesh.indexBuffer.ref.value;
+            const groups = mesh.groupBuffer.ref.value;
+            await this.addMeshWithColors(vertices, normals, indices, groups, vertices.length / 3, indices.length, values, instanceIndex, ctx);
+        }
+    }
+
+    add(renderObject: GraphicsRenderObject, ctx: RuntimeContext) {
+        if (!renderObject.state.visible) return;
+
+        switch (renderObject.type) {
+            case 'mesh':
+                return this.addMesh(renderObject.values as MeshValues, ctx);
+            case 'lines':
+                return this.addLines(renderObject.values as LinesValues, ctx);
+            case 'points':
+                return this.addPoints(renderObject.values as PointsValues, ctx);
+            case 'spheres':
+                return this.addSpheres(renderObject.values as SpheresValues, ctx);
+            case 'cylinders':
+                return this.addCylinders(renderObject.values as CylindersValues, ctx);
+        }
+    }
+
+    getData() {
+        return {
+            obj: StringBuilder.getString(this.obj),
+            mtl: StringBuilder.getString(this.mtl)
+        };
+    }
+
+    constructor(filename: string) {
+        StringBuilder.writeSafe(this.obj, `mtllib ${filename}.mtl\n`);
+    }
+}

+ 30 - 0
src/extensions/geo-export/index.ts

@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
+ */
+
+import { PluginBehavior } from '../../mol-plugin/behavior/behavior';
+import { GeometryExporterUI } from './ui';
+
+export const GeometryExport = PluginBehavior.create<{ }>({
+    name: 'extension-geo-export',
+    category: 'misc',
+    display: {
+        name: 'Geometry Export'
+    },
+    ctor: class extends PluginBehavior.Handler<{ }> {
+        register(): void {
+            this.ctx.customStructureControls.set('geo-export', GeometryExporterUI as any);
+        }
+
+        update() {
+            return false;
+        }
+
+        unregister() {
+            this.ctx.customStructureControls.delete('geo-export');
+        }
+    },
+    params: () => ({ })
+});

+ 64 - 0
src/extensions/geo-export/ui.tsx

@@ -0,0 +1,64 @@
+/**
+ * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
+ */
+
+import { CollapsableControls, CollapsableState } from '../../mol-plugin-ui/base';
+import { Button } from '../../mol-plugin-ui/controls/common';
+import { GetAppSvg, CubeSendSvg } from '../../mol-plugin-ui/controls/icons';
+import { download } from '../../mol-util/download';
+import { GeometryControls } from './controls';
+
+interface State {
+    busy?: boolean
+}
+
+export class GeometryExporterUI extends CollapsableControls<{}, State> {
+    private _controls: GeometryControls | undefined;
+
+    get controls() {
+        return this._controls || (this._controls = new GeometryControls(this.plugin));
+    }
+
+    protected defaultState(): State & CollapsableState {
+        return {
+            header: 'Export Geometries',
+            isCollapsed: true,
+            brand: { accent: 'cyan', svg: CubeSendSvg }
+        };
+    }
+
+    protected renderControls(): JSX.Element {
+        return <>
+            <Button icon={GetAppSvg}
+                onClick={this.saveObj} style={{ marginTop: 1 }}
+                disabled={this.state.busy || !this.plugin.canvas3d?.reprCount.value}>
+                Save OBJ + MTL
+            </Button>
+        </>;
+    }
+
+    componentDidMount() {
+        this.subscribe(this.plugin.canvas3d!.reprCount, () => {
+            if (!this.state.isCollapsed) this.forceUpdate();
+        });
+    }
+
+    componentWillUnmount() {
+        this._controls?.dispose();
+        this._controls = void 0;
+    }
+
+    saveObj = async () => {
+        try {
+            this.setState({ busy: true });
+            const data = await this.controls.exportObj();
+            this.setState({ busy: false });
+
+            download(new Blob([data.zipData]), data.filename);
+        } catch {
+            this.setState({ busy: false });
+        }
+    }
+}

+ 3 - 0
src/mol-plugin-ui/controls/icons.tsx

@@ -41,6 +41,9 @@ export function MoleculeSvg() { return _Molecule; }
 const _CubeOutline = <svg width='24px' height='24px' viewBox='0 0 24 24' strokeWidth='0.1px'><path d="M21,16.5C21,16.88 20.79,17.21 20.47,17.38L12.57,21.82C12.41,21.94 12.21,22 12,22C11.79,22 11.59,21.94 11.43,21.82L3.53,17.38C3.21,17.21 3,16.88 3,16.5V7.5C3,7.12 3.21,6.79 3.53,6.62L11.43,2.18C11.59,2.06 11.79,2 12,2C12.21,2 12.41,2.06 12.57,2.18L20.47,6.62C20.79,6.79 21,7.12 21,7.5V16.5M12,4.15L6.04,7.5L12,10.85L17.96,7.5L12,4.15M5,15.91L11,19.29V12.58L5,9.21V15.91M19,15.91V9.21L13,12.58V19.29L19,15.91Z" /></svg>;
 export function CubeOutlineSvg() { return _CubeOutline; }
 
+const _CubeSend = <svg width='24px' height='24px' viewBox='0 0 24 24' strokeWidth='0.1px'><path d="M16,4L9,8.04V15.96L16,20L23,15.96V8.04M16,6.31L19.8,8.5L16,10.69L12.21,8.5M0,7V9H7V7M11,10.11L15,12.42V17.11L11,14.81M21,10.11V14.81L17,17.11V12.42M2,11V13H7V11M4,15V17H7V15" /></svg>;
+export function CubeSendSvg() { return _CubeSend; }
+
 const _CursorDefaultOutline = <svg width='24px' height='24px' viewBox='0 0 24 24'><path d='M10.07,14.27C10.57,14.03 11.16,14.25 11.4,14.75L13.7,19.74L15.5,18.89L13.19,13.91C12.95,13.41 13.17,12.81 13.67,12.58L13.95,12.5L16.25,12.05L8,5.12V15.9L9.82,14.43L10.07,14.27M13.64,21.97C13.14,22.21 12.54,22 12.31,21.5L10.13,16.76L7.62,18.78C7.45,18.92 7.24,19 7,19A1,1 0 0,1 6,18V3A1,1 0 0,1 7,2C7.24,2 7.47,2.09 7.64,2.23L7.65,2.22L19.14,11.86C19.57,12.22 19.62,12.85 19.27,13.27C19.12,13.45 18.91,13.57 18.7,13.61L15.54,14.23L17.74,18.96C18,19.46 17.76,20.05 17.26,20.28L13.64,21.97Z' /></svg>;
 export function CursorDefaultOutlineSvg() { return _CursorDefaultOutline; }