diff --git a/CHANGES.md b/CHANGES.md index c40cf20dfd4c..4f43d7cf42c6 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,7 +6,10 @@ Beta Releases ### b29 - 2014-06-02 +* Breaking changes + * Removed `CesiumWidget.onRenderLoopError` and `Viewer.renderLoopError`. They have been replaced by `Scene.renderError`. * Improved terrain and imagery rendering performance when very close to the surface. +* Added `preRender` and `postRender` events to `Scene`. ### b28 - 2014-05-01 diff --git a/Source/Scene/Scene.js b/Source/Scene/Scene.js index 2a0c6d53f808..07a1b12d6982 100644 --- a/Source/Scene/Scene.js +++ b/Source/Scene/Scene.js @@ -169,6 +169,21 @@ define([ this._transitioner = new SceneTransitioner(this); + this._renderError = new Event(); + this._preRender = new Event(); + this._postRender = new Event(); + + /** + * Exceptions occurring in render are always caught in order to raise the + * renderError event. If this property is true, the error is rethrown + * after the event is raised. If this property is false, the render function + * returns normally after raising the event. + * + * @type {Boolean} + * @default false + */ + this.rethrowRenderErrors = false; + /** * Determines whether or not to instantly complete the * scene transition animation on user input. @@ -530,7 +545,7 @@ define([ * @memberof Scene.prototype * @type {ImageryLayerCollection} */ - imageryLayers: { + imageryLayers : { get : function() { return this.globe.imageryLayers; } @@ -541,13 +556,51 @@ define([ * @memberof Scene.prototype * @type {TerrainProvider} */ - terrainProvider: { + terrainProvider : { get : function() { return this.globe.terrainProvider; }, set : function(terrainProvider) { this.globe.terrainProvider = terrainProvider; } + }, + + /** + * Gets the event that will be raised when an error is thrown inside the render function. + * The Scene instance and the thrown error are the only two parameters passed to the event handler. + * By default, errors are not rethrown after this event is raised, but that can be changed by setting + * the rethrowRenderErrors property. + * @memberof Scene.prototype + * @type {Event} + */ + renderError : { + get : function() { + return this._renderError; + } + }, + + /** + * Gets the event that will be raised at the start of each call to render. Subscribers to the event + * receive the Scene instance as the first parameter and the current time as the second parameter. + * @memberof Scene.prototype + * @type {Event} + */ + preRender : { + get : function() { + return this._preRender; + } + }, + + /** + * Gets the event that will be raised at the end of each call to render. Subscribers to the event + * receive the Scene instance as the first parameter and the current time as the second parameter. + * @memberof Scene.prototype + * @type {Event} + */ + postRender : { + get : function() { + return this._postRender; + } } }); @@ -1137,60 +1190,76 @@ define([ this._screenSpaceCameraController.update(this.mode); }; - /** - * DOC_TBA - * @memberof Scene - */ - Scene.prototype.render = function(time) { + function render(scene, time) { if (!defined(time)) { time = new JulianDate(); } - var us = this._context.uniformState; - var frameState = this._frameState; + scene._preRender.raiseEvent(scene, time); + + var us = scene._context.uniformState; + var frameState = scene._frameState; var frameNumber = CesiumMath.incrementWrap(frameState.frameNumber, 15000000.0, 1.0); - updateFrameState(this, frameNumber, time); + updateFrameState(scene, frameNumber, time); frameState.passes.render = true; frameState.creditDisplay.beginFrame(); - var context = this._context; + var context = scene._context; us.update(context, frameState); - this._commandList.length = 0; - this._overlayCommandList.length = 0; + scene._commandList.length = 0; + scene._overlayCommandList.length = 0; - updatePrimitives(this); - createPotentiallyVisibleSet(this); + updatePrimitives(scene); + createPotentiallyVisibleSet(scene); - var passState = this._passState; + var passState = scene._passState; - executeCommands(this, passState, defaultValue(this.backgroundColor, Color.BLACK)); - executeOverlayCommands(this, passState); + executeCommands(scene, passState, defaultValue(scene.backgroundColor, Color.BLACK)); + executeOverlayCommands(scene, passState); frameState.creditDisplay.endFrame(); - if (this.debugShowFramesPerSecond) { - if (!defined(this._performanceDisplay)) { + if (scene.debugShowFramesPerSecond) { + if (!defined(scene._performanceDisplay)) { var performanceContainer = document.createElement('div'); performanceContainer.style.position = 'absolute'; performanceContainer.style.top = '10px'; performanceContainer.style.left = '10px'; - var container = this._canvas.parentNode; + var container = scene._canvas.parentNode; container.appendChild(performanceContainer); var performanceDisplay = new PerformanceDisplay({container: performanceContainer}); - this._performanceDisplay = performanceDisplay; - this._performanceContainer = performanceContainer; + scene._performanceDisplay = performanceDisplay; + scene._performanceContainer = performanceContainer; } - this._performanceDisplay.update(); - } else if (defined(this._performanceDisplay)) { - this._performanceDisplay = this._performanceDisplay && this._performanceDisplay.destroy(); - this._performanceContainer.parentNode.removeChild(this._performanceContainer); + scene._performanceDisplay.update(); + } else if (defined(scene._performanceDisplay)) { + scene._performanceDisplay = scene._performanceDisplay && scene._performanceDisplay.destroy(); + scene._performanceContainer.parentNode.removeChild(scene._performanceContainer); } context.endFrame(); callAfterRenderFunctions(frameState); + + scene._postRender.raiseEvent(scene, time); + } + + /** + * DOC_TBA + * @memberof Scene + */ + Scene.prototype.render = function(time) { + try { + render(this, time); + } catch (error) { + this._renderError.raiseEvent(this, error); + + if (this.rethrowRenderErrors) { + throw error; + } + } }; var orthoPickingFrustum = new OrthographicFrustum(); diff --git a/Source/Widgets/CesiumWidget/CesiumWidget.js b/Source/Widgets/CesiumWidget/CesiumWidget.js index 870453b8935b..9ce6341477bf 100644 --- a/Source/Widgets/CesiumWidget/CesiumWidget.js +++ b/Source/Widgets/CesiumWidget/CesiumWidget.js @@ -61,24 +61,12 @@ define([ return; } - try { - if (widget._useDefaultRenderLoop) { - widget.resize(); - widget.render(); - requestAnimationFrame(render); - } else { - widget._renderLoopRunning = false; - } - } catch (error) { - widget._useDefaultRenderLoop = false; + if (widget._useDefaultRenderLoop) { + widget.resize(); + widget.render(); + requestAnimationFrame(render); + } else { widget._renderLoopRunning = false; - widget._renderLoopError.raiseEvent(widget, error); - if (widget._showRenderLoopErrors) { - var title = 'An error occurred while rendering. Rendering has stopped.'; - var message = formatError(error); - widget.showErrorPanel(title, message); - console.error(title + ' ' + message); - } } } @@ -234,7 +222,6 @@ define([ this._creditContainer = creditContainer; this._canRender = false; this._showRenderLoopErrors = defaultValue(options.showRenderLoopErrors, true); - this._renderLoopError = new Event(); if (options.sceneMode) { if (options.sceneMode === SceneMode.SCENE2D) { @@ -247,6 +234,17 @@ define([ this.useDefaultRenderLoop = defaultValue(options.useDefaultRenderLoop, true); + var that = this; + scene.renderError.addEventListener(function(scene, error) { + that._useDefaultRenderLoop = false; + that._renderLoopRunning = false; + if (that._showRenderLoopErrors) { + var title = 'An error occurred while rendering. Rendering has stopped.'; + var message = formatError(error); + that.showErrorPanel(title, message); + console.error(title + ' ' + message); + } + }); } catch (error) { var title = 'Error constructing CesiumWidget. Check if WebGL is enabled.'; this.showErrorPanel(title, error); @@ -327,27 +325,16 @@ define([ } }, - /** - * Gets the event that will be raised when an error is encountered during the default render loop. - * The widget instance and the generated exception are the only two parameters passed to the event handler. - * useDefaultRenderLoop will be set to false whenever an exception is generated and must - * be set back to true to continue rendering after an exception. - * @memberof Viewer.prototype - * @type {Event} - */ - onRenderLoopError : { - get : function() { - return this._renderLoopError; - } - }, - /** * Gets or sets whether or not this widget should control the render loop. * If set to true the widget will use {@link requestAnimationFrame} to * perform rendering and resizing of the widget, as well as drive the * simulation clock. If set to false, you must manually call the * resize, render methods as part of a custom - * render loop. + * render loop. If an error occurs during rendering, {@link Scene}'s + * renderError event will be raised and this property + * will be set to false. It must be set back to true to continue rendering + * after the error. * @memberof CesiumWidget.prototype * * @type {Boolean} diff --git a/Source/Widgets/Viewer/Viewer.js b/Source/Widgets/Viewer/Viewer.js index 0a4368eb6e45..a14a449a2f00 100644 --- a/Source/Widgets/Viewer/Viewer.js +++ b/Source/Widgets/Viewer/Viewer.js @@ -67,39 +67,6 @@ define([ clock.shouldAnimate = false; } - function startRenderLoop(viewer) { - viewer._renderLoopRunning = true; - - function render() { - if (viewer.isDestroyed()) { - return; - } - - try { - if (viewer._useDefaultRenderLoop) { - viewer.resize(); - viewer.render(); - requestAnimationFrame(render); - } else { - viewer._renderLoopRunning = false; - } - } catch (error) { - viewer._useDefaultRenderLoop = false; - viewer._renderLoopRunning = false; - viewer._renderLoopError.raiseEvent(viewer, error); - if (viewer._showRenderLoopErrors) { - /*global console*/ - var title = 'An error occurred while rendering. Rendering has stopped.'; - var message = formatError(error); - viewer.cesiumWidget.showErrorPanel(title, message); - console.error(title + ' ' + message); - } - } - } - - requestAnimationFrame(render); - } - /** * A base widget for building applications. It composites all of the standard Cesium widgets into one reusable package. * The widget can always be extended by using mixins, which add functionality useful for a variety of applications. @@ -243,7 +210,8 @@ Either specify options.terrainProvider instead or set options.baseLayerPicker to skyBox : options.skyBox, sceneMode : options.sceneMode, contextOptions : options.contextOptions, - useDefaultRenderLoop : false + useDefaultRenderLoop : options.useDefaultRenderLoop, + showRenderLoopErrors : options.showRenderLoopErrors }); var dataSourceCollection = new DataSourceCollection(); @@ -468,14 +436,12 @@ Either specify options.terrainProvider instead or set options.baseLayerPicker to this._eventHelper = eventHelper; this._lastWidth = 0; this._lastHeight = 0; - this._useDefaultRenderLoop = undefined; - this._renderLoopRunning = false; - this._showRenderLoopErrors = defaultValue(options.showRenderLoopErrors, true); - this._renderLoopError = new Event(); this._allowDataSourcesToSuspendAnimation = true; - //Start the render loop if not explicitly disabled in options. - this.useDefaultRenderLoop = defaultValue(options.useDefaultRenderLoop, true); + // Prior to each render, check if anything needs to be resized. + cesiumWidget.scene.preRender.addEventListener(function(scene, time) { + resizeViewer(that); + }); }; defineProperties(Viewer.prototype, { @@ -677,42 +643,26 @@ Either specify options.terrainProvider instead or set options.baseLayerPicker to } }, - /** - * Gets the event that will be raised when an error is encountered during the default render loop. - * The viewer instance and the generated exception are the only two parameters passed to the event handler. - * useDefaultRenderLoop will be set to false whenever an exception is generated and must - * be set back to true to continue rendering after an exception. - * @memberof Viewer.prototype - * @type {Event} - */ - renderLoopError : { - get : function() { - return this._renderLoopError; - } - }, - /** * Gets or sets whether or not this widget should control the render loop. * If set to true the widget will use {@link requestAnimationFrame} to * perform rendering and resizing of the widget, as well as drive the * simulation clock. If set to false, you must manually call the * resize, render methods - * as part of a custom render loop. + * as part of a custom render loop. If an error occurs during rendering, {@link Scene}'s + * renderError event will be raised and this property + * will be set to false. It must be set back to true to continue rendering + * after the error. * @memberof Viewer.prototype * * @type {Boolean} */ useDefaultRenderLoop : { get : function() { - return this._useDefaultRenderLoop; + return this._cesiumWidget.useDefaultRenderLoop; }, set : function(value) { - if (this._useDefaultRenderLoop !== value) { - this._useDefaultRenderLoop = value; - if (value && !this._renderLoopRunning) { - startRenderLoop(this); - } - } + this._cesiumWidget.useDefaultRenderLoop = value; } }, @@ -767,88 +717,7 @@ Either specify options.terrainProvider instead or set options.baseLayerPicker to Viewer.prototype.resize = function() { var cesiumWidget = this._cesiumWidget; cesiumWidget.resize(); - - var container = this._container; - var width = container.clientWidth; - var height = container.clientHeight; - if (width === this._lastWidth && height === this._lastHeight) { - return; - } - - var panelMaxHeight = height - 125; - var baseLayerPickerDropDown = this._baseLayerPickerDropDown; - - if (defined(baseLayerPickerDropDown)) { - baseLayerPickerDropDown.style.maxHeight = panelMaxHeight + 'px'; - } - - if (defined(this._infoBox)) { - this._infoBox.viewModel.maxHeight = panelMaxHeight; - } - - var timeline = this._timeline; - var timelineExists = defined(timeline); - var animationExists = defined(this._animation); - var animationContainer; - var resizeWidgets = !animationExists; - var animationWidth = 0; - - if (animationExists) { - var lastWidth = this._lastWidth; - animationContainer = this._animation.container; - if (width > 900) { - if (lastWidth <= 900) { - animationWidth = 169; - animationContainer.style.width = '169px'; - animationContainer.style.height = '112px'; - resizeWidgets = true; - this._animation.resize(); - } - } else if (width >= 600) { - if (lastWidth < 600 || lastWidth > 900) { - animationWidth = 136; - animationContainer.style.width = '136px'; - animationContainer.style.height = '90px'; - resizeWidgets = true; - this._animation.resize(); - } - } else if (lastWidth > 600 || lastWidth === 0) { - animationWidth = 106; - animationContainer.style.width = '106px'; - animationContainer.style.height = '70px'; - resizeWidgets = true; - this._animation.resize(); - } - } - - if (resizeWidgets) { - var logoBottom = 0; - var logoLeft = animationWidth + 5; - if (timelineExists) { - var fullscreenButton = this._fullscreenButton; - var timelineContainer = timeline.container; - var timelineStyle = timelineContainer.style; - - logoBottom = timelineContainer.clientHeight + 3; - timelineStyle.left = animationWidth + 'px'; - - if (defined(fullscreenButton)) { - timelineStyle.right = fullscreenButton.container.clientWidth + 'px'; - } - } - if (timelineExists || animationExists) { - var creditContainer = cesiumWidget.creditContainer; - creditContainer.style.bottom = logoBottom + 'px'; - creditContainer.style.left = logoLeft + 'px'; - } - } - - if (timelineExists) { - timeline.resize(); - } - - this._lastWidth = width; - this._lastHeight = height; + resizeViewer(this); }; /** @@ -937,5 +806,89 @@ Either specify options.terrainProvider instead or set options.baseLayerPicker to return destroyObject(this); }; + function resizeViewer(viewer) { + var container = viewer._container; + var width = container.clientWidth; + var height = container.clientHeight; + if (width === viewer._lastWidth && height === viewer._lastHeight) { + return; + } + + var panelMaxHeight = height - 125; + var baseLayerPickerDropDown = viewer._baseLayerPickerDropDown; + + if (defined(baseLayerPickerDropDown)) { + baseLayerPickerDropDown.style.maxHeight = panelMaxHeight + 'px'; + } + + if (defined(viewer._infoBox)) { + viewer._infoBox.viewModel.maxHeight = panelMaxHeight; + } + + var timeline = viewer._timeline; + var timelineExists = defined(timeline); + var animationExists = defined(viewer._animation); + var animationContainer; + var resizeWidgets = !animationExists; + var animationWidth = 0; + + if (animationExists) { + var lastWidth = viewer._lastWidth; + animationContainer = viewer._animation.container; + if (width > 900) { + if (lastWidth <= 900) { + animationWidth = 169; + animationContainer.style.width = '169px'; + animationContainer.style.height = '112px'; + resizeWidgets = true; + viewer._animation.resize(); + } + } else if (width >= 600) { + if (lastWidth < 600 || lastWidth > 900) { + animationWidth = 136; + animationContainer.style.width = '136px'; + animationContainer.style.height = '90px'; + resizeWidgets = true; + viewer._animation.resize(); + } + } else if (lastWidth > 600 || lastWidth === 0) { + animationWidth = 106; + animationContainer.style.width = '106px'; + animationContainer.style.height = '70px'; + resizeWidgets = true; + viewer._animation.resize(); + } + } + + if (resizeWidgets) { + var logoBottom = 0; + var logoLeft = animationWidth + 5; + if (timelineExists) { + var fullscreenButton = viewer._fullscreenButton; + var timelineContainer = timeline.container; + var timelineStyle = timelineContainer.style; + + logoBottom = timelineContainer.clientHeight + 3; + timelineStyle.left = animationWidth + 'px'; + + if (defined(fullscreenButton)) { + timelineStyle.right = fullscreenButton.container.clientWidth + 'px'; + } + } + if (timelineExists || animationExists) { + var creditContainer = viewer._cesiumWidget.creditContainer; + creditContainer.style.bottom = logoBottom + 'px'; + creditContainer.style.left = logoLeft + 'px'; + } + } + + if (timelineExists) { + timeline.resize(); + } + + viewer._lastWidth = width; + viewer._lastHeight = height; + } + return Viewer; }); diff --git a/Source/Widgets/Viewer/viewerCesiumInspectorMixin.js b/Source/Widgets/Viewer/viewerCesiumInspectorMixin.js index ad893aaca340..aea40b5a7f2e 100644 --- a/Source/Widgets/Viewer/viewerCesiumInspectorMixin.js +++ b/Source/Widgets/Viewer/viewerCesiumInspectorMixin.js @@ -14,33 +14,6 @@ define([ "use strict"; /*global console*/ - function startRenderLoop(viewer) { - viewer._renderLoopRunning = true; - - function render() { - if (viewer.isDestroyed()) { - return; - } - try { - viewer.resize(); - viewer.render(); - viewer.cesiumInspector.viewModel.update(); - requestAnimationFrame(render); - } catch (e) { - viewer._useDefaultRenderLoop = false; - viewer._renderLoopRunning = false; - viewer._renderLoopError.raiseEvent(viewer, e); - if (viewer._showRenderLoopErrors) { - /*global console*/ - viewer.cesiumWidget.showErrorPanel('An error occurred while rendering. Rendering has stopped.', e); - console.error(e); - } - } - } - - requestAnimationFrame(render); - } - /** * A mixin which adds the CesiumInspector widget to the Viewer widget. * Rather than being called directly, this function is normally passed as @@ -52,7 +25,6 @@ define([ * @exception {DeveloperError} viewer is required. * * @example - * // Add basic drag and drop support and pop up an alert window on error. * var viewer = new Cesium.Viewer('cesiumContainer'); * viewer.extend(Cesium.viewerCesiumInspectorMixin); */ @@ -64,9 +36,7 @@ define([ var cesiumInspectorContainer = document.createElement('div'); cesiumInspectorContainer.className = 'cesium-viewer-cesiumInspectorContainer'; viewer.container.appendChild(cesiumInspectorContainer); - var cesiumInspector = new CesiumInspector(cesiumInspectorContainer, viewer.cesiumWidget.scene); - - viewer.useDefaultRenderLoop = false; + var cesiumInspector = new CesiumInspector(cesiumInspectorContainer, viewer.scene); defineProperties(viewer, { cesiumInspector : { @@ -76,7 +46,9 @@ define([ } }); - startRenderLoop(viewer); + viewer.scene.postRender.addEventListener(function() { + viewer.cesiumInspector.viewModel.update(); + }); }; return viewerCesiumInspectorMixin; diff --git a/Specs/Scene/SceneSpec.js b/Specs/Scene/SceneSpec.js index 6045e9774151..20004a9a7f3a 100644 --- a/Specs/Scene/SceneSpec.js +++ b/Specs/Scene/SceneSpec.js @@ -519,4 +519,69 @@ defineSuite([ destroyScene(s); expect(s.isDestroyed()).toEqual(true); }); + + it('raises renderError when render throws', function() { + var s = createScene(); + + var spyListener = jasmine.createSpy('listener'); + s.renderError.addEventListener(spyListener); + + var error = 'foo'; + s.primitives.update = function() { + throw error; + }; + + s.render(); + + expect(spyListener).toHaveBeenCalledWith(s, error); + + destroyScene(s); + }); + + it('a render error is rethrown if rethrowRenderErrors is true', function() { + var s = createScene(); + s.rethrowRenderErrors = true; + + var spyListener = jasmine.createSpy('listener'); + s.renderError.addEventListener(spyListener); + + var error = 'foo'; + s.primitives.update = function() { + throw error; + }; + + expect(function() { + s.render(); + }).toThrow(); + + expect(spyListener).toHaveBeenCalledWith(s, error); + + destroyScene(s); + }); + + it('raises the preRender event prior to rendering', function() { + var s = createScene(); + + var spyListener = jasmine.createSpy('listener'); + s.preRender.addEventListener(spyListener); + + s.render(); + + expect(spyListener.callCount).toBe(1); + + destroyScene(s); + }); + + it('raises the postRender event after rendering', function() { + var s = createScene(); + + var spyListener = jasmine.createSpy('listener'); + s.postRender.addEventListener(spyListener); + + s.render(); + + expect(spyListener.callCount).toBe(1); + + destroyScene(s); + }); }, 'WebGL'); diff --git a/Specs/Widgets/CesiumWidget/CesiumWidgetSpec.js b/Specs/Widgets/CesiumWidget/CesiumWidgetSpec.js index 4e298aff89e8..2106a5546b93 100644 --- a/Specs/Widgets/CesiumWidget/CesiumWidgetSpec.js +++ b/Specs/Widgets/CesiumWidget/CesiumWidgetSpec.js @@ -175,33 +175,25 @@ defineSuite([ }).toThrowDeveloperError(); }); - it('raises onRenderLoopError and stops the render loop when render throws', function() { + it('stops the render loop when render throws', function() { widget = new CesiumWidget(container); expect(widget.useDefaultRenderLoop).toEqual(true); - var spyListener = jasmine.createSpy('listener'); - widget.onRenderLoopError.addEventListener(spyListener); - var error = 'foo'; - widget.render = function() { + widget.scene.primitives.update = function() { throw error; }; waitsFor(function() { - return spyListener.wasCalled; - }); - - runs(function() { - expect(spyListener).toHaveBeenCalledWith(widget, error); - expect(widget.useDefaultRenderLoop).toEqual(false); - }); + return !widget.useDefaultRenderLoop; + }, 'render loop to be disabled.'); }); it('shows the error panel when render throws', function() { widget = new CesiumWidget(container); var error = 'foo'; - widget.render = function() { + widget.scene.primitives.update = function() { throw error; }; @@ -226,7 +218,7 @@ defineSuite([ }); var error = 'foo'; - widget.render = function() { + widget.scene.primitives.update = function() { throw error; }; diff --git a/Specs/Widgets/Viewer/ViewerSpec.js b/Specs/Widgets/Viewer/ViewerSpec.js index 97d336b5fc20..4d8e001bd78c 100644 --- a/Specs/Widgets/Viewer/ViewerSpec.js +++ b/Specs/Widgets/Viewer/ViewerSpec.js @@ -391,26 +391,18 @@ defineSuite([ }).toThrowDeveloperError(); }); - it('raises renderLoopError and stops the render loop when render throws', function() { + it('stops the render loop when render throws', function() { viewer = new Viewer(container); expect(viewer.useDefaultRenderLoop).toEqual(true); - var spyListener = jasmine.createSpy('listener'); - viewer.renderLoopError.addEventListener(spyListener); - var error = 'foo'; - viewer.render = function() { + viewer.scene.primitives.update = function() { throw error; }; waitsFor(function() { - return spyListener.wasCalled; - }); - - runs(function() { - expect(spyListener).toHaveBeenCalledWith(viewer, error); - expect(viewer.useDefaultRenderLoop).toEqual(false); - }); + return !viewer.useDefaultRenderLoop; + }, 'render loop to be disabled.'); }); it('sets the clock and timeline based on the first data source', function() { @@ -550,7 +542,7 @@ defineSuite([ viewer = new Viewer(container); var error = 'foo'; - viewer.render = function() { + viewer.scene.primitives.update = function() { throw error; }; @@ -575,7 +567,7 @@ defineSuite([ }); var error = 'foo'; - viewer.render = function() { + viewer.scene.primitives.update = function() { throw error; };