/**
* @fileoverview
* 3D renderer using three.js library.
* @author Partridge Jiang
*/
/*
* requires three.js
* requires /render/kekule.utils.js
* requires /render/kekule.render.base.js
* requires /render/2d/kekule.render.renderer3D.js
* requires /xbrowsers/kekule.x.js
*/
if (Kekule.$jsRoot.THREE)
{
/** @ignore */
THREE.Object3D.prototype.clear = function(){
var children = this.children;
for (var i = children.length - 1; i >= 0; i--)
{
var child = children[i];
child.clear();
this.remove(child);
}
};
/** @ignore */
THREE.Scene.prototype.clearMesh = function()
{
var children = this.children;
for (var i = children.length - 1; i >= 0; i--)
{
var child = children[i];
if ((child instanceof THREE.Mesh) || (child instanceof THREE.Line)
|| (child.__objGroup__)) // a special flag to indicate that this is a object created by createGroup
{
child.clear();
this.remove(child);
}
}
}
}
/** @ignore */
Kekule.Render.ThreeObjectCache = Class.create(
/** @lends Kekule.Render.ThreeObjectCache# */
{
/** @private */
CLASS_NAME: 'Kekule.Render.ThreeObjectCache',
/**
* @ignore
*/
initialize: function()
{
this.cache = new Kekule.MapEx(true); // non-weak, to keep objects
},
finalize: function()
{
this.cache.finalize();
this.cache = null;
},
/** @private */
createInstance: function(objClass, params)
{
return null; // descendant need to override
},
/** @private */
isSameParams: function(params1, params2)
{
if (params1.length !== params2.length)
{
return false;
}
for (var i = 0, l = params1.length; i < l; ++i)
{
if (!this.isParamEqual(params1[i], params2[i]))
{
return false;
}
}
return true;
},
/** @private */
isParamEqual: function(param1, param2)
{
var result = (param1 === param2);
if (!result)
{
if (DataType.isObjectValue(param1) && DataType.isObjectValue(param2))
result = Kekule.ObjUtils.equal(param1, param2);
}
return result;
},
/**
* add(geometryClass, scene, param1, param2...)
* @ignore
*/
add: function()
{
var a = Array.prototype.slice.call(arguments);
var gClass = a.shift();
var items = this.cache.get(gClass);
if (!items)
{
items = [];
this.cache.set(gClass, items);
}
var item = {'params': a};
item.instance = this.createInstance(gClass, a);
items.push(item);
return item.instance;
},
/**
* get(geometryClass, scene, param1, param2...)
* @ignore
*/
get: function()
{
/*
* IMPORTANT: different Three scene must use different object cache!!!!
*/
var a = Array.prototype.slice.call(arguments);
var gClass = a[0];
var items = this.cache.get(gClass);
if (items)
{
a.shift();
for (var i = 0, l = items.length; i < l; ++i)
{
var params = items[i].params;
if (this.isSameParams(params, a))
{
return items[i].instance;
}
}
a.unshift(gClass);
}
// not found, create new
return this.add.apply(this, a);
}
});
/** @ignore */
Kekule.Render.ThreeGeometryCache = Class.create(Kekule.Render.ThreeObjectCache, {
/** @private */
CLASS_NAME: 'Kekule.Render.ThreeGeometryCache',
/** @private */
createInstance: function(objClass, params)
{
var instance = null;
var a = params;
// unshift scene param
//var scene = a.shift();
var scene = a[0];
switch (objClass)
{
case THREE.SphereGeometry:
//instance = new THREE.SphereGeometry(a[0], a[1], a[2]);
instance = new THREE.SphereGeometry(a[1], a[2], a[3]);
break;
case THREE.CylinderGeometry:
//instance = new THREE.CylinderGeometry(a[0], a[1], a[2], a[3], a[4], a[5]);
instance = new THREE.CylinderGeometry(a[1], a[2], a[3], a[4], a[5], a[6]);
break;
}
//a.unshift(scene);
return instance;
}
});
Kekule.ClassUtils.makeSingleton(Kekule.Render.ThreeGeometryCache);
/** @ignore */
Kekule.Render.ThreeMaterialCache = Class.create(Kekule.Render.ThreeObjectCache, {
/** @private */
CLASS_NAME: 'Kekule.Render.ThreeMaterialCache',
/** @private */
createInstance: function(objClass, params)
{
//return new objClass(params[0]);
// param[0] is scene data, param[1] is material data
return new objClass(params[1]);
}
});
Kekule.ClassUtils.makeSingleton(Kekule.Render.ThreeMaterialCache);
/**
* A combination of context, camera and renderer of three.js.
* @class
*/
Kekule.Render.ThreeContext = Class.create(ObjectEx,
/** @lends Kekule.Render.ThreeContext# */
{
/** @private */
CLASS_NAME: 'Kekule.Render.ThreeContext',
/** @constructs */
initialize: function($super, scene, camera, lights, renderer)
{
$super();
this.setScene(scene);
this.setCamera(camera);
this.setLights(lights);
this.setRenderer(renderer);
},
/** @private */
initProperties: function()
{
this.defineProp('scene', {'dataType': DataType.OBJECT, 'serializable': false});
this.defineProp('camera', {'dataType': DataType.OBJECT, 'serializable': false});
this.defineProp('lights', {'dataType': DataType.ARRAY, 'serializable': false});
this.defineProp('renderer', {'dataType': DataType.OBJECT, 'serializable': false});
this.defineProp('width', {'dataType': DataType.INT, 'serializable': false, 'setter': null});
this.defineProp('height', {'dataType': DataType.INT, 'serializable': false, 'setter': null});
},
/**
* Get width and height of context.
* @returns {Hash} {width, height}
*/
getDimension: function()
{
return {'width': this.getWidth(), 'height': this.getHeight()};
},
/**
* Set width and height of context.
* @param {Int} width
* @param {Int} height
*/
setDimension: function(width, height)
{
this.setPropStoreFieldValue('width', width);
this.setPropStoreFieldValue('height', height);
this.getRenderer().setSize(width, height);
var c = this.getCamera();
if (c instanceof THREE.OrthographicCamera)
{
c.left = width / -2;
c.right = width / 2;
c.top = height / 2;
c.bottom = height / -2;
}
else // c instanceof THREE.PerspectiveCamera
{
c.aspect = width / height;
}
c.updateProjectionMatrix(); // IMPORTANT, otherwise there may be stretch in drawing
// TODO: need to repaint?
//this.getRenderer().render(this.getScene(), this.getCamera());
}
});
/**
* Render bridge class of three.js.
* In this bridge, context is a {@link Kekule.Render.ThreeContext} object, thus we can handle both scene and camera at the same time.
* @class
*/
Kekule.Render.ThreeRendererBridge = Class.create(
/** @lends Kekule.Render.ThreeRendererBridge# */
{
/** @private */
CLASS_NAME: 'Kekule.Render.ThreeRendererBridge',
/** @private */
_qualitySettings: [
null,
// EXTREME_LOW
{
'sphereSegments': 6,
'sphereRings': 6,
'cylinderSegmentsRadius': 6
},
// LOW
{
'sphereSegments': 8,
'sphereRings': 8,
'cylinderSegmentsRadius': 8
},
// MEDIUM
{
'sphereSegments': 16,
'sphereRings': 16,
'cylinderSegmentsRadius': 16
},
// HIGH
{
'sphereSegments': 32,
'sphereRings': 32,
'cylinderSegmentsRadius': 32
},
// EXTREME_HIGH
{
'sphereSegments': 64,
'sphereRings': 64,
'cylinderSegmentsRadius': 64
}
],
/** @constructs */
initialize: function()
{
this.geometryCache = Kekule.Render.ThreeGeometryCache.getInstance();
this.materialCache = Kekule.Render.ThreeMaterialCache.getInstance();
this._quality = null; // used internally
this._modelParams = {};
this._webglEnabled = true; // assume first
},
/** @ignore */
finalize: function()
{
//this.geometryCache.finalize();
this.geometryCache = null;
//this.materialCache.finalize();
this.materialCache = null;
},
getGraphicQualityLevel: function()
{
return this._quality;
},
setGraphicQualityLevel: function(value)
{
if (this._webglEnabled)
this._quality = value;
else
this._quality = Kekule.Render.Render3DGraphicQuality.EXTREME_LOW; // always use low quality when webgl is not available
this._modelParams = this.calcModelParams(this._quality);
},
/** @private */
_updateGraphicQualityLevel: function()
{
if (!this._webglEnabled)
this.setGraphicQualityLevel(Kekule.Render.Render3DGraphicQuality.EXTREME_LOW); // always use low quality when webgl is not available
},
calcModelParams: function(qualityLevel)
{
var result = this._qualitySettings[qualityLevel];
if (!result)
result = this._qualitySettings[Kekule.Render.Render3DGraphicQuality.MEDIUM]; // default setting
return result;
},
/** @private */
getInitialLightPositions: function(context)
{
return [{'x': 5, 'y': 5, 'z': 10}];
},
/**
* Create a context element for drawing.
* @param {Element} parentElem
* @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)
{
var BF = Kekule.BrowserFeature;
var renderer = BF.webgl?
new THREE.WebGLRenderer({
preserveDrawingBuffer: true, // use to enable screenshot
alpha: true
}):
BF.canvas?
new THREE.CanvasRenderer():
new THREE.SVGRenderer();
var camera =
new THREE.PerspectiveCamera();
camera.fov = 5; // very small fov to avoid too much morphs
this._webglEnabled = (renderer instanceof THREE.WebGLRenderer);
this._updateGraphicQualityLevel();
// TODO: OrthographicCamera currently unavailable, as left/right/top/bottom need further calculation
/*
var camera =
new THREE.OrthographicCamera();
*/
var scene = new THREE.Scene();
scene.add(camera);
renderer.setSize(width, height);
// TODO: now light is fixed
var lightPositions = this.getInitialLightPositions();
var lights = [];
for (var i = 0, l = lightPositions.length; i < l; ++i)
{
var alight = new THREE.DirectionalLight(0xcccccc, 1, 10, true);
var lightCoord = lightPositions[i];
alight.position.set(lightCoord.x, lightCoord.y, lightCoord.z);
scene.add(alight);
lights.push(alight);
}
/*
var alight = new THREE.DirectionalLight(0xcccccc, 1, 10, true);
alight.position.set(-5, -5, -10);
scene.add(alight);
*/
var alight = new THREE.AmbientLight( 0x202020 ); // soft white light
scene.add(alight);
parentElem.appendChild(renderer.domElement);
var result = new Kekule.Render.ThreeContext(scene, camera, lights, renderer);
/*
result.setWidth(width);
result.setHeight(height);
*/
result.setDimension(width, height);
return result;
},
/**
* Destroy context created.
* @param {Object} context
*/
releaseContext: function(context)
{
var elem = this.getContextElem(context);
context.finalize();
if (elem)
elem.parentNode.removeChild(elem);
},
/**
* Get context related element.
* @param {Object} context
*/
getContextElem: function(context)
{
return context? context.getRenderer().domElement: null;
},
/**
* Get width and height of context.
* @param {Object} context
* @returns {Hash} {width, height}
*/
getContextDimension: function(context)
{
return {'width': context.getWidth(), 'height': context.getHeight()};
},
/**
* Set new width and height of context.
* Note in canvas, the content should be redrawn after resizing.
* @param {Object} context
* @param {Int} width
* @param {Int} height
*/
setContextDimension: function(context, width, height)
{
context.setDimension(width, height);
},
/**
* Transform a context based coord to screen based one (usually in pixel).
* @param {Object} context
* @param {Hash} coord
* @return {Hash}
*/
transformContextCoordToScreen: function(context, coord)
{
var dim = context.getDimension();
// TODO: need 3D projector calculation
return {'x': coord.x + dim.width / 2, 'y': coord.y + dim.height / 2};
},
/**
* Transform a screen based coord to context based one.
* @param {Object} context
* @param {Hash} coord
* @return {Hash}
*/
transformScreenCoordToContext: function(context, coord)
{
var dim = context.getDimension();
// TODO: need 3D projector calculation
return {'x': coord.x - dim.width / 2, 'y': coord.y - dim.height / 2, 'z': 0};
},
/**
* Clear the whole context.
* @param {Object} context
*/
clearContext: function(context)
{
context.clear();
},
/**
* 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 true;
},
/**
* Transform a 3D context based coord to screen based one (usually 2D in pixel).
* @param {Object} context
* @param {Hash} coord
* @return {Hash}
*/
// TODO: unfinished yet.
transformContextCoordToScreen: function(context, coord)
{
return {x: coord.x, y: coord.y};
},
/**
* @private
*/
colorStrToHex: function(str)
{
return Kekule.StyleUtils.colorStrToValue(str);
},
/** @private */
createGroup: function(context)
{
var r = new THREE.Object3D();
r.__objGroup__ = true;
context.getScene().add(r);
return r;
},
/** @private */
addToGroup: function(elem, group)
{
group.add(elem);
return group;
},
/** @private */
removeFromGroup: function(elem, group)
{
group.remove(elem);
},
/** @private */
setClearColor: function(context, color)
{
var r = context.getRenderer();
if (r)
{
if (r.setClearColorHex)
{
if (color)
context.getRenderer().setClearColorHex(this.colorStrToHex(color), 1);
else // color not set, transparent
context.getRenderer().setClearColorHex(null, 0);
}
else if (r.setClearColor) // in new version, setClearColorHex method has been removed
{
if (color)
context.getRenderer().setClearColor(this.colorStrToHex(color), 1);
else // color not set, transparent
context.getRenderer().setClearColor(null, 0);
}
}
},
/** @private */
clearContext: function(context)
{
context.getScene().clearMesh();
this.renderContext(context);
},
/** @private */
renderContext: function(context)
{
context.getRenderer().render(context.getScene(), context.getCamera());
},
/** @private */
drawSphere: function(context, coord, radius, options)
{
// DONE: seqments and rings are fixed now
var segments = this._modelParams.sphereSegments; //16;
var rings = this._modelParams.sphereRings; //16;
// create a sphere mesh
var geometry = this.geometryCache.get(THREE.SphereGeometry, context.getScene(), /*radius*/1, segments, rings);
var material = this.materialCache.get(THREE.MeshLambertMaterial, context.getScene(),
{'color': this.colorStrToHex(options.color), 'opacity': options.opacity || 1});
var result = new THREE.Mesh(geometry, material);
result.scale.x = radius;
result.scale.y = radius;
result.scale.z = radius;
result.position.x = coord.x;
result.position.y = coord.y;
result.position.z = coord.z;
context.getScene().add(result);
return result;
},
/** @private */
drawLine: function(context, coord1, coord2, options)
{
var icolor = this.colorStrToHex(options.color);
var lineMat = this.materialCache.get(THREE.LineBasicMaterial, context.getScene(),
{ 'color': this.colorStrToHex(options.color), 'opacity': options.opacity || 1, linewidth: options.lineWidth || 1});
var geom = new THREE.Geometry();
geom.vertices.push(new THREE.Vector3(coord1.x, coord1.y, coord1.z));
geom.vertices.push(new THREE.Vector3(coord2.x, coord2.y, coord2.z));
line = new THREE.Line(geom, lineMat);
context.getScene().add(line);
return line;
},
/** @private */
drawCylinder: function(context, coord1, coord2, radius, options)
{
//console.log('Draw sphere at', coord1, coord2);
var C = Kekule.CoordUtils;
var segmentsRadius = this._modelParams.cylinderSegmentsRadius; //16;
var segmentsHeight = 1;
var height = C.getDistance(coord1, coord2);
//var geometry = this.geometryCache.get(THREE.CylinderGeometry, context.getScene(), radius, radius, 1/*height*/, segmentsRadius, segmentsHeight, true); // open ended
var geometry = this.geometryCache.get(THREE.CylinderGeometry, context.getScene(), 1, 1, 1/*height*/, segmentsRadius, segmentsHeight, true); // open ended
var material = this.materialCache.get(THREE.MeshLambertMaterial, context.getScene(),
{'color': this.colorStrToHex(options.color), 'opacity': options.opacity || 1});
var result = new THREE.Mesh(geometry, material);
// adjust length
result.scale.y = height;
result.scale.x = radius;
result.scale.z = radius;
// adjust rotation
var quaternion = Kekule.ObjUtils.isUnset(options.quaternion)? this._calcQuaternion(coord1, coord2): options.quaternion;
if (quaternion)
{
if (result.applyQuaternion) // new version of THREE
result.applyQuaternion(quaternion);
else // old version
{
result.useQuaternion = true;
result.quaternion = quaternion;
result.updateMatrix();
}
}
// adjust position
result.position.x = (coord1.x + coord2.x) / 2;
result.position.y = (coord1.y + coord2.y) / 2;
result.position.z = (coord1.z + coord2.z) / 2;
context.getScene().add(result);
return result;
},
/** @private */
drawParallelCylinders: function(context, cylinderInfos, drawEndCaps)
{
var C = Kekule.CoordUtils;
// calculate rotation quaternion for all cylinders
var first = cylinderInfos[0];
var quaternion = this._calcQuaternion(first.coord1, first.coord2);
// since all cylinders are parallel, quaternion are all the same
// just calc once will save a lot of time
var result = this.createGroup(context);
for (var i = 0, l = cylinderInfos.length; i < l; ++i)
{
var info = cylinderInfos[i];
var obj = this.drawCylinder(context, info.coord1, info.coord2, info.radius,
{'color': info.color, 'quaternion': quaternion, 'opacity': info.opacity});
//result.push(obj);
this.addToGroup(obj, result);
}
// then draw small caps of cylinder if needed
if (drawEndCaps)
{
var capCoords;
for (var i = 0, l = cylinderInfos.length; i < l; ++i)
{
var info = cylinderInfos[i];
if (Kekule.ObjUtils.isUnset(info.jointCoordIndex)) // both end need cap
capCoords = [info.coord1, info.coord2];
else
{
capCoords = (info.jointCoordIndex === 1)? [info.coord2]: [info.coord1];
}
// draw small spheres at end of cylinder
for (var j = 0, k = capCoords.length; j < k; ++j)
{
var obj = this.drawSphere(context, capCoords[j], info.radius, {'color': info.color, 'opacity': info.opacity});
//result.push(obj);
this.addToGroup(obj, result);
}
}
}
return result;
},
/** @private */
removeDrawnElem: function(context, elem)
{
if (elem.parent && elem.parent.remove)
elem.parent.remove(elem);
},
/** @private */
_calcQuaternion: function(coord1, coord2)
{
var C = Kekule.CoordUtils;
var alignVector = C.substract(coord2, coord1); // direction vector of cylinder
if (alignVector.x || alignVector.z) // not to y-axis direction
{
var crossVector = {'x': alignVector.z, 'y': 0, 'z': -alignVector.x};
crossVector = C.standardize(crossVector);
var angle = Math.atan2(Math.sqrt(Math.sqr(alignVector.x) + Math.sqr(alignVector.z)), alignVector.y);
var halfAngle = angle / 2;
var cosHalf = Math.cos(halfAngle);
var sinHalf = Math.sin(halfAngle);
var quaternion = new THREE.Quaternion(sinHalf * crossVector.x, sinHalf * crossVector.y,
sinHalf * crossVector.z, cosHalf);
return quaternion;
}
else
return null;
},
// methods about light
/**
* Returns count of lights in context.
* @param {Object} context
* @returns {Int}
*/
getLightCount: function(context)
{
var ls = context.getLights();
return ls? (ls.length || 0): 0;
},
/**
* Get properties of light at index.
* @param {Object} context
* @param {Int} lightIndex
* @returns {Hash}
*/
getLightProps: function(context, lightIndex)
{
var light = context.getLights()[lightIndex];
return {'position': light.position};
},
/**
* Get properties of light at index.
* @param {Object} context
* @param {Int} lightIndex
* @param {Hash} props
*/
setLightProps: function(context, lightIndex, props)
{
var light = context.getLights()[lightIndex];
var p = props.position;
if (p)
{
light.position.x = p.x;
light.position.y = p.y;
light.position.z = p.z;
}
light.updateMatrix();
},
// methods about camera
/**
* Returns properties of current camera, including position(coord), fov, aspect and so on.
* @param {Object} context
* @returns {Hash}
*/
getCameraProps: function(context)
{
var c = context.getCamera();
return {'position': c.position, 'fov': c.fov * Math.PI / 180, 'aspect': c.aspect,
'left': c.left, 'right': c.right, 'top': c.top, 'bottom': c.bottom,
'lookAtVector': c.lookAtVector, 'upVector': c.up};
},
/**
* Set properties of current camera, including position(coord), fov, aspect, lookAtVector and so on.
* @param {Object} context
* @param {Hash} props
*/
setCameraProps: function(context, props)
{
//console.log(props);
var c = context.getCamera();
var notUnset = Kekule.ObjUtils.notUnset;
var p = props.position;
if (p)
{
c.position.x = p.x;
c.position.y = p.y;
c.position.z = p.z;
}
if (notUnset(props.near))
c.near = props.near;
if (notUnset(props.far))
c.near = props.far;
// for perspective projection
if (props.fov)
c.fov = props.fov * 180 / Math.PI;
if (props.aspect)
c.aspect = props.aspect;
// for orthographic projection
if (notUnset(props.left))
c.left = props.left;
if (notUnset(props.top))
c.top = props.top;
if (notUnset(props.bottom))
c.bottom = props.bottom;
if (notUnset(props.right))
c.right = props.right;
if (notUnset(props.upVector))
{
var v = props.upVector;
c.up = new THREE.Vector3(v.x, v.y, v.z);
}
if (notUnset(props.lookAtVector))
{
var v = props.lookAtVector;
c.lookAt(new THREE.Vector3(v.x, v.y, v.z));
}
c.updateMatrix();
},
/** @ignore */
exportToDataUri: function(context, dataType, options)
{
var renderer = context.getRenderer();
if (renderer instanceof THREE.SVGRenderer)
{
var domElem = context.getRenderer().domElement;
//var svg = (new XMLSerializer()).serializeToString(domElem);
var svg = XmlUtility.serializeNode(domElem);
return 'data:image/svg+xml;base64,' + btoa(svg);
}
else // canvas or webgl
{
var domElem = context.getRenderer().domElement;
return domElem? domElem.toDataURL(dataType, options): null;
}
}
});
/**
* Check if current environment supports Three.js drawing.
* @returns {Bool}
*/
Kekule.Render.ThreeRendererBridge.isSupported = function()
{
var result = (typeof(Kekule.$jsRoot.THREE) !== 'undefined');
if (result)
{
var F = Kekule.BrowserFeature;
result = F.webgl || F.canvas || F.svg;
}
return !!result;
//return Kekule.Render.ThreeRendererBridge.CheckSupporting().isSupported;
};
/*
* Check if current environment supports Three.js drawing.
* This function will returns more detailed information than {@link Kekule.Render.ThreeRendererBridge.isSupported}.
* @returns {Object} Info about supporting, including:
* {
* isSupported: Bool,
* message: If not supported, provides error message.
* }
*/
/*
Kekule.Render.ThreeRendererBridge.CheckSupporting = function()
{
var msg;
var isSupported = (typeof($jsRoot.THREE) !== 'undefined');
if (!isSupported) // Three.js lib not loaded
{
msg = Kekule.$L('ErrorMsg.LIB_THREE_JS_NOT_LOADED');
}
else // lib loaded, check if 3D context is supported
{
var F = Kekule.BrowserFeature;
isSupported = F.webgl || F.canvas || F.svg;
if (!supported)
msg = Kekule.$L('ErrorMsg.BROWSER_3D_DRAWING_NOT_SUPPORTED');
}
return {'isSupported': isSupported, 'message': msg};
};
*/
//Kekule.ClassUtils.makeSingleton(Kekule.Render.ThreeRendererBridge);
Kekule.Render.DrawBridge3DMananger.register(Kekule.Render.ThreeRendererBridge, 20);