"use strict";
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.MomentData = void 0;
/** @packageDocumentation
 * @module Numerics
 */
/* eslint-disable @typescript-eslint/naming-convention */
// cspell:word ABAT
const Geometry_1 = require("../Geometry");
const Matrix3d_1 = require("../geometry3d/Matrix3d");
const Point3dVector3d_1 = require("../geometry3d/Point3dVector3d");
const Transform_1 = require("../geometry3d/Transform");
const Matrix4d_1 = require("./Matrix4d");
const Point4d_1 = require("./Point4d");
/**
 * A MomentData structure exists in several levels:
 * * First level: as a carrier of sums of inertial products that determine moments.
 *   * origin = local origin used as moments are summed.
 *   * sums = array of summed moments.
 *     * The [i,j] entry of the sums is a summed or integrated moment for product of axis i and j.
 *       * axes 0,1,2 are x,y,z.
 *         * e.g. entry [0,1] is summed product xy
 *       * axis 3 is "w", which is 1 in sums.
 *         * e.g. entry 03 is summed x.
 *    * In this level:
 *        * the `absoluteQuantity` member is undefined.
 *        * the `localToWorldMap` and `radiiOfGyration` are created but have undefined contents.
 *  * Second level: after a call to `inertiaProductsToPrincipalAxes`, the `localToWorldMap`, `absoluteQuantity` and
 *    `radiiOfGyration` are filled in.
 * @public
 */
