diff --git a/package-lock.json b/package-lock.json index 45cbc96d867..86327c6ea82 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4653,9 +4653,9 @@ } }, "gl-plot3d": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/gl-plot3d/-/gl-plot3d-2.3.0.tgz", - "integrity": "sha512-qg269QiLpaw16d2D5Gz9fa8vsLcA8kbX/cv1u9S7BsH6jD9qGYxsY8iWJ8ea9/68WhPS5En2kUavkXINkmHsOQ==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/gl-plot3d/-/gl-plot3d-2.4.0.tgz", + "integrity": "sha512-ZPs7gvWaCqK99GXoB0XJTMTLpChB/CiDUt3MthIawVlFhknSJLNPAJSbUU3f4pWzKCbbGtVARQr/i6XdM3MnKA==", "requires": { "3d-view": "^2.0.0", "a-big-triangle": "^1.0.3", diff --git a/package.json b/package.json index 528ff8959f7..52c6dc3fe7c 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,7 @@ "gl-mat4": "^1.2.0", "gl-mesh3d": "^2.2.0", "gl-plot2d": "^1.4.2", - "gl-plot3d": "^2.3.0", + "gl-plot3d": "^2.4.0", "gl-pointcloud2d": "^1.0.2", "gl-scatter3d": "^1.2.2", "gl-select-box": "^1.0.3", @@ -93,6 +93,7 @@ "glslify": "^7.0.0", "has-hover": "^1.0.1", "has-passive-events": "^1.0.0", + "is-mobile": "^2.1.0", "mapbox-gl": "1.3.2", "matrix-camera-controller": "^2.1.3", "mouse-change": "^1.4.0", diff --git a/src/plots/gl3d/scene.js b/src/plots/gl3d/scene.js index 7385e8cf1c4..4f12b0dc4da 100644 --- a/src/plots/gl3d/scene.js +++ b/src/plots/gl3d/scene.js @@ -15,6 +15,7 @@ var createPlot = glPlot3d.createScene; var getContext = require('webgl-context'); var passiveSupported = require('has-passive-events'); +var isMobile = require('is-mobile')({ tablet: true }); var Registry = require('../../registry'); var Lib = require('../../lib'); @@ -33,7 +34,233 @@ var computeTickMarks = require('./layout/tick_marks'); var STATIC_CANVAS, STATIC_CONTEXT; -function render(scene) { +function Scene(options, fullLayout) { + // create sub container for plot + var sceneContainer = document.createElement('div'); + var plotContainer = options.container; + + // keep a ref to the graph div to fire hover+click events + this.graphDiv = options.graphDiv; + + // create SVG container for hover text + var svgContainer = document.createElementNS( + 'http://www.w3.org/2000/svg', + 'svg'); + svgContainer.style.position = 'absolute'; + svgContainer.style.top = svgContainer.style.left = '0px'; + svgContainer.style.width = svgContainer.style.height = '100%'; + svgContainer.style['z-index'] = 20; + svgContainer.style['pointer-events'] = 'none'; + sceneContainer.appendChild(svgContainer); + this.svgContainer = svgContainer; + + // Tag the container with the sceneID + sceneContainer.id = options.id; + sceneContainer.style.position = 'absolute'; + sceneContainer.style.top = sceneContainer.style.left = '0px'; + sceneContainer.style.width = sceneContainer.style.height = '100%'; + plotContainer.appendChild(sceneContainer); + + this.fullLayout = fullLayout; + this.id = options.id || 'scene'; + this.fullSceneLayout = fullLayout[this.id]; + + // Saved from last call to plot() + this.plotArgs = [ [], {}, {} ]; + + /* + * Move this to calc step? Why does it work here? + */ + this.axesOptions = createAxesOptions(fullLayout, fullLayout[this.id]); + this.spikeOptions = createSpikeOptions(fullLayout[this.id]); + this.container = sceneContainer; + this.staticMode = !!options.staticPlot; + this.pixelRatio = this.pixelRatio || options.plotGlPixelRatio || 2; + + // Coordinate rescaling + this.dataScale = [1, 1, 1]; + + this.contourLevels = [ [], [], [] ]; + + this.convertAnnotations = Registry.getComponentMethod('annotations3d', 'convert'); + this.drawAnnotations = Registry.getComponentMethod('annotations3d', 'draw'); + + this.initializeGLPlot(); +} + +var proto = Scene.prototype; + +proto.tryCreatePlot = function() { + var scene = this; + var opts = { + canvas: scene.canvas, + gl: scene.gl, + glOptions: { + preserveDrawingBuffer: isMobile, + premultipliedAlpha: true, + antialias: true + }, + container: scene.container, + axes: scene.axesOptions, + spikes: scene.spikeOptions, + pickRadius: 10, + snapToData: true, + autoScale: true, + autoBounds: false, + cameraObject: scene.camera, + pixelRatio: scene.pixelRatio + }; + + // for static plots, we reuse the WebGL context + // as WebKit doesn't collect them reliably + if(scene.staticMode) { + if(!STATIC_CONTEXT) { + STATIC_CANVAS = document.createElement('canvas'); + STATIC_CONTEXT = getContext({ + canvas: STATIC_CANVAS, + preserveDrawingBuffer: true, + premultipliedAlpha: true, + antialias: true + }); + if(!STATIC_CONTEXT) { + throw new Error('error creating static canvas/context for image server'); + } + } + + opts.gl = STATIC_CONTEXT; + opts.canvas = STATIC_CANVAS; + } + + var failed = 0; + + try { + scene.glplot = createPlot(opts); + } catch(e) { + failed++; + try { // try second time to fix issue with Chrome 77 https://github.com/plotly/plotly.js/issues/4233 + scene.glplot = createPlot(opts); + } catch(e) { + failed++; + } + } + + return failed < 2; +}; + +proto.initializeGLCamera = function() { + var scene = this; + var cameraData = scene.fullSceneLayout.camera; + var isOrtho = (cameraData.projection.type === 'orthographic'); + + scene.camera = createCamera(scene.container, { + center: [cameraData.center.x, cameraData.center.y, cameraData.center.z], + eye: [cameraData.eye.x, cameraData.eye.y, cameraData.eye.z], + up: [cameraData.up.x, cameraData.up.y, cameraData.up.z], + _ortho: isOrtho, + zoomMin: 0.01, + zoomMax: 100, + mode: 'orbit' + }); +}; + +proto.initializeGLPlot = function() { + var scene = this; + + scene.initializeGLCamera(); + + var success = scene.tryCreatePlot(); + /* + * createPlot will throw when webgl is not enabled in the client. + * Lets return an instance of the module with all functions noop'd. + * The destroy method - which will remove the container from the DOM + * is overridden with a function that removes the container only. + */ + if(!success) return showNoWebGlMsg(scene); + + // List of scene objects + scene.traces = {}; + + scene.make4thDimension(); + + 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 = makeUpdate(); + scene.saveLayout(layout); + scene.graphDiv.emit('plotly_relayout', update); + }; + + scene.glplot.canvas.addEventListener('mouseup', function() { + relayoutCallback(scene); + }); + + scene.glplot.canvas.addEventListener('wheel', function(e) { + if(gd._context._scrollZoom.gl3d) { + if(scene.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); + + scene.glplot.canvas.addEventListener('mousemove', function() { + if(scene.fullSceneLayout.dragmode === false) return; + if(scene.camera.mouseListener.buttons === 0) return; + + var update = makeUpdate(); + scene.graphDiv.emit('plotly_relayouting', update); + }); + + if(!scene.staticMode) { + scene.glplot.canvas.addEventListener('webglcontextlost', function(event) { + if(gd && gd.emit) { + gd.emit('plotly_webglcontextlost', { + event: event, + layer: scene.id + }); + } + }, false); + } + + scene.glplot.oncontextloss = function() { + scene.recoverContext(); + }; + + scene.glplot.onrender = function() { + scene.render(); + }; + + return true; +}; + +proto.render = function() { + var scene = this; var gd = scene.graphDiv; var trace; @@ -128,7 +355,7 @@ function render(scene) { } tx = vectorTx.join('
'); } else if(trace.type === 'isosurface' || trace.type === 'volume') { - labels.valueLabel = Axes.tickText(scene.mockAxis, scene.mockAxis.d2l(selection.traceCoordinate[3]), 'hover').text; + labels.valueLabel = Axes.tickText(scene._mockAxis, scene._mockAxis.d2l(selection.traceCoordinate[3]), 'hover').text; vectorTx.push('value: ' + labels.valueLabel); if(selection.textLabel) { vectorTx.push(selection.textLabel); @@ -195,242 +422,25 @@ function render(scene) { } scene.drawAnnotations(scene); -} - -function tryCreatePlot(scene, cameraObject, pixelRatio, canvas, gl) { - var glplotOptions = { - canvas: canvas, - gl: gl, - container: scene.container, - axes: scene.axesOptions, - spikes: scene.spikeOptions, - pickRadius: 10, - snapToData: true, - autoScale: true, - autoBounds: false, - cameraObject: cameraObject, - pixelRatio: pixelRatio - }; - - // for static plots, we reuse the WebGL context - // as WebKit doesn't collect them reliably - if(scene.staticMode) { - if(!STATIC_CONTEXT) { - STATIC_CANVAS = document.createElement('canvas'); - STATIC_CONTEXT = getContext({ - canvas: STATIC_CANVAS, - preserveDrawingBuffer: true, - premultipliedAlpha: true, - antialias: true - }); - if(!STATIC_CONTEXT) { - throw new Error('error creating static canvas/context for image server'); - } - } - glplotOptions.pixelRatio = scene.pixelRatio; - glplotOptions.gl = STATIC_CONTEXT; - glplotOptions.canvas = STATIC_CANVAS; - } - - var failed = 0; - - try { - scene.glplot = createPlot(glplotOptions); - } catch(e) { - failed++; - try { // try second time to fix issue with Chrome 77 https://github.com/plotly/plotly.js/issues/4233 - scene.glplot = createPlot(glplotOptions); - } catch(e) { - failed++; - } - } - - return failed < 2; -} - -function initializeGLPlot(scene, canvas, gl) { - scene.initializeGLCamera(); - - 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. - * The destroy method - which will remove the container from the DOM - * is overridden with a function that removes the container only. - */ - 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 = makeUpdate(); - scene.saveLayout(layout); - scene.graphDiv.emit('plotly_relayout', update); - }; - - scene.glplot.canvas.addEventListener('mouseup', function() { - relayoutCallback(scene); - }); - - 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); - - scene.glplot.canvas.addEventListener('mousemove', function() { - if(scene.fullSceneLayout.dragmode === false) return; - if(scene.camera.mouseListener.buttons === 0) return; - - var update = makeUpdate(); - scene.graphDiv.emit('plotly_relayouting', update); - }); - - if(!scene.staticMode) { - scene.glplot.canvas.addEventListener('webglcontextlost', function(event) { - if(gd && gd.emit) { - gd.emit('plotly_webglcontextlost', { - event: event, - layer: scene.id - }); - } - }, false); - } - - scene.glplot.camera = scene.camera; - - scene.glplot.oncontextloss = function() { - scene.recoverContext(); - }; - - scene.glplot.onrender = render.bind(null, scene); - - // List of scene objects - scene.traces = {}; - - scene.make4thDimension(); - - return true; -} - -function Scene(options, fullLayout) { - // create sub container for plot - var sceneContainer = document.createElement('div'); - var plotContainer = options.container; - - // keep a ref to the graph div to fire hover+click events - this.graphDiv = options.graphDiv; - - // create SVG container for hover text - var svgContainer = document.createElementNS( - 'http://www.w3.org/2000/svg', - 'svg'); - svgContainer.style.position = 'absolute'; - svgContainer.style.top = svgContainer.style.left = '0px'; - svgContainer.style.width = svgContainer.style.height = '100%'; - svgContainer.style['z-index'] = 20; - svgContainer.style['pointer-events'] = 'none'; - sceneContainer.appendChild(svgContainer); - this.svgContainer = svgContainer; - - // Tag the container with the sceneID - sceneContainer.id = options.id; - sceneContainer.style.position = 'absolute'; - sceneContainer.style.top = sceneContainer.style.left = '0px'; - sceneContainer.style.width = sceneContainer.style.height = '100%'; - plotContainer.appendChild(sceneContainer); - - this.fullLayout = fullLayout; - this.id = options.id || 'scene'; - this.fullSceneLayout = fullLayout[this.id]; - - // Saved from last call to plot() - this.plotArgs = [ [], {}, {} ]; - - /* - * Move this to calc step? Why does it work here? - */ - this.axesOptions = createAxesOptions(fullLayout, fullLayout[this.id]); - this.spikeOptions = createSpikeOptions(fullLayout[this.id]); - this.container = sceneContainer; - this.staticMode = !!options.staticPlot; - this.pixelRatio = this.pixelRatio || options.plotGlPixelRatio || 2; - - // Coordinate rescaling - this.dataScale = [1, 1, 1]; - - this.contourLevels = [ [], [], [] ]; - - this.convertAnnotations = Registry.getComponentMethod('annotations3d', 'convert'); - this.drawAnnotations = Registry.getComponentMethod('annotations3d', 'draw'); - - initializeGLPlot(this); -} - -var proto = Scene.prototype; - -proto.initializeGLCamera = function() { - var cameraData = this.fullSceneLayout.camera; - var isOrtho = (cameraData.projection.type === 'orthographic'); - - this.camera = createCamera(this.container, { - center: [cameraData.center.x, cameraData.center.y, cameraData.center.z], - eye: [cameraData.eye.x, cameraData.eye.y, cameraData.eye.z], - up: [cameraData.up.x, cameraData.up.y, cameraData.up.z], - _ortho: isOrtho, - zoomMin: 0.01, - zoomMax: 100, - mode: 'orbit' - }); }; proto.recoverContext = function() { var scene = this; - var gl = this.glplot.gl; - var canvas = this.glplot.canvas; - this.glplot.dispose(); + scene.glplot.dispose(); - function tryRecover() { - if(gl.isContextLost()) { + var tryRecover = function() { + if(scene.glplot.gl.isContextLost()) { requestAnimationFrame(tryRecover); return; } - if(!initializeGLPlot(scene, canvas, gl)) { + if(!scene.initializeGLPlot()) { Lib.error('Catastrophic and unrecoverable WebGL error. Context lost.'); return; } scene.plot.apply(scene, scene.plotArgs); - } + }; + requestAnimationFrame(tryRecover); }; @@ -498,39 +508,35 @@ function computeAnnotationBounds(scene, bounds) { } proto.plot = function(sceneData, fullLayout, layout) { + var scene = this; + // Save parameters - this.plotArgs = [sceneData, fullLayout, layout]; + scene.plotArgs = [sceneData, fullLayout, layout]; - if(this.glplot.contextLost) return; + if(scene.glplot.contextLost) return; var data, trace; var i, j, axis, axisType; - var fullSceneLayout = fullLayout[this.id]; - var sceneLayout = layout[this.id]; - - if(fullSceneLayout.bgcolor) this.glplot.clearColor = str2RGBAarray(fullSceneLayout.bgcolor); - else this.glplot.clearColor = [0, 0, 0, 0]; - - this.glplot.snapToData = true; + var fullSceneLayout = fullLayout[scene.id]; + var sceneLayout = layout[scene.id]; // Update layout - this.fullLayout = fullLayout; - this.fullSceneLayout = fullSceneLayout; + scene.fullLayout = fullLayout; + scene.fullSceneLayout = fullSceneLayout; - this.glplotLayout = fullSceneLayout; - this.axesOptions.merge(fullLayout, fullSceneLayout); - this.spikeOptions.merge(fullSceneLayout); + scene.axesOptions.merge(fullLayout, fullSceneLayout); + scene.spikeOptions.merge(fullSceneLayout); // Update camera and camera mode - this.setViewport(fullSceneLayout); - this.updateFx(fullSceneLayout.dragmode, fullSceneLayout.hovermode); - this.camera.enableWheel = this.graphDiv._context._scrollZoom.gl3d; + scene.setViewport(fullSceneLayout); + scene.updateFx(fullSceneLayout.dragmode, fullSceneLayout.hovermode); + scene.camera.enableWheel = scene.graphDiv._context._scrollZoom.gl3d; - // Update scene - this.glplot.update({}); + // Update scene background + scene.glplot.setClearColor(str2RGBAarray(fullSceneLayout.bgcolor)); // Update axes functions BEFORE updating traces - this.setConvert(axis); + scene.setConvert(axis); // Convert scene data if(!sceneData) sceneData = []; @@ -560,10 +566,10 @@ proto.plot = function(sceneData, fullLayout, layout) { } // Save scale - this.dataScale = dataScale; + scene.dataScale = dataScale; // after computeTraceBounds where ax._categories are filled in - this.convertAnnotations(this); + scene.convertAnnotations(this); // Update traces for(i = 0; i < sceneData.length; ++i) { @@ -571,24 +577,24 @@ proto.plot = function(sceneData, fullLayout, layout) { if(data.visible !== true || data._length === 0) { continue; } - trace = this.traces[data.uid]; + trace = scene.traces[data.uid]; if(trace) { if(trace.data.type === data.type) { trace.update(data); } else { trace.dispose(); trace = data._module.plot(this, data); - this.traces[data.uid] = trace; + scene.traces[data.uid] = trace; } } else { trace = data._module.plot(this, data); - this.traces[data.uid] = trace; + scene.traces[data.uid] = trace; } trace.name = data.name; } // Remove empty traces - var traceIds = Object.keys(this.traces); + var traceIds = Object.keys(scene.traces); traceIdLoop: for(i = 0; i < traceIds.length; ++i) { @@ -598,13 +604,13 @@ proto.plot = function(sceneData, fullLayout, layout) { continue traceIdLoop; } } - trace = this.traces[traceIds[i]]; + trace = scene.traces[traceIds[i]]; trace.dispose(); - delete this.traces[traceIds[i]]; + delete scene.traces[traceIds[i]]; } // order object per trace index - this.glplot.objects.sort(function(a, b) { + scene.glplot.objects.sort(function(a, b) { return a._trace.data.index - b._trace.data.index; }); @@ -631,8 +637,8 @@ proto.plot = function(sceneData, fullLayout, layout) { sceneBounds[0][i] = Infinity; sceneBounds[1][i] = -Infinity; - var objects = this.glplot.objects; - var annotations = this.fullSceneLayout.annotations || []; + var objects = scene.glplot.objects; + var annotations = scene.fullSceneLayout.annotations || []; var axLetter = axis._name.charAt(0); for(j = 0; j < objects.length; j++) { @@ -690,8 +696,10 @@ proto.plot = function(sceneData, fullLayout, layout) { axisDataRange[i] = sceneBounds[1][i] - sceneBounds[0][i]; // Update plot bounds - this.glplot.bounds[0][i] = sceneBounds[0][i] * dataScale[i]; - this.glplot.bounds[1][i] = sceneBounds[1][i] * dataScale[i]; + scene.glplot.setBounds(i, { + min: sceneBounds[0][i] * dataScale[i], + max: sceneBounds[1][i] * dataScale[i] + }); } var axesScaleRatio = [1, 1, 1]; @@ -746,11 +754,11 @@ 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.setAspectratio(fullSceneLayout.aspectratio); + scene.glplot.setAspectratio(fullSceneLayout.aspectratio); // save 'initial' camera view settings for modebar button - if(!this.viewInitial.aspectratio) { - this.viewInitial.aspectratio = { + if(!scene.viewInitial.aspectratio) { + scene.viewInitial.aspectratio = { x: fullSceneLayout.aspectratio.x, y: fullSceneLayout.aspectratio.y, z: fullSceneLayout.aspectratio.z @@ -762,7 +770,7 @@ proto.plot = function(sceneData, fullLayout, layout) { var size = fullLayout._size || null; if(domain && size) { - var containerStyle = this.container.style; + var containerStyle = scene.container.style; containerStyle.position = 'absolute'; containerStyle.left = (size.l + domain.x[0] * size.w) + 'px'; containerStyle.top = (size.t + (1 - domain.y[1]) * size.h) + 'px'; @@ -771,18 +779,19 @@ proto.plot = function(sceneData, fullLayout, layout) { } // force redraw so that promise is returned when rendering is completed - this.glplot.redraw(); + scene.glplot.redraw(); }; proto.destroy = function() { - if(!this.glplot) return; - - this.camera.mouseListener.enabled = false; - this.container.removeEventListener('wheel', this.camera.wheelListener); - this.camera = this.glplot.camera = null; - this.glplot.dispose(); - this.container.parentNode.removeChild(this.container); - this.glplot = null; + var scene = this; + + if(!scene.glplot) return; + scene.camera.mouseListener.enabled = false; + scene.container.removeEventListener('wheel', scene.camera.wheelListener); + scene.camera = null; + scene.glplot.dispose(); + scene.container.parentNode.removeChild(scene.container); + scene.glplot = null; }; // getCameraArrays :: plotly_coords -> gl-plot3d_coords @@ -808,42 +817,34 @@ function getLayoutCamera(camera) { // 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); + var scene = this; + scene.camera.view.recalcMatrix(scene.camera.view.lastT()); + return getLayoutCamera(scene.camera); }; // set gl-plot3d camera position and scene aspects with a set of plotly coords proto.setViewport = function(sceneLayout) { + var scene = this; var cameraData = sceneLayout.camera; - this.glplot.camera.lookAt.apply(this, getCameraArrays(cameraData)); - this.glplot.setAspectratio(sceneLayout.aspectratio); + scene.camera.lookAt.apply(this, getCameraArrays(cameraData)); + scene.glplot.setAspectratio(sceneLayout.aspectratio); var newOrtho = (cameraData.projection.type === 'orthographic'); - var oldOrtho = this.glplot.camera._ortho; + var oldOrtho = scene.camera._ortho; if(newOrtho !== oldOrtho) { - this.glplot.redraw(); - - var RGBA = this.glplot.clearColor; - this.glplot.gl.clearColor( - RGBA[0], RGBA[1], RGBA[2], RGBA[3] - ); - this.glplot.gl.clear( - this.glplot.gl.DEPTH_BUFFER_BIT | - this.glplot.gl.COLOR_BUFFER_BIT - ); - - this.glplot.dispose(); - - initializeGLPlot(this); - this.glplot.camera._ortho = newOrtho; + scene.glplot.redraw(); // TODO: figure out why we need to redraw here? + scene.glplot.clearRGBA(); + scene.glplot.dispose(); + scene.initializeGLPlot(); } }; proto.isCameraChanged = function(layout) { - var cameraData = this.getCamera(); - var cameraNestedProp = Lib.nestedProperty(layout, this.id + '.camera'); + var scene = this; + var cameraData = scene.getCamera(); + var cameraNestedProp = Lib.nestedProperty(layout, scene.id + '.camera'); var cameraDataLastSave = cameraNestedProp.get(); function same(x, y, i, j) { @@ -876,8 +877,9 @@ proto.isCameraChanged = function(layout) { }; proto.isAspectChanged = function(layout) { - var aspectData = this.glplot.getAspectratio(); - var aspectNestedProp = Lib.nestedProperty(layout, this.id + '.aspectratio'); + var scene = this; + var aspectData = scene.glplot.getAspectratio(); + var aspectNestedProp = Lib.nestedProperty(layout, scene.id + '.aspectratio'); var aspectDataLastSave = aspectNestedProp.get(); return ( @@ -890,7 +892,8 @@ proto.isAspectChanged = function(layout) { // save camera to user layout (i.e. gd.layout) proto.saveLayout = function(layout) { - var fullLayout = this.fullLayout; + var scene = this; + var fullLayout = scene.fullLayout; var cameraData; var cameraNestedProp; @@ -900,42 +903,42 @@ proto.saveLayout = function(layout) { var aspectNestedProp; var aspectDataLastSave; - var cameraChanged = this.isCameraChanged(layout); - var aspectChanged = this.isAspectChanged(layout); + var cameraChanged = scene.isCameraChanged(layout); + var aspectChanged = scene.isAspectChanged(layout); var hasChanged = cameraChanged || aspectChanged; if(hasChanged) { var preGUI = {}; if(cameraChanged) { - cameraData = this.getCamera(); - cameraNestedProp = Lib.nestedProperty(layout, this.id + '.camera'); + cameraData = scene.getCamera(); + cameraNestedProp = Lib.nestedProperty(layout, scene.id + '.camera'); cameraDataLastSave = cameraNestedProp.get(); - preGUI[this.id + '.camera'] = cameraDataLastSave; + preGUI[scene.id + '.camera'] = cameraDataLastSave; } if(aspectChanged) { - aspectData = this.glplot.getAspectratio(); - aspectNestedProp = Lib.nestedProperty(layout, this.id + '.aspectratio'); + aspectData = scene.glplot.getAspectratio(); + aspectNestedProp = Lib.nestedProperty(layout, scene.id + '.aspectratio'); aspectDataLastSave = aspectNestedProp.get(); - preGUI[this.id + '.aspectratio'] = aspectDataLastSave; + preGUI[scene.id + '.aspectratio'] = aspectDataLastSave; } Registry.call('_storeDirectGUIEdit', layout, fullLayout._preGUI, preGUI); if(cameraChanged) { cameraNestedProp.set(cameraData); - var cameraFullNP = Lib.nestedProperty(fullLayout, this.id + '.camera'); + var cameraFullNP = Lib.nestedProperty(fullLayout, scene.id + '.camera'); cameraFullNP.set(cameraData); } if(aspectChanged) { aspectNestedProp.set(aspectData); - var aspectFullNP = Lib.nestedProperty(fullLayout, this.id + '.aspectratio'); + var aspectFullNP = Lib.nestedProperty(fullLayout, scene.id + '.aspectratio'); aspectFullNP.set(aspectData); - this.glplot.redraw(); + scene.glplot.redraw(); } } @@ -943,7 +946,8 @@ proto.saveLayout = function(layout) { }; proto.updateFx = function(dragmode, hovermode) { - var camera = this.camera; + var scene = this; + var camera = scene.camera; if(camera) { // rotate and orbital are synonymous if(dragmode === 'orbit') { @@ -957,15 +961,15 @@ proto.updateFx = function(dragmode, hovermode) { // The setter for camera.mode animates the transition to z-up, // but only if we *don't* explicitly set z-up earlier via the // relayout. So push `up` back to layout & fullLayout manually now. - var gd = this.graphDiv; + var gd = scene.graphDiv; var fullLayout = gd._fullLayout; - var fullCamera = this.fullSceneLayout.camera; + var fullCamera = scene.fullSceneLayout.camera; var x = fullCamera.up.x; var y = fullCamera.up.y; var z = fullCamera.up.z; // only push `up` back to (full)layout if it's going to change if(z / Math.sqrt(x * x + y * y + z * z) < 0.999) { - var attr = this.id + '.camera.up'; + var attr = scene.id + '.camera.up'; var zUp = {x: 0, y: 0, z: 1}; var edits = {}; edits[attr] = zUp; @@ -981,19 +985,20 @@ proto.updateFx = function(dragmode, hovermode) { } // to put dragmode and hovermode on the same grounds from relayout - this.fullSceneLayout.hovermode = hovermode; + scene.fullSceneLayout.hovermode = hovermode; }; proto.toImage = function(format) { - if(!format) format = 'png'; + var scene = this; - if(this.staticMode) this.container.appendChild(STATIC_CANVAS); + if(!format) format = 'png'; + if(scene.staticMode) scene.container.appendChild(STATIC_CANVAS); // Force redraw - this.glplot.redraw(); + scene.glplot.redraw(); // Grab context and yank out pixels - var gl = this.glplot.gl; + var gl = scene.glplot.gl; var w = gl.drawingBufferWidth; var h = gl.drawingBufferHeight; @@ -1034,31 +1039,32 @@ proto.toImage = function(format) { dataURL = canvas.toDataURL('image/png'); } - if(this.staticMode) this.container.removeChild(STATIC_CANVAS); + if(scene.staticMode) scene.container.removeChild(STATIC_CANVAS); return dataURL; }; proto.setConvert = function() { + var scene = this; for(var i = 0; i < 3; i++) { - var ax = this.fullSceneLayout[axisProperties[i]]; - Axes.setConvert(ax, this.fullLayout); + var ax = scene.fullSceneLayout[axisProperties[i]]; + Axes.setConvert(ax, scene.fullLayout); ax.setScale = Lib.noop; } }; proto.make4thDimension = function() { - var _this = this; - var gd = _this.graphDiv; + var scene = this; + var gd = scene.graphDiv; var fullLayout = gd._fullLayout; // mock axis for hover formatting - _this.mockAxis = { + scene._mockAxis = { type: 'linear', showexponent: 'all', exponentformat: 'B' }; - Axes.setConvert(_this.mockAxis, fullLayout); + Axes.setConvert(scene._mockAxis, fullLayout); }; module.exports = Scene; diff --git a/test/jasmine/tests/gl3d_plot_interact_test.js b/test/jasmine/tests/gl3d_plot_interact_test.js index 91915c621f8..5f60b2e3130 100644 --- a/test/jasmine/tests/gl3d_plot_interact_test.js +++ b/test/jasmine/tests/gl3d_plot_interact_test.js @@ -93,7 +93,7 @@ describe('Test gl3d before/after plot', function() { }) .then(delay(20)) .then(function() { - var cameraBefore = gd._fullLayout.scene._scene.glplot.camera; + var cameraBefore = gd._fullLayout.scene._scene.camera; expect(cameraBefore.up[0]).toBeCloseTo(0, 2, 'cameraBefore.up[0]'); expect(cameraBefore.up[1]).toBeCloseTo(0, 2, 'cameraBefore.up[1]'); expect(cameraBefore.up[2]).toBeCloseTo(1, 2, 'cameraBefore.up[2]'); @@ -108,7 +108,7 @@ describe('Test gl3d before/after plot', function() { .then(_clickThere) .then(delay(20)) .then(function() { - var cameraAfter = gd._fullLayout.scene._scene.glplot.camera; + var cameraAfter = gd._fullLayout.scene._scene.camera; expect(cameraAfter.up[0]).toBeCloseTo(0, 2, 'cameraAfter.up[0]'); expect(cameraAfter.up[1]).toBeCloseTo(0, 2, 'cameraAfter.up[1]'); expect(cameraAfter.up[2]).toBeCloseTo(1, 2, 'cameraAfter.up[2]'); @@ -123,7 +123,7 @@ describe('Test gl3d before/after plot', function() { .then(_clickOtherplace) .then(delay(20)) .then(function() { - var cameraFinal = gd._fullLayout.scene._scene.glplot.camera; + var cameraFinal = gd._fullLayout.scene._scene.camera; expect(cameraFinal.up[0]).toBeCloseTo(0, 2, 'cameraFinal.up[0]'); expect(cameraFinal.up[1]).toBeCloseTo(0, 2, 'cameraFinal.up[1]'); expect(cameraFinal.up[2]).toBeCloseTo(1, 2, 'cameraFinal.up[2]'); @@ -300,7 +300,7 @@ describe('Test gl3d plots', function() { .then(delay(20)) .then(function() { expect(gd._fullLayout.scene.camera.projection.type === 'perspective').toBe(true); - expect(gd._fullLayout.scene._scene.glplot.camera._ortho === false).toBe(true); + expect(gd._fullLayout.scene._scene.camera._ortho === false).toBe(true); }) .then(done); }); @@ -326,7 +326,7 @@ describe('Test gl3d plots', function() { .then(delay(20)) .then(function() { expect(gd._fullLayout.scene.camera.projection.type === 'orthographic').toBe(true); - expect(gd._fullLayout.scene._scene.glplot.camera._ortho === true).toBe(true); + expect(gd._fullLayout.scene._scene.camera._ortho === true).toBe(true); }) .then(done); }); @@ -355,28 +355,28 @@ describe('Test gl3d plots', function() { }) .then(function() { expect(gd._fullLayout.scene.camera.projection.type === 'orthographic').toBe(true); - expect(gd._fullLayout.scene._scene.glplot.camera._ortho === true).toBe(true); + expect(gd._fullLayout.scene._scene.camera._ortho === true).toBe(true); }) .then(function() { return Plotly.relayout(gd, 'scene.camera.eye.z', 2); }) .then(function() { expect(gd._fullLayout.scene.camera.projection.type === 'orthographic').toBe(true); - expect(gd._fullLayout.scene._scene.glplot.camera._ortho === true).toBe(true); + expect(gd._fullLayout.scene._scene.camera._ortho === true).toBe(true); }) .then(function() { return Plotly.relayout(gd, 'scene.camera.projection.type', 'perspective'); }) .then(function() { expect(gd._fullLayout.scene.camera.projection.type === 'perspective').toBe(true); - expect(gd._fullLayout.scene._scene.glplot.camera._ortho === false).toBe(true); + expect(gd._fullLayout.scene._scene.camera._ortho === false).toBe(true); }) .then(function() { return Plotly.relayout(gd, 'scene.camera.eye.z', 3); }) .then(function() { expect(gd._fullLayout.scene.camera.projection.type === 'perspective').toBe(true); - expect(gd._fullLayout.scene._scene.glplot.camera._ortho === false).toBe(true); + expect(gd._fullLayout.scene._scene.camera._ortho === false).toBe(true); }) .then(done); });