UNPKG

@itwin/core-frontend

Version:
453 lines • 24.2 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.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