app/scripts/views/mainPage/view3d.js (614 lines of code) (raw):
/**
* @type Ember.Mixin
* Mixin which allow to handle window.resize event (resize, schange scale)
*/
App.WindowResizeMixin = Ember.Mixin.create({
/**
* Attach resize event listener on window
*/
bindWindowResize: function () {
var that = this;
var onResizeHandler = function () {
that.onWindowResize();
};
this.set('onResizeHandler', onResizeHandler);
$(window).bind('resize', this.get('onResizeHandler'));
},
/**
* Detach resize event listener on window
*/
unbindWindowResize: function () {
$(window).unbind('resize', this.get('onResizeHandler'));
},
/**
* Resize event handler
*/
onWindowResize: function () {
}
});
/**
* Threejs geometry loade manager
*/
App.TheejsLoadManager = Ember.Object.extend({
loadedGeometries: {},
loadedGeometriesCount: 0,
expectedCount: 0,
urls: 0,
callback: null,
context: null,
/**
* Init method
* @param arrayOfUrl
* @param callback
* @param context
*/
init: function (arrayOfUrl, callback, context) {
this._chekArguments(arrayOfUrl, callback, context);
this.callback = callback;
this.context = context;
this.urls = arrayOfUrl;
this.expectedCount = arrayOfUrl.length;
},
/**
* Loads geometries for given arrayOfUrl, and call callback after all geometries loaded
*/
load: function () {
this.urls.forEach(function (url) {
var loader = new THREE.JSONLoader();
loader.load(url, function (geometry) {
this._onGeometryLoadComplete(url, geometry);
}.bind(this));
}, this);
},
/*
* Check if agruments are correct
* @param arrayOfUrl
* @param callback
* @param context
*/
_chekArguments: function (arrayOfUrl, callback, context) {
if (!Ember.isArray(arrayOfUrl)) {
throw '@arrayOfUrl should be an array';
}
if (typeof callback !== 'function') {
throw '@callback should be a function';
}
if (Ember.isNone(context)) {
throw '@context can\'t be empty';
}
},
/**
* Handler for THREE.JSONLoader.load event
* @params url
* @param geometry
*/
_onGeometryLoadComplete: function (url, geometry) {
this.loadedGeometriesCount++;
this.loadedGeometries[url] = geometry;
if (this.loadedGeometriesCount === this.expectedCount) {
this._onAllGeometiesLoaded();
}
},
/**
* This function will be executed after all data loaded
*/
_onAllGeometiesLoaded: function () {
var geometriesArray = [];
this.urls.forEach(function (url) {
geometriesArray.push(this.loadedGeometries[url]);
}, this);
this.callback.apply(this.context, [geometriesArray]);
}
});
/**
*
* @type Ember.View
* ThreeJs view to display network nodes in 3d view
*/
App.View3dView = Ember.View.extend(App.WindowResizeMixin, {
classNames: ['topology-3d'],
controller: Ember.Controller.create(),
nodes: [],
lastSelectedNode: {},
nodeLabelOffset: {x: 0, y: -12, z: 0},
nodeColor: 0x5555aa,
selectedNodeColor: 0x55cc55,
nodeArrowsColor: 0xffffff,
nodeLinkColor: 0xffffff,
connectedNodeColor: 0x99ffff,
lightColor: 0xdddddd,
nodeConnections: {},
cameraFov: 45,
nodeLookUpVectorMultiplier: 10,
nodeLines: {},
dragPlaneWidth: 6000,
dragPlaneHeight: 6000,
dragPlaneSelectedScale: 1 / 300,
cameraNear: 1,
cameraFar: 3000,
cameraControlsMinDistance: 10,
cameraControlsMaxDistance: 1000,
eventConfig: null,
fogEnabled: false,
/**
*
* @returns canvas container
*/
getContainer: function () {
return $(this.get('element'));
},
/**
* @returns canvas container width
*/
getContainerWidth: function () {
if (this.get('containerWidth') === undefined) {
this.set('containerWidth', this.getContainer().width());
}
return this.get('containerWidth');
},
/**
* @returns canvas container height
*/
getContainerHeigth: function () {
if (this.get('containerHeight') === undefined) {
this.set('containerHeight', this.getContainer().height());
}
return this.get('containerHeight');
},
/**
* @returns canvas container left position in pixels
*/
getContainerLeft: function () {
if (this.get('containerLeft') === undefined) {
this.set('containerLeft', this.getContainerPosiotion().left);
}
return this.get('containerLeft');
},
/**
* @returns canvas container top position in pixels
*/
getContainerTop: function () {
if (this.get('containerTop') === undefined) {
this.set('containerTop', this.getContainerPosiotion().top);
}
return this.get('containerTop');
},
/**
* Return container position
*/
getContainerPosiotion: function () {
return this.getContainer().offset();
},
/**
* Inits ThreeJs scene and mouse events
*/
didInsertElement: function () {
this._super();
this.bindWindowResize();
this.initTheeJsRenderer();
this.initTheeJsMouseEvents();
this.initThreeTrackBallControls();
this.animateScene();
this.loadGeometries(this.loadNodeData);
this.addNodeDataListener();
this.eventConfig = this.fillEventConfig();
},
addNodeDataListener: function () {
App.EventManager.view3dEventManager.on('widgetSelectedNode', this, this.widgetSelectedNodeListener);
},
removeNodeDataListener: function () {
App.EventManager.view3dEventManager.off('widgetSelectedNode', this, this.widgetSelectedNodeListener);
},
widgetSelectedNodeListener: function (name) {
var scene = this.get("scene");
var nodes = scene.children.filter(function (node) {
return node.name === name;
});
if (nodes.length) {
this.selectNode(nodes[0]);
App.EventManager.view3dEventManager.sendNodeData(nodes[0].name);
}
},
selectNode: function (node) {
if (node) {
var previousNode = this.get("lastSelectedNode");
if (!Ember.isNone(previousNode) && previousNode.objectType === this.getNodeType()) {
this.resetNodeStyling(previousNode);
}
this.applySelectedNodeStyles(node);
this.set("lastSelectedNode", node);
}
},
/**
* Reset node styling to unselected
*/
resetNodeStyling: function (node) {
this.applyNodeStyles(node, this.nodeColor, this.nodeColor);
},
/**
* Adds some styling for selected node
* @param node
*/
applySelectedNodeStyles: function (node) {
this.applyNodeStyles(node, this.selectedNodeColor, this.connectedNodeColor);
},
/**
* Applies node styles
* @param node
* @param nodeColor
* @param connectedNodeColor
*/
applyNodeStyles: function (node, nodeColor, connectedNodeColor) {
var scene = this.get("scene");
node.material.color.setHex(nodeColor);
var connectedNodes = this.getNodeConnections(node.name);
if (connectedNodes) {
connectedNodes.forEach(function (nodeName) {
var connectedNode = scene.getObjectByName(nodeName);
connectedNode.material.color.setHex(connectedNodeColor);
}, this);
}
},
/**
* Load geometries
*/
loadGeometries: function (callBack) {
var loadManager = new App.TheejsLoadManager(['/geometries/node.json', '/geometries/nodeArrows.json'], callBack, this);
loadManager.load();
},
/**
* Load nodes
*/
loadNodeData: function (geometriesArray) {
App.ApiProvider.getTopology(function (json) {
this.addThreeJsScene.apply(this, [json, geometriesArray[0], geometriesArray[1]]);
}.bind(this));
},
/**
* window resize handler, sets calculated width, height, top and left to undefined
* so the values will be updated to actual
*/
onWindowResize: function () {
this.set('containerWidth', undefined);
this.set('containerHeight', undefined);
this.set('containerLeft', undefined);
this.set('containerTop', undefined);
var width = this.getContainerWidth();
var height = this.getContainerHeigth();
var renderer = this.get('renderer');
renderer.setSize(width, height);
var camera = this.get('camera');
camera.aspect = width / height;
camera.updateProjectionMatrix();
},
initThreeTrackBallControls: function () {
var controls = new THREE.TrackballControls(this.get('camera'), this.getContainer()[0]);
controls.minDistance = this.cameraControlsMinDistance;
controls.maxDistance = this.cameraControlsMaxDistance;
this.set('cameraControls', controls);
},
/**
* Inits ThreeJs renderer
*/
initTheeJsRenderer: function () {
var container = this.getContainer();
var width = this.getContainerWidth();
var height = this.getContainerHeigth();
var renderer = new THREE.WebGLRenderer({alpha: true, antialias: true});
//renderer = new THREE.CanvasRenderer();
renderer.setClearColor(0x000000, 0); //- default value
renderer.setSize(width, height);
this.set('renderer', renderer);
var camera = new THREE.PerspectiveCamera(this.cameraFov, width / height, this.cameraNear, this.cameraFar);
this.set('camera', camera);
var scene = new THREE.Scene();
if (this.fogEnabled === true) {
scene.fog = new THREE.FogExp2(0x242424, 0.0018);
}
this.set('scene', scene);
container.append($(renderer.domElement));
},
/**
* Inits ThreeJs scene
* @param {array} data node data array
*/
addThreeJsScene: function (json, nodeGeometry, nodeArrowsGeometry) {
if (!Ember.isNone(json.nodes)) {
var camera = this.get('camera');
var scene = this.get('scene');
this.clearScene();
if (nodeGeometry === undefined) {
nodeGeometry = new THREE.CubeGeometry(25, 25, 25);
}
var nodes = [];
this.set('nodes', json.nodes);
var firstNode = null;
for (var nodeId in json.nodes) {
var nodeData = json.nodes[nodeId];
var node = this.createNodeObject(nodeGeometry, nodeArrowsGeometry, nodeData);
scene.add(node);
if (firstNode === null) {
firstNode = node;
}
}
this.addSceneLigths(scene);
camera.position.z = 450;
this.initConnections(json.connections);
this.drawLines(json.connections);
var plane = new THREE.Mesh(
new THREE.PlaneBufferGeometry(this.dragPlaneWidth, this.dragPlaneHeight, 8, 8),
new THREE.MeshBasicMaterial({color: 0x000000, opacity: 0.25, transparent: true})
);
plane.visible = false;
plane.scale.x = this.dragPlaneSelectedScale;
plane.scale.y = this.dragPlaneSelectedScale;
plane.scale.z = this.dragPlaneSelectedScale;
plane.position.copy(firstNode.position);
scene.add(plane);
this.set('nodeMoveHelperPlane', plane);
}
},
/**
* Init nodeConnections object
* @param connections
*/
initConnections: function (connections) {
this.nodeConnections = {};
connections.forEach(function (node) {
var nodeId = this.generateNodeName(node.edge.headNodeConnector.node);
if (this.nodeConnections[nodeId] === undefined) {
this.nodeConnections[nodeId] = [];
}
this.nodeConnections[nodeId].push(this.generateNodeName(node.edge.tailNodeConnector.node));
}, this);
},
/**
* Return nodes connected to given node
* @param node
* @return array of connected nodes
*/
getNodeConnections: function (nodeId) {
return this.nodeConnections[nodeId];
},
/**
* Adds lights to scene
* @param scene
*/
addSceneLigths: function (scene) {
var intensity = 0.58;
var directionalLight = new THREE.DirectionalLight(this.lightColor, intensity);
directionalLight.position.set(0, 0, 350); // x y z
scene.add(directionalLight);
directionalLight = new THREE.DirectionalLight(this.lightColor, intensity);
directionalLight.position.set(0, 0, -350);
scene.add(directionalLight);
directionalLight = new THREE.DirectionalLight(this.lightColor, intensity);
directionalLight.position.set(0, 100, 0);
scene.add(directionalLight);
directionalLight = new THREE.DirectionalLight(this.lightColor, intensity);
directionalLight.position.set(0, -100, 0);
scene.add(directionalLight);
directionalLight = new THREE.DirectionalLight(this.lightColor, intensity);
directionalLight.position.set(250, 0, 0);
scene.add(directionalLight);
directionalLight = new THREE.DirectionalLight(this.lightColor, intensity);
directionalLight.position.set(-250, 0, 0);
scene.add(directionalLight);
},
/**
* Creates node object
* @param geometry
* @param nodeData
*/
createNodeObject: function (nodeGeometry, nodeArrowsGeometry, nodeData) {
var node = new THREE.Mesh(nodeGeometry, new THREE.MeshPhongMaterial({color: this.nodeColor}));
node.position.x = nodeData.coords.x;
node.position.y = nodeData.coords.y;
node.position.z = nodeData.coords.z;
node.type = nodeData.type;
node.name = this.generateNodeName(nodeData);
node.objectType = this.getNodeType();
node.nodeId = nodeData.id;
node.add(this.createNodeLabel(nodeData));
node.add(this.createNodeArrows(nodeArrowsGeometry));
return node;
},
/**
* Generates node id
* @param node
* @return id
*/
generateNodeName: function (nodeData) {
return nodeData.type + " | " + nodeData.id;
},
createNodeArrows: function (nodeArrowsGeometry) {
if (nodeArrowsGeometry === undefined) {
return null;
}
var nodeArrow = new THREE.Mesh(nodeArrowsGeometry, new THREE.MeshLambertMaterial({color: this.nodeArrowsColor}));
nodeArrow.position.x = 0;
nodeArrow.position.y = 0;
nodeArrow.position.z = 0;
return nodeArrow;
},
/**
* Create node label
* @param nodeData
*/
createNodeLabel: function (nodeData) {
var textShapes = THREE.FontUtils.generateShapes(nodeData.type + '|' + nodeData.id, {
font: "helvetiker",
size: 5
});
var textGeometry = new THREE.ShapeGeometry(textShapes);
var textMaterial = new THREE.MeshBasicMaterial();
var textMesh = new THREE.Mesh(textGeometry, textMaterial);
textGeometry.computeBoundingBox();
var maxX = textGeometry.boundingBox.max.x;
textMesh.position.x = this.nodeLabelOffset.x - maxX / 2;
textMesh.position.y = this.nodeLabelOffset.y;
textMesh.position.z = this.nodeLabelOffset.z;
return textMesh;
},
getNodeType: function () {
return 'node';
},
/**
* Draw connection between nodes
* @param {array} data
*/
drawLines: function (data) {
var scene = this.get('scene');
var nodes = this.get('nodes');
data.forEach(function (value) {
var fromNode = value.edge.tailNodeConnector.node.id;
var nodeTo = value.edge.headNodeConnector.node.id;
var line = this.createLine(nodes[fromNode], nodes[nodeTo]);
this.addNodeLineData(value.edge.tailNodeConnector.node, line.geometry.vertices[0], line.geometry);
this.addNodeLineData(value.edge.headNodeConnector.node, line.geometry.vertices[1], line.geometry);
scene.add(line);
}.bind(this));
},
addNodeLineData: function (node, lineVertex, geometry) {
var nodeName = this.generateNodeName(node);
if (!Ember.isArray(this.nodeLines[nodeName])) {
this.nodeLines[nodeName] = [];
}
this.nodeLines[nodeName].push({vertex: lineVertex, geometry: geometry});
},
/**
* Creates a line from vectorFrom to vectorTo
* @param {type} vectorFrom
* @param {type} vectorTo
* @returns {THREE.Line} line
*/
createLine: function (nodeFrom, nodeTo) {
var vectorFrom = new THREE.Vector3(nodeFrom.coords.x, nodeFrom.coords.y, nodeFrom.coords.z);
var vectorTo = new THREE.Vector3(nodeTo.coords.x, nodeTo.coords.y, nodeTo.coords.z);
var geometry = new THREE.Geometry();
geometry.vertices.push(vectorFrom);
geometry.vertices.push(vectorTo);
var line = new THREE.Line(geometry, new THREE.LineBasicMaterial({color: this.nodeLinkColor}));
return line;
},
/**
* Removes all objects from the scene
*/
clearScene: function () {
var scene = this.get('scene');
if (Ember.isArray(scene.children)) {
$.each(Ember.copy(scene.children), function (index, object) {
scene.remove(object);
});
}
},
/**
* Start animate
*/
animateScene: function () {
var renderer = this.get('renderer');
var camera = this.get('camera');
var scene = this.get('scene');
var render = function () {
requestAnimationFrame(render);
var controls = this.get('cameraControls');
if (controls !== undefined) {
controls.update();
}
for (var i = 0, l = scene.children.length; i < l; i++) {
if ((scene.children[i] instanceof THREE.Mesh && scene.children[i].objectType === this.getNodeType())) {
scene.children[i].up = camera.up;
var lookAtVector = camera.position.clone();
lookAtVector.x = lookAtVector.x * this.nodeLookUpVectorMultiplier;
lookAtVector.y = lookAtVector.y * this.nodeLookUpVectorMultiplier;
lookAtVector.z = lookAtVector.z * this.nodeLookUpVectorMultiplier;
scene.children[i].lookAt(lookAtVector);
}
}
renderer.render(scene, camera);
}.bind(this);
render();
},
/**
* Inits mouse over and mouse leave events for scene objects
*/
initTheeJsMouseEvents: function () {
var config = this.fillEventConfig();
this.addMouseMoveListeners(config);
this.addMouseDownListeners(config);
},
removeThreeJsMouseEvents: function () {
var domElement = this.get('renderer').domElement;
$(document).unbind('mousemove', $.proxy(this.onDocumentMouseMove, this));
$(domElement).unbind('mousedown', $.proxy(this.onDocumentMouseDown, this));
$(domElement).unbind('mouseup', $.proxy(this.onDocumentMouseUp, this));
},
addMouseMoveListeners: function (config) {
this.set('selectedNode', null);
$(document).bind('mousemove', $.proxy(this.onDocumentMouseMove, this));
},
addMouseDownListeners: function (config) {
var domElement = this.get('renderer').domElement;
$(domElement).bind('mousedown', $.proxy(this.onDocumentMouseDown, this));
$(domElement).bind('mouseup', $.proxy(this.onDocumentMouseUp, this));
},
onDocumentMouseMove: function (event) {
var config = this.eventConfig;
var offset = this.get('offset');
var camera = this.get('camera');
var plane = this.get('nodeMoveHelperPlane');
var vector = this.getMouseVector(event, config).unproject(camera);
var raycaster = new THREE.Raycaster(camera.position, vector.sub(camera.position).normalize());
var dragNode = this.get('dragNode');
if (dragNode !== null && dragNode !== undefined) {
var intersectsWithPlane = raycaster.intersectObject(plane);
dragNode.position.copy(intersectsWithPlane[0].point.sub(offset));
var lineData = this.nodeLines[dragNode.name];
if (Ember.isArray(lineData)) {
lineData.forEach(function (lineData) {
lineData.vertex.copy(dragNode.position);
lineData.geometry.verticesNeedUpdate = true;
});
}
return;
}
var current = this.get('selectedNode');
var intersects = this.calculateIntersects(event, config);
//TODO: fix selected objects (use array to store selected objects)
if (intersects.length > 0) {
if (current !== intersects[0].object) {
current = intersects[0].object;
this.mouseOverThreeJsSceneObject(current);
//d&d
if (current.objectType === this.getNodeType()) {
plane.position.copy(current.position);
var lookAtVector = camera.position.clone();
lookAtVector.x = lookAtVector.x * 10;
lookAtVector.y = lookAtVector.y * 10;
lookAtVector.z = lookAtVector.z * 10;
plane.lookAt(lookAtVector);
}
}
} else if (current !== null) {
this.mouseLeaveThreeJsSceneObject(current);
current = null;
}
this.set('selectedNode', current);
this.get('renderer').render(this.get('scene'), this.get('camera'));
},
onDocumentMouseUp: function (event) {
event.preventDefault();
var config = this.eventConfig;
this.get('cameraControls').enabled = true;
var dragNode = this.get('dragNode');
if (dragNode !== undefined && dragNode !== null) {
var plane = this.get('nodeMoveHelperPlane');
plane.position.copy(dragNode.position);
plane.scale.x = this.dragPlaneSelectedScale;
plane.scale.y = this.dragPlaneSelectedScale;
plane.scale.z = this.dragPlaneSelectedScale;
App.ApiProvider.storeNodeCoords(dragNode.nodeId, {
x: dragNode.position.x,
y: dragNode.position.y,
z: dragNode.position.z
});
this.set('dragNode', null);
}
},
onDocumentMouseDown: function (event) {
var config = this.eventConfig;
var intersects = this.calculateIntersects(event, config);
var sceneObject = null;
if (intersects.length) {
sceneObject = intersects[0].object;
if (sceneObject.objectType === this.getNodeType()) {
this.onNodeSelect(sceneObject);
}
}
//d&d
this.get('cameraControls').enabled = true;
var offset = new THREE.Vector3(0, 0, 0);
var camera = this.get('camera');
var plane = this.get('nodeMoveHelperPlane');
var vector = this.getMouseVector(event, config).unproject(camera);
var raycaster = new THREE.Raycaster(camera.position, vector.sub(camera.position).normalize());
if (sceneObject !== undefined && sceneObject !== null && sceneObject.objectType === this.getNodeType()) {
var intersectsWithPlane = raycaster.intersectObject(plane);
if (intersectsWithPlane && intersectsWithPlane[0]) {
plane.scale.x = 1;
plane.scale.y = 1;
plane.scale.z = 1;
this.set('dragNode', sceneObject);
this.get('cameraControls').enabled = false;
offset.copy(intersectsWithPlane[0].point).sub(plane.position);
this.set('offset', offset);
}
}
},
onNodeSelect: function (node) {
this.selectNode(node);
App.EventManager.view3dEventManager.sendNodeData(node.name);
},
calculateIntersects: function (event, config) {
var camera = this.get('camera'), scene = this.get('scene'), renderer = this.get('renderer');
var vector = this.getMouseVector(event, config);
vector.unproject(camera);
config.raycaster.set(camera.position, vector.sub(camera.position).normalize());
return config.raycaster.intersectObjects(scene.children);
},
getMouseVector: function (event, config) {
var camera = this.get('camera'), scene = this.get('scene'), renderer = this.get('renderer');
config.mouse.x = ( ( event.clientX - this.getContainerLeft() + $(window).scrollLeft() ) /
this.getContainerWidth()) * 2 - 1;
config.mouse.y = -( ( event.clientY - this.getContainerTop() + $(window).scrollTop() ) /
this.getContainerHeigth()) * 2 + 1;
var vector = new THREE.Vector3(config.mouse.x, config.mouse.y, 0.5);
return vector;
},
fillEventConfig: function (config_) {
var config = config_ || {};
config.projector = new THREE.Projector();
config.raycaster = new THREE.Raycaster();
config.mouse = new THREE.Vector2();
return config;
},
/**
* Mouse over scene oject handler
* @param {THREE.object} sceneObject event target
*/
mouseOverThreeJsSceneObject: function (sceneObject) {
//sceneObject.material.color.setRGB(1, 0,0);
},
/**
* Mouse leave scene oject handler
* @param {THREE.object} sceneObject event target
*/
mouseLeaveThreeJsSceneObject: function (sceneObject) {
// sceneObject.material.color.setRGB(0, 1,0);
},
/**
* Removed attached handlers
*/
willDestroyElement: function () {
this.unbindWindowResize();
this.removeNodeDataListener();
this.removeThreeJsMouseEvents();
}
});