diff --git a/src/lib/prepare_regl.js b/src/lib/prepare_regl.js index 13931e4eaa4..d262a510aa9 100644 --- a/src/lib/prepare_regl.js +++ b/src/lib/prepare_regl.js @@ -48,6 +48,17 @@ module.exports = function prepareRegl(gd, extensions) { } catch(e) { success = false; } + + if(success) { + this.addEventListener('webglcontextlost', function(event) { + if(gd && gd.emit) { + gd.emit('plotly_webglcontextlost', { + event: event, + layer: d.key + }); + } + }, false); + } }); if(!success) { diff --git a/src/plots/gl3d/scene.js b/src/plots/gl3d/scene.js index 2a3c9a9d489..99dc45152ec 100644 --- a/src/plots/gl3d/scene.js +++ b/src/plots/gl3d/scene.js @@ -170,6 +170,8 @@ function render(scene) { } function initializeGLPlot(scene, fullLayout, canvas, gl) { + var gd = scene.graphDiv; + var glplotOptions = { canvas: canvas, gl: gl, @@ -220,7 +222,7 @@ function initializeGLPlot(scene, fullLayout, canvas, gl) { var update = {}; update[scene.id + '.camera'] = getLayoutCamera(scene.camera); - scene.saveCamera(scene.graphDiv.layout); + scene.saveCamera(gd.layout); scene.graphDiv.emit('plotly_relayout', update); }; @@ -228,10 +230,14 @@ function initializeGLPlot(scene, fullLayout, canvas, gl) { scene.glplot.canvas.addEventListener('wheel', relayoutCallback.bind(null, scene), passiveSupported ? {passive: false} : false); if(!scene.staticMode) { - scene.glplot.canvas.addEventListener('webglcontextlost', function(ev) { - Lib.warn('Lost WebGL context.'); - ev.preventDefault(); - }); + scene.glplot.canvas.addEventListener('webglcontextlost', function(event) { + if(gd && gd.emit) { + gd.emit('plotly_webglcontextlost', { + event: event, + layer: scene.id + }); + } + }, false); } if(!scene.camera) { diff --git a/test/jasmine/tests/gl2d_plot_interact_test.js b/test/jasmine/tests/gl2d_plot_interact_test.js index 2982a653529..ecc46e7968b 100644 --- a/test/jasmine/tests/gl2d_plot_interact_test.js +++ b/test/jasmine/tests/gl2d_plot_interact_test.js @@ -214,6 +214,53 @@ describe('Test gl plot side effects', function() { .catch(failTest) .then(done); }); + + it('@gl should fire *plotly_webglcontextlost* when on webgl context lost', function(done) { + var _mock = Lib.extendDeep({}, require('@mocks/gl2d_12.json')); + + function _trigger(name) { + var ev = new window.WebGLContextEvent('webglcontextlost'); + var canvas = gd.querySelector('.gl-canvas-' + name); + canvas.dispatchEvent(ev); + } + + Plotly.plot(gd, _mock).then(function() { + return new Promise(function(resolve, reject) { + gd.once('plotly_webglcontextlost', resolve); + setTimeout(reject, 10); + _trigger('context'); + }); + }) + .then(function(eventData) { + expect((eventData || {}).event).toBeDefined(); + expect((eventData || {}).layer).toBe('contextLayer'); + }) + .then(function() { + return new Promise(function(resolve, reject) { + gd.once('plotly_webglcontextlost', resolve); + setTimeout(reject, 10); + _trigger('focus'); + }); + }) + .then(function(eventData) { + expect((eventData || {}).event).toBeDefined(); + expect((eventData || {}).layer).toBe('focusLayer'); + }) + .then(function() { + return new Promise(function(resolve, reject) { + gd.once('plotly_webglcontextlost', reject); + setTimeout(resolve, 10); + _trigger('pick'); + }); + }) + .then(function(eventData) { + // should add event listener on pick canvas which + // isn't used for scattergl traces + expect(eventData).toBeUndefined(); + }) + .catch(failTest) + .then(done); + }); }); describe('Test gl2d plots', function() { diff --git a/test/jasmine/tests/gl3d_plot_interact_test.js b/test/jasmine/tests/gl3d_plot_interact_test.js index 64b22e447bc..89d00c3ac11 100644 --- a/test/jasmine/tests/gl3d_plot_interact_test.js +++ b/test/jasmine/tests/gl3d_plot_interact_test.js @@ -1425,4 +1425,25 @@ describe('Test removal of gl contexts', function() { }) .then(done); }); + + it('@gl should fire *plotly_webglcontextlost* when on webgl context lost', function(done) { + var _mock = Lib.extendDeep({}, require('@mocks/gl3d_marker-arrays.json')); + + Plotly.plot(gd, _mock).then(function() { + return new Promise(function(resolve, reject) { + gd.on('plotly_webglcontextlost', resolve); + setTimeout(reject, 10); + + var ev = new window.WebGLContextEvent('webglcontextlost'); + var canvas = gd.querySelector('div#scene > canvas'); + canvas.dispatchEvent(ev); + }); + }) + .then(function(eventData) { + expect((eventData || {}).event).toBeDefined(); + expect((eventData || {}).layer).toBe('scene'); + }) + .catch(failTest) + .then(done); + }); }); diff --git a/test/jasmine/tests/parcoords_test.js b/test/jasmine/tests/parcoords_test.js index fe05a10bf67..d1dea69c844 100644 --- a/test/jasmine/tests/parcoords_test.js +++ b/test/jasmine/tests/parcoords_test.js @@ -1052,6 +1052,36 @@ describe('parcoords basic use', function() { .catch(failTest) .then(done); }); + + it('@gl should fire *plotly_webglcontextlost* when on webgl context lost', function() { + var eventData; + var cnt = 0; + gd.on('plotly_webglcontextlost', function(d) { + eventData = d; + cnt++; + }); + + function trigger(name) { + var ev = new window.WebGLContextEvent('webglcontextlost'); + var canvas = gd.querySelector('.gl-canvas-' + name); + canvas.dispatchEvent(ev); + } + + function _assert(d, c) { + expect((eventData || {}).event).toBeDefined(); + expect((eventData || {}).layer).toBe(d); + expect(cnt).toBe(c); + } + + trigger('context'); + _assert('contextLayer', 1); + + trigger('focus'); + _assert('focusLayer', 2); + + trigger('pick'); + _assert('pickLayer', 3); + }); }); describe('@noCI parcoords constraint interactions', function() {