partridgejiang / Kekule.js

A Javascript cheminformatics toolkit.
http://partridgejiang.github.io/Kekule.js
MIT License
247 stars 61 forks source link

fit molecule in fixed-size Viewer #33

Open rmrmg opened 6 years ago

rmrmg commented 6 years ago

In fixed-sized Viewer molecule are not scaled to Viewer size. It is not a problem for small molecule but for big only part of structure is display. Is is possible somehow to force scaling big (which not fit to viewer size) molecule ?

partridgejiang commented 6 years ago

Just set autofit property of viewer to true:

viewer.setAutofit(true);

or in HTML tag:

<span data-widget="Kekule.ChemWidget.Viewer" data-autofit="true"></span>
rmrmg commented 6 years ago

@partridgejiang: This indeed decrease size of big structure and make it able to fit in Viewer. Unfortunately it also increase size of small molecule which I dont want.

partridgejiang commented 6 years ago

Such a feature may be implemented in the future. Currently there is a workaround:

var box = viewer.getPainter().estimateRenderBox(viewer.getDrawContext());
var deltaX = box.x2 - box.x1;
var deltaY = box.y2 - box.y1;
var widgetDimension = viewer.getDimension();
if (deltaX > widgetDimension .width || deltaY > widgetDimension .height)
  viewer.setAutofit(true);
richardoptibrium commented 4 years ago
var deltaX = box.x2 - box.x1;
var deltaY = box.y2 - box.y1;
var widgetDimension = viewer.getDimension();
if (deltaX > widgetDimension .width || deltaY > widgetDimension .height)
  viewer.setAutofit(true);

Unfortunately, this does not seem to work for me:

    const { Molecule } = require('openchemlib');

    try {
        const molfile = Molecule.fromSmiles(this.props.structure).toMolfile();

        const mol = Kekule.IO.loadFormatData(molfile, 'mol')

        const parentElem = this.cardRef;
        let moleculeContainer = parentElem.current.querySelector('.molecule-render-container');

        let renderType = Kekule.Render.RendererType.R2D//R3D  // do 2D or 3D drawing

        let chemViewer = new Kekule.ChemWidget.Viewer(moleculeContainer);

        Kekule.DomUtils.clearChildContent(moleculeContainer);

        // create painter, bind with molecule
        let painter = new Kekule.Render.ChemObjPainter(renderType, mol);
        let box = painter.estimateRenderBox(chemViewer.getDrawContext());

        let deltaX = box.x2 - box.x1;
        let deltaY = box.y2 - box.y1;
        let widgetDimension = chemViewer.getDimension();
        if (deltaX > widgetDimension.width || deltaY > widgetDimension.height) {
            console.log("AUTOFIT!");
            chemViewer.setAutofit(true);
        }

        // create context inside parentElem
        let dim = Kekule.HtmlElementUtils.getElemOffsetDimension(moleculeContainer); // get width/height of parent element
        const { width, height } = dim;
        // const { offsetWidth: width, offsetHeight: height } = moleculeContainer;//this.props;

        let context = painter.createContext(moleculeContainer, width, height); // create context fulfill parent element

        // at last, draw the molecule at the center of context
        painter.draw(context, { 'x': width / 2, 'y': height / 2 });

    } catch (error) {
        console.log(error);
    }
partridgejiang commented 4 years ago

Hi @richardoptibrium, in the code above, you mixed the usage of the low level painter and the Viewer widget, and did the actual render task by the painter. By using the painter, you had to deal with many low level details manually, including the scale of molecule sizes. The autofit property of Viewer actually taken no effect there. So in most cases, the low level painter is not recommended, just use Viewer widget alone instead. The following code may fit your purpose:

    const { Molecule } = require('openchemlib');

    try {
        const molfile = Molecule.fromSmiles(this.props.structure).toMolfile();

        const mol = Kekule.IO.loadFormatData(molfile, 'mol')

        const parentElem = this.cardRef;
        let moleculeContainer = parentElem.current.querySelector('.molecule-render-container');
        let renderType = Kekule.Render.RendererType.R2D;
        let chemViewer = new Kekule.ChemWidget.Viewer(moleculeContainer, null, renderType);
        chemViewer.setAutoSize(false).setAutofit(false).setChemObj(mol);

        let box = chemViewer.getPainter().estimateRenderBox(chemViewer.getDrawContext());  // an internal painter is already encapsulated by viewer, no need to manually created one
        let deltaX = box.x2 - box.x1;
        let deltaY = box.y2 - box.y1;
        let widgetDimension = chemViewer.getDimension();
        if (deltaX > widgetDimension.width || deltaY > widgetDimension.height) {
            console.log("AUTOFIT!");
            chemViewer.setAutofit(true);
        }       
    } catch (error) {
        console.log(error);
    }
