diff --git a/src/traces/mesh3d/convert.js b/src/traces/mesh3d/convert.js index 713f252282e..9b2c926f738 100644 --- a/src/traces/mesh3d/convert.js +++ b/src/traces/mesh3d/convert.js @@ -52,7 +52,53 @@ proto.handlePick = function(selection) { }; function parseColorArray(colors) { - return colors.map(str2RgbaArray); + var b = []; + var len = colors.length; + for(var i = 0; i < len; i++) { + b[i] = str2RgbaArray(colors[i]); + } + return b; +} + +// Unpack position data +function toDataCoords(axis, coord, scale, calendar) { + var b = []; + var len = coord.length; + for(var i = 0; i < len; i++) { + b[i] = axis.d2l(coord[i], 0, calendar) * scale; + } + return b; +} + +// Round indices if passed as floats +function toRoundIndex(a) { + var b = []; + var len = a.length; + for(var i = 0; i < len; i++) { + b[i] = Math.round(a[i]); + } + return b; +} + +function delaunayCells(delaunayaxis, positions) { + var d = ['x', 'y', 'z'].indexOf(delaunayaxis); + var b = []; + var len = positions.length; + for(var i = 0; i < len; i++) { + b[i] = [positions[i][(d + 1) % 3], positions[i][(d + 2) % 3]]; + } + return triangulate(b); +} + +// Validate indices +function hasValidIndices(list, numVertices) { + var len = list.length; + for(var i = 0; i < len; i++) { + if(list[i] <= -0.5 || list[i] >= numVertices - 0.5) { // Note: the indices would be rounded -0.49 is valid. + return false; + } + } + return true; } proto.update = function(data) { @@ -61,33 +107,37 @@ proto.update = function(data) { this.data = data; - // Unpack position data - function toDataCoords(axis, coord, scale, calendar) { - return coord.map(function(x) { - return axis.d2l(x, 0, calendar) * scale; - }); - } + var numVertices = data.x.length; var positions = zip3( toDataCoords(layout.xaxis, data.x, scene.dataScale[0], data.xcalendar), toDataCoords(layout.yaxis, data.y, scene.dataScale[1], data.ycalendar), - toDataCoords(layout.zaxis, data.z, scene.dataScale[2], data.zcalendar)); + toDataCoords(layout.zaxis, data.z, scene.dataScale[2], data.zcalendar) + ); var cells; if(data.i && data.j && data.k) { - cells = zip3(data.i, data.j, data.k); - } - else if(data.alphahull === 0) { + + if( + data.i.length !== data.j.length || + data.j.length !== data.k.length || + !hasValidIndices(data.i, numVertices) || + !hasValidIndices(data.j, numVertices) || + !hasValidIndices(data.k, numVertices) + ) { + return; + } + cells = zip3( + toRoundIndex(data.i), + toRoundIndex(data.j), + toRoundIndex(data.k) + ); + } else if(data.alphahull === 0) { cells = convexHull(positions); - } - else if(data.alphahull > 0) { + } else if(data.alphahull > 0) { cells = alphaShape(data.alphahull, positions); - } - else { - var d = ['x', 'y', 'z'].indexOf(data.delaunayaxis); - cells = triangulate(positions.map(function(c) { - return [c[(d + 1) % 3], c[(d + 2) % 3]]; - })); + } else { + cells = delaunayCells(data.delaunayaxis, positions); } var config = { @@ -113,16 +163,13 @@ proto.update = function(data) { config.vertexIntensity = data.intensity; config.vertexIntensityBounds = [data.cmin, data.cmax]; config.colormap = parseColorScale(data.colorscale); - } - else if(data.vertexcolor) { + } else if(data.vertexcolor) { this.color = data.vertexcolor[0]; config.vertexColors = parseColorArray(data.vertexcolor); - } - else if(data.facecolor) { + } else if(data.facecolor) { this.color = data.facecolor[0]; config.cellColors = parseColorArray(data.facecolor); - } - else { + } else { this.color = data.color; config.meshColor = str2RgbaArray(data.color); } diff --git a/src/traces/mesh3d/defaults.js b/src/traces/mesh3d/defaults.js index d5691b42a8f..952f8cd73c5 100644 --- a/src/traces/mesh3d/defaults.js +++ b/src/traces/mesh3d/defaults.js @@ -34,18 +34,20 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout } var coords = readComponents(['x', 'y', 'z']); - var indices = readComponents(['i', 'j', 'k']); - if(!coords) { traceOut.visible = false; return; } - if(indices) { - // otherwise, convert all face indices to ints - indices.forEach(function(index) { - for(var i = 0; i < index.length; ++i) index[i] |= 0; - }); + readComponents(['i', 'j', 'k']); + // three indices should be all provided or not + if( + (traceOut.i && (!traceOut.j || !traceOut.k)) || + (traceOut.j && (!traceOut.k || !traceOut.i)) || + (traceOut.k && (!traceOut.i || !traceOut.j)) + ) { + traceOut.visible = false; + return; } var handleCalendarDefaults = Registry.getComponentMethod('calendars', 'handleTraceDefaults'); diff --git a/test/jasmine/tests/mesh3d_test.js b/test/jasmine/tests/mesh3d_test.js index e268057caaf..6ee4d407bb3 100644 --- a/test/jasmine/tests/mesh3d_test.js +++ b/test/jasmine/tests/mesh3d_test.js @@ -3,55 +3,456 @@ var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); var failTest = require('../assets/fail_test'); -describe('Test mesh3d restyle', function() { - afterEach(destroyGraphDiv); - - it('should clear *cauto* when restyle *cmin* and/or *cmax*', function(done) { - var gd = createGraphDiv(); - - function _assert(user, full) { - var trace = gd.data[0]; - var fullTrace = gd._fullData[0]; - - expect(trace.cauto).toBe(user[0], 'user cauto'); - expect(trace.cmin).toBe(user[1], 'user cmin'); - expect(trace.cmax).toBe(user[2], 'user cmax'); - expect(fullTrace.cauto).toBe(full[0], 'full cauto'); - expect(fullTrace.cmin).toBe(full[1], 'full cmin'); - expect(fullTrace.cmax).toBe(full[2], 'full cmax'); +describe('Test mesh3d', function() { + 'use strict'; + + describe('restyle', function() { + afterEach(destroyGraphDiv); + + it('should clear *cauto* when restyle *cmin* and/or *cmax*', function(done) { + var gd = createGraphDiv(); + + function _assert(user, full) { + var trace = gd.data[0]; + var fullTrace = gd._fullData[0]; + + expect(trace.cauto).toBe(user[0], 'user cauto'); + expect(trace.cmin).toBe(user[1], 'user cmin'); + expect(trace.cmax).toBe(user[2], 'user cmax'); + expect(fullTrace.cauto).toBe(full[0], 'full cauto'); + expect(fullTrace.cmin).toBe(full[1], 'full cmin'); + expect(fullTrace.cmax).toBe(full[2], 'full cmax'); + } + + Plotly.plot(gd, [{ + type: 'mesh3d', + x: [0, 1, 2, 0], + y: [0, 0, 1, 2], + z: [0, 2, 0, 1], + i: [0, 0, 0, 1], + j: [1, 2, 3, 2], + k: [2, 3, 1, 3], + intensity: [0, 0.33, 0.66, 3] + }]) + .then(function() { + _assert([undefined, undefined, undefined], [true, 0, 3]); + + return Plotly.restyle(gd, 'cmin', 0); + }) + .then(function() { + _assert([false, 0, undefined], [false, 0, 3]); + + return Plotly.restyle(gd, 'cmax', 10); + }) + .then(function() { + _assert([false, 0, 10], [false, 0, 10]); + + return Plotly.restyle(gd, 'cauto', true); + }) + .then(function() { + _assert([true, 0, 10], [true, 0, 3]); + + return Plotly.purge(gd); + }) + .catch(failTest) + .then(done); + }); + }); + + describe('dimension and expected visibility check and cell/position tests', function() { + var gd; + + beforeEach(function() { + gd = createGraphDiv(); + }); + + afterEach(function() { + Plotly.purge(gd); + destroyGraphDiv(); + }); + + function assertVisibility(exp, msg) { + expect(gd._fullData[0]).not.toBe(undefined, 'no visibility!'); + expect(gd._fullData[0].visible).toBe(exp, msg); } - Plotly.plot(gd, [{ - type: 'mesh3d', - x: [0, 1, 2, 0], - y: [0, 0, 1, 2], - z: [0, 2, 0, 1], - i: [0, 0, 0, 1], - j: [1, 2, 3, 2], - k: [2, 3, 1, 3], - intensity: [0, 0.33, 0.66, 3] - }]) - .then(function() { - _assert([undefined, undefined, undefined], [true, 0, 3]); - - return Plotly.restyle(gd, 'cmin', 0); - }) - .then(function() { - _assert([false, 0, undefined], [false, 0, 3]); - - return Plotly.restyle(gd, 'cmax', 10); - }) - .then(function() { - _assert([false, 0, 10], [false, 0, 10]); - - return Plotly.restyle(gd, 'cauto', true); - }) - .then(function() { - _assert([true, 0, 10], [true, 0, 3]); - - return Plotly.purge(gd); - }) - .catch(failTest) - .then(done); + function assertPositions(exp, msg) { + expect(gd._fullLayout.scene._scene.glplot.objects[0].positions.length).toBe(exp, msg); + } + + function assertCells(exp, msg) { + expect(gd._fullLayout.scene._scene.glplot.objects[0].cells.length).toBe(exp, msg); + } + + it('@gl mesh3d should be visible when the indices are not integer', function(done) { + Plotly.plot(gd, [{ + x: [0, 1, 0.5, 0.5], + y: [0, 0.5, 1, 0.5], + z: [0, 0.5, 0.5, 1], + i: [0, 0, 0, 1.00001], + j: [1, 1, 2, 1.99999], + k: [2, 3, 3, 3.00001], + type: 'mesh3d' + }]) + .then(function() { + assertVisibility(true, 'to be visible'); + }) + .then(function() { + assertPositions(4, 'to be OK positions'); + }) + .then(function() { + assertCells(4, 'to be OK cells'); + }) + .catch(failTest) + .then(done); + }); + + it('@gl mesh3d should be visible when the indices could be rounded to be in vertex range', function(done) { + Plotly.plot(gd, [{ + x: [0, 1, 0.5, 0.5], + y: [0, 0.5, 1, 0.5], + z: [0, 0.5, 0.5, 1], + i: [-0.49, 0, 0, 1], + j: [1, 1, 2, 2], + k: [2, 3, 3, 3.49], + type: 'mesh3d' + }]) + .then(function() { + assertVisibility(true, 'to be visible'); + }) + .then(function() { + assertPositions(4, 'to be OK positions'); + }) + .then(function() { + assertCells(4, 'to be OK cells'); + }) + .catch(failTest) + .then(done); + }); + + it('@gl mesh3d should be visible when the indices are equal or greater than the number of vertices', function(done) { + Plotly.plot(gd, [{ + x: [0, 1, 0.5, 0.5], + y: [0, 0.5, 1, 0.5], + z: [0, 0.5, 0.5, 1], + i: [0, 0, 0, 1], + j: [1, 1, 2, 2], + k: [2, 3, 3, 4], + type: 'mesh3d' + }]) + .then(function() { + assertVisibility(true, 'to be visible'); + }) + .then(function() { + assertPositions(0, 'to be OK positions'); + }) + .then(function() { + assertCells(0, 'to be OK cells'); + }) + .catch(failTest) + .then(done); + }); + + it('@gl mesh3d should be visible when the indices are negative', function(done) { + Plotly.plot(gd, [{ + x: [0, 1, 0.5, 0.5], + y: [0, 0.5, 1, 0.5], + z: [0, 0.5, 0.5, 1], + i: [0, 0, 0, -1], + j: [1, 1, 2, 2], + k: [2, 3, 3, 3], + type: 'mesh3d' + }]) + .then(function() { + assertVisibility(true, 'to be visible'); + }) + .then(function() { + assertPositions(0, 'to be OK positions'); + }) + .then(function() { + assertCells(0, 'to be OK cells'); + }) + .catch(failTest) + .then(done); + }); + + it('@gl mesh3d should be visible when the indices have different sizes', function(done) { + Plotly.plot(gd, [{ + x: [0, 1, 0.5, 0.5], + y: [0, 0.5, 1, 0.5], + z: [0, 0.5, 0.5, 1], + i: [0, 0, 0, 1], + j: [1, 1, 2], + k: [2, 3, 3, 3], + type: 'mesh3d' + }]) + .then(function() { + assertVisibility(true, 'to be visible'); + }) + .then(function() { + assertPositions(0, 'to be OK positions'); + }) + .then(function() { + assertCells(0, 'to be OK cells'); + }) + .catch(failTest) + .then(done); + }); + + it('@gl mesh3d should be visible when the indices are provided and OK', function(done) { + Plotly.plot(gd, [{ + x: [0, 1, 0.5, 0.5], + y: [0, 0.5, 1, 0.5], + z: [0, 0.5, 0.5, 1], + i: [0, 0, 0, 1], + j: [1, 1, 2, 2], + k: [2, 3, 3, 3], + type: 'mesh3d' + }]) + .then(function() { + assertVisibility(true, 'to be visible'); + }) + .then(function() { + assertPositions(4, 'to be OK positions'); + }) + .then(function() { + assertCells(4, 'to be OK cells'); + }) + .catch(failTest) + .then(done); + }); + + it('@gl mesh3d should be visible when values are passed in string format', function(done) { + Plotly.plot(gd, [{ + x: ['0', '1', '0.5', '0.5'], + y: ['0', '0.5', '1', '0.5'], + z: ['0', '0.5', '0.5', '1'], + i: ['0', '0', '0', '1'], + j: ['1', '1', '2', '2'], + k: ['2', '3', '3', '3'], + type: 'mesh3d' + }]).then(function() { + assertVisibility(true, 'not to be visible'); + }) + .then(function() { + assertPositions(4, 'to be OK positions'); + }) + .then(function() { + assertCells(4, 'to be OK cells'); + }) + .catch(failTest) + .then(done); + }); + + it('@gl mesh3d should be visible when the index arrays are empty', function(done) { + Plotly.plot(gd, [{ + x: [0, 1, 0.5, 0.5], + y: [0, 0.5, 1, 0.5], + z: [0, 0.5, 0.5, 1], + i: [], + j: [], + k: [], + type: 'mesh3d' + }]) + .then(function() { + assertVisibility(true, 'to be visible'); + }) + .then(function() { + assertPositions(4, 'to be OK positions'); + }) + .then(function() { + assertCells(0, 'to be OK cells'); + }) + .catch(failTest) + .then(done); + }); + + it('@gl mesh3d should be visible when the index arrays are not provided', function(done) { + Plotly.plot(gd, [{ + x: [0, 1, 0.5, 0.5], + y: [0, 0.5, 1, 0.5], + z: [0, 0.5, 0.5, 1], + type: 'mesh3d' + }]) + .then(function() { + assertVisibility(true, 'to be visible'); + }) + .then(function() { + assertPositions(4, 'to be OK positions'); + }) + .then(function() { + assertCells(3, 'to be OK cells'); + }) + .catch(failTest) + .then(done); + }); + + it('@gl mesh3d should be visible when the vertex arrays are empty', function(done) { + Plotly.plot(gd, [{ + x: [], + y: [], + z: [], + type: 'mesh3d' + }]) + .then(function() { + assertVisibility(true, 'not to be visible'); + }) + .then(function() { + assertPositions(0, 'to be OK positions'); + }) + .then(function() { + assertCells(0, 'to be OK cells'); + }) + .catch(failTest) + .then(done); + }); + + it('@gl mesh3d should be invisible when the vertex arrays missing', function(done) { + Plotly.plot(gd, [{ + type: 'mesh3d' + }]) + .then(function() { + assertVisibility(false, 'to be visible'); + }) + .catch(failTest) + .then(done); + }); + + it('@gl mesh3d should be invisible when the vertex arrays are not arrays - number case', function(done) { + Plotly.plot(gd, [{ + x: [0, 1, 0.5, 0.5], + y: [0, 0.5, 1, 0.5], + z: 1, + i: [0, 0, 0, 1], + j: [1, 1, 2, 2], + k: [2, 3, 3, 3], + type: 'mesh3d' + }]) + .then(function() { + assertVisibility(false, 'to be visible'); + }) + .catch(failTest) + .then(done); + }); + + it('@gl mesh3d should be invisible when the vertex arrays are not arrays - boolean case', function(done) { + Plotly.plot(gd, [{ + x: [0, 1, 0.5, 0.5], + y: [0, 0.5, 1, 0.5], + z: true, + i: [0, 0, 0, 1], + j: [1, 1, 2, 2], + k: [2, 3, 3, 3], + type: 'mesh3d' + }]) + .then(function() { + assertVisibility(false, 'to be visible'); + }) + .catch(failTest) + .then(done); + }); + + it('@gl mesh3d should be invisible when the vertex arrays are not arrays - object case', function(done) { + Plotly.plot(gd, [{ + x: [0, 1, 0.5, 0.5], + y: [0, 0.5, 1, 0.5], + z: {}, + i: [0, 0, 0, 1], + j: [1, 1, 2, 2], + k: [2, 3, 3, 3], + type: 'mesh3d' + }]) + .then(function() { + assertVisibility(false, 'to be visible'); + }) + .catch(failTest) + .then(done); + }); + + it('@gl mesh3d should be invisible when the vertex arrays are not arrays - string case', function(done) { + Plotly.plot(gd, [{ + x: [0, 1, 0.5, 0.5], + y: [0, 0.5, 1, 0.5], + z: '[0, 0.5, 0.5, 1]', + i: [0, 0, 0, 1], + j: [1, 1, 2, 2], + k: [2, 3, 3, 3], + type: 'mesh3d' + }]) + .then(function() { + assertVisibility(false, 'to be visible'); + }) + .catch(failTest) + .then(done); + }); + + it('@gl mesh3d should be invisible when the index arrays are not arrays - string case', function(done) { + Plotly.plot(gd, [{ + x: [0, 1, 0.5, 0.5], + y: [0, 0.5, 1, 0.5], + z: [0, 0.5, 0.5, 1], + i: [0, 0, 0, 1], + j: [1, 1, 2, 2], + k: '[2, 3, 3, 3]', + type: 'mesh3d' + }]) + .then(function() { + assertVisibility(false, 'to be visible'); + }) + .catch(failTest) + .then(done); + }); + + it('@gl mesh3d should be invisible when the index arrays are not arrays - object case', function(done) { + Plotly.plot(gd, [{ + x: [0, 1, 0.5, 0.5], + y: [0, 0.5, 1, 0.5], + z: [0, 0.5, 0.5, 1], + i: [0, 0, 0, 1], + j: [1, 1, 2, 2], + k: {}, + type: 'mesh3d' + }]) + .then(function() { + assertVisibility(false, 'to be visible'); + }) + .catch(failTest) + .then(done); + }); + + it('@gl mesh3d should be invisible when the index arrays are not arrays - boolean case', function(done) { + Plotly.plot(gd, [{ + x: [0, 1, 0.5, 0.5], + y: [0, 0.5, 1, 0.5], + z: [0, 0.5, 0.5, 1], + i: [0, 0, 0, 1], + j: [1, 1, 2, 2], + k: true, + type: 'mesh3d' + }]) + .then(function() { + assertVisibility(false, 'to be visible'); + }) + .catch(failTest) + .then(done); + }); + + it('@gl mesh3d should be invisible when the index arrays are not arrays - number case', function(done) { + Plotly.plot(gd, [{ + x: [0, 1, 0.5, 0.5], + y: [0, 0.5, 1, 0.5], + z: [0, 0.5, 0.5, 1], + i: [0, 0, 0, 1], + j: [1, 1, 2, 2], + k: 1, + type: 'mesh3d' + }]) + .then(function() { + assertVisibility(false, 'to be visible'); + }) + .catch(failTest) + .then(done); + }); + }); });