class MomentData {
    /** Origin used for sums. */
    origin;
    /** Flag to request deferred origin setup. */
    needOrigin;
    /**
     * Moment sums.
     * * Set to zero at initialization and if requested later.
     * * Accumulated during data entry phase.
     */
    sums;
    /**
     * The mapping between principal and world system.
     * * This set up with its inverse already constructed.
     */
    localToWorldMap;
    /** Radii of gyration (square roots of principal second moments). */
    radiusOfGyration;
    /**
     * Principal quantity (e.g. length, area, or volume). This is undefined in raw moments, and becomes defined by
     * inertiaProductsToPrincipalAxes.
     */
    absoluteQuantity;
    // private variables
    static _vectorA;
    static _vectorB;
    static _vectorC;
    _point0 = Point3dVector3d_1.Point3d.create();
    _point1 = Point3dVector3d_1.Point3d.create();
    /** Constructor. */
    constructor() {
        this.origin = Point3dVector3d_1.Point3d.createZero();
        this.needOrigin = false;
        this.sums = Matrix4d_1.Matrix4d.createZero();
        this.localToWorldMap = Transform_1.Transform.createIdentity();
        this.radiusOfGyration = Point3dVector3d_1.Vector3d.create();
        this.absoluteQuantity = 0.1; // so optimizer sees its type
        this.absoluteQuantity = undefined;
    }
    /**
     * Return the lower-right (3,3) entry in the sums.
     * * This is the quantity (i.e. length, area, or volume) summed.
     */
    get quantitySum() {
        return this.sums.weight();
    }
    /**
     * Return a scale factor to make these sums match the target orientation sign.
     * * 1.0 if `this.quantitySum` has the same sign as `targetSign`.
     * * -1.0 if `this.quantitySum` has the opposite sign from `targetSign`
     */
    signFactor(targetSign) {
        return targetSign * this.quantitySum > 0 ? 1.0 : -1.0;
    }
    /** If `this.needOrigin` flag is set, copy `origin` to `this.origin` and clear the flag. */
    setOriginIfNeeded(origin) {
        if (this.needOrigin) {
            this.origin.setFromPoint3d(origin);
            this.needOrigin = false;
        }
    }
    /** If `this.needOrigin` flag is set, copy `origin` to `this.origin` and clear the flag. */
    setOriginFromGrowableXYZArrayIfNeeded(points) {
        if (this.needOrigin && points.length > 0) {
            points.getPoint3dAtCheckedPointIndex(0, this.origin);
            this.needOrigin = false;
        }
    }
    /** If `this.needOrigin` flag is set, copy `origin` to `this.origin` and clear the flag. */
    setOriginXYZIfNeeded(x, y, z) {
        if (this.needOrigin) {
            this.origin.set(x, y, z);
            this.needOrigin = false;
        }
    }
    /**
     * Create moments with optional origin.
     * * Origin and needOrigin are quirky.
     *   * (undefined, true) sets up to use first incoming point as origin.
     *   * (origin) sets up to use that durable origin, set needsOrigin flag false.
     *   * (origin, true) the "true" is meaningless.
     *   * (undefined, false) makes 000 the durable origin.
    */
    static create(origin, needOrigin = false) {
        const data = new MomentData();
        data.needOrigin = needOrigin;
        if (origin) {
            data.origin.setFromPoint3d(origin);
            data.needOrigin = false;
        }
        return data;
    }
    /**
     * Return the formal tensor of integrated values `[yy+zz,xy,xz][yx,xx+zz,yz][zx,xy,xx+yy]`.
     * @param products matrix of (integrated) `[xx,xy,xz][yx,yy,yz][zx,xy,zz]`.
     */
    static momentTensorFromInertiaProducts(products) {
        const rr = products.sumDiagonal();
        const result = Matrix3d_1.Matrix3d.createScale(rr, rr, rr);
        result.addScaledInPlace(products, -1.0);
        return result;
    }
    /** Sort the columns of the matrix for increasing moments. */
    static sortColumnsForIncreasingMoments(axes, moments) {
        const points = [
            axes.indexedColumnWithWeight(0, moments.x),
            axes.indexedColumnWithWeight(1, moments.y),
            axes.indexedColumnWithWeight(2, moments.z),
        ].sort((dataA, dataB) => {
            if (dataA.w < dataB.w)
                return -1;
            if (dataA.w > dataB.w)
                return 1;
            return 0;
        });
        axes.setColumnsPoint4dXYZ(points[0], points[1], points[2]);
        if (axes.determinant() < 0)
            axes.scaleColumnsInPlace(-1.0, -1.0, -1.0);
        // prefer x and z positive; y falls wherever
        if (axes.at(0, 0) < 0.0)
            axes.scaleColumnsInPlace(-1.0, -1.0, 1.0);
        if (axes.at(2, 2) < 0.0)
            axes.scaleColumnsInPlace(1.0, -1.0, -1.0);
        moments.set(points[0].w, points[1].w, points[2].w);
    }
    /**
     * Return the principal moment data for an array of points.
     * @param points array of points.
     */
    static pointsToPrincipalAxes(points) {
        const moments = new MomentData();
        if (points.length === 0)
            return moments;
        moments.clearSums(points[0]);
        moments.accumulatePointMomentsFromOrigin(points);
        return this.inertiaProductsToPrincipalAxes(moments.origin, moments.sums);
    }
    /**
     * Compute principal axes from inertial products.
     * * The radii of gyration are sorted smallest to largest.
     * * Hence x axis is long direction.
     * * Hence planar data generates large moment as Z.
     * @param origin The origin used for the inertia products.
     * @param inertiaProducts The inertia products: sums or integrals of
     * [xx,xy,xz,xw; yx,yy,yz,yw; zx,zy,zz,zw; wx,wy,wz,w].
     */
    static inertiaProductsToPrincipalAxes(origin, inertiaProducts) {
        const moments = new MomentData();
        moments.sums.setFrom(inertiaProducts);
        moments.origin.setFrom(origin);
        if (!moments.shiftOriginAndSumsToCentroidOfSums())
            return undefined;
        const products = moments.sums.matrixPart();
        const w = moments.sums.weight();
        if (w < 0.0)
            products.scaleColumnsInPlace(-1, -1, -1);
        const tensor = MomentData.momentTensorFromInertiaProducts(products);
        const moment2 = Point3dVector3d_1.Vector3d.create();
        const axisVectors = Matrix3d_1.Matrix3d.createZero();
        tensor.fastSymmetricEigenvalues(axisVectors, moment2);
        if (moment2.x < 0.0)
            return undefined;
        MomentData.sortColumnsForIncreasingMoments(axisVectors, moment2);
        if (w < 0.0)
            axisVectors.scaleColumnsInPlace(1, -1, -1);
        moments.localToWorldMap = Transform_1.Transform.createOriginAndMatrix(moments.origin, axisVectors);
        moments.radiusOfGyration.set(Math.sqrt(Math.abs(moment2.x)), Math.sqrt(Math.abs(moment2.y)), Math.sqrt(Math.abs(moment2.z)));
        moments.radiusOfGyration.scaleInPlace(1.0 / Math.sqrt(Math.abs(w)));
        moments.absoluteQuantity = Math.abs(w);
        return moments;
    }
    /**
     * Test for match among selected members as they exist after `inertiaProductsToPrincipalAxes`.
     * * The members considered are:
     *   * origin of local to world map (i.e. centroid),
     *   * radius of gyration,
     *   * axes of localToWorldMap.
     * * Axis direction tests allow these quirks:
     *   * opposite orientation is considered matched.
     * * Full xyz symmetry: If x,y,z radii are matched, axes are not tested.
     * * Symmetry in xy plane: If x and y radii are matched, the x and y axes area allowed to spin freely. Only Z direction
     *   is tested.
     * * If either or both are undefined, returns false.
     * @param dataA first set of moments.
     * @param dataB second set of moments.
     */
    static areEquivalentPrincipalAxes(dataA, dataB) {
        if (dataA && dataB
            && Geometry_1.Geometry.isSameCoordinate(dataA.quantitySum, dataB.quantitySum)) { // TODO: need different tolerance for area, volume?
            if (dataA.localToWorldMap.getOrigin().isAlmostEqual(dataB.localToWorldMap.getOrigin())
                && dataA.radiusOfGyration.isAlmostEqual(dataB.radiusOfGyration)) {
                if (Geometry_1.Geometry.isSameCoordinate(dataA.radiusOfGyration.x, dataA.radiusOfGyration.y)) {
                    // we have at least xy symmetry
                    if (Geometry_1.Geometry.isSameCoordinate(dataA.radiusOfGyration.x, dataA.radiusOfGyration.z))
                        return true;
                    // just xy; allow opposite z directions; if the z's are aligned, x and y can spin freely
                    const zA = dataA.localToWorldMap.matrix.columnZ();
                    const zB = dataB.localToWorldMap.matrix.columnZ();
                    if (zA.isParallelTo(zB, true))
                        return true;
                    return false;
                }
                // no symmetry; test all three axes
                const vectorA = Point3dVector3d_1.Vector3d.create();
                const vectorB = Point3dVector3d_1.Vector3d.create();
                for (let i = 0; i < 3; i++) {
                    dataA.localToWorldMap.matrix.getColumn(i, vectorA);
                    dataB.localToWorldMap.matrix.getColumn(i, vectorB);
                    if (!vectorA.isParallelTo(vectorB, true))
                        return false;
                }
                return true;
            }
        }
        return false;
    }
    /** Clear the MomentData sums to zero, and establish a new origin. */
    clearSums(origin) {
        this.sums.setZero();
        if (origin)
            this.origin.setFrom(origin);
        else
            this.origin.setZero();
    }
    /** Accumulate products-of-components for given points. */
    accumulatePointMomentsFromOrigin(points) {
        for (const p of points) {
            this.sums.addMomentsInPlace(p.x - this.origin.x, p.y - this.origin.y, p.z - this.origin.z, 1.0);
        }
    }
    /** Revise the accumulated sums to be "around the centroid". */
    shiftOriginAndSumsToCentroidOfSums() {
        const xyz = this.sums.columnW().realPoint(); // centroid of the geometry
        if (xyz) {
            this.shiftOriginAndSumsByXYZ(xyz.x, xyz.y, xyz.z);
            return true;
        }
        return false;
    }
    /**
     * Revise the accumulated sums.
     * * Add (ax,ay,az) to the origin coordinates.
     * * Apply the negative translation to the sums.
     */
    shiftOriginAndSumsByXYZ(ax, ay, az) {
        this.origin.addXYZInPlace(ax, ay, az);
        this.sums.multiplyTranslationSandwichInPlace(-ax, -ay, -az);
    }
    /** Revise the accumulated sums so they are based at a specified origin. */
    shiftOriginAndSumsToNewOrigin(newOrigin) {
        this.shiftOriginAndSumsByXYZ(newOrigin.x - this.origin.x, newOrigin.y - this.origin.y, newOrigin.z - this.origin.z);
    }
    /**
     * Compute moments of a triangle from the origin. Accumulate them to `this.sums`.
     * * If `this.needOrigin` is set, `this.origin` is set to `pointB`.
     * * If `pointA` is undefined, use `this.origin` as `pointA`.
     */
    accumulateTriangleMomentsXY(pointA, pointB, pointC) {
        this.setOriginXYZIfNeeded(pointB.x, pointB.y, 0.0);
        const x0 = this.origin.x;
        const y0 = this.origin.y;
        const vectorA = MomentData._vectorA = (pointA !== undefined) ?
            Point4d_1.Point4d.create(pointA.x - x0, pointA.y - y0, 0.0, 1.0, MomentData._vectorA) :
            Point4d_1.Point4d.create(0.0, 0.0, 0.0, 1.0, MomentData._vectorA);
        const vectorB = MomentData._vectorB = Point4d_1.Point4d.create(pointB.x - x0, pointB.y - y0, 0.0, 1.0, MomentData._vectorB);
        const vectorC = MomentData._vectorC = Point4d_1.Point4d.create(pointC.x - x0, pointC.y - y0, 0.0, 1.0, MomentData._vectorC);
        // Below we calculate 16 double integrals: \iint_T [x y 0 1]^ [x y 0 1] dT over triangle T=(A,B,C).
        // Each accumulates contributions from 9 scaled outer products. Integration computations use the barycentric
        // change of variables [B-A C-A][u,v]^ = [x,y]^ with Jacobian detJ = B-A x C-A = twice the area of T.
        // This converts the integration domain from T to the triangle bounded by u=0, v=0 and v=1-u, yielding e.g.,
        // \iint_T x^2 dT = detJ \int_0^1 \int_0^{1-u} u^2 dv du = detJ / 12, and similarly \iint_T xy dT = detJ / 24.
        const detJ = Geometry_1.Geometry.crossProductXYXY(vectorB.x - vectorA.x, vectorB.y - vectorA.y, vectorC.x - vectorA.x, vectorC.y - vectorA.y);
        if (detJ !== 0.0) {
            const r1_12 = detJ / 12.0;
            const r1_24 = detJ / 24.0;
            this.sums.addScaledOuterProductInPlace(vectorA, vectorA, r1_12);
            this.sums.addScaledOuterProductInPlace(vectorA, vectorB, r1_24);
            this.sums.addScaledOuterProductInPlace(vectorA, vectorC, r1_24);
            this.sums.addScaledOuterProductInPlace(vectorB, vectorA, r1_24);
            this.sums.addScaledOuterProductInPlace(vectorB, vectorB, r1_12);
            this.sums.addScaledOuterProductInPlace(vectorB, vectorC, r1_24);
            this.sums.addScaledOuterProductInPlace(vectorC, vectorA, r1_24);
            this.sums.addScaledOuterProductInPlace(vectorC, vectorB, r1_24);
            this.sums.addScaledOuterProductInPlace(vectorC, vectorC, r1_12);
        }
    }
    /** Add scaled outer product of (4d, unit weight) point to `this.sums`. */
    accumulateScaledOuterProduct(point, scaleFactor) {
        this.setOriginXYZIfNeeded(point.x, point.y, 0.0);
        const vectorA = MomentData._vectorA = Point4d_1.Point4d.create(point.x - this.origin.x, point.y - this.origin.y, point.z - this.origin.z, 1.0, MomentData._vectorA);
        this.sums.addScaledOuterProductInPlace(vectorA, vectorA, scaleFactor);
    }
    /** Accumulate wire moment integral from pointA to pointB */
    accumulateLineMomentsXYZ(pointA, pointB) {
        this.setOriginXYZIfNeeded(pointA.x, pointA.y, pointA.z);
        const x0 = this.origin.x;
        const y0 = this.origin.y;
        const z0 = this.origin.z;
        const vectorA = MomentData._vectorA = Point4d_1.Point4d.create(pointA.x - x0, pointA.y - y0, pointA.z - z0, 1.0, MomentData._vectorA);
        const vectorB = MomentData._vectorB = Point4d_1.Point4d.create(pointB.x - x0, pointB.y - y0, pointB.z - z0, 1.0, MomentData._vectorB);
        const detJ = pointA.distance(pointB);
        const r1_3 = detJ / 3.0;
        const r1_6 = detJ / 6.0;
        this.sums.addScaledOuterProductInPlace(vectorA, vectorA, r1_3);
        this.sums.addScaledOuterProductInPlace(vectorA, vectorB, r1_6);
        this.sums.addScaledOuterProductInPlace(vectorB, vectorA, r1_6);
        this.sums.addScaledOuterProductInPlace(vectorB, vectorB, r1_3);
    }
    /**
     * Compute moments of triangles from a base point to the given linestring. Accumulate them to `this.sums`.
     * * If `this.needOrigin` is set, `this.origin` is set to the first point of the array.
     * * If `sweepBase` is undefined, use `this.origin` as `sweepBase`.
     */
    accumulateTriangleToLineStringMomentsXY(sweepBase, points) {
        const n = points.length;
        if (n > 1) {
            points.getPoint3dAtUncheckedPointIndex(0, this._point0);
            // The linestring forms a polygon with sweepBase. Integrate over this polygon using Shoelace algorithm.
            for (let i = 1; i < n; i++) {
                points.getPoint3dAtUncheckedPointIndex(i, this._point1);
                this.accumulateTriangleMomentsXY(sweepBase, this._point0, this._point1);
                this._point0.setFromPoint3d(this._point1);
            }
        }
    }
    /**
     * Assemble XX, YY, XY products into a full matrix form [xx,xy,0,0; xy,yy,0,0; 0,0,0,0; 0,0,0,1].
     * * Sandwich this between transforms with columns [vectorU, vectorV, 0000, origin].
     *   (column weights 0001; only xy parts of vectors).
     * * Scale by detJ for the xy-only determinant of the vectors.
     * @param productXX
     * @param productXY
     * @param productYY
     * @param area area in caller's system.
     * @param origin caller's origin.
     * @param vectorU caller's U axis (not necessarily unit).
     * @param vectorV caller's V axis (not necessarily unit).
     */
    accumulateXYProductsInCentroidalFrame(productXX, productXY, productYY, area, origin, vectorU, vectorV) {
        const centroidalProducts = Matrix4d_1.Matrix4d.createRowValues(productXX, productXY, 0, 0, productXY, productYY, 0, 0, 0, 0, 0, 0, 0, 0, 0, area);
        const detJ = Geometry_1.Geometry.crossProductXYXY(vectorU.x, vectorV.x, vectorU.y, vectorV.y);
        const placement = Matrix4d_1.Matrix4d.createRowValues(vectorU.x, vectorV.x, 0, origin.x - this.origin.x, vectorU.y, vectorV.y, 0, origin.y - this.origin.y, 0, 0, 0, 0, 0, 0, 0, 1);
        const AB = placement.multiplyMatrixMatrix(centroidalProducts);
        const ABAT = AB.multiplyMatrixMatrixTranspose(placement);
        this.sums.addScaledInPlace(ABAT, detJ);
    }
    /**
     * Accumulate sums from other moments.
     * * Scale by given scaleFactor (e.g. sign to correct orientation).
     * * Pull the origin from `other` if `this` needs an origin.
     */
    accumulateProducts(other, scale) {
        this.setOriginIfNeeded(other.origin);
        this.sums.addTranslationSandwichInPlace(other.sums, this.origin.x - other.origin.x, this.origin.y - other.origin.y, this.origin.z - other.origin.z, scale);
    }
    /**
     * Accumulate sums from Matrix4d and origin.
     * * Scale by given scaleFactor (e.g. sign to correct orientation).
     * * Trap the origin if `this` needs an origin.
     */
    accumulateProductsFromOrigin(origin, products, scale) {
        this.setOriginIfNeeded(origin);
        this.sums.addTranslationSandwichInPlace(products, this.origin.x - origin.x, this.origin.y - origin.y, this.origin.z - origin.z, scale);
    }
    /** Convert to a json data object. */
    toJSON() {
        return {
            origin: this.origin,
            sums: this.sums.toJSON(),
            radiusOfGyration: this.radiusOfGyration.toJSON(),
            localToWorld: this.localToWorldMap.toJSON(),
        };
    }
}
exports.MomentData = MomentData;
//# sourceMappingURL=MomentData.js.map