@itwin/core-frontend
Version:
iTwin.js frontend components
453 lines • 24.2 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.ViewingSpace = void 0;
const core_geometry_1 = require("@itwin/core-geometry");
const core_common_1 = require("@itwin/core-common");
const ApproximateTerrainHeights_1 = require("./ApproximateTerrainHeights");
const CoordSystem_1 = require("./CoordSystem");
const ViewRect_1 = require("./common/ViewRect");
const Frustum2d_1 = require("./Frustum2d");
const BackgroundMapGeometry_1 = require("./BackgroundMapGeometry");
const internal_1 = require("./tile/internal");
/** Describes a [[Viewport]]'s viewing volume, plus its size on the screen. A new
* instance of ViewingSpace is created every time the Viewport's frustum changes.
* @see [[Viewport.viewingSpace]].
* @public
* @extensions
*/
class ViewingSpace {
_viewRange = new ViewRect_1.ViewRect(); // scratch variable
_viewCorners = new core_geometry_1.Range3d(); // scratch variable
/** @internal */
frustFraction = 1.0;
/** Maximum ratio of frontplane to backplane distance for 24 bit non-logarithmic zbuffer
* @internal
*/
static nearScaleNonLog24 = 0.0003;
/** Maximum fraction of frontplane to backplane distance for 24 bit logarithmic zbuffer
* @internal
*/
static nearScaleLog24 = 1.0E-8;
/** View origin, potentially expanded */
viewOrigin = new core_geometry_1.Point3d();
/** View delta, potentially expanded */
viewDelta = new core_geometry_1.Vector3d();
/** View origin (from ViewState, unexpanded) */
viewOriginUnexpanded = new core_geometry_1.Point3d();
/** View delta (from ViewState, unexpanded) */
viewDeltaUnexpanded = new core_geometry_1.Vector3d();
/** View rotation matrix (copied from ViewState) */
rotation = new core_geometry_1.Matrix3d();
/** Provides conversions between world and view coordinates. */
worldToViewMap = core_geometry_1.Map4d.createIdentity();
/** Providers conversions between world and Npc (non-dimensional perspective) coordinates. */
worldToNpcMap = core_geometry_1.Map4d.createIdentity();
/** @internal */
zClipAdjusted = false; // were the view z clip planes adjusted due to front/back clipping off?
/** Eye point - undefined if not a perspective projection. */
eyePoint;
_view;
/** The ViewState for this Viewport */
get view() { return this._view; }
set view(view) { this._view = view; }
_clientWidth;
_clientHeight;
/** Get the rectangle of this Viewport in ViewCoordinates. */
get _viewRect() {
this._viewRange.init(0, 0, this._clientWidth, this._clientHeight);
return this._viewRange;
}
static _copyOutput(from, to) {
let pt = from;
if (to) {
to.setFrom(from);
pt = to;
}
return pt;
}
/** @internal */
toViewOrientation(from, to) { this.rotation.multiplyVectorInPlace(ViewingSpace._copyOutput(from, to)); }
/** @internal */
fromViewOrientation(from, to) { this.rotation.multiplyTransposeVectorInPlace(ViewingSpace._copyOutput(from, to)); }
/** Ensure the rotation matrix for this view is aligns the root z with the view out (i.e. a "2d view"). */
alignWithRootZ() {
const zUp = core_geometry_1.Vector3d.unitZ();
if (zUp.isAlmostEqual(this.rotation.rowZ()))
return;
const r = this.rotation.transpose();
r.setColumn(2, zUp);
core_geometry_1.Matrix3d.createRigidFromMatrix3d(r, core_geometry_1.AxisOrder.ZXY, r);
r.transpose(this.rotation);
this.view.setRotation(this.rotation); // Don't let viewState and viewport rotation be different.
}
validateCamera() {
const view = this.view;
if (!view.is3d())
return;
const camera = view.camera;
camera.validateLens();
if (camera.isFocusValid)
return;
const vDelta = view.getExtents();
const maxDelta = vDelta.x > vDelta.y ? vDelta.x : vDelta.y;
let focusDistance = maxDelta / (2.0 * Math.tan(camera.getLensAngle().radians / 2.0));
if (focusDistance < vDelta.z / 2.0)
focusDistance = vDelta.z / 2.0;
const eyePoint = new core_geometry_1.Point3d(vDelta.x / 2.0, vDelta.y / 2.0, (vDelta.z / 2.0) + focusDistance);
this.fromViewOrientation(eyePoint);
eyePoint.plus(view.getOrigin(), eyePoint);
camera.setEyePoint(eyePoint);
camera.setFocusDistance(focusDistance);
}
/** @internal */
getTerrainHeightRange() {
const frustum = this.getFrustum();
const cartoRange = core_geometry_1.Range2d.createNull();
for (let i = 0; i < 8; i++) {
const corner = frustum.getCorner(i);
const carto = this.view.iModel.spatialToCartographicFromEcef(corner);
cartoRange.extendXY(carto.longitude, carto.latitude);
}
return ApproximateTerrainHeights_1.ApproximateTerrainHeights.instance.getMinimumMaximumHeights(cartoRange);
}
static _minDepth = 1; // Allowing very small depth will cause frustum calculations to fail.
/** Compute the bounding box of this viewing space in [[CoordSystem.World]] coordinates, including the extents of any [[TiledGraphicsProvider]]s registered with the [[Viewport]]. */
getViewedExtents;
/** Adjust the front and back planes to encompass the entire viewed volume */
adjustZPlanes(origin, delta) {
const view = this.view;
if (!view.is3d()) // only necessary for 3d views
return;
delta.z = Math.max(delta.z, ViewingSpace._minDepth);
const extents = this.getViewedExtents();
const frustum = new core_common_1.Frustum();
const worldToNpc = this.view.computeWorldToNpc(this.rotation, this.viewOrigin, this.viewDelta, false).map;
if (worldToNpc === undefined)
return;
worldToNpc.transform1.multiplyPoint3dArrayQuietNormalize(frustum.points);
const clipPlanes = frustum.getRangePlanes(false, false, 0);
const viewedExtentCorners = extents.corners();
// Only extend depth to include viewed geometry if it is within the frustum. (if viewing global locations).
if (clipPlanes.classifyPointContainment(viewedExtentCorners, false) === core_geometry_1.ClipPlaneContainment.StronglyOutside)
extents.setNull();
let depthRange;
let gridPlane;
if (this.view.viewFlags.grid) {
const gridOrigin = this.view.isSpatialView() ? this.view.iModel.globalOrigin : core_geometry_1.Point3d.create();
switch (this.view.getGridOrientation()) {
case core_common_1.GridOrientationType.WorldXY:
gridPlane = core_geometry_1.Plane3dByOriginAndUnitNormal.create(gridOrigin, core_geometry_1.Vector3d.create(0, 0, 1));
break;
case core_common_1.GridOrientationType.WorldYZ:
gridPlane = core_geometry_1.Plane3dByOriginAndUnitNormal.create(gridOrigin, core_geometry_1.Vector3d.create(1, 0, 0));
break;
case core_common_1.GridOrientationType.WorldXZ:
gridPlane = core_geometry_1.Plane3dByOriginAndUnitNormal.create(gridOrigin, core_geometry_1.Vector3d.create(0, 1, 0));
break;
case core_common_1.GridOrientationType.AuxCoord:
if (this.view.auxiliaryCoordinateSystem)
gridPlane = core_geometry_1.Plane3dByOriginAndUnitNormal.create(gridOrigin, this.view.auxiliaryCoordinateSystem.getRotation().rowZ());
break;
}
}
const globalGeometry = this.view.displayStyle.getGlobalGeometryAndHeightRange();
if (undefined !== globalGeometry) {
const viewZ = this.rotation.getRow(2);
const eyeDepth = this.eyePoint ? viewZ.dotProduct(this.eyePoint) : undefined;
depthRange = globalGeometry.geometry.getFrustumIntersectionDepthRange(frustum, extents, globalGeometry.heightRange, gridPlane, this.view.maxGlobalScopeFactor > 1);
if (eyeDepth !== undefined) {
const maxBackgroundFrontBackRatio = 1.0E6;
const frontDist = Math.max(.1, eyeDepth - depthRange.high);
const backDist = eyeDepth - depthRange.low;
if (backDist / frontDist > maxBackgroundFrontBackRatio)
depthRange.high = eyeDepth - backDist / maxBackgroundFrontBackRatio;
}
}
else
depthRange = gridPlane ? (0, BackgroundMapGeometry_1.getFrustumPlaneIntersectionDepthRange)(frustum, gridPlane) : core_geometry_1.Range1d.createNull();
if (!extents.isNull) {
const viewZ = this.rotation.getRow(2);
const corners = extents.corners();
for (const corner of corners)
depthRange.extendX(viewZ.dotProduct(corner));
}
if (depthRange.isNull)
return;
this.rotation.multiplyVectorInPlace(origin); // put origin in view coordinates
origin.z = depthRange.low; // set origin to back of viewed extents
delta.z = Math.max(depthRange.high - depthRange.low, ViewingSpace._minDepth); // and delta to front of viewed extents
this.rotation.multiplyTransposeVectorInPlace(origin);
if (!view.isCameraOn)
return;
// if the camera is on, we need to make sure that the viewed volume is not behind the eye
const eyeOrg = this.eyePoint.minus(origin);
this.rotation.multiplyVectorInPlace(eyeOrg);
// if the distance from the eye to origin in less than 1 meter, move the origin away from the eye. Usually, this means
// that the camera is outside the viewed extents and pointed away from it. There's nothing to see anyway.
if (eyeOrg.z < 1.0) {
this.rotation.multiplyVectorInPlace(origin);
origin.z -= (2.0 - eyeOrg.z);
this.rotation.multiplyTransposeVectorInPlace(origin);
delta.z = 1.0;
return;
}
// if part of the viewed extents are behind the eye, don't include that.
if (delta.z > eyeOrg.z)
delta.z = eyeOrg.z;
}
/* get the mapping from NPC to view
* @internal
*/
calcNpcToView() {
const corners = this.getViewCorners();
const map = core_geometry_1.Map4d.createBoxMap(core_common_1.NpcCorners[core_common_1.Npc._000], core_common_1.NpcCorners[core_common_1.Npc._111], corners.low, corners.high);
// The map may be undefined if the view rect's width or height is zero.
return undefined === map ? core_geometry_1.Map4d.createIdentity() : map;
}
/* Get the extents of this view, in ViewCoordinates, as a Range3d */
getViewCorners() {
const corners = this._viewCorners;
const viewRect = this._viewRect;
corners.high.x = viewRect.right;
corners.low.y = viewRect.bottom; // y's are swapped on the screen!
corners.low.x = 0;
corners.high.y = 0;
corners.low.z = -32767;
corners.high.z = 32767;
return corners;
}
constructor(vp) {
const view = this._view = vp.view;
const viewRect = vp.viewRect;
this._clientWidth = viewRect.width;
this._clientHeight = viewRect.height;
const origin = view.getOrigin().clone();
const delta = view.getExtents().clone();
this.rotation.setFrom(view.getRotation());
this.getViewedExtents = () => {
const extents = this._view.getViewedExtents();
for (const provider of vp.tiledGraphicsProviders) {
for (const ref of internal_1.TiledGraphicsProvider.getTileTreeRefs(provider, vp)) {
ref.unionFitRange(extents);
}
}
return extents;
};
// first, make sure none of the deltas are negative
delta.x = Math.abs(delta.x);
delta.y = Math.abs(delta.y);
delta.z = Math.abs(delta.z);
this.viewOriginUnexpanded.setFrom(origin);
this.viewDeltaUnexpanded.setFrom(delta);
this.viewOrigin.setFrom(origin);
this.viewDelta.setFrom(delta);
this.zClipAdjusted = false;
this.eyePoint = undefined;
if (view.is3d()) {
if (!view.allow3dManipulations()) {
// we're in a "2d" view of a physical model. That means that we must have our orientation with z out of the screen with z=0 at the center.
this.alignWithRootZ(); // make sure we're in a z Up view
const extents = this.getViewedExtents();
if (extents.isNull) {
extents.low.z = Frustum2d_1.Frustum2d.minimumZExtents.low;
extents.high.z = Frustum2d_1.Frustum2d.minimumZExtents.high;
}
let zMax = Math.max(Math.abs(extents.low.z), Math.abs(extents.high.z));
zMax = Math.max(zMax, 1.0); // make sure we have at least +-1m. Data may be purely planar
delta.z = 2.0 * zMax;
origin.z = -zMax;
const ds = this.view.displayStyle;
if (ds.getIsBackgroundMapVisible() && undefined !== ds.getBackgroundMapGeometry())
this.adjustZPlanes(origin, delta); // make sure view volume includes background map
}
else {
if (view.isCameraOn)
this.validateCamera();
if (view.isCameraOn)
this.eyePoint = view.camera.getEyePoint().clone();
this.adjustZPlanes(origin, delta); // make sure view volume includes entire volume of view
// if the camera is on, don't allow front plane behind camera
if (this.eyePoint) {
const eyeOrg = this.eyePoint.minus(origin); // vector from eye to origin
this.toViewOrientation(eyeOrg);
const frontDist = eyeOrg.z - delta.z; // front distance is backDist - delta.z
// allow ViewState to specify a minimum front dist, but in no case less than 6 inches
const minFrontDist = Math.max(15.2 * core_geometry_1.Constant.oneCentimeter, view.forceMinFrontDist);
if (frontDist < minFrontDist) {
// camera is too close to front plane, move origin away from eye to maintain a minimum front distance.
this.toViewOrientation(origin);
origin.z -= (minFrontDist - frontDist);
this.fromViewOrientation(origin);
}
}
// if we moved the z planes, set the "zClipAdjusted" flag.
if (!origin.isExactEqual(this.viewOriginUnexpanded) || !delta.isExactEqual(this.viewDeltaUnexpanded))
this.zClipAdjusted = true;
}
}
else { // 2d viewport
this.alignWithRootZ();
}
this.viewOrigin.setFrom(origin);
this.viewDelta.setFrom(delta);
const newRootToNpc = this.view.computeWorldToNpc(this.rotation, origin, delta, !this.view.displayStyle.getIsBackgroundMapVisible() /* if displaying background map, don't enforce front/back ratio as no Z-Buffer */);
if (newRootToNpc.map === undefined) {
this.frustFraction = 0; // invalid frustum
return;
}
this.worldToNpcMap.setFrom(newRootToNpc.map);
this.frustFraction = newRootToNpc.frustFraction;
this.worldToViewMap.setFrom(this.calcNpcToView().multiplyMapMap(this.worldToNpcMap));
}
/** Create from a Viewport. */
static createFromViewport(vp) {
return new ViewingSpace(vp);
}
/** Convert an array of points from CoordSystem.View to CoordSystem.Npc */
viewToNpcArray(pts) {
const corners = this.getViewCorners();
const scrToNpcTran = core_geometry_1.Transform.createIdentity();
core_geometry_1.Transform.initFromRange(corners.low, corners.high, undefined, scrToNpcTran);
scrToNpcTran.multiplyPoint3dArrayInPlace(pts);
}
/** Convert an array of points from CoordSystem.Npc to CoordSystem.View */
npcToViewArray(pts) {
const corners = this.getViewCorners();
for (const p of pts)
corners.fractionToPoint(p.x, p.y, p.z, p);
}
/** Convert a point from CoordSystem.View to CoordSystem.Npc
* @param pt the point to convert
* @param out optional location for result. If undefined, a new Point3d is created.
*/
viewToNpc(pt, out) {
const corners = this.getViewCorners();
const scrToNpcTran = core_geometry_1.Transform.createIdentity();
core_geometry_1.Transform.initFromRange(corners.low, corners.high, undefined, scrToNpcTran);
return scrToNpcTran.multiplyPoint3d(pt, out);
}
/** Convert a point from CoordSystem.Npc to CoordSystem.View
* @param pt the point to convert
* @param out optional location for result. If undefined, a new Point3d is created.
*/
npcToView(pt, out) {
const corners = this.getViewCorners();
return corners.fractionToPoint(pt.x, pt.y, pt.z, out);
}
/** Convert an array of points from CoordSystem.World to CoordSystem.Npc */
worldToNpcArray(pts) { this.worldToNpcMap.transform0.multiplyPoint3dArrayQuietNormalize(pts); }
/** Convert an array of points from CoordSystem.Npc to CoordSystem.World */
npcToWorldArray(pts) { this.worldToNpcMap.transform1.multiplyPoint3dArrayQuietNormalize(pts); }
/** Convert an array of points from CoordSystem.World to CoordSystem.View */
worldToViewArray(pts) { this.worldToViewMap.transform0.multiplyPoint3dArrayQuietNormalize(pts); }
/** Convert an array of points from CoordSystem.World to CoordSystem.View, as Point4ds */
worldToView4dArray(worldPts, viewPts) { this.worldToViewMap.transform0.multiplyPoint3dArray(worldPts, viewPts); }
/** Convert an array of points from CoordSystem.View to CoordSystem.World */
viewToWorldArray(pts) { this.worldToViewMap.transform1.multiplyPoint3dArrayQuietNormalize(pts); }
/** Convert an array of points from CoordSystem.View as Point4ds to CoordSystem.World */
view4dToWorldArray(viewPts, worldPts) { this.worldToViewMap.transform1.multiplyPoint4dArrayQuietRenormalize(viewPts, worldPts); }
/**
* Convert a point from CoordSystem.World to CoordSystem.Npc
* @param pt the point to convert
* @param out optional location for result. If undefined, a new Point3d is created.
*/
worldToNpc(pt, out) { return this.worldToNpcMap.transform0.multiplyPoint3dQuietNormalize(pt, out); }
/**
* Convert a point from CoordSystem.Npc to CoordSystem.World
* @param pt the point to convert
* @param out optional location for result. If undefined, a new Point3d is created.
*/
npcToWorld(pt, out) { return this.worldToNpcMap.transform1.multiplyPoint3dQuietNormalize(pt, out); }
/**
* Convert a point from CoordSystem.World to CoordSystem.View
* @param pt the point to convert
* @param out optional location for result. If undefined, a new Point3d is created.
*/
worldToView(input, out) { return this.worldToViewMap.transform0.multiplyPoint3dQuietNormalize(input, out); }
/**
* Convert a point from CoordSystem.World to CoordSystem.View as Point4d
* @param input the point to convert
* @param out optional location for result. If undefined, a new Point4d is created.
*/
worldToView4d(input, out) { return this.worldToViewMap.transform0.multiplyPoint3d(input, 1.0, out); }
/**
* Convert a point from CoordSystem.View to CoordSystem.World
* @param pt the point to convert
* @param out optional location for result. If undefined, a new Point3d is created.
*/
viewToWorld(input, out) { return this.worldToViewMap.transform1.multiplyPoint3dQuietNormalize(input, out); }
/**
* Convert a point from CoordSystem.View as a Point4d to CoordSystem.View
* @param input the point to convert
* @param out optional location for result. If undefined, a new Point3d is created.
*/
view4dToWorld(input, out) { return this.worldToViewMap.transform1.multiplyXYZWQuietRenormalize(input.x, input.y, input.z, input.w, out); }
/** Get an 8-point Frustum corresponding to the 8 corners of the Viewport in the specified coordinate system.
*
* There are two sets of corners that may be of interest.
* The "adjusted" box is the one that is computed by examining the "viewed extents" and moving
* the front and back planes to enclose everything in the view.
* The "unadjusted" box is the one that is stored in the ViewState.
* @param sys Coordinate system for points
* @param adjustedBox If true, retrieve the adjusted box. Otherwise retrieve the box that came from the view definition.
* @param box optional Frustum for return value
* @return the view frustum
* @note The "adjusted" box may be either larger or smaller than the "unadjusted" box.
*/
getFrustum(sys = CoordSystem_1.CoordSystem.World, adjustedBox = true, box) {
box = box ? box.initNpc() : new core_common_1.Frustum();
// if they are looking for the "unexpanded" (that is before f/b clipping expansion) box, we need to get the npc
// coordinates that correspond to the unexpanded box in the npc space of the Expanded view (that's the basis for all
// of the root-based maps.)
if (!adjustedBox && this.zClipAdjusted) {
// to get unexpanded box, we have to go recompute rootToNpc from original View.
const ueRootToNpc = this.view.computeWorldToNpc(this.rotation, this.viewOriginUnexpanded, this.viewDeltaUnexpanded);
if (undefined === ueRootToNpc.map)
return box; // invalid frustum
// get the root corners of the unexpanded box
const ueRootBox = new core_common_1.Frustum();
ueRootToNpc.map.transform1.multiplyPoint3dArrayQuietNormalize(ueRootBox.points);
// and convert them to npc coordinates of the expanded view
this.worldToNpcArray(ueRootBox.points);
box.setFrom(ueRootBox);
}
// now convert from NPC space to the specified coordinate system.
switch (sys) {
case CoordSystem_1.CoordSystem.View:
this.npcToViewArray(box.points);
break;
case CoordSystem_1.CoordSystem.World:
this.npcToWorldArray(box.points);
break;
}
return box;
}
/** @internal */
getPixelSizeAtPoint(inPoint) {
const viewPt = !!inPoint ? this.worldToView(inPoint) : this.npcToView(new core_geometry_1.Point3d(0.5, 0.5, 0.5));
const viewPt2 = new core_geometry_1.Point3d(viewPt.x + 1.0, viewPt.y, viewPt.z);
return this.viewToWorld(viewPt).distance(this.viewToWorld(viewPt2));
}
/** @internal */
getPreloadFrustum(transformOrScale, result) {
const viewFrustum = this.getFrustum(CoordSystem_1.CoordSystem.World, true);
if (transformOrScale && transformOrScale instanceof core_geometry_1.Transform) {
return viewFrustum.transformBy(transformOrScale, result);
}
else {
const scale = transformOrScale === undefined ? 2 : transformOrScale;
const expandedFrustum = viewFrustum.clone(result);
expandedFrustum.scaleXYAboutCenter(scale);
return expandedFrustum;
}
}
}
exports.ViewingSpace = ViewingSpace;
//# sourceMappingURL=ViewingSpace.js.map