Source: render/3d/kekule.render.renderer3D.js

/**
 * @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;

/**
 * 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',

	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;
	},

	/**
	 * 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)
	{
		$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)
	{
		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)
	{
		$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)
	{
		$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);
		}
		$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 = $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.invTransformMtrix = 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);
		}
		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(obox.x2 - obox.x1, obox.y2 - obox.y1);
				var cameraInfo = this.getCameraProps(context);

				var l = w / 2 / Math.tan(cameraInfo.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 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)
	{
		$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;
		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;
		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} context
	 * @param {Object} group
	 * @param {Object} connector
	 * @param {Object} options
	 * @param {Object} finalTransformOptions
	 * @returns {Object} A rendered object.
	 * @private
	 */
	doDrawConnector: function(context, group, connector, parentChemObj, options, finalTransformOptions)
	{
		var op = Object.create(options);
		//op = Object.extend(op, connector.getRender3DOptions());
		op = Kekule.Render.RenderOptionUtils.mergeObjLocalRender3DOptions(connector, options);

		// draw connector shape between every two connected objects
		var hiddenNodes = this.getHiddenNodes(context);
		var objs = connector.getConnectedExposedObjs();
		objs = Kekule.ArrayUtils.exclude(objs, hiddenNodes);
		var objCount = objs.length;
		var obj1, obj2;
		var coord1, coord2;
		var subGroup = (objCount > 2)? this.createDrawGroup(context): null;
		var elem;
		for (var i = 0; i < objCount; ++i)
		{
			obj1 = objs[i];
			coord1 = this.getTransformedCoord3D(context, obj1, finalTransformOptions.allowCoordBorrow);
			if (coord1)
			{
				for (var j = i + 1; j < objCount; ++j) // do not draw on self
 				{
					obj2 = objs[j];
					coord2 = this.getTransformedCoord3D(context, obj2, finalTransformOptions.allowCoordBorrow);
					if (coord2)
					{
						elem = this.doDrawConnectorShape(context, connector, [obj1, obj2], parentChemObj, coord1, coord2, op, finalTransformOptions);
						if (elem && subGroup)
							this.addToDrawGroup(elem, subGroup);
					}
				}
			}
		}
		if (subGroup)
			this.addToDrawGroup(subGroup, group);
		else
			this.addToDrawGroup(elem, group);
		return subGroup || elem;
	},

	/**
	 * Draw a connector (bond) connecting nodes with a specified shape on context.
	 * @param {Object} context
	 * @param {Kekule.ChemStructureConnector} connector
	 * @param {Array} nodes
	 * @param {Object} parentChemObj
	 * @param {Hash} coord1
	 * @param {Hash} coord2
	 * @param {Hash} options
	 * @param {Object} finalTransformOptions
	 * @private
	 */
	doDrawConnectorShape: function(context, connector, nodes, parentChemObj, coord1, coord2, options, finalTransformOptions)
	{
		var C = Kekule.CoordUtils;
		var localOptions = connector.getOverriddenRender3DOptions() || {};
		var unitLength = options.unitLength;

		var node1 = nodes[0];
		var node2 = nodes[1];

		var spliceMode = oneOf(localOptions.bondSpliceMode, options.bondSpliceMode/*, options.defBondSpliceMode*/);

		var splicePosRatio = (spliceMode === BSM.MID_SPLIT)? 0.5: null;
			//(spliceMode === BSM.WEIGHTING_SPLIT)? 0.5:  // TODO: need calculate weight
			//null;  // no splice
		if (spliceMode === BSM.WEIGHTING_SPLIT)
		{
			var r1 = this.getNodeBaseRadius(context, node1);
			var r2 = this.getNodeBaseRadius(context, node2);
			if (r1 && r2)
				splicePosRatio = r1 / (r1 + r2);
			else
				splicePosRatio = 0.5;
		}
		var spliceCoord = null;
		var needSplit = false;

		// colors to draw different part of bond
		var colors = [];
		if (splicePosRatio)  // may need splice
		{
			//var defColor = oneOf(options.bondColor, options.color/*, options.defBondColor*/);
			var defColor = oneOf(options.color || options.bondColor/*, options.defBondColor*/);
			//var color = oneOf(localOptions.bondColor, localOptions.color);
			var color = oneOf(localOptions.color || localOptions.bondColor);
			needSplit = false;
			if (color)  // color set by bond object itself
				colors.push(color);
			else  // color decided by node
			{
				var color1 = oneOf(this.getObjDrawColor(context, node1), defColor);
				var color2 = oneOf(this.getObjDrawColor(context, node2), defColor);
				if (color1 !== color2)
				{
					colors.push(color1);
					colors.push(color2);
					needSplit = true;
				}
				else  // same color, no need to split
				{
					//colors.push(oneOf(color1,	options.bondColor, options.color, options.defBondColor));
					colors.push(oneOf(color1,	options.color, options.bondColor, options.defBondColor));
				}
			}
		}
		else
		{
			/*
			colors.push(oneOf(localOptions.bondColor, localOptions.color,
				options.bondColor, options.color, options.defBondColor));
			*/
			colors.push(oneOf(localOptions.color, localOptions.bondColor,
				options.color, options.bondColor, options.defBondColor));
		}
		if (splicePosRatio && needSplit)
		{
			var dx = coord2.x - coord1.x;
			var dy = coord2.y - coord1.y;
			var dz = coord2.z - coord1.z;
			spliceCoord= {};
			spliceCoord.x = coord1.x + dx * splicePosRatio;
			spliceCoord.y = coord1.y + dy * splicePosRatio;
			spliceCoord.z = coord1.z + dz * splicePosRatio;
		}
		// coords to draw different part of bond
		var geometryCoords = needSplit? [[coord1, spliceCoord], [spliceCoord, coord2]]: [[coord1, coord2]];

		var connectorRenderMode = this.getRenderCache(context).connectorRenderMode;
		var renderType = this._getBondRenderType(connector, connectorRenderMode);
		var isMultiCylinder = false;
		// cylinder radius and line width
		var cylinderRadius = oneOf(localOptions.connectorRadius, options.connectorRadius/*, options.baseConnectorRadius*/)
			* oneOf(localOptions.connectorRadiusRatio, options.connectorRadiusRatio) * options.unitLength;
		cylinderRadius *= finalTransformOptions.scaleX;
   	var lineWidth = oneOf(localOptions.connectorLineWidth, options.connectorLineWidth) * options.unitLength;

		if ((renderType !== BRT.SINGLE) && (renderType !== BRT.DASH))
		{
			cylinderRadius *= oneOf(localOptions.multiConnectorRadiusRatio, options.multiConnectorRadiusRatio);
			isMultiCylinder = (connectorRenderMode === BRM.MULTI_CYLINDER);
		}
		else
			isMultiCylinder = false;

		// fill shape infos, prepare to draw
		var connectorShapeInfos = [];

		// get 3D shape needed for drawing
		var shapeCount = 1, lineCount = 0;
		switch (renderType)
		{
			case BRT.SINGLE: shapeCount = 1; break;
			case BRT.DOUBLE: shapeCount = 2; break;
			case BRT.TRIPLE: shapeCount = 3; break;
			case BRT.DASH: case BRT.SOLID_DASH:  // TODO: dash not handled
		}
		lineCount = shapeCount;
		var offsetStart = null;
		if (lineCount > 1)
		{
			var shapeOffsetLen = oneOf(localOptions.multiConnectorMarginRatio, options.multiConnectorMarginRatio) * cylinderRadius;
			if (isMultiCylinder)
				shapeOffsetLen += cylinderRadius * 2;
			// calculate offset on x/y direction
			var connectorLength = C.getDistance(coord1, coord2);
			var sub = C.substract(coord2, coord1);
			var sinAngle = sub.y / connectorLength;
			var cosAngle = sub.x / connectorLength;
			var shapeOffset = {'x': -shapeOffsetLen * sinAngle, 'y': shapeOffsetLen * cosAngle, 'z': 0};
			offsetStart = ((lineCount % 2) ? 0 : 0.5) - Math.floor(lineCount / 2);
			var startOffset = C.multiply(shapeOffset, offsetStart);
		}
		for (var i = 0, l = geometryCoords.length; i < l; ++i)
		{
			// if needSplit, the second coord of item 0 and first coord of item 1 is join point.
			if (lineCount === 1) // single line, no bond offset
			{
				var shapeInfo = {};
				shapeInfo.coord1 = geometryCoords[i][0];
				shapeInfo.coord2 = geometryCoords[i][1];
				if (needSplit)
					shapeInfo.jointCoordIndex = (i === 0)? 2: 1;
				shapeInfo.color = colors[i];
				shapeInfo.radius = cylinderRadius;
				shapeInfo.lineWidth = lineWidth;
				shapeInfo.opacity = options.opacity;
				connectorShapeInfos.push(shapeInfo);
			}
			else  // multipline or bond
			{

				var currCoord1 = C.add(geometryCoords[i][0], startOffset);
				var currCoord2 = C.add(geometryCoords[i][1], startOffset);
				for (var j = 0; j < lineCount; ++j)
				{
					var shapeInfo = {};
					shapeInfo.coord1 = currCoord1;
					shapeInfo.coord2 = currCoord2;
					currCoord1 = C.add(currCoord1, shapeOffset);
					currCoord2 = C.add(currCoord2, shapeOffset);
					if (needSplit)
						shapeInfo.jointCoordIndex = (i === 0)? 2: 1;
					shapeInfo.color = colors[i];
					shapeInfo.radius = cylinderRadius;
					shapeInfo.lineWidth = lineWidth;
					shapeInfo.opacity = options.opacity;
					connectorShapeInfos.push(shapeInfo);
				}
			}
		}

		var renderMode = connectorRenderMode;
		// add bound info
		var boundInfo;
		if ((renderMode === BRM.WIRE) || (renderMode === BRM.MULTI_WIRE))  // draw wire
		{
			boundInfo = this.createLineBoundInfo(coord1, coord2, {width: lineWidth});
		}
		else
		{
			boundInfo = this.createCylinderBoundInfo(coord1, coord2, {radius: cylinderRadius});
		}
		this.basicDrawObjectUpdated(context, connector, parentChemObj, boundInfo, Kekule.Render.ObjectUpdateType.ADD);

		//return this.doDrawConnectorSet(context, geometryCoords, colors, cylinderRadius, this._connectorRenderMode, renderType, options);
		return this.doDrawConnectorSet(context, connectorShapeInfos, renderMode, renderType, options);
	},
	/** @private */
	doDrawConnectorSet: function(context, connectorShapeInfos, renderMode, renderType, options)
	{
		if ((renderMode === BRM.WIRE) || (renderMode === BRM.MULTI_WIRE))  // draw wire
		{
			return this.drawParallelLines(context, connectorShapeInfos);
		}
		else  // draw cylinder
		{
			var drawEndCaps = this.getRenderCache(context).nodeRenderMode === NRM.NONE;
			return this.drawParallelCylinders(context, connectorShapeInfos, drawEndCaps);
		}
	},
	/** @private */
	_getBondRenderType: function(connector, renderMode)
	{
		var localRenderOptions = connector.getOverriddenRender3DOptions() || {};
		var rt = Kekule.Render.Render3DOptionUtils.getConnectorRenderType(localRenderOptions);
		if (rt)
			return rt;
		else
			return Kekule.Render.ConnectorDrawUtils.getConnectorRender3DType(connector, renderMode);
	}
});
Kekule.ClassDefineUtils.addExtraTwoTupleObjMapSupport(Kekule.Render.ChemCtab3DRenderer);


