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