UNPKG

@itwin/core-frontend

Version:
461 lines • 22.2 kB
/*--------------------------------------------------------------------------------------------- * 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