/**
 * Class to render for {@link Kekule.StructureFragment}.
 * The class will use {@link Kekule.Render.ChemCtab3DRenderer} to draw actual structure.
 * Note that 3D formula form is not implemented yet.
 * @class
 * @augments Kekule.Render.ChemObj3DRenderer
 *
 * @param {Kekule.ChemObject} chemObj Object to be drawn.
 * @param {Object} drawBridge A object that implements the actual draw job.
 * @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.
 */
Kekule.Render.StructFragment3DRenderer = Class.create(Kekule.Render.ChemObj3DRenderer,
/** @lends Kekule.Render.StructFragment3DRenderer# */
{
	/** @private */
	CLASS_NAME: 'Kekule.Render.StructFragment3DRenderer',
	/** @constructs */
	initialize: function($super, chemObj, drawBridge, /*renderConfigs,*/ parent)
	{
		$super(chemObj, drawBridge, /*renderConfigs,*/ parent);

		this._concreteRenderer = null;
		if (chemObj.hasCtab())
			this._concreteRenderer = new Kekule.Render.ChemCtab3DRenderer(chemObj.getCtab(), drawBridge, /*renderConfigs*/ this);
		else //if (chemObj.hasFormula())
		{
			//this._concreteRenderer = new Kekule.Render.Formula3DRenderer(chemObj.getFormula(), drawBridge, renderConfigs);
			this._concreteRenderer = null;
			Kekule.raise(/*Kekule.ErrorMsg.FORMULA_RENDERER_3D_NOT_AVAILABLE*/Kekule.$L('ErrorMsg.FORMULA_RENDERER_3D_NOT_AVAILABLE'));
		}
		this._concreteRenderer.setParentRenderer(this);
		//this.setMoleculeDisplayType(moleculeDisplayType || Kekule.Render.MoleculeDisplayType.BOND_LINE);
	},
	finalize: function($super)
	{
		$super();
		if (this._concreteRenderer)
			this._concreteRenderer.finalize();
	},
	//* @private */
	/*
	applyConfigs: function()
	{
		this._concreteRenderer.setRenderConfigs(this.getRenderConfigs());
		this._concreteRenderer.setDrawBridge(this.getDrawBridge());
	},
	*/

	/** @ignore */
	getRenderCache: function(context)
	{
		return this._concreteRenderer.getRenderCache(context);
	},

	/** @private */
	_getConcreteRendererDrawOptions: function(options)
	{
		var ops = Object.create(options);
		var chemObj = this.getChemObj();
		var objOptions = (this.getRendererType() === Kekule.Render.RendererType.R3D)?
			chemObj.getOverriddenRender3DOptions(): chemObj.getOverriddenRenderOptions();
		/*
		if (objOptions)
			console.log('OBJ options', objOptions);
		*/

		ops = Object.extend(ops, objOptions || {});
		return ops;
	},

	/** @ignore */
	isChemObjRenderedBySelf: function($super, context, obj)
	{
		var result = $super(context, obj) || (obj === this.getChemObj()) || this._concreteRenderer.isChemObjRenderedBySelf(context, obj);
		return result;
	},
	/** @ignore */
	isChemObjRenderedDirectlyBySelf: function($super, context, obj)
	{
		return $super(context, obj) || (obj === this.getChemObj());
	},

	/** @private */
	doSetRedirectContext: function($super, value)
	{
		$super(value);
		this._concreteRenderer.setRedirectContext(value);
	},

	/** @ignore */
	doDrawSelf: function($super, context, baseCoord, options)
	{
		//this.applyConfigs();
		$super(context, baseCoord, options);

		return this._concreteRenderer.draw(context, baseCoord, this._getConcreteRendererDrawOptions(options));
	},
	/** @ignore */
	doRedraw: function(context)
	{
		return this._concreteRenderer.redraw(context);
	},
	/** @ignore */
	doUpdateSelf: function(context, updatedObjDetails, updateType)
	{
		var objs = this._extractObjsOfUpdateObjDetails(updatedObjDetails);

		if (objs.indexOf(this.getChemObj()) >= 0)  // root object need to be updated
		{
			var p = this.getRenderCache(context);
			this.doClear(context);
			return this.draw(context, p.baseCoord, p.options);
		}

		return this._concreteRenderer.doUpdate(context, updatedObjDetails, updateType);
	},
	/** @ignore */
	doClearSelf: function(context)
	{
		var r = this._concreteRenderer;
		if (r)
			return r.clear(context);
	},
	/** @ignore */
	estimateRenderBox: function(context, options, allowCoordBorrow)
	{
		return this._concreteRenderer.estimateRenderBox(context, this._getConcreteRendererDrawOptions(options), allowCoordBorrow);
	},

	/** @ignore */
	getChildObjs: function($super)
	{
		var chemObj = this.getChemObj();
		if (chemObj && chemObj.getAttachedMarkers)
		{
			var r = [];
			var childObjs = (chemObj.getExposedNodes() || []).concat(chemObj.getExposedConnectors() || []);
			for (var i = 0, l = childObjs.length; i < l; ++i)
			{
				var obj = childObjs[i];
				r = r.concat(obj.getAttachedMarkers() || []);
			}
			return r.concat($super());
		}
		else
			return $super();
	},
	/** @ignore */
	_needWholelyDraw: function($super, partialDrawObjs, context)
	{
		var result = $super(partialDrawObjs, context);
		if (!result)
		{
			var chemObj = this.getChemObj();
			var hasStructObjs = false;
			for (var i = 0, l = partialDrawObjs.length; i < l; ++i)
			{
				var pObj = partialDrawObjs[i];
				if (pObj.isChildOf(chemObj) /* && (pObj instanceof Kekule.ChemStructureObject)*/)  // child attachers also need redraw whole
				{
					hasStructObjs = true;
					break;
				}
			}
			result = hasStructObjs;
		}
		return result;
	},

	/** @ignore */
	transformCoordToObj: function(context, chemObj, coord)
	{
		var obj = (this.getChemObj() === chemObj)? this._concreteChemObj: chemObj;
		return this._concreteRenderer.transformCoordToObj(context, obj, coord);
	},
	/** @ignore */
	transformCoordToContext: function(context, chemObj, coord)
	{
		var obj = (this.getChemObj() === chemObj)? this._concreteChemObj: chemObj;
		return this._concreteRenderer.transformCoordToContext(context, obj, coord);
	}
});

