@itwin/core-frontend
Version:
iTwin.js frontend components
945 lines • 103 kB
JavaScript
"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