diff --git a/package-lock.json b/package-lock.json index d0c338db239..d5e1167a128 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4646,9 +4646,9 @@ } }, "gl-plot3d": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/gl-plot3d/-/gl-plot3d-2.2.2.tgz", - "integrity": "sha512-is8RoDVUEbUM7kJ2qjhKJlfGLECH3ML9pTCW1V7ylUdmUACmcZ4lzJrQr/NIRkHC5WcUNOp3QJKPjBND3ngZ2A==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/gl-plot3d/-/gl-plot3d-2.3.0.tgz", + "integrity": "sha512-qg269QiLpaw16d2D5Gz9fa8vsLcA8kbX/cv1u9S7BsH6jD9qGYxsY8iWJ8ea9/68WhPS5En2kUavkXINkmHsOQ==", "requires": { "3d-view": "^2.0.0", "a-big-triangle": "^1.0.3", diff --git a/package.json b/package.json index 13dc9f775a7..b30b2848d01 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ "gl-mat4": "^1.2.0", "gl-mesh3d": "^2.1.1", "gl-plot2d": "^1.4.2", - "gl-plot3d": "^2.2.2", + "gl-plot3d": "^2.3.0", "gl-pointcloud2d": "^1.0.2", "gl-scatter3d": "^1.2.2", "gl-select-box": "^1.0.3", diff --git a/src/components/modebar/buttons.js b/src/components/modebar/buttons.js index 5a9fb54164c..bc9e259f1fc 100644 --- a/src/components/modebar/buttons.js +++ b/src/components/modebar/buttons.js @@ -344,17 +344,27 @@ function handleCamera3d(gd, ev) { for(var i = 0; i < sceneIds.length; i++) { var sceneId = sceneIds[i]; - var key = sceneId + '.camera'; + var camera = sceneId + '.camera'; + var aspectratio = sceneId + '.aspectratio'; var scene = fullLayout[sceneId]._scene; + var didUpdate; if(attr === 'resetLastSave') { - aobj[key + '.up'] = scene.viewInitial.up; - aobj[key + '.eye'] = scene.viewInitial.eye; - aobj[key + '.center'] = scene.viewInitial.center; + aobj[camera + '.up'] = scene.viewInitial.up; + aobj[camera + '.eye'] = scene.viewInitial.eye; + aobj[camera + '.center'] = scene.viewInitial.center; + didUpdate = true; } else if(attr === 'resetDefault') { - aobj[key + '.up'] = null; - aobj[key + '.eye'] = null; - aobj[key + '.center'] = null; + aobj[camera + '.up'] = null; + aobj[camera + '.eye'] = null; + aobj[camera + '.center'] = null; + didUpdate = true; + } + + if(didUpdate) { + aobj[aspectratio + '.x'] = scene.viewInitial.aspectratio.x; + aobj[aspectratio + '.y'] = scene.viewInitial.aspectratio.y; + aobj[aspectratio + '.z'] = scene.viewInitial.aspectratio.z; } } diff --git a/src/plot_api/subroutines.js b/src/plot_api/subroutines.js index df87fbcd474..f8cfecddc7e 100644 --- a/src/plot_api/subroutines.js +++ b/src/plot_api/subroutines.js @@ -578,8 +578,7 @@ exports.doCamera = function(gd) { var sceneLayout = fullLayout[sceneIds[i]]; var scene = sceneLayout._scene; - var cameraData = sceneLayout.camera; - scene.setCamera(cameraData); + scene.setViewport(sceneLayout); } }; diff --git a/src/plots/gl3d/scene.js b/src/plots/gl3d/scene.js index 8ef7cc2d424..d24abe7b41b 100644 --- a/src/plots/gl3d/scene.js +++ b/src/plots/gl3d/scene.js @@ -9,8 +9,10 @@ 'use strict'; -var createCamera = require('gl-plot3d').createCamera; -var createPlot = require('gl-plot3d').createScene; +var glPlot3d = require('gl-plot3d'); +var createCamera = glPlot3d.createCamera; +var createPlot = glPlot3d.createScene; + var getContext = require('webgl-context'); var passiveSupported = require('has-passive-events'); @@ -246,10 +248,10 @@ function tryCreatePlot(scene, cameraObject, pixelRatio, canvas, gl) { return failed < 2; } -function initializeGLPlot(scene, pixelRatio, canvas, gl) { +function initializeGLPlot(scene, canvas, gl) { scene.initializeGLCamera(); - var success = tryCreatePlot(scene, scene.camera, pixelRatio, canvas, gl); + var success = tryCreatePlot(scene, scene.camera, scene.pixelRatio, canvas, gl); /* * createPlot will throw when webgl is not enabled in the client. * Lets return an instance of the module with all functions noop'd. @@ -259,13 +261,29 @@ function initializeGLPlot(scene, pixelRatio, canvas, gl) { if(!success) return showNoWebGlMsg(scene); var gd = scene.graphDiv; + var layout = gd.layout; + + var makeUpdate = function() { + var update = {}; + + if(scene.isCameraChanged(layout)) { + // camera updates + update[scene.id + '.camera'] = scene.getCamera(); + } + + if(scene.isAspectChanged(layout)) { + // scene updates + update[scene.id + '.aspectratio'] = scene.glplot.getAspectratio(); + } + + return update; + }; var relayoutCallback = function(scene) { if(scene.fullSceneLayout.dragmode === false) return; - var update = {}; - update[scene.id + '.camera'] = getLayoutCamera(scene.camera); - scene.saveCamera(gd.layout); + var update = makeUpdate(); + scene.saveLayout(layout); scene.graphDiv.emit('plotly_relayout', update); }; @@ -273,8 +291,18 @@ function initializeGLPlot(scene, pixelRatio, canvas, gl) { relayoutCallback(scene); }); - scene.glplot.canvas.addEventListener('wheel', function() { + scene.glplot.canvas.addEventListener('wheel', function(e) { if(gd._context._scrollZoom.gl3d) { + if(scene.glplot.camera._ortho) { + var s = (e.deltaX > e.deltaY) ? 1.1 : 1.0 / 1.1; + var o = scene.glplot.getAspectratio(); + scene.glplot.setAspectratio({ + x: s * o.x, + y: s * o.y, + z: s * o.z + }); + } + relayoutCallback(scene); } }, passiveSupported ? {passive: false} : false); @@ -283,8 +311,7 @@ function initializeGLPlot(scene, pixelRatio, canvas, gl) { if(scene.fullSceneLayout.dragmode === false) return; if(scene.camera.mouseListener.buttons === 0) return; - var update = {}; - update[scene.id + '.camera'] = getLayoutCamera(scene.camera); + var update = makeUpdate(); scene.graphDiv.emit('plotly_relayouting', update); }); @@ -366,7 +393,7 @@ function Scene(options, fullLayout) { this.convertAnnotations = Registry.getComponentMethod('annotations3d', 'convert'); this.drawAnnotations = Registry.getComponentMethod('annotations3d', 'draw'); - initializeGLPlot(this, this.pixelRatio); + initializeGLPlot(this); } var proto = Scene.prototype; @@ -390,8 +417,7 @@ proto.recoverContext = function() { var scene = this; var gl = this.glplot.gl; var canvas = this.glplot.canvas; - var camera = this.glplot.camera; - var pixelRatio = this.glplot.pixelRatio; + this.glplot.dispose(); function tryRecover() { @@ -399,7 +425,7 @@ proto.recoverContext = function() { requestAnimationFrame(tryRecover); return; } - if(!initializeGLPlot(scene, camera, pixelRatio, canvas, gl)) { + if(!initializeGLPlot(scene, canvas, gl)) { Lib.error('Catastrophic and unrecoverable WebGL error. Context lost.'); return; } @@ -496,7 +522,7 @@ proto.plot = function(sceneData, fullLayout, layout) { this.spikeOptions.merge(fullSceneLayout); // Update camera and camera mode - this.setCamera(fullSceneLayout.camera); + this.setViewport(fullSceneLayout); this.updateFx(fullSceneLayout.dragmode, fullSceneLayout.hovermode); this.camera.enableWheel = this.graphDiv._context._scrollZoom.gl3d; @@ -720,8 +746,16 @@ proto.plot = function(sceneData, fullLayout, layout) { * Finally assign the computed aspecratio to the glplot module. This will have an effect * on the next render cycle. */ - this.glplot.aspect = aspectRatio; - + this.glplot.setAspectratio(fullSceneLayout.aspectratio); + + // save 'initial' camera view settings for modebar button + if(!this.viewInitial.aspectratio) { + this.viewInitial.aspectratio = { + x: fullSceneLayout.aspectratio.x, + y: fullSceneLayout.aspectratio.y, + z: fullSceneLayout.aspectratio.z + }; + } // Update frame position for multi plots var domain = fullSceneLayout.domain || null; @@ -751,9 +785,9 @@ proto.destroy = function() { this.glplot = null; }; -// getOrbitCamera :: plotly_coords -> orbit_camera_coords +// getCameraArrays :: plotly_coords -> gl-plot3d_coords // inverse of getLayoutCamera -function getOrbitCamera(camera) { +function getCameraArrays(camera) { return [ [camera.eye.x, camera.eye.y, camera.eye.z], [camera.center.x, camera.center.y, camera.center.z], @@ -761,8 +795,8 @@ function getOrbitCamera(camera) { ]; } -// getLayoutCamera :: orbit_camera_coords -> plotly_coords -// inverse of getOrbitCamera +// getLayoutCamera :: gl-plot3d_coords -> plotly_coords +// inverse of getCameraArrays function getLayoutCamera(camera) { return { up: {x: camera.up[0], y: camera.up[1], z: camera.up[2]}, @@ -772,15 +806,18 @@ function getLayoutCamera(camera) { }; } -// get camera position in plotly coords from 'orbit-camera' coords -proto.getCamera = function getCamera() { +// get camera position in plotly coords from 'gl-plot3d' coords +proto.getCamera = function() { this.glplot.camera.view.recalcMatrix(this.camera.view.lastT()); return getLayoutCamera(this.glplot.camera); }; -// set camera position with a set of plotly coords -proto.setCamera = function setCamera(cameraData) { - this.glplot.camera.lookAt.apply(this, getOrbitCamera(cameraData)); +// set gl-plot3d camera position and scene aspects with a set of plotly coords +proto.setViewport = function(sceneLayout) { + var cameraData = sceneLayout.camera; + + this.glplot.camera.lookAt.apply(this, getCameraArrays(cameraData)); + this.glplot.setAspectratio(sceneLayout.aspectratio); var newOrtho = (cameraData.projection.type === 'orthographic'); var oldOrtho = this.glplot.camera._ortho; @@ -788,8 +825,6 @@ proto.setCamera = function setCamera(cameraData) { if(newOrtho !== oldOrtho) { this.glplot.redraw(); - var pixelRatio = this.glplot.pixelRatio; - var RGBA = this.glplot.clearColor; this.glplot.gl.clearColor( RGBA[0], RGBA[1], RGBA[2], RGBA[3] @@ -801,18 +836,15 @@ proto.setCamera = function setCamera(cameraData) { this.glplot.dispose(); - initializeGLPlot(this, pixelRatio); + initializeGLPlot(this); this.glplot.camera._ortho = newOrtho; } }; -// save camera to user layout (i.e. gd.layout) -proto.saveCamera = function saveCamera(layout) { - var fullLayout = this.fullLayout; +proto.isCameraChanged = function(layout) { var cameraData = this.getCamera(); var cameraNestedProp = Lib.nestedProperty(layout, this.id + '.camera'); var cameraDataLastSave = cameraNestedProp.get(); - var hasChanged = false; function same(x, y, i, j) { var vectors = ['up', 'center', 'eye']; @@ -820,13 +852,14 @@ proto.saveCamera = function saveCamera(layout) { return y[vectors[i]] && (x[vectors[i]][components[j]] === y[vectors[i]][components[j]]); } + var changed = false; if(cameraDataLastSave === undefined) { - hasChanged = true; + changed = true; } else { for(var i = 0; i < 3; i++) { for(var j = 0; j < 3; j++) { if(!same(cameraData, cameraDataLastSave, i, j)) { - hasChanged = true; + changed = true; break; } } @@ -835,19 +868,75 @@ proto.saveCamera = function saveCamera(layout) { if(!cameraDataLastSave.projection || ( cameraData.projection && cameraData.projection.type !== cameraDataLastSave.projection.type)) { - hasChanged = true; + changed = true; } } + return changed; +}; + +proto.isAspectChanged = function(layout) { + var aspectData = this.glplot.getAspectratio(); + var aspectNestedProp = Lib.nestedProperty(layout, this.id + '.aspectratio'); + var aspectDataLastSave = aspectNestedProp.get(); + + return ( + aspectDataLastSave === undefined || ( + aspectDataLastSave.x !== aspectData.x || + aspectDataLastSave.y !== aspectData.y || + aspectDataLastSave.z !== aspectData.z + )); +}; + +// save camera to user layout (i.e. gd.layout) +proto.saveLayout = function(layout) { + var fullLayout = this.fullLayout; + + var cameraData; + var cameraNestedProp; + var cameraDataLastSave; + + var aspectData; + var aspectNestedProp; + var aspectDataLastSave; + + var cameraChanged = this.isCameraChanged(layout); + var aspectChanged = this.isAspectChanged(layout); + + var hasChanged = cameraChanged || aspectChanged; if(hasChanged) { var preGUI = {}; - preGUI[this.id + '.camera'] = cameraDataLastSave; + if(cameraChanged) { + cameraData = this.getCamera(); + cameraNestedProp = Lib.nestedProperty(layout, this.id + '.camera'); + cameraDataLastSave = cameraNestedProp.get(); + + preGUI[this.id + '.camera'] = cameraDataLastSave; + } + if(aspectChanged) { + aspectData = this.glplot.getAspectratio(); + aspectNestedProp = Lib.nestedProperty(layout, this.id + '.aspectratio'); + aspectDataLastSave = aspectNestedProp.get(); + + preGUI[this.id + '.aspectratio'] = aspectDataLastSave; + } Registry.call('_storeDirectGUIEdit', layout, fullLayout._preGUI, preGUI); - cameraNestedProp.set(cameraData); + if(cameraChanged) { + cameraNestedProp.set(cameraData); - var cameraFullNP = Lib.nestedProperty(fullLayout, this.id + '.camera'); - cameraFullNP.set(cameraData); + var cameraFullNP = Lib.nestedProperty(fullLayout, this.id + '.camera'); + cameraFullNP.set(cameraData); + } + + if(aspectChanged) { + aspectNestedProp.set(aspectData); + + var aspectFullNP = Lib.nestedProperty(fullLayout, this.id + '.aspectratio'); + aspectFullNP.set(aspectData); + + this.glplot.redraw(); + } } return hasChanged; diff --git a/test/jasmine/tests/gl3d_plot_interact_test.js b/test/jasmine/tests/gl3d_plot_interact_test.js index 0f3f80acacb..91915c621f8 100644 --- a/test/jasmine/tests/gl3d_plot_interact_test.js +++ b/test/jasmine/tests/gl3d_plot_interact_test.js @@ -412,7 +412,7 @@ describe('Test gl3d plots', function() { }); }); -describe('Test gl3d modebar handlers', function() { +describe('Test gl3d modebar handlers - perspective case', function() { var gd, modeBar; function assertScenes(cont, attr, val) { @@ -444,8 +444,17 @@ describe('Test gl3d modebar handlers', function() { { type: 'surface', scene: 'scene2' } ], layout: { - scene: { camera: { eye: { x: 0.1, y: 0.1, z: 1 }}}, - scene2: { camera: { eye: { x: 2.5, y: 2.5, z: 2.5 }}} + scene: { + camera: { + eye: { x: 0.1, y: 0.1, z: 1 } + } + }, + scene2: { + camera: { + eye: { x: 2.5, y: 2.5, z: 2.5 } + }, + aspectratio: { x: 3, y: 2, z: 1 } + } } }; @@ -566,6 +575,242 @@ describe('Test gl3d modebar handlers', function() { buttonDefault.click(); }); + it('@gl button resetCameraDefault3d should reset to initial aspectratios', function(done) { + var buttonDefault = selectButton(modeBar, 'resetCameraDefault3d'); + + expect(gd._fullLayout.scene._scene.viewInitial.aspectratio).toEqual({ x: 1, y: 1, z: 1 }); + expect(gd._fullLayout.scene2._scene.viewInitial.aspectratio).toEqual({ x: 3, y: 2, z: 1 }); + + gd.once('plotly_relayout', function() { + expect(gd._fullLayout.scene._scene.glplot.getAspectratio().x).toBeCloseTo(1); + expect(gd._fullLayout.scene._scene.glplot.getAspectratio().y).toBeCloseTo(1); + expect(gd._fullLayout.scene._scene.glplot.getAspectratio().z).toBeCloseTo(1); + expect(gd._fullLayout.scene2._scene.glplot.getAspectratio().x).toBeCloseTo(3); + expect(gd._fullLayout.scene2._scene.glplot.getAspectratio().y).toBeCloseTo(2); + expect(gd._fullLayout.scene2._scene.glplot.getAspectratio().z).toBeCloseTo(1); + + done(); + }); + + buttonDefault.click(); + }); + + it('@gl button resetCameraLastSave3d should reset to initial aspectratios', function(done) { + var buttonDefault = selectButton(modeBar, 'resetCameraDefault3d'); + + expect(gd._fullLayout.scene._scene.viewInitial.aspectratio).toEqual({ x: 1, y: 1, z: 1 }); + expect(gd._fullLayout.scene2._scene.viewInitial.aspectratio).toEqual({ x: 3, y: 2, z: 1 }); + + gd.once('plotly_relayout', function() { + expect(gd._fullLayout.scene._scene.glplot.getAspectratio().x).toBeCloseTo(1); + expect(gd._fullLayout.scene._scene.glplot.getAspectratio().y).toBeCloseTo(1); + expect(gd._fullLayout.scene._scene.glplot.getAspectratio().z).toBeCloseTo(1); + expect(gd._fullLayout.scene2._scene.glplot.getAspectratio().x).toBeCloseTo(3); + expect(gd._fullLayout.scene2._scene.glplot.getAspectratio().y).toBeCloseTo(2); + expect(gd._fullLayout.scene2._scene.glplot.getAspectratio().z).toBeCloseTo(1); + + done(); + }); + + buttonDefault.click(); + }); + + it('@gl button resetCameraLastSave3d should reset camera to default', function(done) { + var buttonDefault = selectButton(modeBar, 'resetCameraDefault3d'); + var buttonLastSave = selectButton(modeBar, 'resetCameraLastSave3d'); + + Plotly.relayout(gd, { + 'scene.camera.eye.z': 4, + 'scene2.camera.eye.z': 5 + }) + .then(function() { + assertCameraEye(gd._fullLayout.scene, 0.1, 0.1, 4); + assertCameraEye(gd._fullLayout.scene2, 2.5, 2.5, 5); + + return new Promise(function(resolve) { + gd.once('plotly_relayout', resolve); + buttonLastSave.click(); + }); + }) + .then(function() { + assertCameraEye(gd._fullLayout.scene, 0.1, 0.1, 1); + assertCameraEye(gd._fullLayout.scene2, 2.5, 2.5, 2.5); + + return new Promise(function(resolve) { + gd.once('plotly_relayout', resolve); + buttonDefault.click(); + }); + }) + .then(function() { + assertCameraEye(gd._fullLayout.scene, 1.25, 1.25, 1.25); + assertCameraEye(gd._fullLayout.scene2, 1.25, 1.25, 1.25); + + return new Promise(function(resolve) { + gd.once('plotly_relayout', resolve); + buttonLastSave.click(); + }); + }) + .then(function() { + assertCameraEye(gd._fullLayout.scene, 0.1, 0.1, 1); + assertCameraEye(gd._fullLayout.scene2, 2.5, 2.5, 2.5); + + delete gd._fullLayout.scene._scene.viewInitial; + delete gd._fullLayout.scene2._scene.viewInitial; + + Plotly.relayout(gd, { + 'scene.bgcolor': '#d3d3d3', + 'scene.camera.eye.z': 4, + 'scene2.camera.eye.z': 5 + }); + }) + .then(function() { + assertCameraEye(gd._fullLayout.scene, 0.1, 0.1, 4); + assertCameraEye(gd._fullLayout.scene2, 2.5, 2.5, 5); + + return new Promise(function(resolve) { + gd.once('plotly_relayout', resolve); + buttonDefault.click(); + }); + }) + .then(function() { + assertCameraEye(gd._fullLayout.scene, 1.25, 1.25, 1.25); + assertCameraEye(gd._fullLayout.scene2, 1.25, 1.25, 1.25); + + return new Promise(function(resolve) { + gd.once('plotly_relayout', resolve); + buttonLastSave.click(); + }); + }) + .then(function() { + assertCameraEye(gd._fullLayout.scene, 0.1, 0.1, 4); + assertCameraEye(gd._fullLayout.scene2, 2.5, 2.5, 5); + }) + .then(done); + }); +}); + + +describe('Test gl3d modebar handlers - orthographic case', function() { + var gd, modeBar; + + function assertScenes(cont, attr, val) { + var sceneIds = cont._subplots.gl3d; + + sceneIds.forEach(function(sceneId) { + var thisVal = Lib.nestedProperty(cont[sceneId], attr).get(); + expect(thisVal).toEqual(val); + }); + } + + function assertCameraEye(sceneLayout, eyeX, eyeY, eyeZ) { + expect(sceneLayout.camera.eye.x).toEqual(eyeX); + expect(sceneLayout.camera.eye.y).toEqual(eyeY); + expect(sceneLayout.camera.eye.z).toEqual(eyeZ); + + var camera = sceneLayout._scene.getCamera(); + expect(camera.eye.x).toBeCloseTo(eyeX); + expect(camera.eye.y).toBeCloseTo(eyeY); + expect(camera.eye.z).toBeCloseTo(eyeZ); + } + + beforeEach(function(done) { + gd = createGraphDiv(); + + var mock = { + data: [ + { type: 'scatter3d' }, + { type: 'surface', scene: 'scene2' } + ], + layout: { + scene: { + camera: { + eye: { x: 0.1, y: 0.1, z: 1 }, + projection: {type: 'orthographic'} + } + }, + scene2: { + camera: { + eye: { x: 2.5, y: 2.5, z: 2.5 }, + projection: {type: 'orthographic'} + }, + aspectratio: { x: 3, y: 2, z: 1 } + } + } + }; + + Plotly.plot(gd, mock) + .then(delay(20)) + .then(function() { + modeBar = gd._fullLayout._modeBar; + }) + .then(done); + }); + + afterEach(function() { + Plotly.purge(gd); + destroyGraphDiv(); + }); + + it('@gl button resetCameraDefault3d should reset camera to default', function(done) { + var buttonDefault = selectButton(modeBar, 'resetCameraDefault3d'); + + expect(gd._fullLayout.scene._scene.viewInitial.eye).toEqual({ x: 0.1, y: 0.1, z: 1 }); + expect(gd._fullLayout.scene2._scene.viewInitial.eye).toEqual({ x: 2.5, y: 2.5, z: 2.5 }); + + gd.once('plotly_relayout', function() { + assertScenes(gd._fullLayout, 'camera.eye.x', 1.25); + assertScenes(gd._fullLayout, 'camera.eye.y', 1.25); + assertScenes(gd._fullLayout, 'camera.eye.z', 1.25); + + expect(gd._fullLayout.scene._scene.getCamera().eye.z).toBeCloseTo(1.25); + expect(gd._fullLayout.scene2._scene.getCamera().eye.z).toBeCloseTo(1.25); + + done(); + }); + + buttonDefault.click(); + }); + + it('@gl button resetCameraDefault3d should reset to initial aspectratios', function(done) { + var buttonDefault = selectButton(modeBar, 'resetCameraDefault3d'); + + expect(gd._fullLayout.scene._scene.viewInitial.aspectratio).toEqual({ x: 1, y: 1, z: 1 }); + expect(gd._fullLayout.scene2._scene.viewInitial.aspectratio).toEqual({ x: 3, y: 2, z: 1 }); + + gd.once('plotly_relayout', function() { + expect(gd._fullLayout.scene._scene.glplot.getAspectratio().x).toBeCloseTo(1); + expect(gd._fullLayout.scene._scene.glplot.getAspectratio().y).toBeCloseTo(1); + expect(gd._fullLayout.scene._scene.glplot.getAspectratio().z).toBeCloseTo(1); + expect(gd._fullLayout.scene2._scene.glplot.getAspectratio().x).toBeCloseTo(3); + expect(gd._fullLayout.scene2._scene.glplot.getAspectratio().y).toBeCloseTo(2); + expect(gd._fullLayout.scene2._scene.glplot.getAspectratio().z).toBeCloseTo(1); + + done(); + }); + + buttonDefault.click(); + }); + + it('@gl button resetCameraLastSave3d should reset to initial aspectratios', function(done) { + var buttonDefault = selectButton(modeBar, 'resetCameraDefault3d'); + + expect(gd._fullLayout.scene._scene.viewInitial.aspectratio).toEqual({ x: 1, y: 1, z: 1 }); + expect(gd._fullLayout.scene2._scene.viewInitial.aspectratio).toEqual({ x: 3, y: 2, z: 1 }); + + gd.once('plotly_relayout', function() { + expect(gd._fullLayout.scene._scene.glplot.getAspectratio().x).toBeCloseTo(1); + expect(gd._fullLayout.scene._scene.glplot.getAspectratio().y).toBeCloseTo(1); + expect(gd._fullLayout.scene._scene.glplot.getAspectratio().z).toBeCloseTo(1); + expect(gd._fullLayout.scene2._scene.glplot.getAspectratio().x).toBeCloseTo(3); + expect(gd._fullLayout.scene2._scene.glplot.getAspectratio().y).toBeCloseTo(2); + expect(gd._fullLayout.scene2._scene.glplot.getAspectratio().z).toBeCloseTo(1); + + done(); + }); + + buttonDefault.click(); + }); + it('@gl button resetCameraLastSave3d should reset camera to default', function(done) { var buttonDefault = selectButton(modeBar, 'resetCameraDefault3d'); var buttonLastSave = selectButton(modeBar, 'resetCameraLastSave3d'); @@ -773,22 +1018,28 @@ describe('Test gl3d drag and wheel interactions', function() { return Plotly.relayout(gd, {'scene.dragmode': 'orbit', 'scene2.dragmode': 'turntable'}); }) .then(function() { - expect(relayoutCallback).toHaveBeenCalledTimes(1); - relayoutCallback.calls.reset(); + _assertAndReset(1); return drag({node: sceneTarget, pos0: [0, 0], posN: [100, 100], noCover: true}); }) .then(function() { + _assertAndReset(1); + return drag({node: sceneTarget2, pos0: [0, 0], posN: [100, 100], noCover: true}); }) .then(function() { - _assertAndReset(2); + _assertAndReset(1); + return Plotly.plot(gd, [], {}, {scrollZoom: false}); }) .then(function() { + _assertAndReset(0); + return scroll(sceneTarget); }) .then(function() { + _assertAndReset(0); + return scroll(sceneTarget2); }) .then(function() { @@ -796,13 +1047,17 @@ describe('Test gl3d drag and wheel interactions', function() { return Plotly.plot(gd, [], {}, {scrollZoom: 'gl3d'}); }) .then(function() { + _assertAndReset(0); + return scroll(sceneTarget); }) .then(function() { + _assertAndReset(1); + return scroll(sceneTarget2); }) .then(function() { - _assertAndReset(2); + _assertAndReset(1); }) .catch(failTest) .then(done); @@ -877,22 +1132,28 @@ describe('Test gl3d drag and wheel interactions', function() { return Plotly.relayout(gd, {'scene.dragmode': 'orbit', 'scene2.dragmode': 'turntable'}); }) .then(function() { - expect(relayoutCallback).toHaveBeenCalledTimes(1); - relayoutCallback.calls.reset(); + _assertAndReset(1); return drag({node: sceneTarget, pos0: [0, 0], posN: [100, 100], noCover: true}); }) .then(function() { + _assertAndReset(1); + return drag({node: sceneTarget2, pos0: [0, 0], posN: [100, 100], noCover: true}); }) .then(function() { - _assertAndReset(2); + _assertAndReset(1); + return Plotly.plot(gd, [], {}, {scrollZoom: false}); }) .then(function() { + _assertAndReset(0); + return scroll(sceneTarget); }) .then(function() { + _assertAndReset(0); + return scroll(sceneTarget2); }) .then(function() { @@ -902,17 +1163,180 @@ describe('Test gl3d drag and wheel interactions', function() { .then(function() { return scroll(sceneTarget); }) + .then(function() { + _assertAndReset(1); + + return scroll(sceneTarget2); + }) + .then(function() { + _assertAndReset(1); + }) + .catch(failTest) + .then(done); + }); + + it('@gl should update the scene aspectratio when zooming with scroll wheel i.e. orthographic case', function(done) { + var sceneLayout, sceneLayout2, sceneTarget, sceneTarget2; + + var mock = { + data: [ + { type: 'scatter3d', x: [1, 2, 3], y: [2, 3, 1], z: [3, 1, 2] }, + { type: 'surface', scene: 'scene2', x: [1, 2], y: [2, 1], z: [[1, 2], [2, 1]] } + ], + layout: { + scene: { camera: { projection: {type: 'orthographic'}}}, + scene2: { camera: { projection: {type: 'orthographic'}}, aspectratio: { x: 3, y: 2, z: 1 }} + } + }; + + var aspectratio; + var relayoutEvent; + var relayoutCnt = 0; + + Plotly.plot(gd, mock) + .then(function() { + gd.on('plotly_relayout', function(e) { + relayoutCnt++; + relayoutEvent = e; + }); + + sceneLayout = gd._fullLayout.scene; + sceneLayout2 = gd._fullLayout.scene2; + sceneTarget = gd.querySelector('.svg-container .gl-container #scene canvas'); + sceneTarget2 = gd.querySelector('.svg-container .gl-container #scene2 canvas'); + + expect(sceneLayout.aspectratio).toEqual({x: 1, y: 1, z: 1}); + expect(sceneLayout2.aspectratio).toEqual({x: 3, y: 2, z: 1}); + }) + .then(function() { + return scroll(sceneTarget); + }) + .then(function() { + expect(relayoutCnt).toEqual(1); + + aspectratio = relayoutEvent['scene.aspectratio']; + expect(aspectratio.x).toBeCloseTo(0.909, 3, 'aspectratio.x'); + expect(aspectratio.y).toBeCloseTo(0.909, 3, 'aspectratio.y'); + expect(aspectratio.z).toBeCloseTo(0.909, 3, 'aspectratio.z'); + }) .then(function() { return scroll(sceneTarget2); }) .then(function() { - _assertAndReset(2); + expect(relayoutCnt).toEqual(2); + + aspectratio = relayoutEvent['scene2.aspectratio']; + expect(aspectratio.x).toBeCloseTo(2.727, 3, 'aspectratio.x'); + expect(aspectratio.y).toBeCloseTo(1.818, 3, 'aspectratio.y'); + expect(aspectratio.z).toBeCloseTo(0.909, 3, 'aspectratio.z'); }) .catch(failTest) .then(done); }); - it('@gl should fire plotly_relayouting events', function(done) { + it('@gl should fire plotly_relayouting events when dragged - perspective case', function(done) { + var sceneTarget, relayoutEvent; + + var nsteps = 10; + var relayoutCnt = 0; + var events = []; + + var mock = { + data: [ + { type: 'scatter3d', x: [1, 2, 3], y: [2, 3, 1], z: [3, 1, 2] } + ], + layout: { + scene: { camera: { projection: {type: 'perspective'}, eye: { x: 0.1, y: 0.1, z: 1 }}}, + width: 400, height: 400 + } + }; + + Plotly.plot(gd, mock) + .then(function() { + gd.on('plotly_relayout', function(e) { + relayoutCnt++; + relayoutEvent = e; + }); + gd.on('plotly_relayouting', function(e) { + events.push(e); + }); + + sceneTarget = gd.querySelector('.svg-container .gl-container #scene canvas'); + + return drag({ + node: sceneTarget, + pos0: [200, 200], + posN: [100, 100], + nsteps: nsteps, + buttons: 1, + noCover: true + }); + }) + .then(function() { + expect(events.length).toEqual(nsteps); + expect(relayoutCnt).toEqual(1); + + Object.keys(relayoutEvent).sort().forEach(function(key) { + expect(Object.keys(events[0])).toContain(key); + expect(key).not.toBe('scene.aspectratio'); + }); + }) + .catch(failTest) + .then(done); + }); + + it('@gl should fire plotly_relayouting events when dragged - orthographic case', function(done) { + var sceneTarget, relayoutEvent; + + var nsteps = 10; + var relayoutCnt = 0; + var events = []; + + var mock = { + data: [ + { type: 'scatter3d', x: [1, 2, 3], y: [2, 3, 1], z: [3, 1, 2] } + ], + layout: { + scene: { camera: { projection: {type: 'orthographic'}, eye: { x: 0.1, y: 0.1, z: 1 }}}, + width: 400, height: 400 + } + }; + + Plotly.plot(gd, mock) + .then(function() { + gd.on('plotly_relayout', function(e) { + relayoutCnt++; + relayoutEvent = e; + }); + gd.on('plotly_relayouting', function(e) { + events.push(e); + }); + + sceneTarget = gd.querySelector('.svg-container .gl-container #scene canvas'); + + return drag({ + node: sceneTarget, + pos0: [200, 200], + posN: [100, 100], + nsteps: nsteps, + buttons: 1, + noCover: true + }); + }) + .then(function() { + expect(events.length).toEqual(nsteps); + expect(relayoutCnt).toEqual(1); + Object.keys(relayoutEvent).sort().forEach(function(key) { + expect(Object.keys(events[0])).toContain(key); + expect(key).not.toBe('scene.aspectratio'); + }); + }) + .catch(failTest) + .then(done); + }); + + + it('@gl should fire plotly_relayouting events when dragged - orthographic case', function(done) { var sceneTarget, relayoutEvent; var nsteps = 10; @@ -1169,7 +1593,10 @@ describe('Test gl3d annotations', function() { var camera = scene.getCamera(); camera.eye = {x: x, y: y, z: z}; - scene.setCamera(camera); + scene.setViewport({ + camera: camera, + aspectratio: gd._fullLayout.scene.aspectratio + }); // need a fairly long delay to let the camera update here // 300 was not robust for me (AJ), 500 seems to be. return delay(500)(); diff --git a/test/jasmine/tests/plot_api_react_test.js b/test/jasmine/tests/plot_api_react_test.js index b3e047d1283..e622f3bb175 100644 --- a/test/jasmine/tests/plot_api_react_test.js +++ b/test/jasmine/tests/plot_api_react_test.js @@ -1968,11 +1968,14 @@ describe('Test Plotly.react + interactions under uirevision:', function() { function _mouseup() { var sceneLayout = gd._fullLayout.scene; var cameraOld = sceneLayout.camera; - sceneLayout._scene.setCamera({ - projection: {type: 'perspective'}, - eye: {x: 2, y: 2, z: 2}, - center: cameraOld.center, - up: cameraOld.up + sceneLayout._scene.setViewport({ + camera: { + projection: {type: 'perspective'}, + eye: {x: 2, y: 2, z: 2}, + center: cameraOld.center, + up: cameraOld.up + }, + aspectratio: gd._fullLayout.scene.aspectratio }); var target = gd.querySelector('.svg-container .gl-container #scene canvas');