// Molecule renderer, actually an alias of structFragment renderer
Kekule.Render.Mol3DRenderer = Kekule.Render.StructFragment3DRenderer;

/**
 * Base class to render composite chem objects, such as composite molecule, chem space and so on.
 * The class will use concrete renderer for each child object inside.
 * @class
 * @augments Kekule.Render.ChemObj3DRenderer
 *
 * @param {Kekule.ChemObject} chemObj Object to be drawn.
 * @param {Object} drawBridge A object that implements the actual draw job.
 * @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.
 */
Kekule.Render.CompositeObj3DRenderer = Class.create(Kekule.Render.ChemObj3DRenderer,
/** @lends Kekule.Render.CompositeObj3DRenderer# */
{
	/** @private */
	CLASS_NAME: 'Kekule.Render.CompositeObj3DRenderer'
});
//Kekule.Render.RendererDefineUtils.addCompositeRenderSupport(Kekule.Render.CompositeObj3DRenderer);

/**
 * Class to render composite molecule.
 * @class
 * @augments Kekule.Render.CompositeObj3DRenderer
 */
Kekule.Render.CompositeMolecule3DRenderer = Class.create(Kekule.Render.CompositeObj3DRenderer,
/** @lends Kekule.Render.CompositeMolecule3DRenderer# */
{
	/** @private */
	CLASS_NAME: 'Kekule.Render.CompositeMolecule3DRenderer',

	/** @ignore */
	getChildObjs: function($super)
	{
		var r = [];
		var group = this.getChemObj().getSubMolecules();
		for (var i = 0, l = group.getItemCount(); i < l; ++i)
		{
			var o = group.getObjAt(i);
			r.push(o);
		}
		return r.concat($super());
	}
});

