'use strict';
/**
* @classdesc Class representing a basic robot's representation.
* @author Loris Tissino (http://loris.tissino.it)
* @release 0.71
* @license MIT
* @constructor
*/
var RobotRepresentation = function () {}
/**
* Sets the id of the robot and prepares the containers it needs.
* @param {string} id - The id of the robot
* @return {RobotRepresentation} - The robot
*/
RobotRepresentation.prototype.setId = function setId ( id ) {
this.id = id;
this.isBuilt = false;
this.components = []; // we keep track of components with constraints here, because we need to move the separetely
this.data = {}; // the data sent and received when updating with HTTP posts
this.dataPropertiesIn = [];
this.registeredProcessFunctions = [];
this.debugging = false;
return this;
}
/**
* Sets the initialValues of the robot.
* @param {Object} values - The values
* @return {RobotRepresentation} - The robot
*/
RobotRepresentation.prototype.setInitialValues = function setInitialValues ( values ) {
this.initialValues = values;
return this;
}
/**
* Sets the robot's manager.
* @param {RobotsManager} robotsManager - The Robots' Manager to set
* @return {RobotRepresentation} - The robot
*/
RobotRepresentation.prototype.setRobotManager = function setRobotsManager ( robotsManager ) {
this.robotsManager = robotsManager;
this.scene = this.robotsManager.simulator.scene; // a shortcut reference
return this;
}
/**
* Sets the url for the simulated remote control or similar device.
* @param {string} url - The URL
* @return {RobotRepresentation} - The robot
*/
RobotRepresentation.prototype.setControllerUrl = function setControllerUrl ( url ) {
this.controllerUrl = url;
return this;
}
/**
* Returns true if the robot has been assigned a remote control.
* @return {boolean} - Whether the robot has been assigned a remote control
*/
RobotRepresentation.prototype.hasController = function hasController () {
return typeof this.controllerUrl !== 'undefined';
}
/**
* Returns true if the robot has available a (virtual) camera.
* @return {boolean} - Whether the robot has been assigned a remote control
*/
RobotRepresentation.prototype.hasCamera = function hasCamera () {
return typeof this.camera !== 'undefined';
}
/**
* Writes information about the robot on the console.
* @return {RobotRepresentation} - The robot
*/
RobotRepresentation.prototype.show = function show () {
console.log ( "I am a robot, id: " + this.id );
return this;
}
/**
* Returns a Physijs Material based on a THREE.MeshLambertMaterial.
* @return {object} - The material created
*/
RobotRepresentation.prototype.getLambertPjsMaterial = function getMaterial ( options ) {
var values = $.extend ({
color: 0xC1F5F6,
opacity: 0.4,
transparent: true,
friction: .5,
restitution: .5
}, options );
return Physijs.createMaterial(
new THREE.MeshLambertMaterial( {color: values.color, opacity: values.opacity, transparent: values.transparent} ),
values.friction,
values.restitution
);
}
/**
* Returns a Physijs CylinderMesh representing a wheel.
* @param {Object} options - The options
* @return {Physijs.CylinderMesh} - The wheel created
*/
RobotRepresentation.prototype.createWheel = function createWheel ( options ) {
// position, radius, thickness, mass
var values = $.extend ({
color: 0x000000,
position: new THREE.Vector3 ( 0, 0, 0) ,
radius: 10,
thickness: 2,
transparent: false,
mass: 32,
friction: .99, // high friction
restitution: .01 // low restition
}, options );
var wheel_material = this.getLambertPjsMaterial( { color: values.color, transparent: values.transparent, friction: values.friction, restitution: values.restitution } );
var wheel_geometry = new THREE.CylinderGeometry( values.radius, values.radius, values.thickness, 24 /* number of "sides" */ );
var wheel = new Physijs.CylinderMesh(
wheel_geometry,
wheel_material,
values.mass
);
wheel.rotation.x = Math.PI / 2;
wheel.castShadow = true;
wheel.receiveShadow = true;
wheel.position.copy( values.position );
return wheel;
}
/**
* Updates the speed of a wheel.
* @param {Object} wheel - The wheel to update
* @param {float} speed - The speed to set
* @return {RobotRepresentation} - The robot
*/
RobotRepresentation.prototype.updateWheelSpeed = function updateWheelSpeed ( wheel, speed ) {
if ( typeof speed !== 'undefined' ) {
this[wheel + 'WheelConstraint'].configureAngularMotor( 2, 0.1, 0, 5*speed, 15000 );
}
return this;
}
/**
* Returns a Physijs DOF Constraint.
* @param {Object} mainObject - First object to be constrained
* @param {Object} constrainedObject - Second object to be constrained
* @param {THREE.Vector3} position - Point in the scene to apply the constraint
* @return {Physijs.DOFConstraint} - The constraint created
*/
RobotRepresentation.prototype.createDOFConstraint = function createDOFConstraint ( mainObject, constrainedObject, position ) {
return new Physijs.DOFConstraint( mainObject, constrainedObject, position );
}
/**
* Returns a Physijs Hinge Constraint.
* @param {Object} mainObject - First object to be constrained
* @param {Object} constrainedObject - Second object to be constrained
* @param {THREE.Vector3} position - Point in the scene to apply the constraint
* @param {THREE.Vector3} axis - Axis along which the hinge lies
* @return {Physijs.DOFConstraint} - The constraint created
*/
RobotRepresentation.prototype.createHingeConstraint = function createHingeConstraint ( mainObject, constrainedObject, position, axis ) {
return new Physijs.HingeConstraint(
mainObject,
constrainedObject,
position,
axis
);
}
/**
* Builds the robot's representation.
* @abstract
* @returns {boolean} - Whether the robot's representation could be built
*/
RobotRepresentation.prototype.build = function build () {
throw "Error: this should be implemented in the actual robot class";
}
/**
* Updates the data to/from the robot's behavior.
* @abstract
* @param {Object} data - The data received/transmitted
*/
RobotRepresentation.prototype.update = function update ( data ) {
throw "Error: this should be implemented in the actual robot class";
}
/**
* Manages the fact that there has been a communication failure.
* @abstract
*/
RobotRepresentation.prototype.manageCommunicationFailure = function manageCommunicationFailure () {
throw "Error: this should be implemented in the actual robot class";
}
/**
* Moves the robot to a specific place.
* @param {THREE.Vector3} vector - The vector with the coordinates to move the robot to
* @param {boolean} relative - Whether the coordinates are to be considered relative or absolute
* @return {RobotRepresentation} - The robot
*/
RobotRepresentation.prototype.move = function move ( vector, relative ) {
// vector is a THREE.Vector3 object
// relative is a boolean: if true, the vector is considered a change from current position, else a final destination
console.log ('moving to: ');
console.log ( vector );
var offset;
if ( typeof relative === 'undefined' ) {
relative = false;
}
offset = relative ? vector : vector.sub ( this.chassis.position );
this.chassis.position.add ( offset );
this.chassis.__dirtyPosition = true;
this.chassis.__dirtyRotation = true;
$.each ( this.components, function ( index, component ) {
component.position.add( offset );
component.__dirtyPosition = true;
component.__dirtyRotation = true;
});
return this;
}
/**
* Rotates the robot on an axis.
* @param {THREE.Vector3} axis - The axis along which the robot should be rotated
* @param {float} angle - The angle of rotation, in radians
* @return {RobotRepresentation} - The robot
*/
RobotRepresentation.prototype.rotateOnAxis = function rotateOnAxis ( axis, angle ) {
this.chassis.rotateOnAxis ( axis, angle );
this.chassis.__dirtyPosition = true;
this.chassis.__dirtyRotation = true;
return this;
}
/**
* Returns the angle on a plain between two points.
* @param {THREE.Vector3} pos1 - The first point
* @param {THREE.Vector3} pos2 - The second point
* @param {array} axes - An array of two chars, each representing an axis to be considered for the definition of the plane
* @return {float} - The angle in radians
*/
RobotRepresentation.prototype.getAngle = function getAngle ( pos1, pos2, axes ) {
return Math.PI + Math.atan2 ( pos1[axes[0]] - pos2[axes[0]], pos1[axes[1]] - pos2[axes[1]] );
}
/**
* Returns the absolute position of an object.
* @param {Object} obj - The object
* @param {boolean} forceUpdate - Whether the cached value should be used
* @return {THREE.Vector3} - The position
*/
RobotRepresentation.prototype.getAbsolutePositionForObject = function getAbsolutePositionForObject( obj, forceUpdate ) {
if ( typeof obj.userData.absolutePosition === 'undefined' ) {
obj.userData.absolutePosition = new THREE.Vector3();
}
else {
if ( forceUpdate === false ) {
return obj.userData.absolutePosition;
}
}
obj.userData.absolutePosition.setFromMatrixPosition( obj.matrixWorld );
return obj.userData.absolutePosition;
}
/**
* Returns the coordinates of an object mapped on the pixels of the bottom image.
* @param {Object} obj - The object
* @param {boolean} forceUpdate - Whether the cached value should be used
* @return {THREE.Vector2} - The coordinates
*/
RobotRepresentation.prototype.getBottomImagePixelCoordinatesForObject = function getBottomImagePixelCoordinatesForObject( obj, forceUpdate ) {
var coords = this.getAbsolutePositionForObject( obj, forceUpdate );
return new THREE.Vector2 (
Math.round( THREE.Math.mapLinear (
coords.x,
- this.robotsManager.simulator.bottom.width / 2,
this.robotsManager.simulator.bottom.width / 2,
0,
this.robotsManager.simulator.bottom.canvas.width
)),
Math.round( THREE.Math.mapLinear (
coords.z,
- this.robotsManager.simulator.bottom.height / 2,
this.robotsManager.simulator.bottom.height / 2,
0,
this.robotsManager.simulator.bottom.canvas.height
))
);
}
/**
* Represents a Robots' Manager (seen from the client side).
* @author Loris Tissino (http://loris.tissino.it)
* @release 0.71
* @license MIT
* @constructor
* @param {Object} values
* @param {Simulator} simulator - The simulator that uses this robots' manager
*/
var RobotsManager = function ( values, simulator ) {
console.log(values);
this.values = values;
this.url = values.url;
this.simulator = simulator;
this.robots = [];
this.data = {};
};
/**
* Adds a robot to the list of managed robots, after having loaded it's representation source file.
* @param {Object} robot - An object containing the information about the robot to add
* @return {RobotsManager} - The robots' manager
*/
RobotsManager.prototype.addRobot = function ( robot ) {
console.log ( 'adding robot ' + robot.id );
console.log ( robot.class );
var rm = this; // a reference
var url = 'representations/' + robot.class + '.js';
console.log ( 'Loading script file...' );
// TODO avoid multiple loading of the same file
$.getScript( url ) // in a future version we might call the robotsManager via http for this
.done(function( script, textStatus ) {
console.log( textStatus );
console.log( " Loaded class " + url);
var addedRobot = new window[robot.class];
addedRobot
.setId ( robot.id )
.setInitialValues ( robot.initialValues )
.setRobotManager ( rm )
.setControllerUrl ( robot.controllerUrl )
;
//addedRobot.show();
rm.robots.push ( addedRobot );
return rm;
})
.fail(function( jqxhr, settings, exception ) {
console.log ( "Error loading file: " + url );
return rm;
});
}
/**
* Adds all the robots (from the initial setup) to the list of managed robots.
* @return {RobotsManager} - The robots' manager
*/
RobotsManager.prototype.addRobots = function () {
console.log("robots to add:");
console.log(this.values.robots);
var rm = this; // a reference
$.each ( rm.values.robots, function ( index, robot ) {
rm.addRobot( robot );
});
return this;
}
/**
* Makes a POST to the Robots' Manager (server side) to update the data.
* @return {RobotsManager} - The robots' manager
*/
RobotsManager.prototype.update = function () {
var robots = this.robots; // a reference
var rm = this; // a reference
var posting = $.post( this.url, JSON.stringify( this.data ), function() {
// the connection started, we don't need to do anything...
//console.log("contacting url " + this.url);
})
.done(function(data) {
// the connection is done;
//console.log (data);
$.each( robots, function ( index, robot ) {
rm.data[robot.id] = robot.update( data[robot.id] );
});
})
.fail(function() {
//console.log(robots);
$.each( robots, function ( index, robot ) {
rm.data[robot.id] = robot.manageCommunicationFailure();
});
})
.always(function() {
//alert( "finished" );
});
return this;
}
/**
* Represents a Simulator.
* @constructor
* @author Loris Tissino (http://loris.tissino.it)
* @release 0.71
* @license MIT
* @param {Object} defaults - The default values to be used during setup
*/
var Simulator = function ( defaults ) {
this.release = '0.70';
this.defaults = defaults;
this.robotsManagers = {};
this.loader = new THREE.TextureLoader();
/**
* Initializes the main renderer.
* @param {Object} options - The options to be used during initialization
* @return {Simulator} The Simulator
*/
this.initRenderer = function initRenderer ( options ) {
var values = $.extend ( {}, this.defaults.renderer, options );
this.renderer = new THREE.WebGLRenderer( {antialias: values.antialias, preserveDrawingBuffer: true, alpha: true } );
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.renderer.setClearColor( values.backgroundColor, 1 );
this.renderer.shadowMap.enabled = values.shadows;
$('#viewport').append(this.renderer.domElement);
return this;
};
/**
* Initializes the alternate renderer.
* @return {Simulator} The Simulator
*/
this.initAltRenderer = function initAltRenderer () {
this.altRenderer = new THREE.WebGLRenderer( {antialias: false, preserveDrawingBuffer: true, alpha: false } );
this.altRenderer.setSize(160, Math.round( 160 * this.renderer.getSize().height / this.renderer.getSize().width ));
this.altRenderer.setClearColor( this.renderer.getClearColor(), 1);
this.altRenderer.shadowMap.enabled = this.renderer.shadowMapEnabled;
this.altRenderer.domElement.style.position = 'absolute';
this.altRenderer.domElement.style.top = '60px';
this.altRenderer.domElement.style.left = '1px';
this.altRenderer.domElement.style.zIndex = 100;
this.altRenderer.domElement.style['border-style'] = 'double';
this.altRenderer.domElement.style['border-color'] = '#000000';
this.altRenderer.domElement.style.visibility = 'hidden';
$('#viewport').append(this.altRenderer.domElement);
return this;
};
/**
* Initializes the Stats information block.
* @return {Simulator} The Simulator
*/
this.initRenderStats = function initRenderStats () {
this.renderStats = new Stats();
this.renderStats.setMode( this.defaults.stats.mode );
this.renderStats.domElement.style.position = 'absolute';
this.renderStats.domElement.style.top = '1px';
this.renderStats.domElement.style.left = '1px';
this.renderStats.domElement.style.zIndex = 100;
$('#viewport').append(this.renderStats.domElement);
return this;
};
/**
* Adds the InfoBox.
* @return {Simulator} The Simulator
*/
this.addInfoBox = function addInfoBox() {
this.infoBox = $( '<div />' );
this.infoBox
.attr('id', 'infobox')
.html('<a title="RoboThree Project Website" target="_blank" href="https://github.com/loristissino/RoboThree/">RoboThree ' + this.release + '</a>')
.css('top', (window.innerHeight - 20 ) +'px')
;
$('#viewport').append( this.infoBox );
this.debugBox = $('<span />');
this.infoBox.append ( this.debugBox );
this.debugBoxTexts = {};
return this;
}
this.pushDebugText = function addDebugText ( obj ) {
$.extend( this.debugBoxTexts, obj );
}
this.renderDebugText = function renderDebugText () {
if ( this.gui.userData.controls.showDebugText ) {
if ( Object.keys( this.debugBoxTexts ).length > 0 ) {
this.debugBox.text( [ '', JSON.stringify( this.debugBoxTexts ) ].join( ' - ' ) );
}
}
else {
this.debugBox.text( '' );
}
this.debugBoxTexts = {};
}
/**
* Loops on the robots' managers, calling the update() method for each of them.
*/
this.onUpdate = function onUpdate() {
$.each ( this.robotsManagers, function ( id, robotManager ) {
robotManager.update();
});
/*
if ( this.hasOwnProperty('checkedSphere') ) {
//if (this.checkedSphere.position.y <10)
// console.log((this.checkedSphere.position.y-6) + ',' + this.checkedSphere.userData.clock.getElapsedTime());
if (this.checkedSphere.userData.timeq0 === false && (this.checkedSphere.position.y-6 < 1000)) {
this.checkedSphere.userData.timeq0 = this.checkedSphere.userData.clock.getElapsedTime();
this.checkedSphere.userData.posq0 = this.checkedSphere.position.y-6;
console.log("setq0");
}
if (this.checkedSphere.userData.timeq1 === false && (this.checkedSphere.position.y-6 <= 0)) {
this.checkedSphere.userData.timeq1 = this.checkedSphere.userData.clock.getElapsedTime();
this.checkedSphere.userData.posq1 = this.checkedSphere.position.y-6;
console.log("setq1");
console.log([
this.scene.userData.gravity.y,
this.checkedSphere.userData.posq1 - this.checkedSphere.userData.posq0,
this.checkedSphere.userData.timeq1 - this.checkedSphere.userData.timeq0
].join(', '));
}
}*/
}
/**
* Initializes the scene.
* @return {Simulator} The Simulator
*/
this.initScene = function initScene ( options ) {
var values = $.extend ( {}, this.defaults.scene, options );
this.scene = new Physijs.Scene( {reportSize: 10, fixedTimeStep: 1 / 60} );
this.scene.userData = { simulator: this };
this.scene.userData.gravity = values.gravity; // we keep a reference in order to be able to use it later...
this.scene.setGravity(this.scene.userData.gravity);
this.scene.addEventListener( 'update', this.onUpdate.bind( this ) );
return this;
};
/**
* Adds the main camera.
* @param {Object} options - The options to be used during initialization
* @return {Simulator} The Simulator
*/
this.addMainCamera = function addMainCamera ( options ) {
var values = $.extend ( {}, this.defaults.mainCamera, options );
this.mainCamera = new THREE.PerspectiveCamera ( values.fov, values.aspect, values.near, values.far );
this.mainCamera.position.copy ( values.position );
this.mainCamera.lookAt( values.lookAt );
this.mainCamera.name = "main";
this.scene.add( this.mainCamera );
this.availableCameras = {};
this.availableCameras[this.mainCamera.uuid] = this.mainCamera;
this.usedCamera = this.mainCamera; // we keep a reference to the camera that is actually used for each frame
return this;
};
/**
* Adds the main light.
* @param {Object} options - The options to be used during initialization
* @return {Simulator} The Simulator
*/
this.addLight = function addLight ( options ) {
var values = $.extend ( {}, this.defaults.light, options );
this.light = new THREE.SpotLight( values.color, values.intensity );
this.light.position.copy( values.position );
this.light.castShadow = true;
//this.light.shadowMapDebug = true;
this.light.shadowCameraNear = values.near;
this.light.shadowCameraFar = values.far;
this.scene.add( this.light );
return this;
};
/**
* Adds the axis helper.
* @param {Object} options - The options to be used during initialization
* @return {Simulator} The Simulator
*/
this.addAxisHelper = function addAxisHelper ( options ) {
var values = $.extend ( {}, this.defaults.axisHelper, options );
this.axisHelper = new THREE.AxisHelper( values.length );
this.scene.add ( this.axisHelper );
this.axisHelper.visible = values.visible;
return this;
};
/**
* Adds the graphical user interface.
* @return {Simulator} The Simulator
*/
this.addGUI = function addGUI () {
this.gui = guiFactory ( this );
return this;
};
/**
* Adds the ground.
* @param {Object} options - The options to be used during initialization
* @return {Simulator} The Simulator
*/
this.addGround = function addGround ( options ) {
var values = $.extend ( {}, this.defaults.ground, options );
function createPhysijsMaterial ( material ) {
return Physijs.createMaterial (
material,
values.friction,
values.restitution
);
}
var scene = this.scene; // a reference
var simulator = this; // a reference
this.bottom = {};
// Material
this.loader.load(
values.texture,
// Function called when the resource is loaded
function ( texture ) {
simulator.bottom.canvasElement = $('<canvas />');
simulator.bottom.canvasElement.attr('id', 'mycanvas');
simulator.bottom.canvas = simulator.bottom.canvasElement[0];
simulator.bottom.canvas.width = texture.image.width;
simulator.bottom.canvas.height = texture.image.height;
simulator.bottom.canvas.getContext( '2d' ).drawImage( texture.image, 0, 0, texture.image.width, texture.image.height );
var mesh, geometry;
simulator.bottom.canvasMap = new THREE.Texture( simulator.bottom.canvas );
simulator.bottom.canvasMap.needsUpdate = true;
var texturedMaterial = new THREE.MeshBasicMaterial( { map: simulator.bottom.canvasMap, overdraw: 0.5 } );
var texturedPjsMaterial = createPhysijsMaterial( texturedMaterial );
var coloredMaterial = new THREE.MeshLambertMaterial ( {
color: 0xffffff,
opacity: 1,
transparent: false,
});
var coloredPjsMaterial = createPhysijsMaterial( coloredMaterial );
$.each( values.pieces, function ( name, piece ) {
geometry = new THREE.BoxGeometry( piece.sizeX, piece.sizeY, piece.sizeZ );
if ( typeof piece.color === 'undefined' ) {
mesh = new Physijs.BoxMesh( geometry, texturedPjsMaterial, 0 );
}
else {
mesh = new Physijs.BoxMesh( geometry, coloredPjsMaterial, 0 );
mesh.material = mesh.material.clone();
mesh.material.color.setHex( piece.color );
if ( typeof piece.opacity !== 'undefined' ) {
mesh.material.opacity = piece.opacity;
}
};
if ( name == 'bottom' ) {
simulator.bottom.width = piece.sizeX;
simulator.bottom.height = piece.sizeZ;
simulator.bottom.mesh = mesh;
}
mesh.position.copy( piece.position );
if ( typeof piece.rotation !== 'undefined' ) {
mesh.rotation.setFromVector3( piece.rotation );
}
mesh.name = name;
mesh.receiveShadow = true;
mesh.castShadow = true;
scene.add( mesh );
});
},
// Function called when download progresses
function ( xhr ) {
console.log( (xhr.loaded / xhr.total * 100) + '% loaded' );
},
// Function called when download errors
function ( xhr ) {
console.log( 'An error happened' );
}
);
return this;
}
/**
* Adds the robots' managers.
* @param {Object} options - The options to be used during initialization
* @return {Simulator} The Simulator
*/
this.addRobotsManagers = function addRobotsManagers( options ) {
var values = $.extend ( {}, this.defaults.robotsManagers, options );
var rm = this.robotsManagers; // a reference
var simulator = this; // a reference
$.each ( values, function ( id, robotManager ) {
rm[id] = new RobotsManager ( robotManager, simulator );
rm[id].addRobots();
});
return this;
}
/**
* Initializes the whole simulation.
*/
this.initSimulation = function() {
this
.initRenderer()
.initAltRenderer()
.initRenderStats()
.addInfoBox()
.initScene()
.addAxisHelper()
.addMainCamera()
.addLight()
.addGround()
.addRobotsManagers()
.addGUI();
console.log( this );
}
this.getRobotById = function ( id ) {
//console.log ("Looking for robots...");
//console.log (this);
for ( var key in this.robotsManagers ) {
//console.log ( this.robotsManagers[key] );
for ( var i = 0; i < this.robotsManagers[key].robots.length; i++ ) {
if ( this.robotsManagers[key].robots[i].id === id ) {
//console.log ( ' ' + this.robotsManagers[key].robots[i].id );
return this.robotsManagers[key].robots[i];
}
}
}
return false;
}
};
$(function () {
function render () {
if ( simulator.gui.userData.controls.simulate )
{
simulator.scene.simulate( undefined, 2 );
}
requestAnimationFrame ( render );
simulator.renderer.render ( simulator.scene, simulator.usedCamera );
if ( simulator.gui.userData.controls.enableAltCamera && simulator.usedCamera.name !== 'main' ) {
simulator.altRenderer.render ( simulator.scene, simulator.mainCamera );
simulator.altRenderer.domElement.style.visibility = 'visible';
}
else {
simulator.altRenderer.domElement.style.visibility = 'hidden';
}
simulator.axisHelper.visible = simulator.gui.userData.controls.showAxis;
simulator.renderStats.update();
simulator.renderDebugText();
};
Physijs.scripts.worker = 'libs/vendor/physijs_worker.js';
Physijs.scripts.ammo = 'ammo.js';
var simulator = new Simulator( simulationDefaults );
simulator.initSimulation();
render();
});