UNPKG

kekule

Version:

Open source JavaScript toolkit for chemoinformatics

1,517 lines (1,399 loc) 71.1 kB
/** * @fileoverview * A default implementation of 3D molecule renderer. * @author Partridge Jiang */ /* * requires /core/kekule.common.js * requires /data/kekule.dataUtils.js * requires /utils/kekule.utils.js * requires /core/kekule.structures.js * requires /render/kekule.render.base.js * requires /render/kekule.render.utils.js * requires /localization/ */ (function(){ var OU = Kekule.ObjUtils; var BU = Kekule.BoxUtils; var MDM = Kekule.Render.Molecule3DDisplayType; var BRM = Kekule.Render.Bond3DRenderMode; var BRT = Kekule.Render.Bond3DRenderType; var BSM = Kekule.Render.Bond3DSpliceMode; var NRM = Kekule.Render.Node3DRenderMode; var oneOf = Kekule.oneOf; Kekule.globalOptions.add('render.render3D', { autofitOnPrimaryDirection: false }); /** * Different renderer should provide different methods to draw element on context. * Those different implementations are wrapped in draw bridge classes. * Concrete bridge classes do not need to deprived from this class, but they * do need to implement all those essential methods. * * In all drawXXX methods, parameter options contains the style information to draw color and so on. * It may contain the following fields: * { * color: String, '#rrggbb', * opacity: Float, 0-1, * lineWidth: Int (in drawLine method), * withEndCaps: Bool (in drawCylinderEx) * } * * In all drawXXX methods, coord are based on context (not directly on screen). * * @class */ Kekule.Render.Abstract3DDrawBridge = Class.create( /** @lends Kekule.Render.Abstract3DDrawBridge# */ { /** @private */ CLASS_NAME: 'Kekule.Render.Abstract3DDrawBridge', /** @private */ CONTEXT_PARAMS_FIELD: '__$context_params__', getGraphicQualityLevel: function() { return null; }, setGraphicQualityLevel: function(value) { }, /** * Transform a 3D context based coord to screen based one (usually 2D in pixel). * @param {Object} context * @param {Hash} coord * @return {Hash} */ transformContextCoordToScreen: function(context, coord) { return {x: coord.x, y: coord.y}; }, /** * Indicate whether this bridge and context can change glyph content or position after drawing it. * Raphael is a typical environment of this type while canvas should returns false. * @param {Object} context * @returns {Bool} */ canModifyGraphic: function(context) { return false; }, /** * Create a context element for drawing. * @param {Element} parentElem * //@param {Int} contextOffsetX X coord of top-left corner of context, in px. * //@param {Int} contextOffsetY Y coord of top-left corner of context, in px. * @param {Int} width Width of context, in px. * @param {Int} height Height of context, in px. * @returns {Object} Context used for drawing. */ createContext: function(parentElem, width, height) { return null; }, /** * Get width and height of context. * @param {Object} context * @returns {Hash} {width, height} */ getContextDimension: function(context) { return {}; }, /** * Set new width and height of context. * @param {Object} context * @param {Int} width * @param {Int} height */ setContextDimension: function(context, width, height) { return null; }, /** * Clear the whole context. * @param {Object} context */ clearContext: function(context) { return null; }, /** * Returns an additional param associated with context. * @param {Object} context * @param {String} key * @returns {Variant} */ getContextParam: function(context, key) { return (context[this.CONTEXT_PARAMS_FIELD] || {})[key]; }, /** * Set an additional param associated with context. * @param {Object} context * @param {String} key * @param {Variant} value */ setContextParam: function(context, key, value) { if (!context[this.CONTEXT_PARAMS_FIELD]) context[this.CONTEXT_PARAMS_FIELD] = {}; context[this.CONTEXT_PARAMS_FIELD][key] = value; }, /** @private */ _getOverSamplingRatio: function(context) { return this.getContextParam(context, 'overSamplingRatio') || null; }, /** * Set background color of content. * @param {Object} context * @param {String} color Color in '#RRGGBB' mode. Null means transparent. */ setClearColor: function(context, color) { }, drawSphere: function(context, coord, radius, options) { }, drawCylinder: function(context, coord1, coord2, radius, options) { }, /* drawParallelCylinders: function(context, cylinderInfos, drawEndCaps) { }, */ drawLine: function(context, coord1, coord2, options) { }, /* drawParallelLines: function(context, lineInfos) { } */ createDrawGroup: function(context) { }, addToDrawGroup: function(elem, group) { }, removeFromDrawGroup: function(elem, group) { }, /** * Returns properties of current camera, including position(coord), fov, aspect and so on. * @param {Object} context * @returns {Hash} */ getCameraProps: function(context) { }, /** * Set properties of current camera, including position(coord), fov, aspect and so on. * @param {Object} context * @param {Hash} props */ setCameraProps: function(context, props) { }, /** * Export drawing content to a data URL for <img> tag to use. * @param {Object} context * @param {String} dataType Type of image data, e.g. 'image/png'. * @param {Hash} options Export options, usually this is a number between 0 and 1 * indicating image quality if the requested type is image/jpeg or image/webp. * @returns {String} */ exportToDataUri: function(context, dataType, options) { } }); /** * A base implementation of 2D chem object renderer. * You can call renderer.draw(context, chemObj, baseCoord, options) to draw the 2D structure, * where options can contain the settings of drawing style (strokeWidth, color...) and tranform params * (including scale, zoom, translate, rotateAngle, cameraPos...). The options can also have a autoCamera (Bool) field, * if autoCamera is true, the cameraPos value will be determinate by renderer. * Note: zoom is not the same as scale. When scale is set or calculated, zoom will multiply on it and get the actual scale ratio. * for example, scale is 100 and zoom is 1.5, then the actual scale value will be 150. * * @augments Kekule.Render.AbstractRenderer * @param {Kekule.ChemObject} chemObj Object to be drawn. * @param {Object} drawBridge A object that implements the actual draw job. * @param {Hash} options Options to draw object. * //@param {Object} renderConfigs Global configuration for rendering. * // This property should be an instance of {@link Kekule.Render.Render3DConfigs}. * @param {Kekule.ObjectEx} parent Parent object of this renderer, usually another renderer or an instance of {@link Kekule.Render.ChemObjPainter}, or null. * * @property {Object} drawBridge A object that implements the actual draw job. Read only. * @class */ Kekule.Render.Base3DRenderer = Class.create(Kekule.Render.CompositeRenderer, // Kekule.Render.AbstractRenderer, /** @lends Kekule.Render.Base3DRenderer# */ { /** @private */ CLASS_NAME: 'Kekule.Render.Base3DRenderer', /** @constructs */ initialize: function(/*$super, */chemObj, drawBridge, /*renderConfigs,*/ parent) { this.tryApplySuper('initialize', [chemObj, drawBridge, /*renderConfigs,*/ parent]) /* $super(chemObj, drawBridge, \*renderConfigs,*\ parent) */; /* if (!renderConfigs) this.setRenderConfigs(Kekule.Render.getRender3DConfigs()); // use default config */ //this.setRenderConfigs(null); }, /** @private */ getRendererType: function() { return Kekule.Render.RendererType.R3D; }, /** @private */ getActualTargetContext: function(context) { return this.getRedirectContext() || context; }, /** * Returns properties of current camera, including position(coord), fov, aspect and so on. * @param {Object} context * @returns {Hash} */ getCameraProps: function(context) { return this.getDrawBridge().getCameraProps(context); }, /** * Set properties of current camera, including position(coord), fov, aspect and so on. * @param {Object} context * @param {Hash} props */ setCameraProps: function(context, props) { return this.getDrawBridge().setCameraProps(context, props); }, /** @private */ getInitialLightPositions: function(context) { return this.getDrawBridge().getInitialLightPositions && this.getDrawBridge().getInitialLightPositions(context); }, /** * Returns count of lights in context. * @param {Object} context * @returns {Int} */ getLightCount: function(context) { return this.getDrawBridge().getLightCount && this.getDrawBridge().getLightCount(context); }, /** * Get properties of light at index. * @param {Object} context * @param {Int} lightIndex * @returns {Hash} */ getLightProps: function(context, lightIndex) { return this.getDrawBridge().getLightProps && this.getDrawBridge().getLightProps(context, lightIndex); }, /** * Get properties of light at index. * @param {Object} context * @param {Int} lightIndex * @param {Hash} props */ setLightProps: function(context, lightIndex, props) { return this.getDrawBridge().setLightProps(context, lightIndex, props); }, drawSphere: function(context, coord, radius, options) { return this.getDrawBridge().drawSphere(this.getActualTargetContext(context), coord, radius, options); }, drawCylinder: function(context, coord1, coord2, radius, options) { var b = this.getDrawBridge(); return b? b.drawCylinder(this.getActualTargetContext(context), coord1, coord2, radius, options): null; }, drawCylinderEx: function(context, coord1, coord2, radius, options) { var result = null; var elem = this.drawCylinder(context, coord1, coord2, radius, options); if (elem && options.withEndCaps) { result = this.createDrawGroup(context); var cap = this.drawSphere(context, coord1, radius); this.addToDrawGroup(cap, result); var cap = this.drawSphere(context, coord2, radius); this.addToDrawGroup(cap, result); } return result || elem; }, /** * Draw a group of parallel cylinders. * @param {Object} context * @param {Object} cylinderInfos Information of each cylinder. * Contains fields: {coord1, coord2, radius, color} * @param {Bool} drawEndCaps Whether draw small sphere cap at end of cylinder * TODO: this param currently not fully implemented * @returns {Variant} * @private */ drawParallelCylinders: function(context, cylinderInfos, drawEndCaps) { var b = this.getDrawBridge(); if (b.drawParallelCylinders) { return b.drawParallelCylinders(this.getActualTargetContext(context), cylinderInfos, drawEndCaps); } else //if (b.drawCylinder) { var result = this.createDrawGroup(context); for (var i = 0, l = cylinderInfos.length; i < l; ++i) { var info = cylinderInfos[i]; var obj = this.drawCylinderEx(context, info.coord1, info.coord2, info.radius, {'color': info.color, 'withEndCaps': drawEndCaps}); if (obj) this.addToDrawGroup(obj, result); } return result; } }, drawLine: function(context, coord1, coord2, options) { var b = this.getDrawBridge(); return b? b.drawLine(this.getActualTargetContext(context), coord1, coord2, options): null; }, /** * Draw a group of parallel lines. * @param {Object} context * @param {Object} lineInfos Information of each cylinder. * Contains fields: {coord1, coord2, lineWidth, color} * @returns {Variant} * @private */ drawParallelLines: function(context, lineInfos, drawEndCaps) { var b = this.getDrawBridge(); if (b.drawParallelLines) { return b.drawParallelLines(this.getActualTargetContext(context), lineInfos, drawEndCaps); } else if (b.drawLine) { var count = lineInfos.length; /* if (count <= 0) return null; else if (count <= 1) { var info = lineInfos[0]; return this.drawLine(context, info.coord1, info.coord2, { 'lineWidth': info.lineWidth, 'color': info.color }); } else*/ { var lines = []; //this.createDrawGroup(context); for (var i = 0; i < count; ++i) { var info = lineInfos[i]; var elem = this.drawLine(context, info.coord1, info.coord2, { 'lineWidth': info.lineWidth, 'color': info.color, 'opacity': info.opacity }); //this.addToDrawGroup(elem, result); if (elem) lines.push(elem); } var result; if (lines.length <= 1) result = lines[0]; else { result = this.createDrawGroup(context); for (var i = 0, l = lines.length; i < l; ++i) { this.addToDrawGroup(lines[i], result); } } return result; } } }, createDrawGroup: function(context) { return this.getDrawBridge().createGroup(this.getActualTargetContext(context)); }, addToDrawGroup: function(elem, group) { return this.getDrawBridge().addToGroup(elem, group); }, removeFromDrawGroup: function(elem, group) { return this.getDrawBridge().removeFromGroup(elem, group); }, /** * Remove an element in context. * @param {Object} context * @param {Object} elem */ removeDrawnElem: function(context, elem) { // TODO: unfinished to remove drawn elem in 3D context } }); /** * A base class to render a chem object in 3D. * @class * @augments Kekule.Render.Base3DRenderer */ Kekule.Render.ChemObj3DRenderer = Class.create(Kekule.Render.Base3DRenderer, /** @lends Kekule.Render.ChemObj3DRenderer# */ { /** @private */ CLASS_NAME: 'Kekule.Render.ChemObj3DRenderer', /** @constructs */ initialize: function(/*$super, */chemObj, drawBridge, parent) { this.tryApplySuper('initialize', [chemObj, drawBridge, parent]) /* $super(chemObj, drawBridge, parent) */; this._enableTransformByCamera = true; // private flag, if rotation/zoom transform is achieved by camera position }, /** @private */ doEstimateSelfObjBox: function(context, options, allowCoordBorrow) { /* var o = this.getChemObj(); if (o.getExposedContainerBox3D) return o.getExposedContainerBox3D(allowCoordBorrow); else if (o.getContainerBox3D) return o.getContainerBox3D(allowCoordBorrow); else return null; */ return Kekule.Render.ObjUtils.getContainerBox(this.getChemObj(), this.getCoordMode(), allowCoordBorrow); }, /** @private */ doEstimateRenderBox: function(context, baseCoord, options, allowCoordBorrow) { var objBox = this.estimateObjBox(context, options, allowCoordBorrow); if (objBox) // a general approach is to scale the chem object box to context's scope { var p = this.prepareTransformParams(context, baseCoord, options, objBox); //var transformParams = this.getFinalTransformParams(context, p); var transformParams = p; //.transformParams; return BU.transform3D(objBox, transformParams); } else return null; }, /** @private */ beginDraw: function(/*$super, */context, baseCoord, options) { this.tryApplySuper('beginDraw', [context, baseCoord, options]) /* $super(context, baseCoord, options) */; //console.log('draw options', options); if (this.isRootRenderer()) // set graphic quality { var b = this.getDrawBridge(); if (b && b.setGraphicQualityLevel) { //var level = oneOf(options.graphicQuality, this.getRenderConfigs().getEnvironmentConfigs().getGraphicQuality()); var level = options.graphicQuality; //console.log(level, this.getRenderConfigs().getEnvironmentConfigs().getGraphicQuality()); b.setGraphicQualityLevel(level); } } }, /** @private */ endDraw: function(/*$super, */context, baseCoord, options) { if (this.isRootRenderer()) // need to adjust camera pos { this.adjustCamera(context, options.transformParams); } this.tryApplySuper('endDraw', [context, baseCoord, options]) /* $super(context, baseCoord, options) */; }, /** @ignore */ doDraw: function(/*$super, */context, baseCoord, options) { // since options passed by draw method is already protected, we are not worry about change it here. this.prepareTransformParams(context, baseCoord, options); var result = this.tryApplySuper('doDraw', [context, baseCoord, options]) /* $super(context, baseCoord, options) */; return result; }, /** * Repaint with only geometry options changes (without the modification of chemobj or draw color/length settings). * Sometimes this repainting can be achieved by the modify of camera position (without recalc the position of node and connectors) * so that the speed may enhance greatly. * @param {Object} context * @param {Hash} newOptions */ changeGeometryOptions: function(context, baseCoord, newOptions) { var ops = Object.create(newOptions); var params = this.prepareTransformParams(context, baseCoord, ops); if (params.pureCameraTransform) // now only adjust camera pos { //this.adjustCamera(context, params); this.endDraw(context, baseCoord, ops); } else { this.getDrawBridge().clearContext(context); this.draw(context, baseCoord, newOptions); } }, /** * Prepare 3D transform params from baseCoord and drawOptions. * If drawOptions.transformParams already set, this method will do nothing. * @param {Object} context * @param {Hash} baseCoord * @param {Hash} drawOptions * @param {Hash} objBox * @returns {Hash} */ prepareTransformParams: function(context, baseCoord, drawOptions, objBox) { var result; if (drawOptions.transformParams) result = drawOptions.transformParams; else { var p = this.generateTransformParams(context, baseCoord, drawOptions, objBox); drawOptions.transformParams = p; result = p; } var transformMatrix = Kekule.CoordUtils.calcTransform3DMatrix(result); var invTransformMatrix = Kekule.CoordUtils.calcInverseTransform3DMatrix(result); drawOptions.transformParams.transformMatrix = transformMatrix; drawOptions.transformParams.invTransformMatrix = invTransformMatrix; this.getRenderCache(context).transformParams = result; this.getRenderCache(context).transformMatrix = transformMatrix; this.getRenderCache(context).invTransformMatrix = invTransformMatrix; this.getRenderCache(context).transformParams = result; return result; }, /** @private */ canDoPureCameraTransform: function(context, transformParams) { var doCameraTransform = this._enableTransformByCamera && (transformParams.scaleX === transformParams.scaleY) && (transformParams.scaleY === transformParams.scaleZ) && (!transformParams.rotateAngle) && (!transformParams.rotateX) && (!transformParams.rotateY) && (!transformParams.rotateZ); //&& (!transformParams.translateX) && (!transformParams.translateY) && (!transformParams.translateZ); return doCameraTransform; }, /** * Calculate the coordinate transform options from drawOptions. * Descendants can override this method. * Note that unit length and zoom is not take into consideration in this method. * @param {Object} context * @param {Hash} baseCoord * @param {Hash} drawOptions * @param {Hash} objBox * @returns {Hash} */ generateTransformParams: function(context, baseCoord, drawOptions, objBox) { //console.log('generate transform based on', baseCoord); var result = {}; //var generalConfigs = this.getRenderConfigs().getGeneralConfigs(); result.allowCoordBorrow = oneOf(drawOptions.allowCoordBorrow, /*generalConfigs.getAllowCoordBorrow(),*/ false); //var lengthConfigs = this.getRenderConfigs().getLengthConfigs(); //var unitLength = lengthConfigs.getUnitLength() || drawOptions.unitLength; //result.unitLength = unitLength; result.unitLength = drawOptions.unitLength || 1; if (!objBox) { objBox = this.estimateObjBox(context, drawOptions, result.allowCoordBorrow); //console.log('OBJ BOX CALC', objBox); } if (!objBox) // object is actually empty, use a fake one to avoid exceptions { objBox = {'x1': 0, 'x2': 1, 'y1': 0, 'y2': 1, 'z1': 0, 'z2': 1}; } var boxCenter = {'x': (objBox.x1 + objBox.x2) / 2, 'y': (objBox.y1 + objBox.y2) / 2, 'z': (objBox.z1 + objBox.z2) / 2}; var O = Kekule.ObjUtils; if (O.isUnset(drawOptions.translateX) && O.isUnset(drawOptions.translateY) && O.isUnset(drawOptions.translateZ)) // if translate is set, baseCoord will be ignored { if (baseCoord) { result.translateX = baseCoord.x - boxCenter.x; result.translateY = baseCoord.y - boxCenter.y; result.translateZ = baseCoord.z || 0 - boxCenter.z || 0; } } else { result.translateX = drawOptions.translateX || 0; result.translateY = drawOptions.translateY || 0; result.translateZ = drawOptions.translateZ || 0; } result.zoom = drawOptions.zoom || 1; result.scaleX = oneOf(drawOptions.scaleX, drawOptions.scale, 1); result.scaleY = oneOf(drawOptions.scaleY, drawOptions.scale, 1); result.scaleZ = oneOf(drawOptions.scaleZ, drawOptions.scale, 1); //result.scaleY = -result.scaleY; var autoCalcScale = !(drawOptions.scaleX || drawOptions.scaleY || drawOptions.scaleZ || drawOptions.scale); // rotation if (OU.notUnset(drawOptions.rotateMatrix)) { result.rotateMatrix = drawOptions.rotateMatrix; } else if (OU.notUnset(drawOptions.rotateAngle) && OU.notUnset(drawOptions.rotateAxisVector)) { /* result.rotateAngle = drawOptions.rotateAngle; result.rotateAxisVector = drawOptions.rotateAxisVector; */ result.rotateMatrix = Kekule.CoordUtils.calcRotate3DMatrix({ 'rotateAngle': drawOptions.rotateAngle, 'rotateAxisVector': drawOptions.rotateAxisVector }); } else if (result.rotateX || result.rotateY || result.rotateZ) { /* result.rotateX = drawOptions.rotateX || 0; result.rotateY = drawOptions.rotateY || 0; result.rotateZ = drawOptions.rotateZ || 0; */ result.rotateMatrix = Kekule.CoordUtils.calcRotate3DMatrix({ 'rotateX': drawOptions.rotateX || 0, 'rotateY': drawOptions.rotateY || 0, 'rotateZ': drawOptions.rotateZ || 0 }); } if (O.isUnset(drawOptions.center)) // center not set, use center coord of Ctab { result.center = boxCenter; // rotation center } // indicate the absolute center of drawn object if (baseCoord) result.drawBaseCoord = baseCoord; else { result.drawBaseCoord = Kekule.CoordUtils.transform3D(boxCenter, result); } var initialTransformOptions = Object.extend({}, result); result = this.getFinalTransformParams(context, result); result.initialTransformOptions = initialTransformOptions; var doCameraTransform = this.canDoPureCameraTransform(context, result); result.pureCameraTransform = doCameraTransform; if (this.isRootRenderer()) // is root renderer, have to calc camera info { if (drawOptions.autofit || (!drawOptions.cameraPos)) { // TODO: now only consider autofit var inflation = this.getAutofitObjBoxInflation(context, this.getChemObj(), drawOptions); var obox = Kekule.BoxUtils.inflateBox(objBox, inflation.x, inflation.y, inflation.z); //var obox = objBox; //calculate camera position var w = Math.max(Math.abs(obox.x2 - obox.x1), Math.abs(obox.y2 - obox.y1)); var cameraInfo = this.getCameraProps(context); var fov; if (Kekule.globalOptions.render.render3D.autofitOnPrimaryDirection) // original implementation, only auto fit on primary direction fov = cameraInfo.fov; else // new implementation, consider both direction, and use the fovMin value fov = cameraInfo.fovMin || cameraInfo.fov; var l = w / 2 / Math.tan(fov / 2); var dis = Math.sqrt(Math.sqr(l) - Math.sqr(w / 2)); //var doCameraRotation = false; // rotation and zoom can be done by the proper camera position adjustment if (doCameraTransform) // adjust camera and lights { //console.log(dis, dis / result.zoom); dis = dis / result.scaleX; // since camera transform is confirmed in prev code, we are sure scaleX=scaleY=scaleZ result.cameraWidth = w / result.scaleX; result.cameraHeight = result.cameraWidth; // calculate camera pos and up direction var vBaseCameraPos = Kekule.MatrixUtils.create(4, 1, [0, 0, 1, 1]); var vBaseCameraUp = Kekule.MatrixUtils.create(4, 1, [0, 1, 1, 1]); var vCameraPos, coordCameraPos; var vCameraUp, coordCameraUp; if (result.rotateMatrix) { // since result.rotateMatrix is based on object, camera rotation should be in opposite direction, // so the rotateMatrix should be transposed. var cameraRotateMatrix = Kekule.MatrixUtils.transpose(result.rotateMatrix); vCameraPos = Kekule.MatrixUtils.multiply(cameraRotateMatrix, vBaseCameraPos); vCameraUp = Kekule.MatrixUtils.multiply(cameraRotateMatrix, vBaseCameraUp); coordCameraPos = {'x': vCameraPos[0], 'y': vCameraPos[1], 'z': vCameraPos[2]}; coordCameraUp = {'x': vCameraUp[0], 'y': vCameraUp[1], 'z': vCameraUp[2]}; } else { coordCameraPos = {'x': 0, 'y': 0, 'z': 1}; coordCameraUp = {'x': 0, 'y': 1, 'z': 1}; } coordCameraPos = Kekule.CoordUtils.multiply(coordCameraPos, dis); result.cameraPos = { 'x': coordCameraPos.x, // + result.drawBaseCoord.x, 'y': coordCameraPos.y, // + result.drawBaseCoord.y, 'z': coordCameraPos.z // + result.drawBaseCoord.z }; /* result.cameraLookAtVector = Kekule.CoordUtils.add(result.drawBaseCoord, {'x': result.translateX, 'y': result.translateY, 'z': result.translateZ}); result.cameraPos = Kekule.CoordUtils.add(result.cameraPos, result.cameraLookAtVector); */ result.cameraPos = Kekule.CoordUtils.add(result.cameraPos, result.drawBaseCoord); result.cameraUp = coordCameraUp; // clear zoom/scale, translate and rotate info in transformParams //result.zoom = 1; result.scaleX = result.scaleY = result.scaleZ = 1; // lights // TODO: now just change light direction but not distance var initialLightPositions = this.getInitialLightPositions(context); if (cameraRotateMatrix) // Ambient light only need to rotate with camera { var lightCount = this.getLightCount(context); //console.log('need adjust light', lightCount, initialLightPositions); if (initialLightPositions && initialLightPositions.length && lightCount) { var count = Math.min(initialLightPositions.length, lightCount); var newLightPositions = []; for (var i = 0; i < count; ++i) { var pos = initialLightPositions[i]; if (pos) { var vOldPos = Kekule.MatrixUtils.create(4, 1, [pos.x, pos.y, pos.z, 1]); var vNewPos = Kekule.MatrixUtils.multiply(cameraRotateMatrix, vOldPos); var newPos = {'x': vNewPos[0], 'y': vNewPos[1], 'z': vNewPos[2]}; //this.setLightProps(context, i, {'position': newPos}); newLightPositions.push(newPos); } } } } result.lightPositions = newLightPositions || initialLightPositions; // do not transform objects, but just camera and lights result.rotateMatrix = null; } else { // now cameraPos do not consider baseCoord, it will be taken into consideration in getFinalTransformParams result.cameraPos = { 'x': result.drawBaseCoord.x, 'y': result.drawBaseCoord.y, 'z': dis + result.drawBaseCoord.z }; result.cameraUp = {'x': 0, 'y': 1, 'z': 1}; } result.cameraLookAtVector = result.drawBaseCoord || {'x': 0, 'y': 0, 'z': 0}; // consider translate var translateCoord = {'x': -result.translateX, 'y': -result.translateY, 'z': -result.translateZ}; result.translateX = result.translateY = result.translateZ = 0; result.cameraPos = Kekule.CoordUtils.add(result.cameraPos, translateCoord); result.cameraLookAtVector = Kekule.CoordUtils.add(result.cameraLookAtVector, translateCoord); } } return result; }, /** * Calculate the final params for translation. Zoom and unit length are taken into consideration. * @param {Object} context * @param {Hash} transformParams * @returns {Hash} */ getFinalTransformParams: function(context, transformParams) { var result = Object.create(transformParams); if (result.zoom) { result.scaleX *= result.zoom; result.scaleY *= result.zoom; result.scaleZ *= result.zoom; } if (result.unitLength !== 1) { result.translateX *= result.unitLength; result.translateY *= result.unitLength; result.translateZ *= result.unitLength; if (result.drawBaseCoord) { result.drawBaseCoord = Kekule.CoordUtils.multiply(result.drawBaseCoord, result.unitLength); } } return result; }, /** @ignore */ getRenderFinalTransformParams: function(context) { return this.getRenderCache(context).transformParams; }, /** @ignore */ getRenderInitialTransformOptions: function(context) { var p = this.getRenderFinalTransformParams(context); return p? p.initialTransformOptions: null; }, /** * Returns a automatic inflation value in autofit drawing. * Usually a radius of atom ball. * Descendants may override this method. * @param {Object} context * @param {Kekule.ChemObject} chemObj * @param {Object} drawOptions * @returns {Hash} {x, y, z} */ getAutofitObjBoxInflation: function(context, chemObj, drawOptions) { var r = drawOptions.fixedNodeRadius || 0; // || this.getRenderConfigs().getLengthConfigs().getFixedNodeRadius(); return {'x': r, 'y': r, 'z': r}; }, /** @private */ adjustCamera: function(context, transformParams) { var w = transformParams.cameraWidth; var h = transformParams.cameraHeight; var options = { 'position': transformParams.cameraPos, 'upVector': transformParams.cameraUp, 'lookAtVector': transformParams.cameraLookAtVector }; if (w && h) { var hw = w / 2; var hh = h / 2; options.left = -hw; options.right = hw; options.top = hh; options.bottom = -hh; } this.setCameraProps(context, options); this.adjustLights(context, transformParams); //console.log(transformParams.cameraPos); }, /** @private */ adjustLights: function(context, transformParams) { var positions = transformParams.lightPositions || []; for (var i = 0, l = positions.length; i < l; ++i) { var pos = positions[i]; if (pos) { //console.log('set light new pos', i, pos); this.setLightProps(context, i, {'position': pos}); } } } }); /** * A default implementation of 3D a molecule's CTable renderer. * @class * @augments Kekule.Render.ChemObj3DRenderer */ Kekule.Render.ChemCtab3DRenderer = Class.create(Kekule.Render.ChemObj3DRenderer, /** @lends Kekule.Render.ChemCtab3DRenderer# */ { /** @private */ CLASS_NAME: 'Kekule.Render.ChemCtab3DRenderer', /** @private */ TRANSFORM_COORD_FIELD: '__$transCoord3D__', /** @private */ DRAW_ELEM_FIELD: '__$drawElem__', /** @private */ DRAW_COLOR_FIELD: '__$drawColor__', /** @private */ BASE_RADIUS_FIELD: '__$baseRadius__', /** @private */ TRANSFORM_MATRIX_FIELD: '__$transMatrix__', /** @private */ INV_TRANSFORM_MATRIX_FIELD: '__$inverseTransMatrix__', /** @private */ CHILD_TRANSFORM_MATRIX_FIELD: '__$childTransMatrix__', /** @private */ HIDDEN_NODES_FIELD: '__$hiddenNodes__', /** @private */ HIDDEN_CONNECTORS_FIELD: '__$hiddenConnectors__', /** @private */ getObjDrawElem: function(context, obj) { return this.getExtraProp2(context, obj, this.DRAW_ELEM_FIELD); }, /** @private */ setObjDrawElem: function(context, obj, value) { this.setExtraProp2(context, obj, this.DRAW_ELEM_FIELD, value); }, /** @private */ getObjDrawColor: function(context, obj) { return this.getExtraProp2(context, obj, this.DRAW_COLOR_FIELD); }, /** @private */ setObjDrawColor: function(context, obj, value) { this.setExtraProp2(context, obj, this.DRAW_COLOR_FIELD, value); }, /** @private */ getNodeBaseRadius: function(context, obj) { return this.getExtraProp2(context, obj, this.BASE_RADIUS_FIELD); }, /** @private */ setNodeBaseRadius: function(context, obj, value) { this.setExtraProp2(context, obj, this.BASE_RADIUS_FIELD, value); }, /** @private */ getHiddenNodes: function(context) { return this.getExtraProp(context, this.HIDDEN_NODES_FIELD) || []; }, /** @private */ setHiddenNodes: function(context, value) { return this.setExtraProp(context, this.HIDDEN_NODES_FIELD, value); }, /** @private */ getHiddenConnectors: function(context) { return this.getExtraProp(context, this.HIDDEN_CONNECTORS_FIELD) || []; }, /** @private */ setHiddenConnectors: function(context, value) { return this.setExtraProp(context, this.HIDDEN_CONNECTORS_FIELD, value); }, /** @private */ doEstimateSelfObjBox: function(context, options, allowCoordBorrow) { // TODO: just a rough calc var box = this.getChemObj().getExposedContainerBox3D(allowCoordBorrow); return box; }, /** @private */ doPrepare: function(context, chemObj, baseCoord, options) { /* var op = this.getGlobalOptions(this.getRenderConfigs()); op = Object.extend(op, options); */ //var op = Object.create(options); var op = options; var c = this.getRenderCache(context); var nodeMode, connectorMode; switch (op.moleculeDisplayType) { case MDM.WIRE: nodeMode = NRM.NONE; connectorMode = op.displayMultipleBond? BRM.MULTI_WIRE: BRM.WIRE; break; case MDM.STICKS: nodeMode = NRM.NONE; //NRM.SMALL_CAP; connectorMode = op.displayMultipleBond? BRM.MULTI_CYLINDER: BRM.CYLINDER; break; case MDM.SPACE_FILL: nodeMode = NRM.SPACE; connectorMode = BRM.NONE; break; case MDM.BALL_STICK: default: nodeMode = NRM.BALL; connectorMode = op.displayMultipleBond? BRM.MULTI_CYLINDER: BRM.CYLINDER; break; } c.nodeRenderMode = nodeMode; c.connectorRenderMode = connectorMode; c.options = op; var nodes = chemObj.getExposedNodes(); var connectors = chemObj.getExposedConnectors(); //console.log('prepare options', op); this.prepareHiddenObjects(context, nodes, connectors, op); this.prepareNodesDrawColor(context, nodes, op); this.prepareNodeBaseRadii(context, nodes, op); return op; }, /* * Retrieve render options from global renderConfigs object. * @param {Object} renderConfigs * @returns {Hash} * @private */ /* getGlobalOptions: function(renderConfigs) { if (renderConfigs) { var r = renderConfigs.getMoleculeDisplayConfigs().toHash() || {}; r.displayMultipleBond = r.defDisplayMultipleBond; r.bondSpliceMode = r.defBondSpliceMode; r.moleculeDisplayType = r.defMoleculeDisplayType; r = Object.extend(r, renderConfigs.getModelConfigs().toHash() || {}); r = Object.extend(r, renderConfigs.getLengthConfigs().toHash() || {}); r.opacity = renderConfigs.getGeneralConfigs().getDrawOpacity(); return r; } else return {}; }, */ /** * Mark if node or connector need not to be drawn. * @private */ prepareHiddenObjects: function(context, nodes, connectors, renderOptions) { var hiddenNodes = []; this.setHiddenNodes(context, hiddenNodes); for (var i = 0, l = nodes.length; i < l; ++i) { var node = nodes[i]; // check if node is hydrogen atom and need to be hidden if (renderOptions.hideHydrogens && (node instanceof Kekule.Atom) && (node.getAtomicNumber() === 1)) { hiddenNodes.push(node); } } var hiddenConnectors = []; this.setHiddenConnectors(context, hiddenConnectors); for (var i = 0, l = connectors.length; i < l; ++i) { var connector = connectors[i]; // check if connector connected to a hidden node and should not be drawn var connectedObjs = connector.getConnectedExposedObjs(); var shownObjs = Kekule.ArrayUtils.exclude(connectedObjs, hiddenNodes); if (shownObjs < 2) { hiddenConnectors.push(connector); } } //console.log('prepare hide', hiddenNodes, hiddenConnectors); }, /** * Calculate colors need for drawing a series of node. * @param {Array} nodes * @param {Object} renderOptions * @private */ prepareNodesDrawColor: function(context, nodes, renderOptions) { var globalUseAtomSpecifiedColor = renderOptions.useAtomSpecifiedColor; for (var i = 0, l = nodes.length; i < l; ++i) { var node = nodes[i]; var localOptions = node.getOverriddenRender3DOptions() || {}; // get color /* var defColor = renderOptions.useAtomSpecifiedColor? Kekule.Render.RenderColorUtils.getColor(atomicNumber, this.getRendererType()): renderOptions.atomColor; var color = oneOf(localOptions.atomColor, localOptions.color, renderOptions.atomColor, renderOptions.color, defColor); */ //var color = localOptions.atomColor || localOptions.color; var color = localOptions.color || localOptions.atomColor; //console.log(renderOptions, localOptions, globalUseAtomSpecifiedColor); if (color && (!localOptions.useAtomSpecifiedColor)) // local color set { // do nothing, color already set } else { if (globalUseAtomSpecifiedColor || localOptions.useAtomSpecifiedColor) { var atomicNumber = node.getAtomicNumber? node.getAtomicNumber(): 0; if (atomicNumber >= 0) color = Kekule.Render.RenderColorUtils.getColor(atomicNumber, this.getRendererType()); else // may be subgroup or other none-atom node color = Kekule.Render.RenderColorUtils.getColor(node.getClassLocalName(), this.getRendererType()); } else // use global color/atom color settings //color = oneOf(renderOptions.atomColor || localOptions.color); color = oneOf(localOptions.color || renderOptions.atomColor); } /* if (renderOptions.useAtomSpecifiedColor || localOptions.useAtomSpecifiedColor) { var defColor = Kekule.Render.RenderColorUtils.getColor(atomicNumber, this.getRendererType()); } */ //console.log('color', color); this.setObjDrawColor(context, node, color); } }, /** * Calculate base radii need for drawing a series of node * @param {Array} nodes * @param {Object} renderOptions * @private */ prepareNodeBaseRadii: function(context, nodes, renderOptions) { for (var i = 0, l = nodes.length; i < l; ++i) { var radius; var node = nodes[i]; radius = this.calcNodeBaseRadius(node, renderOptions); this.setNodeBaseRadius(context, node, radius); } }, /** @private */ calcNodeBaseRadius: function(node, renderOptions) { var radius; var localOptions = node.getOverriddenRender3DOptions() || {}; var atomicNumber = node.getAtomicNumber? node.getAtomicNumber(): null; if (localOptions.nodeRadius || renderOptions.nodeRadius) // radius explicitly set radius = localOptions.nodeRadius || renderOptions.nodeRadius; else if (oneOf(localOptions.useVdWRadius, renderOptions.useVdWRadius) && atomicNumber) // use vdW radius and is atom { var radiusVdw = Kekule.ChemicalElementsDataUtil.getElementProp(atomicNumber, 'radiiVdw') || localOptions.fixedNodeRadius || renderOptions.fixedNodeRadius; radius = radiusVdw; } else // use fixed radius { radius = oneOf(localOptions.fixedNodeRadius, renderOptions.fixedNodeRadius); } return radius; }, /** @private */ doDrawSelf: function(/*$super, */context, baseCoord, options) { this.tryApplySuper('doDrawSelf', [context, baseCoord, options]) /* $super(context, baseCoord, options) */; var chemObj = this.getChemObj(); //console.log('draw ctab', baseCoord, options); //var transformOptions = this.getFinalTransformParams(context, options.transformParams); var transformOptions = options.transformParams; this.getRenderCache(context).transformOptions = transformOptions; this.transformCtabCoords3DToContext(context, chemObj, transformOptions); var op = this.doPrepare(context, chemObj, baseCoord, options); // return prepared options return this.doDrawCore(context, chemObj, op, transformOptions); }, /** @private */ doRedraw: function(context) { var p = this.getRenderCache(context); //this.clear(context); // no need to prepare, draw directly return this.doDrawCore(context, this.getChemObj(), p.options, p.transformOptions); //return this.doDraw(context, p.baseCoord, p.options); }, /** @private */ doDrawCore: function(context, chemObj, options, finalTransformOptions) { // create a new group to contain whole ctab var group = this.createDrawGroup(context); this.doDrawConnectors(context, group, chemObj, options, finalTransformOptions); this.doDrawNodes(context, group, chemObj, options, finalTransformOptions); this.setObjDrawElem(context, chemObj, group); return group; }, /** * Transform each 3D coordinates of objects in CTab to current render space. * This function should be called before the whole draw phrase. * @private */ transformCtabCoords3DToContext: function(context, ctab, transformOptions) { var allowCoordBorrow = transformOptions.allowCoordBorrow; transformOptions.scaleX = transformOptions.scaleX || transformOptions.scale; transformOptions.scaleY = (transformOptions.scaleY || transformOptions.scale); transformOptions.scaleZ = (transformOptions.scaleZ || transformOptions.scale); var childTransformOptions = Object.extend({}, transformOptions); childTransformOptions.centerX = 0; childTransformOptions.centerY = 0; var coord; var coordTransformMatrix = transformOptions.transformMatrix; //Kekule.CoordUtils.calcTransform3DMatrix(transformOptions); this.setExtraProp2(context, ctab, this.TRANSFORM_MATRIX_FIELD, coordTransformMatrix); //var childCoordTransformMatrix = Kekule.CoordUtils.calcTransform3DMatrix(childTransformOptions); var childCoordTransformMatrix = coordTransformMatrix; this.setExtraProp2(context, ctab, this.CHILD_TRANSFORM_MATRIX_FIELD, coordTransformMatrix); // also calc for inversed transform matrix var invMatrix = transformOptions.invTransformMatrix; //Kekule.CoordUtils.calcInverseTransform3DMatrix(transformOptions); //console.log('INV CHECK', Kekule.MatrixUtils.multiply(this._coordTransformMatrix, invMatrix)); this.setExtraProp2(context, ctab, this.INV_TRANSFORM_MATRIX_FIELD, invMatrix); for (var i = 0, l = ctab.getNodeCount(); i < l; ++i) { var node = ctab.getNodeAt(i); //this.transformObjCoord2D(node, transformOptions, childTransformOptions); this.transformObjCoord3DToContext(context, node, coordTransformMatrix, childCoordTransformMatrix, allowCoordBorrow); } }, /** * Transform 3D coordinates of node or connector to current render space. * This function should be called by transformCtabCoords3D before the whole draw phrase. * @private */ transformObjCoord3DToContext: function(context, obj, transformMatrix, childTransformMatrix, allowCoordBorrow) { var result, coord; if (obj && obj.getAbsBaseCoord3D) { coord = obj.getAbsBaseCoord3D(allowCoordBorrow); if (coord) { var newCoord = Kekule.CoordUtils.transform3DByMatrix(coord, transformMatrix); this.setTransformedCoord3D(context, obj, newCoord); result = newCoord; } if (obj.getNodes) // has child nodes { // Done: not handle nested structure yet for (var i = 0, l = obj.getNodeCount(); i < l; ++i) this.transformObjCoord3DToContext(context, obj.getNodeAt(i), childTransformMatrix, childTransformMatrix, allowCoordBorrow); } } return result; }, /** * Get transformed coord. * @param {Object} context * @param {Object} obj * @private */ getTransformedCoord3D: function(context, obj, allowCoordBorrow) { if (Kekule.ObjUtils.isUnset(allowCoordBorrow)) allowCoordBorrow = this.getRenderCache(context).options.transformParams.allowCoordBorrow; var isNode = obj instanceof Kekule.BaseStructureNode; var result = isNode && this.getExtraProp2(context, obj, this.TRANSFORM_COORD_FIELD); // IMPORTANT: connector center coord is based on node and should not be cached if (!result) { var ctab = this.getChemObj(); var transformMatrix = this.getExtraProp2(context, ctab, this.TRANSFORM_MATRIX_FIELD); var childTransformMatrix = this.getExtraProp2(context, ctab, this.CHILD_TRANSFORM_MATRIX_FIELD); //this.transformObjCoord3DToContext(obj, transformMatrix, childTransformMatrix); if (ctab && (ctab.hasNode(obj, false) || ctab.hasConnector(obj, false))) // is direct child of ctab { result = this.transformObjCoord3DToContext(obj, transformMatrix, childTransformMatrix, allowCoordBorrow); } else // is nested child result = this.transformObjCoord3DToContext(obj, childTransformMatrix, childTransformMatrix, allowCoordBorrow); } //return this.getExtraProp2(context, obj, this.TRANSFORM_COORD_FIELD); return result; }, /** * Set transformed coord. * @param {Object} context * @param {Object} obj * @param {Hash} coord * @private */ setTransformedCoord3D: function(context, obj, coord) { this.setExtraProp2(context, obj, this.TRANSFORM_COORD_FIELD, coord); }, /** @private */ doTransformCoordToObj: function(context, chemObj, coord) { var matrix = this.getExtraProp2(context, chemObj, this.INV_TRANSFORM_MATRIX_FIELD); return Kekule.CoordUtils.transform3DByMatrix(coord, matrix); }, /** @private */ doTransformCoordToContext: function(context, chemObj, coord) { var matrix = this.getExtraProp2(context, chemObj, this.TRANSFORM_MATRIX_FIELD); return Kekule.CoordUtils.transform3DByMatrix(coord, matrix); }, /** * Draw all nodes in ctab on context. * @param {Object} context * @param {Object} group * @param {Object} ctab * @param {Object} options * @param {Object} finalTransformOptions * @returns {Object} A rendered object. * @private */ doDrawNodes: function(context, group, ctab, options, finalTransformOptions) { var nodes = ctab.getExposedNodes(); var hiddenNodes = this.getHiddenNodes(context); for (var i = 0, l = nodes.length; i < l; ++i) { var node = nodes[i]; // check if node is hydrogen atom and need to be hidden if (hiddenNodes.indexOf(node) >= 0) continue; var elem = this.doDrawNode(context, group, node, ctab, options, finalTransformOptions); } }, /** * Draw a node on context. * @param {Object} context * @param {Object} group * @param {Object} node * @param {Object} parentChemObj * @param {Object} options * @param {Object} finalTransformOptions * @returns {Object} A rendered object. * @private */ doDrawNode: function(context, group, node, parentChemObj, options, finalTransformOptions) { var result, op; var nodeRenderMode = this.getRenderCache(context).nodeRenderMode; if (nodeRenderMode === NRM.NONE) // do not need to render node return null; else // draw node ball { //op = Object.extend(op, node.getRender3DOptions()); op = Kekule.Render.RenderOptionUtils.mergeObjLocalRender3DOptions(node, options); var ballRadius; // calc node ball radius { ballRadius = this.getNodeBaseRadius(context, node); if (Kekule.ObjUtils.isUnset(ballRadius)) ballRadius = this.calcNodeBaseRadius(node, options); if (nodeRenderMode === NRM.SPACE) ballRadius *= finalTransformOptions.scaleX; // TODO: Y/Z scale is not considered yet, we can only draw ball now else // ball stick mode ballRadius *= op.nodeRadiusRatio * finalTransformOptions.scaleX; } var color = this.getObjDrawColor(context, node); // store the color for bond draw usage // draw ball var coord = this.getTransformedCoord3D(context, node, finalTransformOptions.allowCoordBorrow); result = this.drawSphere(context, coord, ballRadius, {'color': color, 'opacity': op.opacity}); var boundInfo = this.createSphereBoundInfo(coord, ballRadius); this.basicDrawObjectUpdated(context, node, parentChemObj, boundInfo, Kekule.Render.ObjectUpdateType.ADD); } if (result) { this.setObjDrawElem(context, node, result); if (group) { this.addToDrawGroup(result, group); } } }, /** * Draw all connectors in ctab on context. * @param {Object} context * @param {Object} group * @param {Object} ctab * @param {Object} options * @param {Object} finalTransformOptions * @private */ doDrawConnectors: function(context, group, ctab, options, finalTransformOptions) { if (this.getRenderCache(context).connectorRenderMode === BRM.NONE) return null; var connectors = ctab.getExposedConnectors(); var hiddenConnectors = this.getHiddenConnectors(context); for (var i = 0, l = connectors.length; i < l; ++i) { var connector = connectors[i]; if (hiddenConnectors.indexOf(connector) >= 0) continue; var elem = this.doDrawConnector(context, group, connector, ctab, options, finalTransformOptions); } }, /** * Draw a connector on context. * @param {Object}