/**
 * Class to render ChemObjList or ChemStructureObjectGroup.
 * @class
 * @augments Kekule.Render.CompositeObj3DRenderer
 */
Kekule.Render.ChemObjGroupList3DRenderer = Class.create(Kekule.Render.CompositeObj3DRenderer,
/** @lends Kekule.Render.ChemObjGroupList3DRenderer# */
{
	/** @private */
	CLASS_NAME: 'Kekule.Render.ChemObjGroupList3DRenderer',

	/** @ignore */
	getChildObjs: function($super)
	{
		var obj = this.getChemObj();
		if (obj instanceof Kekule.ChemObjList)
		{
			var r = [];
			for (var i = 0, l = obj.getItemCount(); i < l; ++i)
			{
				r.push(obj.getItemAt(i));
			}
		}
		else if (obj instanceof Kekule.ChemStructureObjectGroup)
		{
			r = obj.getAllObjs();
		}

		return (r || []).concat($super());
	}
});

/**
 * Class to render ChemSpaceElement instance.
 * The class will use concrete renderer for each child object inside.
 * @class
 * @augments Kekule.Render.CompositeObj3DRenderer
 *
 * @param {Kekule.ChemObject} chemObj Object to be drawn.
 * @param {Object} drawBridge A object that implements the actual draw job.
 * @param {Object} renderConfigs Global configuration for rendering.
 *   This property should be an instance of {@link Kekule.Render.Render2DConfigs}.
 */
