UNPKG

@itwin/core-frontend

Version:
945 lines • 103 kB
"use strict"; /*--------------------------------------------------------------------------------------------- * 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 */ Object.defineProperty(exports, "__esModule", { value: true }); exports.ViewState2d = exports.ViewState3d = exports.ViewState = void 0; const core_bentley_1 = require("@itwin/core-bentley"); const core_geometry_1 = require("@itwin/core-geometry"); const core_common_1 = require("@itwin/core-common"); const AuxCoordSys_1 = require("./AuxCoordSys"); const DisplayStyleState_1 = require("./DisplayStyleState"); const EntityState_1 = require("./EntityState"); const Frustum2d_1 = require("./Frustum2d"); const IModelApp_1 = require("./IModelApp"); const ModelState_1 = require("./ModelState"); const NotificationManager_1 = require("./NotificationManager"); const StandardView_1 = require("./StandardView"); const internal_1 = require("./tile/internal"); const ViewGlobalLocation_1 = require("./ViewGlobalLocation"); const ViewingSpace_1 = require("./ViewingSpace"); const ViewPose_1 = require("./ViewPose"); const ViewStatus_1 = require("./ViewStatus"); const EnvironmentDecorations_1 = require("./EnvironmentDecorations"); const Symbols_1 = require("./common/internal/Symbols"); /** Decorates the viewport with the view's grid. Graphics are cached as long as scene remains valid. */ class GridDecorator { _view; constructor(_view) { this._view = _view; } useCachedDecorations = true; decorate(context) { const vp = context.viewport; if (!vp.isGridOn) return; const orientation = this._view.getGridOrientation(); if (core_common_1.GridOrientationType.AuxCoord < orientation) { return; // NEEDSWORK... } if (core_common_1.GridOrientationType.AuxCoord === orientation) { this._view.auxiliaryCoordinateSystem.drawGrid(context); return; } const isoGrid = false; const gridsPerRef = this._view.getGridsPerRef(); const spacing = core_geometry_1.Point2d.createFrom(this._view.getGridSpacing()); const origin = core_geometry_1.Point3d.create(); const matrix = core_geometry_1.Matrix3d.createIdentity(); const fixedRepsAuto = core_geometry_1.Point2d.create(); this._view.getGridSettings(vp, origin, matrix, orientation); context.drawStandardGrid(origin, matrix, spacing, gridsPerRef, isoGrid, orientation !== core_common_1.GridOrientationType.View ? fixedRepsAuto : undefined); } } const scratchCorners = core_geometry_1.Range3d.createNull().corners(); const scratchRay = core_geometry_1.Ray3d.createZero(); const unitRange2d = core_geometry_1.Range2d.createXYXY(0, 0, 1, 1); const scratchRange2d = core_geometry_1.Range2d.createNull(); const scratchRange2dIntersect = core_geometry_1.Range2d.createNull(); /** The front-end state of a [[ViewDefinition]] element. * A ViewState is typically associated with a [[Viewport]] to display the contents of the view on the screen. A ViewState being displayed by a Viewport is considered to be * "attached" to that viewport; a "detached" viewport is not being displayed by any viewport. Because the Viewport modifies the state of its attached ViewState, a ViewState * can only be attached to one Viewport at a time. Technically, two Viewports can display two different ViewStates that both use the same [[DisplayStyleState]], but this is * discouraged - changes made to the style by one Viewport will affect the contents of the other Viewport. * * @see [Views]($docs/learning/frontend/Views.md) * @public * @extensions */ class ViewState extends EntityState_1.ElementState { static get className() { return "ViewDefinition"; } _auxCoordSystem; _extentLimits; _modelDisplayTransformProvider; description; isPrivate; _gridDecorator; _categorySelector; _displayStyle; _unregisterCategorySelectorListeners = []; /** An event raised when the set of categories viewed by this view changes, *only* if the view is attached to a [[Viewport]]. */ onViewedCategoriesChanged = new core_bentley_1.BeEvent(); /** An event raised just before assignment to the [[displayStyle]] property, *only* if the view is attached to a [[Viewport]]. * @see [[DisplayStyleSettings]] for events raised when properties of the display style change. */ onDisplayStyleChanged = new core_bentley_1.BeEvent(); /** Event raised just before assignment to the [[modelDisplayTransformProvider]] property, *only* if the view is attached to a [[Viewport]]. * @beta */ onModelDisplayTransformProviderChanged = new core_bentley_1.BeEvent(); /** Selects the categories that are display by this ViewState. */ get categorySelector() { return this._categorySelector; } set categorySelector(selector) { if (selector === this._categorySelector) return; const isAttached = this.isAttachedToViewport; this.unregisterCategorySelectorListeners(); this._categorySelector = selector; if (isAttached) { this.registerCategorySelectorListeners(); this.onViewedCategoriesChanged.raiseEvent(); } } /** The style that controls how the contents of the view are displayed. */ get displayStyle() { return this._displayStyle; } set displayStyle(style) { if (style === this.displayStyle) return; if (this.isAttachedToViewport) this.onDisplayStyleChanged.raiseEvent(style); this._displayStyle = style; } /** @internal */ constructor(props, iModel, categoryOrClone, displayStyle) { super(props, iModel); this.description = props.description; this.isPrivate = props.isPrivate; this._displayStyle = displayStyle; this._categorySelector = categoryOrClone; this._gridDecorator = new GridDecorator(this); if (!(categoryOrClone instanceof ViewState)) // is this from the clone method? return; // not from clone // from clone, 3rd argument is source ViewState const source = categoryOrClone; this._categorySelector = source.categorySelector.clone(); this._displayStyle = source.displayStyle.clone(); this._extentLimits = source._extentLimits; this._auxCoordSystem = source._auxCoordSystem; this._modelDisplayTransformProvider = source._modelDisplayTransformProvider; } /** Create a new ViewState object from a set of properties. Generally this is called internally by [[IModelConnection.Views.load]] after the properties * have been read from an iModel. But, it can also be used to create a ViewState in memory, from scratch or from properties stored elsewhere. */ static createFromProps(_props, _iModel) { return undefined; } /** Serialize this ViewState as a set of properties that can be used to recreate it via [[ViewState.createFromProps]]. */ toProps() { return { viewDefinitionProps: this.toJSON(), categorySelectorProps: this.categorySelector.toJSON(), displayStyleProps: this.displayStyle.toJSON(), }; } /** Flags controlling various aspects of this view's [[DisplayStyleState]]. * @see [DisplayStyleSettings.viewFlags]($common) */ get viewFlags() { return this.displayStyle.viewFlags; } set viewFlags(flags) { this.displayStyle.viewFlags = flags; } /** See [DisplayStyleSettings.analysisStyle]($common). */ get analysisStyle() { return this.displayStyle.settings.analysisStyle; } /** The [RenderSchedule.Script]($common) that animates the contents of the view, if any. * @see [[DisplayStyleState.scheduleScript]]. */ get scheduleScript() { return this.displayStyle.scheduleScript; } /** @internal */ get [Symbols_1._scheduleScriptReference]() { return this.displayStyle[Symbols_1._scheduleScriptReference]; } /** Get the globe projection mode. * @internal */ get globeMode() { return this.displayStyle.globeMode; } /** Determine whether this ViewState exactly matches another. */ equals(other) { return super.equals(other) && this.categorySelector.equals(other.categorySelector) && this.displayStyle.equals(other.displayStyle); } /** Convert to JSON representation. */ toJSON() { const json = super.toJSON(); json.categorySelectorId = this.categorySelector.id; json.displayStyleId = this.displayStyle.id; json.isPrivate = this.isPrivate; json.description = this.description; return json; } /** * Populates the hydrateRequest object stored on the ViewState with: * not loaded categoryIds based off of the ViewStates categorySelector. * Auxiliary coordinate system id if valid. */ preload(hydrateRequest) { const acsId = this.getAuxiliaryCoordinateSystemId(); if (core_bentley_1.Id64.isValid(acsId)) hydrateRequest.acsId = acsId; } /** Asynchronously load any required data for this ViewState from the backend. * FINAL, No subclass should override load. If additional load behavior is needed, see preload and postload. * @note callers should await the Promise returned by this method before using this ViewState. * @see [Views]($docs/learning/frontend/Views.md) */ async load() { // If the iModel associated with the viewState is a blankConnection, // then no data can be retrieved from the backend. if (this.iModel.isBlank) return; const hydrateRequest = {}; this.preload(hydrateRequest); const promises = [ core_common_1.IModelReadRpcInterface.getClientForRouting(this.iModel.routingContext.token).hydrateViewState(this.iModel.getRpcProps(), hydrateRequest). then(async (hydrateResponse) => this.postload(hydrateResponse)), this.displayStyle.load(), ]; const subcategories = this.iModel.subcategories.load(this.categorySelector.categories); if (undefined !== subcategories) promises.push(subcategories.promise.then((_) => { })); await Promise.all(promises); } async postload(hydrateResponse) { if (hydrateResponse.acsElementProps) this._auxCoordSystem = AuxCoordSys_1.AuxCoordSystemState.fromProps(hydrateResponse.acsElementProps, this.iModel); } /** Returns true if all [[TileTree]]s required by this view have been loaded. * Note that the map tile trees associated to the viewport rather than the view, to check the * map tiles as well call [[Viewport.areAreAllTileTreesLoaded]]. */ get areAllTileTreesLoaded() { for (const ref of this.getTileTreeRefs()) { if (!ref.isLoadingComplete) { return false; } } return true; } /** Get the name of the [[ViewDefinition]] from which this ViewState originated. */ get name() { return this.code.value; } /** Get this view's background color. */ get backgroundColor() { return this.displayStyle.backgroundColor; } /** Query the symbology overrides applied to geometry belonging to a specific subcategory when rendered using this ViewState. * @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. */ getSubCategoryOverride(id) { return this.displayStyle.getSubCategoryOverride(id); } /** Query the symbology overrides applied to a model when rendered using this ViewState. * @param id The Id of the model. * @return The symbology overrides applied to the model, or undefined if no such overrides exist. */ getModelAppearanceOverride(id) { return this.displayStyle.settings.getModelAppearanceOverride(id); } /** @internal */ isSubCategoryVisible(id) { const app = this.iModel.subcategories.getSubCategoryAppearance(id); if (undefined === app) return false; const ovr = this.getSubCategoryOverride(id); if (undefined === ovr || undefined === ovr.invisible) return !app.invisible; return !ovr.invisible; } /** Returns true if this ViewState is-a [[ViewState2d]] */ is2d() { return !this.is3d(); } /** Returns true if this ViewState is-a [[SheetViewState]] */ isSheetView() { return false; } /** set the center of this view to a new position. */ setCenter(center) { const diff = center.minus(this.getCenter()); this.setOrigin(this.getOrigin().plus(diff)); } /** Execute a function against each [[TileTreeReference]] associated with this view. * This may include tile trees not associated with any [[GeometricModelState]] - e.g., context reality data. * @note This method is inefficient (iteration cannot be aborted) and awkward (callback cannot be async nor return a value). Prefer to iterate using [[getTileTreeRefs]]. * @deprecated in 5.0 - will not be removed until after 2026-06-13. Use [[getTileTreeRefs]] instead. */ forEachTileTreeRef(func) { for (const ref of this.getModelTreeRefs()) { func(ref); } for (const ref of this.displayStyle.getTileTreeRefs()) { func(ref); } } *getTileTreeRefs() { yield* this.getModelTreeRefs(); yield* this.displayStyle.getTileTreeRefs(); } /** Disclose *all* TileTrees currently in use by this view. This set may include trees not reported by [[forEachTileTreeRef]] - e.g., those used by view attachments, map-draped terrain, etc. * @internal */ discloseTileTrees(trees) { for (const ref of this.getTileTreeRefs()) { trees.disclose(ref); } } /** Discloses graphics memory consumed by viewed tile trees and other consumers like view attachments. * @internal */ collectStatistics(stats) { const trees = new internal_1.DisclosedTileTreeSet(); this.discloseTileTrees(trees); for (const tree of trees) tree.collectStatistics(stats); this.collectNonTileTreeStatistics(stats); } /** Discloses graphics memory consumed by any consumers *other* than viewed tile trees, like view attachments. * @internal */ collectNonTileTreeStatistics(_stats) { // } /** @internal */ createScene(context) { for (const ref of this.getTileTreeRefs()) { ref.addToScene(context); } } /** Add view-specific decorations. The base implementation draws the grid. Subclasses must invoke super.decorate() * @internal */ decorate(context) { this.drawGrid(context); } /** @internal */ static getStandardViewMatrix(id) { return StandardView_1.StandardView.getStandardRotation(id); } /** Orient this view to one of the [[StandardView]] rotations. */ setStandardRotation(id) { this.setRotation(ViewState.getStandardViewMatrix(id)); } /** Orient this view to one of the [[StandardView]] rotations, if the the view is not viewing the project then the * standard rotation is relative to the global position rather than the project. */ setStandardGlobalRotation(id) { const worldToView = ViewState.getStandardViewMatrix(id); const globeToWorld = this.getGlobeRotation(); if (globeToWorld) return this.setRotation(worldToView.multiplyMatrixMatrix(globeToWorld)); else this.setRotation(worldToView); } /** Get the target point of the view. If there is no camera, center is returned. */ getTargetPoint(result) { return this.getCenter(result); } /** Get the point at the geometric center of the view. */ getCenter(result) { const delta = this.getRotation().multiplyTransposeVector(this.getExtents()); return this.getOrigin().plusScaled(delta, 0.5, result); } /** @internal */ drawGrid(context) { context.addFromDecorator(this._gridDecorator); } /** @internal */ computeWorldToNpc(viewRot, inOrigin, delta, enforceFrontToBackRatio = true) { if (viewRot === undefined) viewRot = this.getRotation(); const xVector = viewRot.rowX(); const yVector = viewRot.rowY(); const zVector = viewRot.rowZ(); if (delta === undefined) delta = this.getExtents(); if (inOrigin === undefined) inOrigin = this.getOrigin(); let frustFraction = 1.0; let xExtent; let yExtent; let zExtent; let origin; // Compute root vectors along edges of view frustum. if (this.is3d() && this.isCameraOn) { const camera = this.camera; const eyeToOrigin = core_geometry_1.Vector3d.createStartEnd(camera.eye, inOrigin); // vector from origin on backplane to eye viewRot.multiplyVectorInPlace(eyeToOrigin); // align with view coordinates. const focusDistance = camera.focusDist; let zDelta = delta.z; let zBack = eyeToOrigin.z; // Distance from eye to backplane. let zFront = zBack + zDelta; // Distance from eye to frontplane. const nearScale = IModelApp_1.IModelApp.renderSystem.supportsLogZBuffer ? ViewingSpace_1.ViewingSpace.nearScaleLog24 : ViewingSpace_1.ViewingSpace.nearScaleNonLog24; if (enforceFrontToBackRatio && zFront / zBack < nearScale) { // In this case we are running up against the zBuffer resolution limitation (currently 24 bits). // Set back clipping plane at 10 kilometer which gives us a front clipping plane about 3 meters. // Decreasing the maximumBackClip (MicroStation uses 1 kilometer) will reduce the minimum front // clip, but also reduce the back clip (so far geometry may not be visible). const maximumBackClip = 10 * core_geometry_1.Constant.oneKilometer; if (-zBack > maximumBackClip) { zBack = -maximumBackClip; eyeToOrigin.z = zBack; } zFront = zBack * nearScale; zDelta = zFront - eyeToOrigin.z; } // z out back of eye ===> origin z coordinates are negative. (Back plane more negative than front plane) const backFraction = -zBack / focusDistance; // Perspective fraction at back clip plane. const frontFraction = -zFront / focusDistance; // Perspective fraction at front clip plane. frustFraction = frontFraction / backFraction; // delta.x,delta.y are view rectangle sizes at focus distance. Scale to back plane: xExtent = xVector.scale(delta.x * backFraction); // xExtent at back == delta.x * backFraction. yExtent = yVector.scale(delta.y * backFraction); // yExtent at back == delta.y * backFraction. // Calculate the zExtent in the View coordinate system. zExtent = new core_geometry_1.Vector3d(eyeToOrigin.x * (frontFraction - backFraction), eyeToOrigin.y * (frontFraction - backFraction), zDelta); viewRot.multiplyTransposeVectorInPlace(zExtent); // rotate back to root coordinates. origin = new core_geometry_1.Point3d(eyeToOrigin.x * backFraction, // Calculate origin in eye coordinates eyeToOrigin.y * backFraction, eyeToOrigin.z); viewRot.multiplyTransposeVectorInPlace(origin); // Rotate back to root coordinates origin.plus(camera.eye, origin); // Add the eye point. } else { origin = inOrigin; xExtent = xVector.scale(delta.x); yExtent = yVector.scale(delta.y); zExtent = zVector.scale(delta.z ? delta.z : 1.0); } // calculate the root-to-npc mapping (using expanded frustum) return { map: core_geometry_1.Map4d.createVectorFrustum(origin, xExtent, yExtent, zExtent, frustFraction), frustFraction }; } /** Calculate the world coordinate Frustum from the parameters of this ViewState. * @param result Optional Frustum to hold result. If undefined a new Frustum is created. * @returns The 8-point Frustum with the corners of this ViewState, or undefined if the parameters are invalid. */ calculateFrustum(result) { const val = this.computeWorldToNpc(); if (undefined === val.map) return undefined; const box = result ? result.initNpc() : new core_common_1.Frustum(); val.map.transform1.multiplyPoint3dArrayQuietNormalize(box.points); return box; } calculateFocusCorners() { const map = this.computeWorldToNpc().map; const focusNpcZ = core_geometry_1.Geometry.clamp(map.transform0.multiplyPoint3dQuietNormalize(this.getTargetPoint()).z, 0, 1.0); const pts = [new core_geometry_1.Point3d(0.0, 0.0, focusNpcZ), new core_geometry_1.Point3d(1.0, 0.0, focusNpcZ), new core_geometry_1.Point3d(0.0, 1.0, focusNpcZ), new core_geometry_1.Point3d(1.0, 1.0, focusNpcZ)]; map.transform1.multiplyPoint3dArrayQuietNormalize(pts); return pts; } /** Initialize the origin, extents, and rotation from an existing Frustum * This function is commonly used in the implementation of [[ViewTool]]s as follows: * 1. Obtain the ViewState's initial frustum. * 2. Modify the frustum based on user input. * 3. Update the ViewState to match the modified frustum. * @param frustum the input Frustum. * @param opts for providing onExtentsError * @return Success if the frustum was successfully updated, or an appropriate error code. */ setupFromFrustum(inFrustum, opts) { const frustum = inFrustum.clone(); // make sure we don't modify input frustum frustum.fixPointOrder(); const frustPts = frustum.points; const viewOrg = frustPts[core_common_1.Npc.LeftBottomRear]; // frustumX, frustumY, frustumZ are vectors along edges of the frustum. They are NOT unit vectors. // X and Y should be perpendicular, and Z should be right handed. const frustumX = core_geometry_1.Vector3d.createFrom(frustPts[core_common_1.Npc.RightBottomRear].minus(viewOrg)); const frustumY = core_geometry_1.Vector3d.createFrom(frustPts[core_common_1.Npc.LeftTopRear].minus(viewOrg)); const frustumZ = core_geometry_1.Vector3d.createFrom(frustPts[core_common_1.Npc.LeftBottomFront].minus(viewOrg)); const frustMatrix = core_geometry_1.Matrix3d.createRigidFromColumns(frustumX, frustumY, core_geometry_1.AxisOrder.XYZ); if (!frustMatrix) return ViewStatus_1.ViewStatus.InvalidWindow; // if we're close to one of the standard views, adjust to it to remove any "fuzz" StandardView_1.StandardView.adjustToStandardRotation(frustMatrix); const xDir = frustMatrix.getColumn(0); const yDir = frustMatrix.getColumn(1); const zDir = frustMatrix.getColumn(2); // set up view Rotation matrix as rows of frustum matrix. const viewRot = frustMatrix.inverse(); if (!viewRot) return ViewStatus_1.ViewStatus.InvalidWindow; // Left handed frustum? const zSize = zDir.dotProduct(frustumZ); if (zSize < 0.0) return ViewStatus_1.ViewStatus.InvalidWindow; const viewDiagRoot = new core_geometry_1.Vector3d(); viewDiagRoot.plus2Scaled(xDir, xDir.dotProduct(frustumX), yDir, yDir.dotProduct(frustumY), viewDiagRoot); // vectors on the back plane viewDiagRoot.plusScaled(zDir, zSize, viewDiagRoot); // add in z vector perpendicular to x,y // use center of frustum and view diagonal for origin. Original frustum may not have been orthogonal frustum.getCenter().plusScaled(viewDiagRoot, -0.5, viewOrg); // delta is in view coordinates const viewDelta = viewRot.multiplyVector(viewDiagRoot); const status = this.adjustViewDelta(viewDelta, viewOrg, viewRot, undefined, opts); if (ViewStatus_1.ViewStatus.Success !== status) return status; this.setOrigin(viewOrg); this.setExtents(viewDelta); this.setRotation(viewRot); this._updateMaxGlobalScopeFactor(); return ViewStatus_1.ViewStatus.Success; } /** Get or set the largest and smallest values allowed for the extents for this ViewState * The default limits vary based on the type of view: * - Spatial and drawing view extents cannot exceed the diameter of the earth. * - Sheet view extents cannot exceed ten times the paper size of the sheet. * Explicitly setting the extent limits overrides the default limits. * @see [[resetExtentLimits]] to restore the default limits. */ get extentLimits() { return undefined !== this._extentLimits ? this._extentLimits : this.defaultExtentLimits; } set extentLimits(limits) { this._extentLimits = limits; } /** Resets the largest and smallest values allowed for the extents of this ViewState to their default values. * @see [[extentLimits]]. */ resetExtentLimits() { this._extentLimits = undefined; } setDisplayStyle(style) { this.displayStyle = style; } /** Adjust the y dimension of this ViewState so that its aspect ratio matches the supplied value. * @internal */ fixAspectRatio(windowAspect) { const origExtents = this.getExtents(); const extents = origExtents.clone(); extents.y = extents.x / (windowAspect * this.getAspectRatioSkew()); if (extents.isAlmostEqual(origExtents)) return; // adjust origin by half of the distance we modified extents to keep centered const origin = this.getOrigin().clone(); origin.addScaledInPlace(this.getRotation().multiplyTransposeVector(extents.vectorTo(origExtents, origExtents)), .5); this.setOrigin(origin); this.setExtents(extents); } /** @internal */ outputStatusMessage(status) { IModelApp_1.IModelApp.notifications.outputMessage(new NotificationManager_1.NotifyMessageDetails(NotificationManager_1.OutputMessagePriority.Error, IModelApp_1.IModelApp.localization.getLocalizedString(`iModelJs:Viewing.${ViewStatus_1.ViewStatus[status]}`))); return status; } /** @internal */ adjustViewDelta(delta, origin, rot, aspect, opts) { const origDelta = delta.clone(); let status = ViewStatus_1.ViewStatus.Success; const limit = this.extentLimits; const limitDelta = (val) => { if (val < limit.min) { val = limit.min; status = ViewStatus_1.ViewStatus.MinWindow; } else if (val > limit.max) { val = limit.max; status = ViewStatus_1.ViewStatus.MaxWindow; } return val; }; delta.x = limitDelta(delta.x); delta.y = limitDelta(delta.y); if (aspect) { // skip if either undefined or 0 aspect *= this.getAspectRatioSkew(); if (delta.x > (aspect * delta.y)) delta.y = delta.x / aspect; else delta.x = delta.y * aspect; } if (!delta.isAlmostEqual(origDelta)) origin.addScaledInPlace(rot.multiplyTransposeVector(delta.vectorTo(origDelta, origDelta)), .5); return (status !== ViewStatus_1.ViewStatus.Success && opts?.onExtentsError) ? opts.onExtentsError(status) : status; } /** Adjust the aspect ratio of this ViewState so it matches the supplied value. The adjustment is accomplished by increasing one dimension * and leaving the other unchanged, depending on the ratio of this ViewState's current aspect ratio to the supplied one. This means the result * always shows everything in the current volume, plus potentially more. * @note The *automatic* adjustment that happens when ViewStates are used in Viewports **always** adjusts the Y axis (making * it potentially smaller). That's so that process can be reversible if the view's aspect ratio changes repeatedly (as happens when panels slide in/out, etc.) */ adjustAspectRatio(aspect) { const extents = this.getExtents(); const origin = this.getOrigin(); this.adjustViewDelta(extents, origin, this.getRotation(), aspect); this.setExtents(extents); this.setOrigin(origin); } /** Set the CategorySelector for this view. */ setCategorySelector(categories) { this.categorySelector = categories; } /** get the auxiliary coordinate system state object for this ViewState. */ get auxiliaryCoordinateSystem() { if (!this._auxCoordSystem) this._auxCoordSystem = this.createAuxCoordSystem(""); return this._auxCoordSystem; } /** Get the Id of the auxiliary coordinate system for this ViewState */ getAuxiliaryCoordinateSystemId() { return this.details.auxiliaryCoordinateSystemId; } /** Set or clear the AuxiliaryCoordinateSystem for this view. * @param acs the new AuxiliaryCoordinateSystem for this view. If undefined, no AuxiliaryCoordinateSystem will be used. */ setAuxiliaryCoordinateSystem(acs) { this._auxCoordSystem = acs; this.details.auxiliaryCoordinateSystemId = undefined !== acs ? acs.id : core_bentley_1.Id64.invalid; } /** Determine whether the specified Category is displayed in this view */ viewsCategory(id) { return this.categorySelector.isCategoryViewed(id); } /** Get the aspect ratio (width/height) of this view */ getAspectRatio() { const extents = this.getExtents(); return extents.x / extents.y; } /** Get the aspect ratio skew (x/y, usually 1.0) that is used to exaggerate the y axis of the view. */ getAspectRatioSkew() { return this.details.aspectRatioSkew; } /** Set the aspect ratio skew (x/y) for this view. To remove aspect ratio skew, pass 1.0 for val. */ setAspectRatioSkew(val) { this.details.aspectRatioSkew = val; } /** Get the unit vector that points in the view X (left-to-right) direction. * @param result optional Vector3d to be used for output. If undefined, a new object is created. */ getXVector(result) { return this.getRotation().getRow(0, result); } /** Get the unit vector that points in the view Y (bottom-to-top) direction. * @param result optional Vector3d to be used for output. If undefined, a new object is created. */ getYVector(result) { return this.getRotation().getRow(1, result); } /** Get the unit vector that points in the view Z (front-to-back) direction. * @param result optional Vector3d to be used for output. If undefined, a new object is created. */ getZVector(result) { return this.getRotation().getRow(2, result); } /** Set or clear the clipping volume for this view. * @param clip the new clipping volume. If undefined, clipping is removed from view. * @note The ViewState takes ownership of the supplied ClipVector - it should not be modified after passing it to this function. */ setViewClip(clip) { this.details.clipVector = clip; } /** Get the clipping volume for this view, if defined * @note Do *not* modify the returned ClipVector. If you wish to change the ClipVector, clone the returned ClipVector, modify it as desired, and pass the clone to [[setViewClip]]. */ getViewClip() { return this.details.clipVector; } /** Set the grid settings for this view */ setGridSettings(orientation, spacing, gridsPerRef) { switch (orientation) { case core_common_1.GridOrientationType.WorldYZ: case core_common_1.GridOrientationType.WorldXZ: if (!this.is3d()) return; break; } this.details.gridOrientation = orientation; this.details.gridsPerRef = gridsPerRef; this.details.gridSpacing = spacing; } /** Populate the given origin and rotation with information from the grid settings from the grid orientation. */ getGridSettings(vp, origin, rMatrix, orientation) { // start with global origin (for spatial views) and identity matrix rMatrix.setIdentity(); origin.setFrom(vp.view.isSpatialView() ? vp.view.iModel.globalOrigin : core_geometry_1.Point3d.create()); switch (orientation) { case core_common_1.GridOrientationType.View: { const centerWorld = core_geometry_1.Point3d.create(0.5, 0.5, 0.5); vp.npcToWorld(centerWorld, centerWorld); rMatrix.setFrom(vp.rotation); rMatrix.multiplyXYZtoXYZ(origin, origin); origin.z = centerWorld.z; rMatrix.multiplyTransposeVectorInPlace(origin); break; } case core_common_1.GridOrientationType.WorldXY: break; case core_common_1.GridOrientationType.WorldYZ: { const rowX = rMatrix.getRow(0); const rowY = rMatrix.getRow(1); const rowZ = rMatrix.getRow(2); rMatrix.setRow(0, rowY); rMatrix.setRow(1, rowZ); rMatrix.setRow(2, rowX); break; } case core_common_1.GridOrientationType.WorldXZ: { const rowX = rMatrix.getRow(0); const rowY = rMatrix.getRow(1); const rowZ = rMatrix.getRow(2); rMatrix.setRow(0, rowX); rMatrix.setRow(1, rowZ); rMatrix.setRow(2, rowY); break; } } } /** Get the grid settings for this view */ getGridOrientation() { return this.details.gridOrientation; } getGridsPerRef() { return this.details.gridsPerRef; } getGridSpacing() { return this.details.gridSpacing; } /** Change the volume that this view displays, keeping its current rotation. * @param volume The new volume, in world-coordinates, for the view. The resulting view will show all of worldVolume, by fitting a * view-axis-aligned bounding box around it. For views that are not aligned with the world coordinate system, this will sometimes * result in a much larger volume than worldVolume. * @param aspect The X/Y aspect ratio of the view into which the result will be displayed. If the aspect ratio of the volume does not * match aspect, the shorter axis is lengthened and the volume is centered. If aspect is undefined, no adjustment is made. * @param options for providing MarginPercent and onExtentsError * @note for 2d views, only the X and Y values of volume are used. */ lookAtVolume(volume, aspect, options) { const rangeBox = core_common_1.Frustum.fromRange(volume).points; this.getRotation().multiplyVectorArrayInPlace(rangeBox); return this.lookAtViewAlignedVolume(core_geometry_1.Range3d.createArray(rangeBox), aspect, options); } /** Look at a volume of space defined by a range in view local coordinates, keeping its current rotation. * @param volume The new volume, in view-aligned coordinates. The resulting view will show all of the volume. * @param aspect The X/Y aspect ratio of the view into which the result will be displayed. If the aspect ratio of the volume does not * match aspect, the shorter axis is lengthened and the volume is centered. If aspect is undefined, no adjustment is made. * @param options for providing MarginPercent and onExtentsError * @see lookAtVolume */ lookAtViewAlignedVolume(volume, aspect, options) { if (volume.isNull) // make sure volume is valid return; const viewRot = this.getRotation(); const newOrigin = volume.low.clone(); const newDelta = volume.diagonal(); const minimumDepth = core_geometry_1.Constant.oneMillimeter; if (newDelta.z < minimumDepth) { newOrigin.z -= (minimumDepth - newDelta.z) / 2.0; newDelta.z = minimumDepth; } if (this.is3d() && this.isCameraOn) { // If the camera is on, the only way to guarantee we can see the entire volume is to set delta at the front plane, not focus plane. // That generally causes the view to be too large (objects in it are too small), since we can't tell whether the objects are at // the front or back of the view. For this reason, don't attempt to add any "margin" to camera views. } else if (undefined !== options?.paddingPercent) { let left, right, top, bottom; const padding = options.paddingPercent; if (typeof padding === "number") { left = right = top = bottom = padding; } else { left = padding.left ?? 0; right = padding.right ?? 0; top = padding.top ?? 0; bottom = padding.bottom ?? 0; } const width = newDelta.x; const height = newDelta.y; newOrigin.x -= left * width; newDelta.x += (right + left) * width; newOrigin.y -= bottom * height; newDelta.y += (top + bottom) * height; } else if (options?.marginPercent) { // compute how much space we'll need for both of X and Y margins in root coordinates const margin = options.marginPercent; const wPercent = margin.left + margin.right; const hPercent = margin.top + margin.bottom; const marginHorizontal = wPercent / (1 - wPercent) * newDelta.x; const marginVert = hPercent / (1 - hPercent) * newDelta.y; // compute left and bottom margins in root coordinates const marginLeft = margin.left / (1 - wPercent) * newDelta.x; const marginBottom = margin.bottom / (1 - hPercent) * newDelta.y; // add the margins to the range newOrigin.x -= marginLeft; newOrigin.y -= marginBottom; newDelta.x += marginHorizontal; newDelta.y += marginVert; } else { const origDelta = newDelta.clone(); newDelta.scale(1.04, newDelta); // default "dilation" newOrigin.addScaledInPlace(origDelta.minus(newDelta, origDelta), .5); } viewRot.multiplyTransposeVectorInPlace(newOrigin); if (ViewStatus_1.ViewStatus.Success !== this.adjustViewDelta(newDelta, newOrigin, viewRot, aspect, options)) return; this.setExtents(newDelta); this.setOrigin(newOrigin); if (!this.is3d()) return; const cameraDef = this.camera; cameraDef.validateLens(); // move the camera back so the entire x,y range is visible at front plane const frontDist = newDelta.x / (2.0 * Math.tan(cameraDef.getLensAngle().radians / 2.0)); const backDist = frontDist + newDelta.z; cameraDef.setFocusDistance(frontDist); // do this even if the camera isn't currently on. this.centerEyePoint(backDist); // do this even if the camera isn't currently on. this.verifyFocusPlane(); // changes delta/origin } /** Set the rotation of this ViewState to the supplied rotation, by rotating it about a point. * @param rotation The new rotation matrix for this ViewState. * @param point The point to rotate about. If undefined, use the [[getTargetPoint]]. */ setRotationAboutPoint(rotation, point) { if (undefined === point) point = this.getTargetPoint(); const inverse = rotation.clone().inverse(); if (undefined === inverse) return; const targetMatrix = inverse.multiplyMatrixMatrix(this.getRotation()); const worldTransform = core_geometry_1.Transform.createFixedPointAndMatrix(point, targetMatrix); const frustum = this.calculateFrustum(); if (undefined !== frustum) { frustum.multiply(worldTransform); this.setupFromFrustum(frustum); } } /** Intended strictly as a temporary solution for interactive editing applications, until official support for such apps is implemented. * Invalidates tile trees for all specified models (or all viewed models, if none specified), causing subsequent requests for tiles to make new requests to back-end for updated tiles. * Returns true if any tile tree was invalidated. * @internal */ refreshForModifiedModels(modelIds) { let refreshed = false; for (const ref of this.getModelTreeRefs()) { const tree = ref.treeOwner.tileTree; if (undefined !== tree && (undefined === modelIds || core_bentley_1.Id64.has(modelIds, tree.modelId))) { ref.treeOwner[Symbol.dispose](); refreshed = true; } } return refreshed; } /** Determine whether this ViewState has the same coordinate system as another one. * They must be from the same iModel, and view a model in common. */ hasSameCoordinates(other) { if (this.iModel !== other.iModel) return false; // Spatial views view any number of spatial models all sharing one coordinate system. if (this.isSpatialView() && other.isSpatialView()) return true; // People sometimes mistakenly stick 2d models into spatial views' model selectors. if (this.isSpatialView() || other.isSpatialView()) return false; // Non-spatial views view exactly one model. If they view the same model, they share a coordinate system. let allowView = false; this.forEachModel((model) => { allowView ||= other.viewsModel(model.id); }); return allowView; } /** Compute the vector in the "up" direction of a specific point in world space. * This is typically a unit Z vector. However, if the point is outside of the iModel's project extents and using ellipsoid [[globeMode]], an up-vector * will be computed relative to the surface of the ellipsoid at that point. */ getUpVector(point) { if (!this.iModel.isGeoLocated || this.globeMode !== core_common_1.GlobeMode.Ellipsoid || this.iModel.projectExtents.containsPoint(point)) return core_geometry_1.Vector3d.unitZ(); const earthCenter = this.iModel.getMapEcefToDb(0).origin; const normal = core_geometry_1.Vector3d.createStartEnd(earthCenter, point); normal.normalizeInPlace(); return normal; } /** Return true if the view is looking at the current iModel project extents or * false if the viewed area do does not include more than one percent of the project. */ getIsViewingProject() { if (!this.isSpatialView()) return false; const worldToNpc = this.computeWorldToNpc(); if (!worldToNpc || !worldToNpc.map) return false; const expandedRange = this.iModel.projectExtents.clone(); expandedRange.expandInPlace(10E3); const corners = expandedRange.corners(scratchCorners); worldToNpc.map.transform0.multiplyPoint3dArrayQuietNormalize(corners); scratchRange2d.setNull(); corners.forEach((corner) => scratchRange2d.extendXY(corner.x, corner.y)); const intersection = scratchRange2d.intersect(unitRange2d, scratchRange2dIntersect); if (!intersection || intersection.isNull) return false; const area = (intersection.high.x - intersection.low.x) * (intersection.high.y - intersection.low.y); return area > 1.0E-2; } /** If the view is not of the project as determined by [[getIsViewingProject]] then return * the rotation from a global reference frame to world coordinates. The global reference frame includes * Y vector towards true north, X parallel to the equator and Z perpendicular to the ellipsoid surface */ getGlobeRotation() { if (!this.iModel.isGeoLocated || this.globeMode !== core_common_1.GlobeMode.Ellipsoid || this.getIsViewingProject()) return undefined; const backgroundMapGeometry = this.displayStyle.getBackgroundMapGeometry(); if (!backgroundMapGeometry) return undefined; const targetRay = core_geometry_1.Ray3d.create(this.getCenter(), this.getRotation().rowZ().negate(), scratchRay); const earthEllipsoid = backgroundMapGeometry.getEarthEllipsoid(); const intersectFractions = new Array(), intersectAngles = new Array(); if (earthEllipsoid.intersectRay(targetRay, intersectFractions, undefined, intersectAngles) < 2) return undefined; let minIndex = 0, minFraction = -1.0E10; for (let i = 0; i < intersectFractions.length; i++) { const fraction = intersectFractions[i]; if (fraction < minFraction) { minFraction = fraction; minIndex = i; } } const angles = intersectAngles[minIndex]; const pointAndDeriv = earthEllipsoid.radiansToPointAndDerivatives(angles.longitudeRadians, angles.latitudeRadians, false); return core_geometry_1.Matrix3d.createRigidFromColumns(pointAndDeriv.vectorU, pointAndDeriv.vectorV, core_geometry_1.AxisOrder.XYZ)?.transpose(); } /** A value that represents the global scope of the view -- a value greater than one indicates that the scope of this view is global (viewing most of Earth). */ get globalScopeFactor() { return this.getExtents().magnitudeXY() / core_geometry_1.Constant.earthRadiusWGS84.equator; } _maxGlobalScopeFactor = 0; /** The maximum global scope is not persistent, but maintained as highest global scope factor. This can be used to determine * if the view is of a limited area or if it has ever viewed the entire globe and therefore may be assumed to view it again * and therefore may warrant resources for displaying the globe, such as an expanded viewing frustum and preloading globe map tiles. * A value greater than one indicates that the viewport has been used to view globally at least once. * @internal */ get maxGlobalScopeFactor() { return this._maxGlobalScopeFactor; } _updateMaxGlobalScopeFactor() { this._maxGlobalScopeFactor = Math.max(this._maxGlobalScopeFactor, this.globalScopeFactor); } /** Return elevation applied to model when displayed. This is strictly relevant to plan projection models. * @internal */ getModelElevation(_modelId) { return 0; } /** An object that can provide per-model transforms to be applied at display time. * @note The transform is used for display purposes only. Operations upon geometry within the model may not take the display transform into account. * @see [[computeDisplayTransform]] to compute a full display transform for a model or an element within it, which may include a transform supplied by this provider. * @beta */ get modelDisplayTransformProvider() { return this._modelDisplayTransformProvider; } set modelDisplayTransformProvider(provider) { if (provider === this.modelDisplayTransformProvider) return; if (this.isAttachedToViewport) this.onModelDisplayTransformProviderChanged.raiseEvent(provider); this._modelDisplayTransformProvider = provider; } /** Compute the transform applied to a model or element at display time, if any. * The display transform may be constructed from any combination of the following: * - [PlanProjectionSettings.elevation]($common) applied to plan projection models by [DisplayStyle3dSettings.planProjectionSettings]($common); * - A per-model transform supplied by this view's [[modelDisplayTransformProvider]]; and/or * - A transform applied to an element by an [RenderSchedule.ElementTimeline]($common) defined by this view's [[scheduleScript]]. * - A transform from the coordinate space of a [ViewAttachment]($backend)'s view to that of the [[SheetViewState]] in which it is displayed. * - A transform from the coordinate space of a [[SpatialViewState]] to that of the [[DrawingViewState]] in which it is displayed, where the spatial view is attached via a [SectionDrawing]($backend). * @param args A description of how to compute the transform. * @returns The computed transform, or `undefined` if no display transform is to be applied. * @beta */ computeDisplayTransform(args) { const elevation = this.getModelElevation(args.modelId); const modelTransform = this.modelDisplayTransformProvider?.getModelDisplayTransform(args.modelId); // NB: A ModelTimeline can apply a transform to all elements in the model, but no code exists which actually applies that at display time. // So for now we continue to only consider the ElementTimeline transform. let scriptTransform; if (this.scheduleScript && args.elementId) { const idPair = core_bentley_1.Id64.getUint32Pair(args.elementId); const modelTimeline = this.scheduleScript.find(args.modelId); const elementTimeline = modelTimeline?.getTimelineForElement(idPair.lower, idPai