richardoptibrium commented 4 years ago

Hi partridgejiang, Thanks for the response, however I'm now seeing molecules rendered in an extreme compressed state, example shown (aspirin): example It also means I have an interactive render when I ideally wanted a static render (that cannot be panned etc.)

partridgejiang commented 4 years ago

@richardoptibrium To do that you have to set the size of the chem viewer element by CSS first or use JavaScript code:

chemViewer.setDimension('300px', '200px');

then you can disable the interaction ability of viewer by:

chemViewer.setPredefinedSetting('static');
richardoptibrium commented 4 years ago

Currently:

        const molfile = Molecule.fromSmiles(this.props.structure).toMolfile();

        const mol = Kekule.IO.loadFormatData(molfile, 'mol')

        const parentElem = this.cardRef;
        let moleculeContainer = parentElem.current.querySelector('.molecule-render-container');

        let renderType = Kekule.Render.RendererType.R2D//R3D  // do 2D or 3D drawing

        let chemViewer = new Kekule.ChemWidget.Viewer(moleculeContainer);
        chemViewer.setAutoSize(false).setAutofit(false).setChemObj(mol).setDimension('100px', '100px').setPredefinedSetting('static');

        // create painter, bind with molecule
        let box = chemViewer.getPainter().estimateRenderBox(chemViewer.getDrawContext());

        let deltaX = box.x2 - box.x1;
        let deltaY = box.y2 - box.y1;
        let widgetDimension = chemViewer.getDimension();
        if (deltaX > widgetDimension.width || deltaY > widgetDimension.height) {
            console.log("AUTOFIT!");
            chemViewer.setAutofit(true);
        }

but same result as shown in my previous post.

partridgejiang commented 4 years ago

@richardoptibrium Try to setChemObj after setDimension? What's more, be sure the .molecule-render-container elements are in document DOM tree before calling setChemObj, otherwise the client size of viewer may not be properly calculated. btw, would you please attach a whole demo here?

richardoptibrium commented 4 years ago

Hi partridgejiang, I have tried that now but same result, sorry. debug confirms dimensions of container are good and I changed the order (in fact made them separate calls). In addition the code is in a componentDidMount (REACT). I'm afraid I can't share the full code, sorry.

        const molfile = Molecule.fromSmiles(this.props.structure).toMolfile();

        const mol = Kekule.IO.loadFormatData(molfile, 'mol')

        const parentElem = this.cardRef;
        let moleculeContainer = parentElem.current.querySelector('.molecule-render-container');

        let renderType = Kekule.Render.RendererType.R2D//R3D  // do 2D or 3D drawing

        let chemViewer = new Kekule.ChemWidget.Viewer(moleculeContainer);
        console.log(moleculeContainer.offsetWidth, moleculeContainer.offsetHeight);
        chemViewer.setAutoSize(false);
        chemViewer.setAutofit(false);
        chemViewer.setDimension(100, 100);
        chemViewer.setChemObj(mol);
        chemViewer.setPredefinedSetting('static');

        // create painter, bind with molecule
        let box = chemViewer.getPainter().estimateRenderBox(chemViewer.getDrawContext());

        let deltaX = box.x2 - box.x1;
        let deltaY = box.y2 - box.y1;
        let widgetDimension = chemViewer.getDimension();
        if (deltaX > widgetDimension.width || deltaY > widgetDimension.height) {
            console.log("AUTOFIT!");
            chemViewer.setAutofit(true);
        }
partridgejiang commented 4 years ago

@richardoptibrium, not the full code, but just extract the related part of it to form a runnabe one?

richardoptibrium commented 4 years ago

Thanks partridgejiang, unzip the attached file sample.zip, run npm install and npm start please

partridgejiang commented 4 years ago

Hi @richardoptibrium, I have found the problem and it is quite simple. In the code, the size of viewer is set to 100X100px, meanwhile, the viewer itself has a default padding of 45px. Then the actual drawing client is only 10X10px, causing all atoms packing together. If you want to keep 100X100px as the viewer's size, just set a smaller padding for the viewer:

