"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
        // Note: this.eyePoint is always defined if view.isCameraOn is true.
        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
        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