      1 /** @module draw */
      2 define(['underscore', 'three', 'jquery'], function(_, THREE, $) {
      3
      4   var NUM_TUBE_SEGMENTS = 3;
      5   var NUM_TUBE_CROSS_SECTION_POINTS = 10;
      6
      7   // useful for some calculations
      8   var ZERO = new THREE.Vector3();
      9
     10   /**
     11    *
     12    * @class EmperorTrajectory
     13    *
     14    * This class represents the internal logic for a linearly interpolated
     15    * tube/trajectory in THREE.js
     16    *
     17    * [This answer]{@link http://stackoverflow.com/a/18580832/379593} on
     18    * StackOverflow helped a lot.
     19    * @return {EmperorTrajectory}
     20    * @extends THREE.Curve
     21    */
     22   THREE.EmperorTrajectory = THREE.Curve.create(
     23       function(points) {
     24         this.points = (points === undefined) ? [] : points;
     25       },
     26
     27       function(t) {
     28         var points = this.points;
     29         var index = (points.length - 1) * t;
     30         var floorIndex = Math.floor(index);
     31
     32         if (floorIndex == points.length - 1) {
     33           return points[floorIndex];
     34         }
     35
     36         var floorPoint = points[floorIndex];
     37         var ceilPoint = points[floorIndex + 1];
     38
     39         return floorPoint.clone().lerp(ceilPoint, index - floorIndex);
     40       }
     41       );
     42
     43   /** @private */
     44   THREE.EmperorTrajectory.prototype.getUtoTmapping = function(u) {
     45     return u;
     46   };
     47
     48   /**
     49    *
     50    * @class EmperorArrowHelper
     51    *
     52    * Subclass of THREE.ArrowHelper to make raycasting work on the line and cone
     53    * children.
     54    *
     55    * For more information about the arguments, see the [online documentation]
     56    * {@link https://threejs.org/docs/#api/helpers/ArrowHelper}.
     57    * @return {EmperorArrowHelper}
     58    * @extends THREE.ArrowHelper
     59    *
     60    */
     61   function EmperorArrowHelper(dir, origin, length, color, headLength,
     62                               headWidth, name) {
     63     THREE.ArrowHelper.call(this, dir, origin, length, color, headLength,
     64                            headWidth);
     65
     66     this.name = name;
     67     this.line.name = this.name;
     68     this.cone.name = this.name;
     69
     70     this.label = makeLabel(this.cone.position.toArray(), this.name, color);
     71     this.add(this.label);
     72
     73     return this;
     74   }
     75   EmperorArrowHelper.prototype = Object.create(THREE.ArrowHelper.prototype);
     76   EmperorArrowHelper.prototype.constructor = THREE.ArrowHelper;
     77
     78   /**
     79    *
     80    * Check for ray casting with arrow's cone.
     81    *
     82    * This class may need to disappear if THREE.ArrowHelper implements the
     83    * raycast method, for more information see the [online documentation]
     84    * {@link https://threejs.org/docs/#api/helpers/ArrowHelper}.
     85    *
     86    */
     87   EmperorArrowHelper.prototype.raycast = function(raycaster, intersects) {
     88     // Two considerations:
     89     // * Don't raycast the label since that one is self-explanatory
     90     // * Don't raycast to the line as it adds a lot of noise to the raycaster.
     91     //   If raycasting is enabled for lines, this will result in incorrect
     92     //   intersects showing as the closest to the ray i.e. wrong labels.
     93     this.cone.raycast(raycaster, intersects);
     94   };
     95
     96   /**
     97    *
     98    * Set the arrow's color
     99    *
    100    * @param {THREE.Color} color The color to set for the line, cone and label.
    101    *
    102    */
    103   EmperorArrowHelper.prototype.setColor = function(color) {
    104     THREE.ArrowHelper.prototype.setColor.call(this, color);
    105     this.label.material.color.set(color);
    106   };
    107
    108   /**
    109    *
    110    * Change the vector where the arrow points to
    111    *
    112    * @param {THREE.Vector3} target The vector where the arrow will point to.
    113    * Note, the label will also change position.
    114    *
    115    */
    116   EmperorArrowHelper.prototype.setPointsTo = function(target) {
    117     var length;
    118
    119     // calculate the length before normalizing to a unit vector
    120     target = target.sub(ZERO);
    121     length = ZERO.distanceTo(target);
    122     target.normalize();
    123
    124     this.setDirection(target.sub(ZERO));
    125     this.setLength(length);
    126
    127     this.label.position.copy(this.cone.position);
    128   };
    129
    130   /**
    131    * Dispose of underlying objects
    132    */
    133   EmperorArrowHelper.prototype.dispose = function() {
    134     // dispose each object according to THREE's guide
    135     this.label.material.map.dispose();
    136     this.label.material.dispose();
    137     this.label.geometry.dispose();
    138
    139     this.cone.material.dispose();
    140     this.cone.geometry.dispose();
    141
    142     this.line.material.dispose();
    143     this.line.geometry.dispose();
    144
    145     this.remove(this.label);
    146     this.remove(this.cone);
    147     this.remove(this.line);
    148
    149     this.label = null;
    150     this.cone = null;
    151     this.line = null;
    152   };
    153
    154   /**
    155    *
    156    * Create a generic THREE.Line object
    157    *
    158    * @param {float[]} start The x, y and z coordinates of one of the ends
    159    * of the line.
    160    * @param {float[]} end The x, y and z coordinates of one of the ends
    161    * of the line.
    162    * @param {integer} color Hexadecimal base that specifies the color of the
    163    * line.
    164    * @param {float} width The width of the line being drawn.
    165    * @param {boolean} transparent Whether the line will be transparent or not.
    166    *
    167    * @return {THREE.Line}
    168    * @function makeLine
    169    */
    170   function makeLine(start, end, color, width, transparent) {
    171     // based on the example described in:
    172     // https://github.com/mrdoob/three.js/wiki/Drawing-lines
    173     var material, geometry, line;
    174
    175     // make the material transparent and with full opacity
    176     material = new THREE.LineBasicMaterial({color: color, linewidth: width});
    177     material.matrixAutoUpdate = true;
    178     material.transparent = transparent;
    179     material.opacity = 1.0;
    180
    181     // add the two vertices to the geometry
    182     geometry = new THREE.Geometry();
    183     geometry.vertices.push(new THREE.Vector3(start[0], start[1], start[2]));
    184     geometry.vertices.push(new THREE.Vector3(end[0], end[1], end[2]));
    185
    186     // the line will contain the two vertices and the described material
    187     line = new THREE.Line(geometry, material);
    188
    189     return line;
    190   }
    191
    192   /**
    193    *
    194    * @class EmperorLineSegments
    195    *
    196    * Subclass of THREE.LineSegments to make vertex modifications easier.
    197    *
    198    * @return {EmperorLineSegments}
    199    * @extends THREE.LineSegments
    200    */
    201   function EmperorLineSegments(geometry, material) {
    202     THREE.LineSegments.call(this, geometry, material);
    203
    204     return this;
    205   }
    206   EmperorLineSegments.prototype = Object.create(THREE.LineSegments.prototype);
    207   EmperorLineSegments.prototype.constructor = THREE.LineSegments;
    208
    209   /**
    210    *
    211    * Set the start and end points for a line in the collection.
    212    *
    213    * @param {Integer} i The index of the line;
    214    * @param {Float[]} start An array of the starting point of the line ([x, y,
    215    * z]).
    216    * @param {Float[]} start An array of the ending point of the line ([x, y,
    217    * z]).
    218    */
    219   EmperorLineSegments.prototype.setLineAtIndex = function(i, start, end) {
    220     var vertices = this.geometry.attributes.position.array;
    221
    222     vertices[(i * 6)] = start[0];
    223     vertices[(i * 6) + 1] = start[1];
    224     vertices[(i * 6) + 2] = start[2];
    225     vertices[(i * 6) + 3] = end[0];
    226     vertices[(i * 6) + 4] = end[1];
    227     vertices[(i * 6) + 5] = end[2];
    228   };
    229
    230   /**
    231    *
    232    * Create a collection of disconnected lines.
    233    *
    234    * This function is specially useful when creating a lot of lines as it uses
    235    * a BufferGeometry for improved performance.
    236    *
    237    * @param {Array[]} vertices List of vertices used to create the lines. Each
    238    * line is connected on as (vertices[i], vertices[i+1),
    239    * (vertices[i+2], vertices[i+3]), etc.
    240    * @param {integer} color Hexadecimal base that specifies the color of the
    241    * line.
    242    *
    243    * @return {EmperorLineSegments}
    244    * @function makeLineCollection
    245    *
    246    */
    247   function makeLineCollection(vertices, color) {
    248     // based on https://jsfiddle.net/wilt/bd8trrLx/
    249     var material = new THREE.LineBasicMaterial({
    250       color: color || 0xff0000
    251     });
    252
    253     var positions = new Float32Array(vertices.length * 3);
    254
    255     for (var i = 0; i < vertices.length; i++) {
    256
    257       positions[i * 3] = vertices[i][0];
    258       positions[i * 3 + 1] = vertices[i][1];
    259       positions[i * 3 + 2] = vertices[i][2];
    260
    261     }
    262
    263     var indices = _.range(vertices.length);
    264     var geometry = new THREE.BufferGeometry();
    265     geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
    266     geometry.setIndex(new THREE.BufferAttribute(new Uint16Array(indices), 1));
    267
    268     return new EmperorLineSegments(geometry, material);
    269   }
    270
    271   /**
    272    *
    273    * Create a generic Arrow object (composite of a cone and line)
    274    *
    275    * @param {float[]} from The x, y and z coordinates where the arrow
    276    * originates from.
    277    * @param {float[]} to The x, y and z coordinates where the arrow points to.
    278    * @param {integer} color Hexadecimal base that specifies the color of the
    279    * line.
    280    * @param {String} name The text to be used in the label, and the name of
    281    * the line and cone (used for raycasting).
    282    *
    283    * @return {THREE.Object3D}
    284    * @function makeArrow
    285    */
    286   function makeArrow(from, to, color, name) {
    287     var target, origin, direction, length, arrow;
    288
    289     target = new THREE.Vector3(to[0], to[1], to[2]);
    290     origin = new THREE.Vector3(from[0], from[1], from[2]);
    291
    292     length = origin.distanceTo(target);
    293
    294     // https://stackoverflow.com/a/20558498/379593
    295     direction = target.sub(origin);
    296
    297     direction.normalize();
    298
    299     // don't set the head size or width, defaults are good enough
    300     arrow = new EmperorArrowHelper(direction, origin, length, color,
    301                                    undefined, undefined, name);
    302
    303     return arrow;
    304   }
    305
    306   /**
    307    * Returns a new trajectory line dynamic mesh
    308    */
    309   function drawTrajectoryLineDynamic(trajectory, currentFrame, color, radius) {
    310     // based on the example described in:
    311     // https://github.com/mrdoob/three.js/wiki/Drawing-lines
    312     var material, points = [], lineGeometry, limit = 0, path;
    313
    314     _trajectory = trajectory.representativeInterpolatedCoordinatesAtIndex(
    315                                                                   currentFrame);
    316     if (_trajectory === null || _trajectory.length == 0)
    317       return null;
    318
    319     material = new THREE.MeshPhongMaterial({
    320         color: color,
    321         transparent: false});
    322
    323     for (var index = 0; index < _trajectory.length; index++) {
    324       points.push(new THREE.Vector3(_trajectory[index].x,
    325                   _trajectory[index].y, _trajectory[index].z));
    326     }
    327
    328     path = new THREE.EmperorTrajectory(points);
    329
    330     // the line will contain the two vertices and the described material
    331     // we increase the number of points to have a smoother transition on
    332     // edges i. e. where the trajectory changes the direction it is going
    333     lineGeometry = new THREE.TubeGeometry(path,
    334                                     (points.length - 1) * NUM_TUBE_SEGMENTS,
    335                                     radius,
    336                                     NUM_TUBE_CROSS_SECTION_POINTS,
    337                                     false);
    338
    339     return new THREE.Mesh(lineGeometry, material);
    340   }
    341
    342   /**
    343    * Disposes a trajectory line dynamic mesh
    344    */
    345   function disposeTrajectoryLineDynamic(mesh) {
    346     mesh.geometry.dispose();
    347     mesh.material.dispose();
    348   }
    349
    350   /**
    351    * Returns a new trajectory line static mesh
    352    */
    353   function drawTrajectoryLineStatic(trajectory, color, radius) {
    354     var _trajectory = trajectory.coordinates;
    355
    356     var material = new THREE.MeshPhongMaterial({
    357       color: color,
    358       transparent: false}
    359     );
    360
    361     var allPoints = [];
    362     for (var index = 0; index < _trajectory.length; index++) {
    363       allPoints.push(new THREE.Vector3(_trajectory[index].x,
    364                   _trajectory[index].y, _trajectory[index].z));
    365     }
    366
    367     var path = new THREE.EmperorTrajectory(allPoints);
    368
    369     //Tubes are straight segments, but adding vertices along them might change
    370     //lighting effects under certain models and lighting conditions.
    371     var tubeBufferGeom = new THREE.TubeBufferGeometry(
    372                                 path,
    373                                 (allPoints.length - 1) * NUM_TUBE_SEGMENTS,
    374                                 radius,
    375                                 NUM_TUBE_CROSS_SECTION_POINTS,
    376                                 false);
    377
    378     return new THREE.Mesh(tubeBufferGeom, material);
    379   }
    380
    381   /**
    382    * Disposes a trajectory line static mesh
    383    */
    384   function disposeTrajectoryLineStatic(mesh) {
    385     mesh.geometry.dispose();
    386     mesh.material.dispose();
    387   }
    388
    389   function updateStaticTrajectoryDrawRange(trajectory, currentFrame, threeMesh)
    390   {
    391     //Reverse engineering the number of points in a THREE tube is not fun, and
    392     //may be implementation/version dependent.
    393     //Number of points drawn per tube segment =
    394     //  2 (triangles) * 3 (points per triangle) * NUM_TUBE_CROSS_SECTION_POINTS
    395     //Number of tube segments per pair of consecutive points =
    396     //  NUM_TUBE_SEGMENTS
    397
    398     var multiplier = 2 * 3 * NUM_TUBE_CROSS_SECTION_POINTS * NUM_TUBE_SEGMENTS;
    399     if (currentFrame < trajectory._intervalValues.length)
    400     {
    401       var intervalValue = trajectory._intervalValues[currentFrame];
    402       threeMesh.geometry.setDrawRange(0, intervalValue * multiplier);
    403     }
    404     else
    405     {
    406       threeMesh.geometry.setDrawRange(0,
    407                             (trajectory.coordinates.length - 1) * multiplier);
    408     }
    409   }
    410
    411
    412   /**
    413    *
    414    * Create a THREE object that displays 2D text, this implementation is based
    415    * on the answer found
    416    * [here]{@link http://stackoverflow.com/a/14106703/379593}
    417    *
    418    * The text is returned scaled to its size in pixels, hence you'll need to
    419    * scale it down depending on the scene's dimensions.
    420    *
    421    * Warning: The text sizes vary slightly depending on the browser and OS you
    422    * use. This is specially important for testing.
    423    *
    424    * @param {float[]} position The x, y, and z location of the label.
    425    * @param {string} text The text to be shown on screen.
    426    * @param {integer|string} Color Hexadecimal base that represents the color
    427    * of the text.
    428    *
    429    * @return {THREE.Sprite} Object with the text displaying in it.
    430    * @function makeLabel
    431    **/
    432   function makeLabel(position, text, color) {
    433     // the font size determines the resolution relative to the sprite object
    434     var fontSize = 32, canvas, context, measure;
    435
    436     canvas = document.createElement('canvas');
    437     context = canvas.getContext('2d');
    438
    439     // set the font size so we can measure the width
    440     context.font = fontSize + 'px Arial';
    441     measure = context.measureText(text);
    442
    443     // make the dimensions a power of 2 (for use in THREE.js)
    444     canvas.width = THREE.Math.ceilPowerOfTwo(measure.width);
    445     canvas.height = THREE.Math.ceilPowerOfTwo(fontSize);
    446
    447     // after changing the canvas' size we need to reset the font attributes
    448     context.textAlign = 'center';
    449     context.textBaseline = 'middle';
    450     context.font = fontSize + 'px Arial';
    451     if (_.isNumber(color)) {
    452       context.fillStyle = '#' + color.toString(16);
    453     }
    454     else {
    455       context.fillStyle = color;
    456     }
    457     context.fillText(text, canvas.width / 2, canvas.height / 2);
    458
    459     var amap = new THREE.Texture(canvas);
    460     amap.needsUpdate = true;
    461
    462     var mat = new THREE.SpriteMaterial({
    463         map: amap,
    464         transparent: true,
    465         color: color
    466     });
    467
    468     var sp = new THREE.Sprite(mat);
    469     sp.position.set(position[0], position[1], position[2]);
    470     sp.scale.set(canvas.width, canvas.height, 1);
    471
    472     // add an extra attribute so we can render this properly when we use
    473     // SVGRenderer
    474     sp.text = text;
    475
    476     return sp;
    477   }
    478
    479   /**
    480    *
    481    * Format an SVG string with labels and colors.
    482    *
    483    * @param {string[]} labels The names for the label.
    484    * @param {integer[]} colors The colors for each label.
    485    *
    486    * @return {string} SVG string with the labels and colors values formated as
    487    * a legend.
    488    * @function formatSVGLegend
    489    */
    490   function formatSVGLegend(labels, colors) {
    491     var labels_svg = '', pos_y = 1, increment = 40, max_len = 0, rect_width,
    492     font_size = 12;
    493
    494     for (var i = 0; i < labels.length; i++) {
    495       // add the rectangle with the corresponding color
    496       labels_svg += '<rect height="27" width="27" y="' + pos_y +
    497         '" x="5" style="stroke-width:1;stroke:rgb(0,0,0)" fill="' +
    498         colors[i] + '"/>';
    499
    500       // add the name of the category
    501       labels_svg += '<text xml:space="preserve" y="' + (pos_y + 20) +
    502         '" x="40" font-size="' + font_size +
    503         '" stroke-width="0" stroke="#000000" fill="#000000">' + labels[i] +
    504         '</text>';
    505
    506       pos_y += increment;
    507     }
    508
    509     // get the name with the maximum number of characters and get the length
    510     max_len = _.max(labels, function(a) {return a.length}).length;
    511
    512       // duplicate the size of the rectangle to make sure it fits the labels
    513       rect_width = font_size * max_len * 2;
    514
    515     labels_svg = '<svg xmlns="http://www.w3.org/2000/svg" width="' +
    516       rect_width + '" height="' + (pos_y - 10) + '"><g>' + labels_svg +
    517       '</g></svg>';
    518
    519     return labels_svg;
    520   }
    521
    522   return {'formatSVGLegend': formatSVGLegend, 'makeLine': makeLine,
    523           'makeLabel': makeLabel, 'makeArrow': makeArrow,
    524           'drawTrajectoryLineStatic': drawTrajectoryLineStatic,
    525           'disposeTrajectoryLineStatic': disposeTrajectoryLineStatic,
    526           'drawTrajectoryLineDynamic': drawTrajectoryLineDynamic,
    527           'disposeTrajectoryLineDynamic': disposeTrajectoryLineDynamic,
    528           'updateStaticTrajectoryDrawRange': updateStaticTrajectoryDrawRange,
    529           'makeLineCollection': makeLineCollection};
    530 });
    531