Kekule.Render.ChemSpaceElement3DRenderer = Class.create(Kekule.Render.CompositeObj3DRenderer,
/** @lends Kekule.Render.ChemSpaceElement3DRenderer# */
{
	/** @private */
	CLASS_NAME: 'Kekule.Render.ChemSpaceElement3DRenderer',

	/** @ignore */
	getChildObjs: function($super)
	{
		return (this.getChemObj().getChildren().toArray() || []).concat($super());
	}
});

/**
 * Class to render ChemSpace instance.
 * The class will use concrete renderer for each child object inside.
 * @class
 * @augments Kekule.Render.CompositeObj3DRenderer
 */
Kekule.Render.ChemSpace3DRenderer = Class.create(Kekule.Render.CompositeObj3DRenderer,
/** @lends Kekule.Render.ChemSpace3DRenderer# */
{
	/** @private */
	CLASS_NAME: 'Kekule.Render.ChemSpace3DRenderer',

	/** @ignore */
	getChildObjs: function($super)
	{
		//return this.getChemObj().getRoot().getChildren().toArray();
		return [this.getChemObj().getRoot()].concat($super());
	}
});

// register renderers
Kekule.Render.Renderer3DFactory.register(Kekule.StructureFragment, Kekule.Render.StructFragment3DRenderer);
Kekule.Render.Renderer3DFactory.register(Kekule.CompositeMolecule, Kekule.Render.CompositeMolecule3DRenderer);
Kekule.Render.Renderer3DFactory.register(Kekule.ChemObjList, Kekule.Render.ChemObjGroupList3DRenderer);
Kekule.Render.Renderer3DFactory.register(Kekule.ChemStructureObjectGroup, Kekule.Render.ChemObjGroupList3DRenderer);
Kekule.Render.Renderer3DFactory.register(Kekule.ChemSpaceElement, Kekule.Render.ChemSpaceElement3DRenderer);
Kekule.Render.Renderer3DFactory.register(Kekule.ChemSpace, Kekule.Render.ChemSpace3DRenderer);

})();