@itwin/core-frontend
Version:
iTwin.js frontend components
461 lines • 22.2 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
* @module Views
*/
import { BeEvent, BentleyStatus, BeUiEvent } from "@itwin/core-bentley";
import { IModelApp } from "./IModelApp";
import { DisclosedTileTreeSet } from "./tile/internal";
import { EventHandled } from "./tools/Tool";
import { System } from "./internal/render/webgl/System";
/** The ViewManager holds the list of opened views, plus the *selected view*. It also provides notifications of view open/close and suspend/resume.
* Applications must call [[addViewport]] when new Viewports that should be associated with user events are created.
*
* A single ViewManager is created when [[IModelApp.startup]] is called. It can be accessed via the static member [[IModelApp.viewManager]].
*
* The ViewManager controls the render loop, which causes the contents of each registered [[Viewport]] to update on the screen.
* @public
* @extensions
*/
export class ViewManager {
inDynamicsMode = false;
cursor = "default";
_viewports = [];
decorators = [];
_selectedView;
_invalidateScenes = false;
_skipSceneCreation = false;
_doIdleWork = false;
_idleWorkTimer;
/** @internal */
toolTipProviders = [];
_beginIdleWork() {
const idleWork = () => {
if (undefined === this._idleWorkTimer)
return;
if (this._viewports.length > 0) {
this._idleWorkTimer = undefined;
return;
}
if (IModelApp.renderSystem.doIdleWork())
this._idleWorkTimer = setTimeout(idleWork, 1);
else
this._idleWorkTimer = undefined;
};
if (undefined === this._idleWorkTimer)
this._idleWorkTimer = setTimeout(idleWork, 1);
}
/** @internal */
onInitialized() {
this.addDecorator(IModelApp.accuSnap);
this.addDecorator(IModelApp.tentativePoint);
this.addDecorator(IModelApp.accuDraw);
this.addDecorator(IModelApp.toolAdmin);
this.cursor = "default";
const options = IModelApp.renderSystem.options;
this._doIdleWork = true === options.doIdleWork;
if (this._doIdleWork)
this._beginIdleWork();
}
/** @internal */
onShutDown() {
if (undefined !== this._idleWorkTimer) {
clearTimeout(this._idleWorkTimer);
this._idleWorkTimer = undefined;
}
this._viewports.map((viewport) => {
if (!viewport.isDisposed)
this.dropViewport(viewport, true);
});
this._viewports.length = 0;
this.decorators.length = 0;
this.toolTipProviders.length = 0;
this._selectedView = undefined;
}
/** Returns true if the specified viewport is currently being managed by this ViewManager.
* @see [[addViewport]] to enable management of a viewport and [[dropViewport]] to disable it.
*/
hasViewport(viewport) {
return this._viewports.includes(viewport);
}
/** Called after the selected view changes.
* @param old Previously selected viewport.
* @param current Currently selected viewport.
*/
onSelectedViewportChanged = new BeUiEvent();
/** Called after a view is opened. This can happen when the iModel is first opened or when a user opens a new view. */
onViewOpen = new BeUiEvent();
/** Called after a view is closed. This can happen when the iModel is closed or when a user closes an open view. */
onViewClose = new BeUiEvent();
/** Called after a view is suspended. This happens when the application is minimized or, on a tablet, when the application
* is moved to the background.
*/
onViewSuspend = new BeUiEvent();
/** Called after a suspended view is resumed. This can happen when a minimized application is restored
* or, on a tablet, when the application is moved to the foreground.
*/
onViewResume = new BeUiEvent();
/** Called at the beginning of each tick of the render loop, before any viewports have been updated.
* The render loop is typically invoked by a requestAnimationFrame() callback. It will not be invoked if the ViewManager is tracking no viewports.
* @note Due to the frequency of this event, avoid performing expensive work inside event listeners.
* @see [[ViewManager.onFinishRender]]
*/
onBeginRender = new BeEvent();
/** Called at the end of each tick of the render loop, after all viewports have been updated.
* The render loop is typically invoked by a requestAnimationFrame() callback. It will not be invoked if the ViewManager is tracking no viewports.
* @note Due to the frequency of this event, avoid performing expensive work inside event listeners.
* @see [[ViewManager.onBeginRender]]
*/
onFinishRender = new BeEvent();
/** @internal */
endDynamicsMode() {
if (!this.inDynamicsMode)
return;
this.inDynamicsMode = false;
const cursorVp = IModelApp.toolAdmin.cursorView;
if (cursorVp)
cursorVp.changeDynamics(undefined, undefined);
for (const vp of this._viewports) {
if (vp !== cursorVp)
vp.changeDynamics(undefined, undefined);
}
}
/** @internal */
beginDynamicsMode() { this.inDynamicsMode = true; }
/** @internal */
get doesHostHaveFocus() { return document.hasFocus(); }
/** Set the selected [[Viewport]] to undefined. */
clearSelectedView() {
const previousVp = this.selectedView;
this._selectedView = undefined;
this.notifySelectedViewportChanged(previousVp, undefined);
}
/** Sets the selected [[Viewport]]. */
async setSelectedView(vp) {
if (undefined === vp)
vp = this.getFirstOpenView();
if (vp === this.selectedView) // already the selected view
return BentleyStatus.SUCCESS;
if (undefined === vp) {
this.clearSelectedView();
return BentleyStatus.ERROR;
}
const previousVp = this.selectedView;
this._selectedView = vp;
this.notifySelectedViewportChanged(previousVp, vp);
if (undefined === previousVp)
await IModelApp.toolAdmin.startDefaultTool();
return BentleyStatus.SUCCESS;
}
/** @internal */
notifySelectedViewportChanged(previous, current) {
IModelApp.toolAdmin.onSelectedViewportChanged(previous, current); // eslint-disable-line @typescript-eslint/no-floating-promises
this.onSelectedViewportChanged.emit({ previous, current });
}
/** The "selected view" is the default for certain operations. */
get selectedView() { return this._selectedView; }
/** Get the first opened view. */
getFirstOpenView() { return this._viewports.length > 0 ? this._viewports[0] : undefined; }
/** Check if only a single viewport is being used. If so, render directly on-screen using its WebGL canvas. Otherwise, render each view offscreen. */
updateRenderToScreen() {
const renderToScreen = 1 === this._viewports.length;
for (const vp of this)
vp.rendersToScreen = renderToScreen;
}
/** Add a new Viewport to the list of opened views and create an EventController for it.
* @param newVp the Viewport to add
* @returns SUCCESS if vp was successfully added, ERROR if it was already present.
* @note raises onViewOpen event with newVp.
*/
addViewport(newVp) {
if (this.hasViewport(newVp)) // make sure its not already added
return BentleyStatus.ERROR;
newVp.onViewManagerAdd();
this._viewports.push(newVp);
this.updateRenderToScreen();
this.setSelectedView(newVp); // eslint-disable-line @typescript-eslint/no-floating-promises
// Start up the render loop if necessary.
if (1 === this._viewports.length)
IModelApp.startEventLoop();
this.onViewOpen.emit(newVp);
return BentleyStatus.SUCCESS;
}
/** Remove a Viewport from the list of opened views, and optionally dispose of it.
* Typically a Viewport is dropped when it is no longer of any use to the application, in which case it should also be
* disposed of as it may hold significant GPU resources.
* However in some cases a Viewport may be temporarily dropped to suspend rendering; and subsequently re-added to
* resume rendering - for example, when the Viewport is temporarily hidden by other UI elements.
* In the latter case it is up to the caller to ensure the Viewport is properly disposed of when it is no longer needed.
* Attempting to invoke any function on a Viewport after it has been disposed is an error.
* @param vp the Viewport to remove.
* @param disposeOfViewport Whether or not to dispose of the Viewport. Defaults to true.
* @return SUCCESS if vp was successfully removed, ERROR if it was not present.
* @note raises onViewClose event with vp.
*/
dropViewport(vp, disposeOfViewport = true) {
const index = this._viewports.indexOf(vp);
if (index === -1)
return BentleyStatus.ERROR;
this.onViewClose.emit(vp);
// make sure tools don't think the cursor is still in this viewport
IModelApp.toolAdmin.forgetViewport(vp);
vp.onViewManagerDrop();
this._viewports.splice(index, 1);
if (this.selectedView === vp) // if removed viewport was selectedView, set it to undefined.
this.setSelectedView(undefined); // eslint-disable-line @typescript-eslint/no-floating-promises
vp.rendersToScreen = false;
this.updateRenderToScreen();
if (disposeOfViewport)
vp[Symbol.dispose]();
if (this._doIdleWork && this._viewports.length === 0)
this._beginIdleWork();
return BentleyStatus.SUCCESS;
}
/** Iterate over the viewports registered with the view manager. */
[Symbol.iterator]() {
return this._viewports[Symbol.iterator]();
}
/** Instruct each registered [[Viewport]] that the cached [[Decorations]] for the specified `decorator` should be discarded and recreated on the next frame.
* @see [[Viewport.invalidateCachedDecorations]] to invalidate the cached decorations for a single viewport.
*/
invalidateCachedDecorationsAllViews(decorator) {
if (decorator.useCachedDecorations)
for (const vp of this)
vp.invalidateCachedDecorations(decorator);
}
/** Force each registered [[Viewport]] to regenerate its [[Decorations]] on the next frame. */
invalidateDecorationsAllViews() {
for (const vp of this)
vp.invalidateDecorations();
}
/** Force each registered [[Viewport]] to regenerate its [[FeatureSymbology.Overrides]] on the next frame.
* This is rarely needed - viewports keep track of their own states to detect when the overrides need to be recreated.
*/
invalidateSymbologyOverridesAllViews() {
for (const vp of this)
vp.setFeatureOverrideProviderChanged();
}
/** @internal */
onSelectionSetChanged(_iModel) {
for (const vp of this)
vp.markSelectionSetDirty();
IModelApp.requestNextAnimation();
}
/** @internal */
invalidateViewportScenes() {
for (const vp of this)
vp.invalidateScene();
}
/** @internal */
validateViewportScenes() {
for (const vp of this)
vp.setValidScene();
}
/** Requests that [[Viewport.createScene]] be invoked for every viewport on the next frame.
* This is rarely useful - viewports keep track of their own states to detect when the scene needs to be recreated.
*/
invalidateScenes() {
this._invalidateScenes = true;
IModelApp.requestNextAnimation();
}
/** @internal */
get sceneInvalidated() { return this._invalidateScenes; }
/** Invoked by ToolAdmin event loop.
* @internal
*/
renderLoop() {
if (0 === this._viewports.length)
return;
if (this._skipSceneCreation)
this.validateViewportScenes();
else if (this._invalidateScenes)
this.invalidateViewportScenes();
this._invalidateScenes = false;
this.onBeginRender.raiseEvent();
for (const vp of this._viewports)
vp.renderFrame();
this.onFinishRender.raiseEvent();
}
/** Purge TileTrees that haven't been drawn since the specified time point and are not currently in use by any ScreenViewport.
* Intended strictly for debugging purposes - TileAdmin takes care of properly purging.
* @internal
*/
purgeTileTrees(olderThan) {
// A single viewport can display tiles from more than one IModelConnection.
// NOTE: A viewport may be displaying no trees - but we need to record its IModel so we can purge those which are NOT being displayed
// NOTE: That won't catch external tile trees previously used by that viewport.
const trees = new DisclosedTileTreeSet();
const treesByIModel = new Map();
for (const vp of this._viewports) {
vp.discloseTileTrees(trees);
if (undefined === treesByIModel.get(vp.iModel))
treesByIModel.set(vp.iModel, new Set());
}
for (const tree of trees) {
let set = treesByIModel.get(tree.iModel);
if (undefined === set) {
set = new Set();
treesByIModel.set(tree.iModel, set);
}
set.add(tree);
}
for (const entry of treesByIModel) {
const iModel = entry[0];
iModel.tiles.purge(olderThan, entry[1]);
}
}
/** Compute the tooltip for a persistent element.
* This method calls the backend method [Element.getToolTipMessage]($backend), and replaces all instances of `${localizeTag}` with localized string from IModelApp.i18n.
*/
async getElementToolTip(hit) {
const msg = await hit.iModel.getToolTipMessage(hit.sourceId); // wait for the locate message(s) from the backend
return IModelApp.formatElementToolTip(msg);
}
/** Register a new [[ToolTipProvider]] to customize the locate tooltip.
* @param provider The new tooltip provider to add.
* @throws Error if `provider` is already registered.
* @returns a function that may be called to remove this provider (in lieu of calling [[dropToolTipProvider]].)
*/
addToolTipProvider(provider) {
if (this.toolTipProviders.includes(provider))
throw new Error("tooltip provider already registered");
this.toolTipProviders.push(provider);
return () => this.dropToolTipProvider(provider);
}
/** Drop (remove) a [[ToolTipProvider]] so it is no longer active.
* @param provider The tooltip provider to drop.
* @note Does nothing if provider is not currently active.
*/
dropToolTipProvider(provider) {
const index = this.toolTipProviders.indexOf(provider);
if (index >= 0)
this.toolTipProviders.splice(index, 1);
}
/** Add a new [[Decorator]] to display decorations into the active views.
* @param decorator The new decorator to add.
* @throws Error if decorator is already active.
* @returns a function that may be called to remove this decorator (in lieu of calling [[dropDecorator]].)
* @see [[dropDecorator]]
*/
addDecorator(decorator) {
if (this.decorators.includes(decorator))
throw new Error("decorator already registered");
this.decorators.push(decorator);
this.invalidateDecorationsAllViews();
return () => this.dropDecorator(decorator);
}
/** Drop (remove) a [[Decorator]] so it is no longer active.
* @param decorator The Decorator to drop.
* @returns true if the decorator was found and removed; false if the decorator was not found.
*/
dropDecorator(decorator) {
const index = this.decorators.indexOf(decorator);
if (index < 0)
return false;
this.invalidateCachedDecorationsAllViews(decorator);
this.decorators.splice(index, 1);
this.invalidateDecorationsAllViews();
return true;
}
/** Get the tooltip for a pickable decoration.
* @internal
*/
async getDecorationToolTip(hit) {
for (const decorator of this.decorators) {
if (undefined !== decorator.testDecorationHit && undefined !== decorator.getDecorationToolTip && decorator.testDecorationHit(hit.sourceId))
return decorator.getDecorationToolTip(hit);
}
return hit.viewport ? hit.viewport.getToolTip(hit) : "";
}
/** Allow a pickable decoration to handle a button event that identified it for the SelectTool.
* @internal
*/
async onDecorationButtonEvent(hit, ev) {
for (const decorator of IModelApp.viewManager.decorators) {
if (undefined !== decorator.testDecorationHit && undefined !== decorator.onDecorationButtonEvent && decorator.testDecorationHit(hit.sourceId))
return decorator.onDecorationButtonEvent(hit, ev);
}
return EventHandled.No;
}
/** Allow a pickable decoration to be snapped to by AccuSnap or TentativePoint.
* @internal
*/
getDecorationGeometry(hit) {
for (const decorator of IModelApp.viewManager.decorators) {
if (undefined !== decorator.testDecorationHit && undefined !== decorator.getDecorationGeometry && decorator.testDecorationHit(hit.sourceId))
return decorator.getDecorationGeometry(hit);
}
return undefined;
}
/** Allow a pickable decoration created using a persistent element id to augment or replace the the persistent element's tooltip.
* @internal
*/
async overrideElementToolTip(hit) {
for (const decorator of this.decorators) {
if (undefined !== decorator.overrideElementHit && undefined !== decorator.getDecorationToolTip && decorator.overrideElementHit(hit))
return decorator.getDecorationToolTip(hit);
}
return this.getElementToolTip(hit);
}
/** Allow a pickable decoration created using a persistent element id to handle a button event that identified it for the SelectTool.
* @internal
*/
async overrideElementButtonEvent(hit, ev) {
for (const decorator of IModelApp.viewManager.decorators) {
if (undefined !== decorator.overrideElementHit && undefined !== decorator.onDecorationButtonEvent && decorator.overrideElementHit(hit))
return decorator.onDecorationButtonEvent(hit, ev);
}
return EventHandled.No;
}
/** Allow a pickable decoration created using a persistent element id to control whether snapping uses the persistent element's geometry.
* @internal
*/
overrideElementGeometry(hit) {
for (const decorator of IModelApp.viewManager.decorators) {
if (undefined !== decorator.overrideElementHit && undefined !== decorator.getDecorationGeometry && decorator.overrideElementHit(hit))
return decorator.getDecorationGeometry(hit);
}
return undefined;
}
get crossHairCursor() { return `url(${IModelApp.publicPath}cursors/crosshair.cur), crosshair`; }
get dynamicsCursor() { return `url(${IModelApp.publicPath}cursors/dynamics.cur), move`; }
get grabCursor() { return `url(${IModelApp.publicPath}cursors/openHand.cur), auto`; }
get grabbingCursor() { return `url(${IModelApp.publicPath}cursors/closedHand.cur), auto`; }
get walkCursor() { return `url(${IModelApp.publicPath}cursors/walk.cur), auto`; }
get rotateCursor() { return `url(${IModelApp.publicPath}cursors/rotate.cur), auto`; }
get lookCursor() { return `url(${IModelApp.publicPath}cursors/look.cur), auto`; }
get zoomCursor() { return `url(${IModelApp.publicPath}cursors/zoom.cur), auto`; }
/** Change the cursor shown in all Viewports.
* @param cursor The new cursor to display. If undefined, the default cursor is used.
*/
setViewCursor(cursor = "default") {
if (cursor === this.cursor)
return;
this.cursor = cursor;
for (const vp of this._viewports)
vp.setCursor(cursor);
}
/** Intended strictly as a temporary solution for interactive editing applications, until official support for such apps is implemented.
* Call this after editing one or more models, passing in the Ids of those models, to cause new tiles to be generated reflecting the changes.
* Pass undefined if you are unsure which models changed (this is less efficient as it discards all tiles for all viewed models in all viewports).
* @internal
*/
refreshForModifiedModels(modelIds) {
for (const vp of this._viewports)
vp.refreshForModifiedModels(modelIds);
}
/** Sets the number of [MSAA]($docs/learning/display/MSAA.md) samples for all currently- and subsequently-opened [[ScreenViewport]]s.
* @param numSamples The number of samples as a power of two. Values of 1 or less indicates anti-aliasing should be disabled. Non-power-of-two values are rounded
* down to the nearest power of two. The maximum number of samples supported depends upon the client's graphics hardware capabilities. Higher values produce
* a higher-quality image but also may also reduce framerate.
* @see [[Viewport.antialiasSamples]] to adjust the number of samples for a specific viewport.
*/
setAntialiasingAllViews(numSamples) {
for (const vp of this)
vp.antialiasSamples = numSamples;
System.instance.antialiasSamples = numSamples;
}
}
//# sourceMappingURL=ViewManager.js.map