diff --git a/emperor/support_files/js/animate.js b/emperor/support_files/js/animate.js index fa8d926f..38aad867 100644 --- a/emperor/support_files/js/animate.js +++ b/emperor/support_files/js/animate.js @@ -137,6 +137,13 @@ function(_, trajectory) { * @default -1 */ this.currentFrame = -1; + + /** + * @type {Integer} + * The previous frame served by the director + */ + this.previousFrame = -1; + /** * @type {Array} * Array where each element in the trajectory is a trajectory with the @@ -276,6 +283,7 @@ function(_, trajectory) { */ AnimationDirector.prototype.updateFrame = function() { if (this.animationCycleFinished() === false) { + this.previousFrame = this.currentFrame; this.currentFrame = this.currentFrame + 1; } }; diff --git a/emperor/support_files/js/animations-controller.js b/emperor/support_files/js/animations-controller.js index 311d1c8f..0fb09c1b 100644 --- a/emperor/support_files/js/animations-controller.js +++ b/emperor/support_files/js/animations-controller.js @@ -10,7 +10,11 @@ define([ ], function($, _, DecompositionView, ViewControllers, AnimationDirector, draw, Color, ColorViewController) { var EmperorViewController = ViewControllers.EmperorViewController; - var drawTrajectoryLine = draw.drawTrajectoryLine; + var drawTrajectoryLineStatic = draw.drawTrajectoryLineStatic; + var drawTrajectoryLineDynamic = draw.drawTrajectoryLineDynamic; + var disposeTrajectoryLineStatic = draw.disposeTrajectoryLineStatic; + var disposeTrajectoryLineDynamic = draw.disposeTrajectoryLineDynamic; + var updateStaticTrajectoryDrawRange = draw.updateStaticTrajectoryDrawRange; var ColorEditor = Color.ColorEditor, ColorFormatter = Color.ColorFormatter; /** @@ -440,13 +444,22 @@ define([ this.playing = false; this.director = null; - view.tubes.forEach(function(tube) { - if (tube.parent !== null) { + view.staticTubes.forEach(function(tube) { + if (tube !== null && tube.parent !== null) { + tube.parent.remove(tube); + disposeTrajectoryLineStatic(tube); + } + }); + view.dynamicTubes.forEach(function(tube) { + if (tube !== null && tube.parent !== null) { tube.parent.remove(tube); + disposeTrajectoryLineDynamic(tube); } }); - view.tubes = []; + view.staticTubes = []; + view.dynamicTubes = []; + view.needsUpdate = true; this._updateButtons(); @@ -538,20 +551,43 @@ define([ var radius = view.getGeometryFactor(); radius *= 0.45 * this.getRadius(); - view.tubes.forEach(function(tube) { - if (tube === undefined) { + for (var i = 0; i < this.director.trajectories.length; i++) { + var trajectory = this.director.trajectories[i]; + + //Ensure static tubes are constructed + if (view.staticTubes[i] === null || view.staticTubes[i] === undefined) + { + var color = this._colors[trajectory.metadataCategoryName] || 'red'; + view.staticTubes[i] = drawTrajectoryLineStatic(trajectory, + color, + radius); + } + + //Ensure static tube draw ranges are set to visible segment + updateStaticTrajectoryDrawRange(trajectory, + this.director.currentFrame, + view.staticTubes[i]); + } + + //Remove any old dynamic tubes from the scene + view.dynamicTubes.forEach(function(tube) { + if (tube === undefined || tube === null) { return; } if (tube.parent !== null) { tube.parent.remove(tube); + disposeTrajectoryLineDynamic(tube); } }); - view.tubes = this.director.trajectories.map(function(trajectory) { - color = scope._colors[trajectory.metadataCategoryName] || 'red'; - - var tube = drawTrajectoryLine(trajectory, scope.director.currentFrame, - color, radius); + //Construct new dynamic tubes containing necessary + //interpolated segment for the current frame + view.dynamicTubes = this.director.trajectories.map(function(trajectory) { + var color = scope._colors[trajectory.metadataCategoryName] || 'red'; + var tube = drawTrajectoryLineDynamic(trajectory, + scope.director.currentFrame, + color, + radius); return tube; }); diff --git a/emperor/support_files/js/draw.js b/emperor/support_files/js/draw.js index 05001be1..f5684190 100644 --- a/emperor/support_files/js/draw.js +++ b/emperor/support_files/js/draw.js @@ -1,5 +1,9 @@ /** @module draw */ define(['underscore', 'three', 'jquery'], function(_, THREE, $) { + + var NUM_TUBE_SEGMENTS = 3; + var NUM_TUBE_CROSS_SECTION_POINTS = 10; + // useful for some calculations var ZERO = new THREE.Vector3(); @@ -275,16 +279,22 @@ define(['underscore', 'three', 'jquery'], function(_, THREE, $) { return arrow; } - function drawTrajectoryLine(trajectory, currentFrame, color, radius) { + /** + * Returns a new trajectory line dynamic mesh + */ + function drawTrajectoryLineDynamic(trajectory, currentFrame, color, radius) { // based on the example described in: // https://github.com/mrdoob/three.js/wiki/Drawing-lines var material, points = [], lineGeometry, limit = 0, path; - _trajectory = trajectory.representativeCoordinatesAtIndex(currentFrame); + _trajectory = trajectory.representativeInterpolatedCoordinatesAtIndex( + currentFrame); + if (_trajectory === null || _trajectory.length == 0) + return null; - material = new THREE.MeshPhongMaterial({color: color}); - material.matrixAutoUpdate = true; - material.transparent = false; + material = new THREE.MeshPhongMaterial({ + color: color, + transparent: false}); for (var index = 0; index < _trajectory.length; index++) { points.push(new THREE.Vector3(_trajectory[index].x, @@ -292,15 +302,88 @@ define(['underscore', 'three', 'jquery'], function(_, THREE, $) { } path = new THREE.EmperorTrajectory(points); + // the line will contain the two vertices and the described material // we increase the number of points to have a smoother transition on // edges i. e. where the trajectory changes the direction it is going - lineGeometry = new THREE.TubeGeometry(path, (points.length - 1) * 3, radius, - 10, false); + lineGeometry = new THREE.TubeGeometry(path, + (points.length - 1) * NUM_TUBE_SEGMENTS, + radius, + NUM_TUBE_CROSS_SECTION_POINTS, + false); return new THREE.Mesh(lineGeometry, material); } + /** + * Disposes a trajectory line dynamic mesh + */ + function disposeTrajectoryLineDynamic(mesh) { + mesh.geometry.dispose(); + mesh.material.dispose(); + } + + /** + * Returns a new trajectory line static mesh + */ + function drawTrajectoryLineStatic(trajectory, color, radius) { + var _trajectory = trajectory.coordinates; + + var material = new THREE.MeshPhongMaterial({ + color: color, + transparent: false} + ); + + var allPoints = []; + for (var index = 0; index < _trajectory.length; index++) { + allPoints.push(new THREE.Vector3(_trajectory[index].x, + _trajectory[index].y, _trajectory[index].z)); + } + + var path = new THREE.EmperorTrajectory(allPoints); + + //Tubes are straight segments, but adding vertices along them might change + //lighting effects under certain models and lighting conditions. + var tubeBufferGeom = new THREE.TubeBufferGeometry( + path, + (allPoints.length - 1) * NUM_TUBE_SEGMENTS, + radius, + NUM_TUBE_CROSS_SECTION_POINTS, + false); + + return new THREE.Mesh(tubeBufferGeom, material); + } + + /** + * Disposes a trajectory line static mesh + */ + function disposeTrajectoryLineStatic(mesh) { + mesh.geometry.dispose(); + mesh.material.dispose(); + } + + function updateStaticTrajectoryDrawRange(trajectory, currentFrame, threeMesh) + { + //Reverse engineering the number of points in a THREE tube is not fun, and + //may be implementation/version dependent. + //Number of points drawn per tube segment = + // 2 (triangles) * 3 (points per triangle) * NUM_TUBE_CROSS_SECTION_POINTS + //Number of tube segments per pair of consecutive points = + // NUM_TUBE_SEGMENTS + + var multiplier = 2 * 3 * NUM_TUBE_CROSS_SECTION_POINTS * NUM_TUBE_SEGMENTS; + if (currentFrame < trajectory._intervalValues.length) + { + var intervalValue = trajectory._intervalValues[currentFrame]; + threeMesh.geometry.setDrawRange(0, intervalValue * multiplier); + } + else + { + threeMesh.geometry.setDrawRange(0, + (trajectory.coordinates.length - 1) * multiplier); + } + } + /** * @@ -414,6 +497,10 @@ define(['underscore', 'three', 'jquery'], function(_, THREE, $) { return {'formatSVGLegend': formatSVGLegend, 'makeLine': makeLine, 'makeLabel': makeLabel, 'makeArrow': makeArrow, - 'drawTrajectoryLine': drawTrajectoryLine, + 'drawTrajectoryLineStatic': drawTrajectoryLineStatic, + 'disposeTrajectoryLineStatic': disposeTrajectoryLineStatic, + 'drawTrajectoryLineDynamic': drawTrajectoryLineDynamic, + 'disposeTrajectoryLineDynamic': disposeTrajectoryLineDynamic, + 'updateStaticTrajectoryDrawRange': updateStaticTrajectoryDrawRange, 'makeLineCollection': makeLineCollection}; }); diff --git a/emperor/support_files/js/sceneplotview3d.js b/emperor/support_files/js/sceneplotview3d.js index a77d852f..94cc02d9 100644 --- a/emperor/support_files/js/sceneplotview3d.js +++ b/emperor/support_files/js/sceneplotview3d.js @@ -677,8 +677,9 @@ define([ }); _.each(this.decViews, function(view) { - view.tubes.forEach(function(tube) { - scope.scene.add(tube); + view.getTubes().forEach(function(tube) { + if (tube !== null) + scope.scene.add(tube); }); }); diff --git a/emperor/support_files/js/trajectory.js b/emperor/support_files/js/trajectory.js index 2a678491..66a2ee30 100644 --- a/emperor/support_files/js/trajectory.js +++ b/emperor/support_files/js/trajectory.js @@ -187,6 +187,35 @@ define([ return output; }; + /** + * + * Grab only the interpolated portion of representativeCoordinatesAtIndex. + * + * @param {integer} idx Value for which to determine the required number of + * points. + * + * @return {Array[]} Array containing the representative float x, y, z + * coordinates needed to draw the interpolated portion of a trajectory at the + * given index. + */ + TrajectoryOfSamples.prototype.representativeInterpolatedCoordinatesAtIndex = + function(idx) { + if (idx === 0) + return null; + if (this.interpolatedCoordinates.length - 1 <= idx) + return null; + + lastStaticPoint = this.coordinates[this._intervalValues[idx]]; + interpPoint = this.interpolatedCoordinates[idx]; + if (lastStaticPoint.x === interpPoint.x && + lastStaticPoint.y === interpPoint.y && + lastStaticPoint.z === interpPoint.z) { + return null; //Shouldn't pass on a zero length segment + } + + return [lastStaticPoint, interpPoint]; + }; + /** * * Function to interpolate a certain number of steps between two three diff --git a/emperor/support_files/js/view.js b/emperor/support_files/js/view.js index 4328872c..ea567bb4 100644 --- a/emperor/support_files/js/view.js +++ b/emperor/support_files/js/view.js @@ -63,10 +63,18 @@ function DecompositionView(decomp, asPointCloud) { */ this.backgroundColor = '#000000'; /** - * Tube objects on screen (used for animations) + * Static tubes objects covering an entire trajectory. + * Can use setDrawRange on the underlying geometry to display + * just part of the trajectory. * @type {THREE.Mesh[]} */ - this.tubes = []; + this.staticTubes = []; + /** + * Dynamic tubes covering the final tube segment of a trajectory + * Must be rebuilt each frame by the animations controller + * @type {THREE.Mesh[]} + */ + this.dynamicTubes = []; /** * Array of THREE.Mesh objects on screen (represent samples). * @type {THREE.Mesh[]} @@ -120,6 +128,14 @@ DecompositionView.prototype.getGeometryFactor = function() { this.decomp.dimensionRanges.min[0]) * 0.012; }; +/** + * Retrieve a shallow copy of concatenated static and dynamic tube arrays + * @type {THREE.Mesh[]} + */ +DecompositionView.prototype.getTubes = function() { + return this.staticTubes.concat(this.dynamicTubes); +}; + /** * * Helper method to initialize the base THREE.js objects. diff --git a/tests/javascript_tests/test_decomposition_view.js b/tests/javascript_tests/test_decomposition_view.js index 0fd26253..1b3fe4f2 100644 --- a/tests/javascript_tests/test_decomposition_view.js +++ b/tests/javascript_tests/test_decomposition_view.js @@ -82,7 +82,8 @@ requirejs([ equal(dv.getVisibleCount(), 2, 'visibleCount set correctly'); deepEqual(dv.visibleDimensions, [0, 1, 2], 'visibleDimensions set correctly'); - deepEqual(dv.tubes, [], 'tubes set correctly'); + deepEqual(dv.staticTubes, [], 'tubes set correctly'); + deepEqual(dv.dynamicTubes, [], 'tubes set correctly'); equal(dv.axesColor, '#FFFFFF'); equal(dv.backgroundColor, '#000000'); @@ -138,7 +139,8 @@ requirejs([ equal(dv.getVisibleCount(), 2, 'visibleCount set correctly'); deepEqual(dv.visibleDimensions, [0, 1], 'visibleDimensions set correctly'); - deepEqual(dv.tubes, [], 'tubes set correctly'); + deepEqual(dv.staticTubes, [], 'tubes set correctly'); + deepEqual(dv.dynamicTubes, [], 'tubes set correctly'); equal(dv.axesColor, '#FFFFFF'); equal(dv.backgroundColor, '#000000'); @@ -160,7 +162,8 @@ requirejs([ equal(view.count, 2); equal(view.getVisibleCount(), 2); deepEqual(view.visibleDimensions, [0, 1, 2]); - deepEqual(view.tubes, []); + deepEqual(view.staticTubes, [], 'tubes set correctly'); + deepEqual(view.dynamicTubes, [], 'tubes set correctly'); equal(view.axesColor, '#FFFFFF'); equal(view.backgroundColor, '#000000'); deepEqual(view.axesOrientation, [1, 1, 1]); @@ -199,7 +202,8 @@ requirejs([ equal(view.count, 2); equal(view.getVisibleCount(), 2); deepEqual(view.visibleDimensions, [0, 1, 2]); - deepEqual(view.tubes, []); + deepEqual(view.staticTubes, [], 'tubes set correctly'); + deepEqual(view.dynamicTubes, [], 'tubes set correctly'); equal(view.axesColor, '#FFFFFF'); equal(view.backgroundColor, '#000000'); deepEqual(view.axesOrientation, [1, 1, 1]); diff --git a/tests/javascript_tests/test_trajectory.js b/tests/javascript_tests/test_trajectory.js index 77f72adb..c09614a2 100644 --- a/tests/javascript_tests/test_trajectory.js +++ b/tests/javascript_tests/test_trajectory.js @@ -361,16 +361,31 @@ requirejs(['underscore', 'trajectory'], function(_, trajectory) { {'x': 6.75, 'y': 6.75, 'z': 6.75}, {'x': 8, 'y': 8, 'z': 8}]; - deepEqual(trajectory.representativeCoordinatesAtIndex(3), + var fullCoordinates3 = trajectory.representativeCoordinatesAtIndex(3); + deepEqual(fullCoordinates3, [{'x': 0, 'y': 0, 'z': 0}, {'x': 0.75, 'y': 0.75, 'z': 0.75}], 'Coordinates are retrieved correctly at index 3'); - deepEqual(trajectory.representativeCoordinatesAtIndex(11), + + var fullCoordinates11 = trajectory.representativeCoordinatesAtIndex(11); + deepEqual(fullCoordinates11, [{'x': 0, 'y': 0, 'z': 0}, {'x': 1, 'y': 1, 'z': 1}, {'x': -9, 'y': -9, 'z': -9}, {'x': 3, 'y': 3, 'z': 3}, {'x': 4.25, 'y': 4.25, 'z': 4.25}], 'Coordinates are retrieved correctly at index 11'); + var dynamicCoordinates3 = + trajectory.representativeInterpolatedCoordinatesAtIndex(3); + var dynamicCoordinates11 = + trajectory.representativeInterpolatedCoordinatesAtIndex(11); + + deepEqual(fullCoordinates3.slice(-2), + dynamicCoordinates3, + 'Dynamic coordinates match on frame 3'); + deepEqual(fullCoordinates11.slice(-2), + dynamicCoordinates11, + 'Dynamic coordinates match on frame 11'); + }); /** @@ -525,5 +540,37 @@ requirejs(['underscore', 'trajectory'], function(_, trajectory) { 'category'); }); + /** + * + * Test trajectories with duplicate points. + * + */ + test('Test Duplicate Points In Trajectories', function() { + var result; + trajectory = new TrajectoryOfSamples(['A', 'B', 'C', 'D'], + 'Nonsense', + [1, 2, 3, 4], + [{'x': 0, 'y': 0, 'z': 0}, + {'x': 10, 'y': 10, 'z': 10}, + {'x': 10, 'y': 10, 'z': 10}, + {'x': 0, 'y': 0, 'z': 0}], + 2, + 5); + + for (var i = 0; i < 20; i++) { + var interpTubeCoords = + trajectory.representativeInterpolatedCoordinatesAtIndex(i); + if (interpTubeCoords !== null) { + var dx = interpTubeCoords[1].x - interpTubeCoords[0].x; + var dy = interpTubeCoords[1].y - interpTubeCoords[0].y; + var dz = interpTubeCoords[1].z - interpTubeCoords[0].z; + var lenSq = dx * dx + dy * dy + dz * dz; + notEqual(lenSq, 0, + 'Interpolated tube should never be built between' + + 'consecutive duplicate points in a trajectory'); + } + } + }); + }); });