@itwin/core-frontend
Version:
iTwin.js frontend components
936 lines • 163 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 { asInstanceOf, assert, BeDuration, BeEvent, BeTimePoint, dispose, Id64, isInstanceOf, StopWatch, } from "@itwin/core-bentley";
import { Angle, AngleSweep, Arc3d, Geometry, Matrix3d, Plane3dByOriginAndUnitNormal, Point3d, Range1d, Range3d, Ray3d, Transform, Vector3d, } from "@itwin/core-geometry";
import { Camera, CartographicRange, ColorDef, Easing, Frustum, GlobeMode, GridOrientationType, Hilite, Interpolation, isPlacement2dProps, ModelMapLayerSettings, Npc, NpcCenter, Placement2d, Placement3d, SubCategoryAppearance, } from "@itwin/core-common";
import { ChangeFlag, MutableChangeFlags } from "./ChangeFlags";
import { CoordSystem } from "./CoordSystem";
import { DecorationsCache } from "./DecorationsCache";
import { ElementPicker, LocateOptions } from "./ElementLocateManager";
import { FrustumAnimator } from "./FrustumAnimator";
import { GlobeAnimator } from "./GlobeAnimator";
import { SnapDetail } from "./HitDetail";
import { IModelApp } from "./IModelApp";
import { linePlaneIntersect } from "./LinePlaneIntersect";
import { PerModelCategoryVisibility } from "./PerModelCategoryVisibility";
import { Decorations } from "./render/Decorations";
import { FeatureSymbology } from "./render/FeatureSymbology";
import { FrameStatsCollector } from "./internal/render/FrameStatsCollector";
import { AnimationBranchStates } from "./internal/render/AnimationBranchState";
import { Pixel } from "./render/Pixel";
import { createRenderPlanFromViewport } from "./internal/render/RenderPlan";
import { StandardView } from "./StandardView";
import { SubCategoriesCache } from "./SubCategoriesCache";
import { DisclosedTileTreeSet, MapCartoRectangle, MapTiledGraphicsProvider, MapTileTreeScaleRangeVisibility, TileBoundingBoxes, TiledGraphicsProvider, TileTreeLoadStatus, TileUser, } from "./tile/internal";
import { EventController } from "./tools/EventController";
import { ToolSettings } from "./tools/ToolSettings";
import { DecorateContext, SceneContext } from "./ViewContext";
import { viewGlobalLocation, ViewGlobalLocationConstants } from "./ViewGlobalLocation";
import { ViewingSpace } from "./ViewingSpace";
import { ViewRect } from "./common/ViewRect";
import { ViewStatus } from "./ViewStatus";
import { queryVisibleFeatures } from "./internal/render/QueryVisibileFeatures";
import { FlashSettings } from "./FlashSettings";
import { GeometricModelState } from "./ModelState";
import { GraphicType } from "./common/render/GraphicType";
import { compareMapLayer } from "./internal/render/webgl/MapLayerParams";
/** Source of depth point returned by [[Viewport.pickDepthPoint]].
* @public
*/
export var DepthPointSource;
(function (DepthPointSource) {
/** Depth point from geometry within specified radius of pick point */
DepthPointSource[DepthPointSource["Geometry"] = 0] = "Geometry";
/** Depth point from reality model within specified radius of pick point */
DepthPointSource[DepthPointSource["Model"] = 1] = "Model";
/** Depth point from ray projection to background map plane */
DepthPointSource[DepthPointSource["BackgroundMap"] = 2] = "BackgroundMap";
/** Depth point from ray projection to ground plane */
DepthPointSource[DepthPointSource["GroundPlane"] = 3] = "GroundPlane";
/** Depth point from ray projection to grid plane */
DepthPointSource[DepthPointSource["Grid"] = 4] = "Grid";
/** Depth point from ray projection to acs plane */
DepthPointSource[DepthPointSource["ACS"] = 5] = "ACS";
/** Depth point from plane passing through view target point */
DepthPointSource[DepthPointSource["TargetPoint"] = 6] = "TargetPoint";
/** Depth point from map/terrain within specified radius of pick point */
DepthPointSource[DepthPointSource["Map"] = 7] = "Map";
})(DepthPointSource || (DepthPointSource = {}));
/** Describes an undo or redo event for a [[Viewport]].
* @see [[Viewport.onViewUndoRedo]].
* @public
*/
export var ViewUndoEvent;
(function (ViewUndoEvent) {
ViewUndoEvent[ViewUndoEvent["Undo"] = 0] = "Undo";
ViewUndoEvent[ViewUndoEvent["Redo"] = 1] = "Redo";
})(ViewUndoEvent || (ViewUndoEvent = {}));
/** @internal */
export const ELEMENT_MARKED_FOR_REMOVAL = Symbol.for("@bentley/imodeljs/Viewport/__element_marked_for_removal__");
/** A Viewport renders the contents of one or more [GeometricModel]($backend)s onto an `HTMLCanvasElement`.
*
* It holds a [[ViewState]] object that defines its viewing parameters; the ViewState in turn defines the [[DisplayStyleState]],
* [[CategorySelectorState]], and - for [[SpatialViewState]]s - the [[ModelSelectorState]]. While a ViewState is being displayed by a Viewport,
* it is considered to be "attached" to that viewport; it remains attached until the Viewport is disposed of or becomes attached to a different ViewState.
* While the ViewState is attached to a Viewport, any changes made to the ViewState or its display style or category/model selectors will be automatically
* reflected in the Viewport. A ViewState can be attached to no more than one Viewport at a time.
*
* As changes to ViewState are made, Viewports also hold a stack of *previous copies* of it, to allow
* for undo/redo (i.e. *View Previous* and *View Next*) of viewing tools.
*
* Changes to a Viewport's state can be monitored by attaching an event listener to a variety of specific events. Most such events are
* triggered only once per frame, just before the Viewport's contents are rendered. For example, if the following sequence of events occurs:
*
* * First frame is rendered
* * ViewFlags are modified
* * ViewFlags are modified again
* * Second frame is rendered
*
* The [[Viewport.onDisplayStyleChanged]] event will be invoked exactly once, when the second frame is rendered.
*
* @see [[ScreenViewport]] for a viewport that can render onto the screen.
* @see [[OffScreenViewport]] for a viewport that can render into an off-screen buffer.
* @public
* @extensions
*/
export class Viewport {
/** Event called whenever this viewport is synchronized with its [[ViewState]].
* @note This event is invoked *very* frequently. To avoid negatively impacting performance, consider using one of the more specific Viewport events;
* otherwise, avoid performing excessive computations in response to this event.
* @see [[onViewportChanged]] for receiving events at more regular intervals with more specific information about what changed.
* @see [[onChangeView]] for an event raised specifically when a different [[ViewState]] becomes associated with the viewport.
*/
onViewChanged = new BeEvent();
/** Event called after reversing the most recent change to the Viewport from the undo stack or reapplying the
* most recently undone change to the Viewport from the redo stack.
*/
onViewUndoRedo = new BeEvent();
/** Event called on the next frame after this viewport's set of always-drawn elements changes. */
onAlwaysDrawnChanged = new BeEvent();
/** Event called on the next frame after this viewport's set of never-drawn elements changes. */
onNeverDrawnChanged = new BeEvent();
/** Event called on the next frame after this viewport's [[DisplayStyleState]] or its members change.
* Aspects of the display style include [ViewFlags]($common), [SubCategoryOverride]($common)s, and [[Environment]] settings.
*/
onDisplayStyleChanged = new BeEvent();
/** Event called on the next frame after this viewport's set of displayed categories changes. */
onViewedCategoriesChanged = new BeEvent();
/** Event called on the next frame after this viewport's set of [[PerModelCategoryVisibility.Overrides]] changes. */
onViewedCategoriesPerModelChanged = new BeEvent();
/** Event called on the next frame after this viewport's set of displayed models changes. */
onViewedModelsChanged = new BeEvent();
/** Event called on the next frame after this viewport's [[FeatureOverrideProvider]] changes,
* or the internal state of the provider changes such that the overrides needed to be recomputed.
*/
onFeatureOverrideProviderChanged = new BeEvent();
/** Event called on the next frame after this viewport's [[FeatureSymbology.Overrides]] change. */
onFeatureOverridesChanged = new BeEvent();
/** Event called on the next frame after any of the viewport's [[ChangeFlags]] changes. */
onViewportChanged = new BeEvent();
/** Event invoked immediately when [[changeView]] is called to replace the current [[ViewState]] with a different one. */
onChangeView = new BeEvent();
/** Event invoked immediately when the viewport is disposed.
* @see [[Viewport.dispose]].
*/
onDisposed = new BeEvent();
/** Event invoked after [[renderFrame]] detects that the dimensions of the viewport's [[ViewRect]] have changed.
*/
onResized = new BeEvent();
/** Event dispatched immediately after [[flashedId]] changes, supplying the Ids of the previously and/or currently-flashed objects.
* @note Attempting to assign to [[flashedId]] from within the event callback will produce an exception.
*/
onFlashedIdChanged = new BeEvent();
/** Event indicating when a map-layer scale range visibility change for the current viewport scale.
* @beta
*/
onMapLayerScaleRangeVisibilityChanged = new BeEvent();
/** Event invoked every time [[invalidateScene]] is called.
* @note This event will be raised **very** frequently. Avoid doing significant work inside of your event listener.
* @beta
*/
onSceneInvalidated = new BeEvent();
/** @internal */
_hasMissingTiles = false;
/** This is initialized by a call to [[changeView]] sometime shortly after the constructor is invoked.
* During that time it can be undefined. DO NOT assign directly to this member - use `setView()`.
*/
_view;
/** A function executed by `setView()` when `this._view` changes. */
_detachFromView = [];
_detachFromDisplayStyle = [];
_viewportId;
_doContinuousRendering = false;
/** @internal */
_inViewChangedEvent = false;
/** If false, indicates that [[Decorations]] should be recreated when rendering the next frame.
* @note prefer to invoke [[invalidateDecorations]] rather than directly assigning to this property.
*/
_decorationsValid = false;
/** @internal */
_sceneValid = false;
/** @internal */
get sceneValid() { return this._sceneValid; }
/** @internal */
_renderPlanValid = false;
/** @internal */
get renderPlanValid() { return this._renderPlanValid; }
/** @internal */
setRenderPlanValid() { this._renderPlanValid = true; }
/** @internal */
_controllerValid = false;
/** @internal */
get controllerValid() { return this._controllerValid; }
_redrawPending = false;
_analysisFractionValid = false;
/** @internal */
get analysisFractionValid() { return this._analysisFractionValid; }
_timePointValid = false;
/** @internal */
get timePointValid() { return this._timePointValid; }
/** Strictly for tests. @internal */
setAllValid() {
this._sceneValid = this._decorationsValid = this._renderPlanValid = this._controllerValid = this._redrawPending
= this._analysisFractionValid = this._timePointValid = true;
}
/** Mark the current set of decorations invalid, so that they will be recreated on the next render frame.
* This can be useful, for example, if an external event causes one or more current decorations to become invalid and you wish to force
* them to be recreated to show the changes.
* @note On the next frame, the `decorate` method of all [[ViewManager.decorators]] will be called. There is no way (or need) to
* invalidate individual decorations.
*/
invalidateDecorations() {
this._decorationsValid = false;
IModelApp.requestNextAnimation();
}
/** Mark the viewport's scene as having changed, so that the next call to [[renderFrame]] will recreate it.
* This method is not typically invoked directly - the scene is automatically invalidated in response to events such as moving the viewing frustum,
* changing the set of viewed models, new tiles being loaded, etc.
*/
invalidateScene() {
this._sceneValid = false;
this._timePointValid = false;
this.onSceneInvalidated.raiseEvent(this);
this.invalidateDecorations();
}
/** Mark the viewport's "render plan" as having changed, so that the next call to [[renderFrame]] will recreate it.
* This method is not typically invoked directly - the render plan is automatically invalidated in response to events such as changing aspects
* of the viewport's [[displayStyle]].
*/
invalidateRenderPlan() {
this._renderPlanValid = false;
this.invalidateScene();
}
/** Mark the viewport's [[ViewState]] as having changed, so that the next call to [[renderFrame]] will invoke [[setupFromView]] to synchronize with the view.
* This method is not typically invoked directly - the controller is automatically invalidated in response to events such as a call to [[changeView]].
* Additionally, refresh the Reality Tile Tree to reflect changes in the map layer.
*/
invalidateController() {
this._controllerValid = this._analysisFractionValid = false;
this.invalidateRenderPlan();
}
/** @internal */
setValidScene() {
this._sceneValid = true;
}
/** Request that the Viewport redraw its contents on the next frame. This is useful when some state outside of the Viewport's control but affecting its display has changed.
* For example, if the parameters affecting a screen-space effect applied to this Viewport are modified, the Viewport's contents should be redrawn to reflect the change.
* @note This does not necessarily cause the viewport to recreate its scene, decorations, or anything else - it only guarantees that the contents will be repainted.
*/
requestRedraw() {
this._redrawPending = true;
IModelApp.requestNextAnimation();
}
_animator;
/** @internal */
_changeFlags = new MutableChangeFlags();
_selectionSetDirty = true;
_perModelCategoryVisibility;
_tileSizeModifier;
/** @internal */
subcategories = new SubCategoriesCache.Queue();
/** Time the current flash started. */
_flashUpdateTime;
/** Current flash intensity from [0..this.flashSettings.maxIntensity] */
_flashIntensity = 0;
/** Id of the currently flashed element. */
_flashedElem;
/** Id of last flashed element. */
_lastFlashedElem;
/** The Id of the most recently flashed element, if any. */
get lastFlashedElementId() {
return this._lastFlashedElem;
}
_wantViewAttachments = true;
/** For debug purposes, controls whether or not view attachments are displayed in sheet views.
* @internal
*/
get wantViewAttachments() { return this._wantViewAttachments; }
set wantViewAttachments(want) {
if (want !== this._wantViewAttachments) {
this._wantViewAttachments = want;
this.invalidateScene();
}
}
_wantViewAttachmentBoundaries = false;
/** For debug purposes, controls whether or not the boundary of each view attachment is displayed in a sheet view.
* @internal
*/
get wantViewAttachmentBoundaries() { return this._wantViewAttachmentBoundaries; }
set wantViewAttachmentBoundaries(want) {
if (want !== this._wantViewAttachmentBoundaries) {
this._wantViewAttachmentBoundaries = want;
this.invalidateScene();
}
}
_wantViewAttachmentClipShapes = false;
/** For debug purposes, controls whether or not graphics representing the clipping shapes of each view attachment are displayed in a sheet view.
* @internal
*/
get wantViewAttachmentClipShapes() { return this._wantViewAttachmentClipShapes; }
set wantViewAttachmentClipShapes(want) {
if (want !== this._wantViewAttachmentClipShapes) {
this._wantViewAttachmentClipShapes = want;
this.invalidateScene();
}
}
/** Don't allow entries in the view undo buffer unless they're separated by more than this amount of time. */
static undoDelay = BeDuration.fromSeconds(.5);
_debugBoundingBoxes = TileBoundingBoxes.None;
_freezeScene = false;
_viewingSpace;
_target;
_fadeOutActive = false;
_neverDrawn;
_alwaysDrawn;
_alwaysDrawnExclusive = false;
_featureOverrideProviders = [];
_tiledGraphicsProviders = new Set();
_mapTiledGraphicsProvider;
_hilite = new Hilite.Settings();
_emphasis = new Hilite.Settings(ColorDef.black, 0, 0, Hilite.Silhouette.Thick);
_flash = new FlashSettings();
/** See [DisplayStyle3dSettings.lights]($common) */
get lightSettings() {
return this.displayStyle.is3d() ? this.displayStyle.settings.lights : undefined;
}
setLightSettings(settings) {
if (this.displayStyle.is3d())
this.displayStyle.settings.lights = settings;
}
/** See [DisplayStyle3dSettings.solarShadows]($common) */
get solarShadowSettings() {
return this.view.displayStyle.is3d() ? this.view.displayStyle.settings.solarShadows : undefined;
}
setSolarShadowSettings(settings) {
if (this.view.displayStyle.is3d())
this.view.displayStyle.solarShadows = settings;
}
/** @public */
get viewingSpace() { return this._viewingSpace; }
/** This viewport's rotation matrix. */
get rotation() { return this._viewingSpace.rotation; }
/** The vector between the opposite corners of this viewport's extents. */
get viewDelta() { return this._viewingSpace.viewDelta; }
/** Provides conversions between world and view coordinates. */
get worldToViewMap() { return this._viewingSpace.worldToViewMap; }
/** Provides conversions between world and Npc (non-dimensional perspective) coordinates. */
get worldToNpcMap() { return this._viewingSpace.worldToNpcMap; }
/** @internal */
get frustFraction() { return this._viewingSpace.frustFraction; }
/** See [DisplayStyleSettings.analysisFraction]($common). */
get analysisFraction() {
return this.displayStyle.settings.analysisFraction;
}
set analysisFraction(fraction) {
this.displayStyle.settings.analysisFraction = fraction;
}
/** See [DisplayStyleSettings.timePoint]($common) */
get timePoint() {
return this.displayStyle.settings.timePoint;
}
set timePoint(time) {
this.displayStyle.settings.timePoint = time;
}
/** @internal */
_viewRange = new ViewRect();
/** @internal */
get isAspectRatioLocked() { return false; }
/** @internal */
get target() {
assert(undefined !== this._target, "Accessing RenderTarget of a disposed Viewport");
return this._target;
}
/** Returns true if this Viewport's [[dispose]] method has been invoked. It is an error to attempt to interact with a disposed Viewport.
* Typically a [[ScreenViewport]] becomes disposed as a result of a call to [[ViewManager.dropViewport]], often indirectly through the unmounting of a nine-zone UI's [[ViewportComponent]] when, e.g., switching front-stages.
* @public
*/
get isDisposed() {
return undefined === this._target;
}
/** The settings that control how elements are hilited in this Viewport. */
get hilite() { return this._hilite; }
set hilite(hilite) {
this._hilite = hilite;
this.invalidateRenderPlan();
}
/** The settings that control how emphasized elements are displayed in this Viewport. The default settings apply a thick black silhouette to the emphasized elements.
* @see [FeatureAppearance.emphasized]($common).
*/
get emphasisSettings() { return this._emphasis; }
set emphasisSettings(settings) {
this._emphasis = settings;
this.invalidateRenderPlan();
}
/** The settings that control how elements are flashed in this viewport. */
get flashSettings() {
return this._flash;
}
set flashSettings(settings) {
this._flash = settings;
this.invalidateRenderPlan();
}
/** Determine whether the Grid display is currently enabled in this Viewport.
* @return true if the grid display is on.
*/
get isGridOn() { return this.viewFlags.grid; }
/** Flags controlling aspects of how the contents of this viewport are rendered.
* @see [DisplayStyleSettings.viewFlags]($common).
*/
get viewFlags() { return this.view.viewFlags; }
set viewFlags(viewFlags) {
this.view.displayStyle.viewFlags = viewFlags;
}
/** See [[ViewState.displayStyle]] */
get displayStyle() { return this.view.displayStyle; }
set displayStyle(style) {
this.view.displayStyle = style;
}
/** Selectively override aspects of this viewport's display style.
* @see [DisplayStyleSettings.applyOverrides]($common)
*/
overrideDisplayStyle(overrides) {
this.displayStyle.settings.applyOverrides(overrides);
}
/** See [DisplayStyleSettings.clipStyle]($common) */
get clipStyle() { return this.displayStyle.settings.clipStyle; }
set clipStyle(style) {
this.displayStyle.settings.clipStyle = style;
}
/** Sets the number of [MSAA]($docs/learning/display/MSAA.md) samples for this viewport.
* The number of samples is 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 [[ViewManager.setAntialiasingAllViews]] to adjust the number of samples for all viewports.
*/
get antialiasSamples() {
return undefined !== this._target ? this._target.antialiasSamples : 1;
}
set antialiasSamples(numSamples) {
if (undefined !== this._target) {
this._target.antialiasSamples = numSamples;
this.invalidateRenderPlan();
}
}
/** return true if viewing globe (globeMode is 3D and eye location is far above globe
* @alpha
*/
get viewingGlobe() {
const view = this.view;
if (!view.is3d())
return false;
return this.displayStyle.globeMode === GlobeMode.Ellipsoid && view.isGlobalView;
}
/** Remove any [[SubCategoryOverride]] for the specified subcategory.
* @param id The Id of the subcategory.
* @see [[overrideSubCategory]]
*/
dropSubCategoryOverride(id) {
this.view.displayStyle.dropSubCategoryOverride(id);
}
/** Override the symbology of geometry belonging to a specific subcategory when rendered within this viewport.
* @param id The Id of the subcategory.
* @param ovr The symbology overrides to apply to all geometry belonging to the specified subcategory.
* @see [[dropSubCategoryOverride]]
*/
overrideSubCategory(id, ovr) {
this.view.displayStyle.overrideSubCategory(id, ovr);
}
/** Query the symbology overrides applied to geometry belonging to a specific subcategory when rendered within this viewport.
* @param id The Id of the subcategory.
* @return The symbology overrides applied to all geometry belonging to the specified subcategory, or undefined if no such overrides exist.
* @see [[overrideSubCategory]]
*/
getSubCategoryOverride(id) {
return this.view.displayStyle.getSubCategoryOverride(id);
}
/** Query the symbology with which geometry belonging to a specific subcategory is rendered within this viewport.
* Every [[SubCategory]] defines a base symbology independent of any [[Viewport]].
* If a [[SubCategoryOverride]] has been applied to the subcategory within the context of this [[Viewport]], it will be applied to the subcategory's base symbology.
* @param id The Id of the subcategory.
* @return The symbology of the subcategory within this viewport, including any overrides.
* @see [[overrideSubCategory]]
*/
getSubCategoryAppearance(id) {
const app = this.iModel.subcategories.getSubCategoryAppearance(id);
if (undefined === app)
return SubCategoryAppearance.defaults;
const ovr = this.getSubCategoryOverride(id);
return undefined !== ovr ? ovr.override(app) : app;
}
/** Determine whether geometry belonging to a specific SubCategory is visible in this viewport, assuming the containing Category is displayed.
* @param id The Id of the subcategory
* @returns true if the subcategory is visible in this viewport.
* @note Because this function does not know the Id of the containing Category, it does not check if the Category is enabled for display. The caller should check that separately if he knows the Id of the Category.
*/
isSubCategoryVisible(id) { return this.view.isSubCategoryVisible(id); }
/** Override the appearance of a model when rendered within this viewport.
* @param id The Id of the model.
* @param ovr The symbology overrides to apply to all geometry belonging to the specified subcategory.
* @see [DisplayStyleSettings.overrideModelAppearance]($common)
*/
overrideModelAppearance(id, ovr) {
this.view.displayStyle.settings.overrideModelAppearance(id, ovr);
}
/** Remove any model appearance override for the specified model.
* @param id The Id of the model.
* @see [DisplayStyleSettings.dropModelAppearanceOverride]($common)
*/
dropModelAppearanceOverride(id) {
this.view.displayStyle.settings.dropModelAppearanceOverride(id);
}
/** Some changes may or may not require us to invalidate the scene.
* Specifically, when shadows are enabled or we are displaying view attachments, the following changes may affect the visibility or transparency of elements or features:
* - Viewed categories and subcategories;
* - Always/never drawn elements
* - Symbology overrides.
*/
maybeInvalidateScene() {
// When shadows are being displayed and the set of displayed categories changes, we must invalidate the scene so that shadows will be regenerated.
// Same occurs when changing feature symbology overrides (e.g., always/never-drawn element sets, transparency override)
if (!this._sceneValid)
return;
if (this.view.displayStyle.wantShadows || this.view.isSheetView())
this.invalidateScene();
}
/** Enable or disable display of elements belonging to a set of categories specified by Id.
* Visibility of individual subcategories belonging to a category can be controlled separately through the use of [[SubCategoryOverride]]s.
* By default, enabling display of a category does not affect display of subcategories thereof which have been overridden to be invisible.
* @param categories The Id(s) of the categories to which the change should be applied. No other categories will be affected.
* @param display Whether or not elements on the specified categories should be displayed in the viewport.
* @param enableAllSubCategories Specifies that when enabling display for a category, all of its subcategories should also be displayed even if they are overridden to be invisible.
*/
changeCategoryDisplay(categories, display, enableAllSubCategories = false) {
if (!display) {
this.view.categorySelector.dropCategories(categories);
return;
}
this.view.categorySelector.addCategories(categories);
const categoryIds = Id64.toIdSet(categories);
this.updateSubCategories(categoryIds, enableAllSubCategories);
}
updateSubCategories(categoryIds, enableAllSubCategories) {
this.subcategories.push(this.iModel.subcategories, categoryIds, () => {
if (enableAllSubCategories)
this.enableAllSubCategories(categoryIds);
this._changeFlags.setViewedCategories();
});
}
enableAllSubCategories(categoryIds) {
if (this.displayStyle.enableAllLoadedSubCategories(categoryIds))
this.maybeInvalidateScene();
}
/** @internal */
getSubCategories(categoryId) { return this.iModel.subcategories.getSubCategories(categoryId); }
/** Change the visibility of geometry belonging to the specified subcategory when displayed in this viewport.
* @param subCategoryId The Id of the subcategory
* @param display: True to make geometry belonging to the subcategory visible within this viewport, false to make it invisible.
*/
changeSubCategoryDisplay(subCategoryId, display) {
if (this.displayStyle.setSubCategoryVisible(subCategoryId, display))
this.maybeInvalidateScene();
}
/** The settings controlling how a background map is displayed within a view.
* @see [[ViewFlags.backgroundMap]] for toggling display of the map on or off.
* @see [DisplayStyleSettings.backgroundMap]($common)
*/
get backgroundMapSettings() { return this.displayStyle.backgroundMapSettings; }
set backgroundMapSettings(settings) {
this.displayStyle.backgroundMapSettings = settings;
}
/** See [[DisplayStyleState.changeBackgroundMapProps]] */
changeBackgroundMapProps(props) {
this.displayStyle.changeBackgroundMapProps(props);
}
/** See [[DisplayStyleState.changeBackgroundMapProvider]] */
changeBackgroundMapProvider(props) {
this.displayStyle.changeBackgroundMapProvider(props);
}
/** A reference to the [[TileTree]] used to display the background map in this viewport, if the background map is being displayed. */
get backgroundMapTileTreeReference() {
return this.backgroundMap;
}
/** @internal */
get backgroundMap() { return this._mapTiledGraphicsProvider?.backgroundMap; }
/** @internal */
get overlayMap() { return this._mapTiledGraphicsProvider?.overlayMap; }
/** @internal */
get backgroundDrapeMap() { return this._mapTiledGraphicsProvider?.backgroundDrapeMap; }
/** Return the imagery provider for the provided map-layer index.
* @param mapLayerIndex the [[MapLayerIndex]] of the map layer.
* @beta
*/
getMapLayerImageryProvider(mapLayerIndex) { return this._mapTiledGraphicsProvider?.getMapLayerImageryProvider(mapLayerIndex); }
/** Return the map-layer scale range visibility for the provided map-layer index.
* @param mapLayerIndex the [[MapLayerIndex]] of the map layer.
* @see [[DisplayStyleState.mapLayerAtIndex]].
* @beta
*/
getMapLayerScaleRangeVisibility(mapLayerIndex) {
const treeRef = (mapLayerIndex.isOverlay ? this._mapTiledGraphicsProvider?.overlayMap : this._mapTiledGraphicsProvider?.backgroundMap);
if (treeRef) {
return treeRef.getMapLayerScaleRangeVisibility(mapLayerIndex.index);
}
return MapTileTreeScaleRangeVisibility.Unknown;
}
/** Return a list of map-layers indexes matching a given MapTile tree Id and a layer imagery tree id.
* Note: A imagery tree can be shared for multiple map-layers.
* @internal
*/
getMapLayerIndexesFromIds(mapTreeId, layerTreeId) {
if (this._mapTiledGraphicsProvider)
return this._mapTiledGraphicsProvider?.getMapLayerIndexesFromIds(mapTreeId, layerTreeId);
return [];
}
/** Returns the cartographic range of a map layer.
* @param mapLayerIndex the [[MapLayerIndex]] of the map layer.
*/
async getMapLayerRange(mapLayerIndex) {
const mapLayerSettings = this.view.displayStyle.mapLayerAtIndex(mapLayerIndex);
if (undefined === mapLayerSettings)
return undefined;
if (mapLayerSettings instanceof ModelMapLayerSettings) {
const ecefTransform = this.iModel.ecefLocation?.getTransform();
if (!ecefTransform)
return undefined;
const model = this.iModel.models.getLoaded(mapLayerSettings.modelId);
if (!model || !(model instanceof GeometricModelState))
return undefined;
const modelRange = await model.queryModelRange();
const cartoRange = new CartographicRange(modelRange, ecefTransform).getLongitudeLatitudeBoundingBox();
return MapCartoRectangle.fromRadians(cartoRange.low.x, cartoRange.low.y, cartoRange.high.x, cartoRange.high.y);
}
const imageryProvider = this.getMapLayerImageryProvider(mapLayerIndex);
if (undefined === imageryProvider)
return undefined;
const tileTreeRef = mapLayerIndex.isOverlay ? this.overlayMap : this.backgroundMap;
const imageryTreeRef = tileTreeRef?.getLayerImageryTreeRef(mapLayerIndex.index);
if (imageryTreeRef?.treeOwner.loadStatus === TileTreeLoadStatus.Loaded) {
return imageryProvider.cartoRange;
}
else {
return undefined;
}
}
/** Changes viewport to include range of a map layer.
* @param mapLayerIndex the [[MapLayerIndex]] of the map layer.
* @param vp the viewport.
*/
async viewMapLayerRange(mapLayerIndex, vp) {
const range = await this.getMapLayerRange(mapLayerIndex);
if (!range)
return false;
if (range.xLength() > 1.5 * Angle.piRadians)
viewGlobalLocation(vp, true, ViewGlobalLocationConstants.satelliteHeightAboveEarthInMeters, undefined, undefined);
else
viewGlobalLocation(vp, true, undefined, undefined, range.globalLocation);
return true;
}
/** Fully reset a map-layer tile tree; by calling this, the map-layer will to go through initialize process again, and all previously fetched tile will be lost.
* @beta
*/
resetMapLayer(mapLayerIndex) { this._mapTiledGraphicsProvider?.resetMapLayer(mapLayerIndex); }
/** Returns true if this Viewport is currently displaying the model with the specified Id. */
viewsModel(modelId) { return this.view.viewsModel(modelId); }
/** Attempt to change the 2d Model this Viewport is displaying, if its ViewState is a ViewState2d.
* @param baseModelId The Id of the new 2d Model to be displayed.
* @param options options that determine how the new view is displayed
* @note This function *only works* if the viewport is viewing a [[ViewState2d]], otherwise it does nothing. Also note that
* the Model of baseModelId should be the same type (Drawing or Sheet) as the current view.
* @note this method clones the current ViewState2d and sets its baseModelId to the supplied value. The DisplayStyle and CategorySelector remain unchanged.
*/
async changeViewedModel2d(baseModelId, options) {
if (!this.view.is2d())
return;
// Clone the current ViewState, change its baseModelId, and ensure the new model is loaded.
const newView = this.view.clone(); // start by cloning the current ViewState
await newView.changeViewedModel(baseModelId);
this.changeView(newView, options); // switch this viewport to use new ViewState2d
if (options && options.doFit) { // optionally fit view to the extents of the new model
const range = await this.iModel.models.queryExtents([baseModelId]);
this.zoomToVolume(Range3d.fromJSON(range[0]?.extents), options);
}
}
/** Attempt to replace the set of models currently viewed by this viewport, if it is displaying a SpatialView
* @param modelIds The Ids of the models to be displayed.
* @returns false if this Viewport is not viewing a [[SpatialViewState]]
* @note This function *only works* if the viewport is viewing a [[SpatialViewState]], otherwise it does nothing.
* @note This function *does not load* any models. If any of the supplied `modelIds` refers to a model that has not been loaded, no graphics will be loaded+displayed in the viewport for that model.
* @see [[replaceViewedModels]] for a similar function that also ensures the requested models are loaded.
*/
changeViewedModels(modelIds) {
if (!this.view.isSpatialView())
return false;
this.view.modelSelector.models.clear();
this.view.modelSelector.addModels(modelIds);
return true;
}
/** Attempt to replace the set of models currently viewed by this viewport, if it is displaying a SpatialView
* @param modelIds The Ids of the models to be displayed.
* @note This function *only works* if the viewport is viewing a [[SpatialViewState]], otherwise it does nothing.
* @note If any of the requested models is not yet loaded this function will asynchronously load them before updating the set of displayed models.
*/
async replaceViewedModels(modelIds) {
if (this.view.isSpatialView()) {
this.view.modelSelector.models.clear();
return this.addViewedModels(modelIds);
}
}
/** Add or remove a set of models from those models currently displayed in this viewport.
* @param modelIds The Ids of the models to add or remove.
* @param display Whether or not to display the specified models in the viewport.
* @returns false if this Viewport is not viewing a [[SpatialViewState]]
* @note This function *only works* if the viewport is viewing a [[SpatialViewState]], otherwise it does nothing.
* @note This function *does not load* any models. If `display` is `true` and any of the supplied `models` refers to a model that has not been loaded, no graphics will be loaded+displayed in the viewport for that model.
* @see [[addViewedModels]] for a similar function that also ensures the requested models are loaded.
*/
changeModelDisplay(models, display) {
if (!this.view.isSpatialView())
return false;
if (display)
this.view.modelSelector.addModels(models);
else
this.view.modelSelector.dropModels(models);
return true;
}
/** Adds a set of models to the set of those currently displayed in this viewport.
* @param modelIds The Ids of the models to add or remove.
* @param display Whether or not to display the specified models in the viewport.
* @note This function *only works* if the viewport is viewing a [[SpatialViewState]], otherwise it does nothing.
* @note If any of the requested models is not yet loaded this function will asynchronously load them before updating the set of displayed models.
*/
async addViewedModels(models) {
// NB: We want the model selector to update immediately, to avoid callers repeatedly requesting we load+display the same models while we are already loading them.
// This will also trigger scene invalidation and changed events.
if (!this.changeModelDisplay(models, true))
return; // means it's a 2d model - this function can do nothing useful in 2d.
const unloaded = this.iModel.models.filterLoaded(models);
if (undefined === unloaded)
return;
// Need to redraw once models are available. Don't want to trigger events again.
await this.iModel.models.load(models);
this.invalidateScene();
assert(this.view.isSpatialView());
this.view.markModelSelectorChanged();
}
/** Determines what type (if any) of debug graphics will be displayed to visualize [[Tile]] volumes. Chiefly for debugging.
* @see [[TileBoundingBoxes]]
*/
get debugBoundingBoxes() { return this._debugBoundingBoxes; }
set debugBoundingBoxes(boxes) {
if (boxes !== this.debugBoundingBoxes) {
this._debugBoundingBoxes = boxes;
this.invalidateScene();
}
}
/** When true, the scene will never be recreated. Chiefly for debugging purposes.
* @internal
*/
get freezeScene() { return this._freezeScene; }
set freezeScene(freeze) {
if (freeze !== this._freezeScene) {
this._freezeScene = freeze;
if (!freeze)
this.invalidateScene();
}
}
/** The iModel of this Viewport */
get iModel() { return this.view.iModel; }
/** @internal */
get isPointAdjustmentRequired() { return this.view.is3d(); }
/** @internal */
get isSnapAdjustmentRequired() { return IModelApp.toolAdmin.acsPlaneSnapLock && this.view.is3d(); }
/** @internal */
get isContextRotationRequired() { return IModelApp.toolAdmin.acsContextLock; }
/** Enables or disables "fade-out" mode. When this mode is enabled, transparent graphics are rendered with a flat alpha weight,
* causing them to appear de-emphasized. This is typically used in contexts in which a handful of elements are to be emphasized in the view,
* while the rest of the graphics are drawn transparently.
*/
get isFadeOutActive() { return this._fadeOutActive; }
set isFadeOutActive(active) {
if (active !== this._fadeOutActive) {
this._fadeOutActive = active;
this.invalidateRenderPlan();
}
}
/** Obtain a tooltip from the map layer or reality model, if any, identified by the specified [[HitDetail]].
* @see [[ElementLocateManager]]
*/
async getToolTip(hit) {
const promises = new Array();
for (const ref of this.getTileTreeRefs()) {
const promise = ref.getToolTipPromise(hit);
if (promise) {
promises.push(promise);
}
}
const results = await Promise.all(promises);
return results.find((result) => undefined !== result) ?? "";
}
/** Obtain feature information from a map layer model, if any, identified by the specified [[HitDetail]].
* @see [[ElementLocateManager]]
* @see [[MapFeatureInfo]]
* @beta
*/
async getMapFeatureInfo(hit, options) {
const promises = new Array();
// Execute 'getMapFeatureInfo' on every tree, and make sure to handle exception for each call,
// so that we get still get results even though a tree has failed.
for (const tree of this.mapTileTreeRefs) {
promises.push(tree.getMapFeatureInfo(hit, options).catch(() => undefined));
}
const featureInfo = {};
const worldPoint = hit.hitPoint.clone();
const backgroundMapGeometry = hit.viewport.displayStyle.getBackgroundMapGeometry();
if (undefined !== backgroundMapGeometry) {
featureInfo.hitPoint = (await backgroundMapGeometry.dbToCartographicFromGcs([worldPoint]))[0];
}
const results = await Promise.all(promises);
for (const result of results)
if (result !== undefined) {
if (featureInfo.layerInfos === undefined) {
featureInfo.layerInfos = [];
}
featureInfo.layerInfos.push(...result);
}
return featureInfo;
}
/** If this event has one or more listeners, collection of timing statistics related to rendering frames is enabled. Frame statistics will be received by the listeners whenever a frame is finished rendering.
* @note The timing data collected using this event only collects the amount of time spent on the CPU. Due to performance considerations, time spent on the GPU is not collected. Therefore, these statistics are not a direct mapping to user experience.
* @note In order to avoid interfering with the rendering loop, take care to avoid performing any intensive tasks in your event listeners.
* @see [[FrameStats]]
* @alpha
*/
onFrameStats = new BeEvent();
_frameStatsCollector = new FrameStatsCollector(this.onFrameStats);
/** A function invoked once, after the constructor, to initialize the viewport's state.
* Subclasses can use this perform additional initialization, as the viewport's constructor is not directly invokable.
*/
initialize() {
}
/** @internal because subclasses must derive from ScreenViewport or OffScreenviewport. */
constructor(target) {
this._target = target;
target.assignFrameStatsCollector(this._frameStatsCollector);
this._viewportId = TileUser.generateId();
this._perModelCategoryVisibility = PerModelCategoryVisibility.createOverrides(this);
IModelApp.tileAdmin.registerUser(this);
}
[Symbol.dispose]() {
if (this.isDisposed)
return;
this._target = dispose(this._target);
this.subcategories[Symbol.dispose]();
IModelApp.tileAdmin.forgetUser(this);
this.onDisposed.raiseEvent(this);
this.detachFromView();
}
/** @deprecated in 5.0 - will not be removed until after 2026-06-13. Use [Symbol.dispose] instead. */
dispose() {
this[Symbol.dispose]();
}
setView(view) {
if (view === this._view)
return;
if (this._mapTiledGraphicsProvider)
this._mapTiledGraphicsProvider.setView(view);
this.detachFromView();
this._view = view;
this.attachToView();
}
/** @internal Invoked when the viewport becomes associated with a new ViewState to register event listeners with the view
* and allow the ViewState to set up internal state that is only relevant when associated with a Viewport.
* Also invoked after changing OffScreenViewport.drawingToSheetTransform.
* @internal
*/
attachToView() {
this.registerDisplayStyleListeners(this.view.displayStyle);
this.registerViewListeners();
this.view.attachToViewport(this);
this._mapTiledGraphicsProvider = new MapTiledGraphicsProvider(this.viewportId, this.displayStyle);
}
registerViewListeners() {
const view = this.view;
const removals = this._detachFromView;
// When we detach from the view, also unregister display style listeners.
removals.push(() => this.detachFromDisplayStyle());
removals.push(view.onModelDisplayTransformProviderChanged.addListener(() => this.invalidateScene()));
removals.push(view.details.onClipVectorChanged.addListener(() => this.invalidateRenderPlan()));
removals.push(view.onViewedCategoriesChanged.addListener(() => {
this._changeFlags.setViewedCategories();
this.maybeInvalidateScene();
}));
removals.push(view.onDisplayStyleChanged.addListener((newStyle) => {
this._changeFlags.setDisplayStyle();
this.setFeatureOverrideProviderChanged();
this.invalidateRenderPlan();
this.detachFromDisplayStyle();
this._mapTiledGraphicsProvider = new MapTiledGraphicsProvider(this.viewportId, newStyle);
this.registerDisplayStyleListeners(newStyle);
}));
if (view.isSpatialView()) {
removals.push(view.onViewedModelsChanged.addListener(() => {
this._changeFlags.setViewedModels();
this.invalidateScene();
}));
removals.push(view.details.onModelClipGroupsChanged.addListener(() => {
this.invalidateScene();
}));
// If a map elevation request is required (only in cases where terrain is not geodetic)
// then the completion of the request will require synching with the view so that the
// frustum depth is recalculated correctly. Register this for removal when the view is detached.
removals.push(this.iModel.onMapElevationLoaded.addListener((_iModel) => {
this.synchWithView();
}));
}
}
registerDisplayStyleListeners(style) {
const settings = style.settings;
const removals = this._detachFromDisplayStyle;
const displayStyleChanged = () => {
this.invalidateRenderPlan();
this._changeFlags.setDisplayStyle();
};
const invalidateControllerAndDisplayStyleChanged = () => {
this.invalidateController();
this._changeFlags.setDisplayStyle();
};
const styleAndOverridesChanged = () => {
displayStyleChanged();
this.setFeatureOverrideProviderChanged();
};
removals.push(settings.onSubCategoryOverridesChanged.addListener(styleAndOverridesChanged));
removals.push(settings.onModelAppearanceOverrideChanged.addListener(styleAndOverridesChanged));
removals.push(settings.onBackgroundColorChanged.addListener(displayStyleChanged));
removals.push(settings.onMonochromeColorChanged.addListener(displayStyleChanged));
removals.push(settings.onMonochromeModeChanged.addListener(displayStyleChanged));
removals.push(settings.onClipStyleChanged.addListener(styleAndOverridesChanged));
removals.push(settings.onPlanarClipMaskChanged.addListener(displayStyleChanged));
removals.push(settings.onWhiteOnWhiteReversalChanged.addListener(displayStyleChanged));
removals.push(settings.contextRealityModels.onPlanarClipMaskChanged.addListener(displayStyleChanged));
removals.push(settings.contextRealityModels.onAppearanceOverridesChanged.addListener(displayStyleChanged));
removals.push(settings.contextRealityModels.onD