chemViewer.setPadding(20);

Afterwards, the issue should be resolved.

btw., there is an error in the line 20 of app.js attached:

chemViewer.renderType(Kekule.Render.RendererType.R2D);

just change it to

chemViewer.setRenderType(Kekule.Render.RendererType.R2D);

or

chemViewer.renderType = Kekule.Render.RendererType.R2D;
richardoptibrium commented 4 years ago

Hi @richardoptibrium, I have found the problem and it is quite simple. In the code, the size of viewer is set to 100X100px, meanwhile, the viewer itself has a default padding of 45px. Then the actual drawing client is only 10X10px, causing all atoms packing together. If you want to keep 100X100px as the viewer's size, just set a smaller padding for the viewer:

chemViewer.setPadding(20);

Afterwards, the issue should be resolved.

btw., there is an error in the line 20 of app.js attached:

chemViewer.renderType(Kekule.Render.RendererType.R2D);

just change it to

chemViewer.setRenderType(Kekule.Render.RendererType.R2D);

or

chemViewer.renderType = Kekule.Render.RendererType.R2D;

That's brilliant, thanks! I'm now also trying to set up a 3d render widget with the following code. If I select 2d, I see a molecule (but not the caption & toolbar), if I change to 3d (as below) I see nothing.

        const { Molecule } = require('openchemlib');
        const molfile = Molecule.fromSmiles('CC(=O)OC1=CC=CC=C1C(=O)O').toMolfile();

        const mol = Kekule.IO.loadFormatData(molfile, 'mol')

        let viewerContainer = document.getElementById('viewer'); 

        let chemViewer = new Kekule.ChemWidget.Viewer(viewerContainer);
        chemViewer.setAutoSize(false).setAutofit(false).setChemObj(mol).setPredefinedSetting('static').setPadding(6)
            .setRenderType(Kekule.Render.RendererType.R3D);
        chemViewer.moleculeDisplayType(Kekule.Render.Molecule3DDisplayType.BALL_STICK);
        chemViewer.setBackgroundColor('#0000cc');
        chemViewer.setCaption('Hooray!');
        chemViewer.setPadding(6);
        chemViewer.setResizable(true);
        chemViewer.setEnableDirectInteraction(true);
        chemViewer.setEnableToolbar(true);

        // create painter, bind with molecule
        let box = chemViewer.getPainter().estimateRenderBox(chemViewer.getDrawContext());

        let deltaX = box.x2 - box.x1;
        let deltaY = box.y2 - box.y1;
        let widgetDimension = chemViewer.getDimension();
        if (deltaX > widgetDimension.width || deltaY > widgetDimension.height) {
            console.log("AUTOFIT!");
            chemViewer.setAutofit(true);
        }
partridgejiang commented 4 years ago

Hi @richardoptibrium, currently the 3D rendering is utilizing a third-party lib Three.js, please include it in your application first, :).

richardoptibrium commented 4 years ago

Hi @richardoptibrium, currently the 3D rendering is utilizing a third-party lib Three.js, please include it in your application first, :).

Thanks, can I do that with npm? I tried npm install which worked but unsure how to import it in react such that kekule can use it. Also, can I set molecule colour for the 2d rendering?

partridgejiang commented 4 years ago

@richardoptibrium Yes you can use Three.js with npm. In the import part of your App.js:

import React from 'react';
import './App.css';
import * as THREE from '../node_modules/three/build/three.module.js';
window.THREE = THREE;  // A trick, make Kekule.js know the exists of Three.js
const Kekule = require('kekule');  // use require instead of import here, since import must be at the top of code
...
partridgejiang commented 4 years ago

@richardoptibrium The color of molecule can be set by renderOptions property, for example, in your code:

const mol2 = Kekule.IO.loadFormatData(molfile, 'mol')
mol2.setRenderOption('color', '#ff0000');  // draw a red molecule
...
richardoptibrium commented 4 years ago
import * as THREE from '../node_modules/three/build/three.module.js';
window.THREE = THREE;  // A trick, make Kekule.js know the exists of Three.js

Sorry, still struggling: sample.zip

partridgejiang commented 4 years ago

@richardoptibrium , the import of Kekule must be after window.THREE = THREE, so require should be used instead of import, just change the line 4-5 of App.js from

import Kekule from 'kekule';
window.THREE = THREE;

to:

window.THREE = THREE;
const Kekule = require('kekule');