"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.CurveOps = void 0;
/** @packageDocumentation
 * @module Curve
 */
const Geometry_1 = require("../Geometry");
const FrameBuilder_1 = require("../geometry3d/FrameBuilder");
const Point3dVector3d_1 = require("../geometry3d/Point3dVector3d");
const Range_1 = require("../geometry3d/Range");
const Ray3d_1 = require("../geometry3d/Ray3d");
const Transform_1 = require("../geometry3d/Transform");
const CurveCollection_1 = require("./CurveCollection");
const CurvePrimitive_1 = require("./CurvePrimitive");
const GeometryQuery_1 = require("./GeometryQuery");
const MultiChainCollector_1 = require("./internalContexts/MultiChainCollector");
const PolygonOffsetContext_1 = require("./internalContexts/PolygonOffsetContext");
const Loop_1 = require("./Loop");
const Path_1 = require("./Path");
/**
 * Static methods for miscellaneous curve operations.
 * @public
 */
class CurveOps {
    /** Recursively sum curve lengths, allowing CurvePrimitive, CurveCollection, or array of such at any level. */
    static sumLengths(curves) {
        let mySum = 0;
        if (curves instanceof CurvePrimitive_1.CurvePrimitive) {
            mySum += curves.curveLength();
        }
        else if (curves instanceof CurveCollection_1.CurveCollection) {
            mySum += curves.sumLengths();
        }
        else if (Array.isArray(curves)) {
            for (const data1 of curves)
                mySum += this.sumLengths(data1);
        }
        return mySum;
    }
    /** Recursively extend the range by each curve's range, allowing CurvePrimitive, CurveCollection, or array of such at any level. */
    static extendRange(range, curves, transform) {
        if (Array.isArray(curves)) {
            for (const data1 of curves)
                this.extendRange(range, data1, transform);
        }
        else {
            curves.extendRange(range, transform);
        }
        return range;
    }
    /**
     * Construct a separate xy-offset for each input curve.
     * * For best offset results, the inputs should be parallel to the xy-plane.
     * @param curves input curve(s), z-coordinates ignored. Only curves of type [[AnyChain]] are handled.
     * @param offset offset distance (positive to left of curve, negative to right)
     * @param result array to collect offset curves
     * @returns summed length of offset curves
     */
    static appendXYOffsets(curves, offset, result) {
        let summedLengths = 0;
        if (curves instanceof CurvePrimitive_1.CurvePrimitive) {
            const resultA = PolygonOffsetContext_1.CurveChainWireOffsetContext.constructCurveXYOffset(Path_1.Path.create(curves), offset);
            if (resultA) {
                summedLengths += this.sumLengths(resultA);
                result.push(resultA);
            }
        }
        else if (curves instanceof Loop_1.Loop || curves instanceof Path_1.Path) {
            const resultA = PolygonOffsetContext_1.CurveChainWireOffsetContext.constructCurveXYOffset(curves, offset);
            if (resultA) {
                summedLengths += this.sumLengths(resultA);
                result.push(resultA);
            }
        }
        else if (curves instanceof CurveCollection_1.BagOfCurves) {
            for (const q of curves.children)
                summedLengths += this.appendXYOffsets(q, offset, result);
        }
        else if (Array.isArray(curves)) {
            for (const q of curves)
                summedLengths += this.appendXYOffsets(q, offset, result);
        }
        return summedLengths;
    }
    /**
     * Restructure curve fragments as Paths and Loops, and construct xy-offsets of the chains.
     * * If the inputs do not form Loop(s), the classification of offsets is suspect.
     * * For best offset results, the inputs should be parallel to the xy-plane.
     * * Chain formation is dependent upon input fragment order, as a greedy algorithm is employed.
     * @param fragments fragments to be chained and offset
     * @param offsetDistance offset distance, applied to both sides of each fragment to produce inside and outside xy-offset curves.
     * @param gapTolerance distance to be treated as "effectively zero" when joining head-to-tail
     * @returns object with named chains, insideOffsets, outsideOffsets
     */
    static collectInsideAndOutsideXYOffsets(fragments, offsetDistance, gapTolerance) {
        const collector = new MultiChainCollector_1.MultiChainCollector(gapTolerance, gapTolerance);
        for (const s of fragments) {
            collector.captureCurve(s);
        }
        const chains = collector.grabResult(true);
        const myOffsetA = [];
        const myOffsetB = [];
        const offsetLengthA = CurveOps.appendXYOffsets(chains, offsetDistance, myOffsetA);
        const offsetLengthB = CurveOps.appendXYOffsets(chains, -offsetDistance, myOffsetB);
        if (offsetLengthA > offsetLengthB) {
            return { outsideOffsets: myOffsetA, insideOffsets: myOffsetB, chains };
        }
        else {
            return { insideOffsets: myOffsetA, outsideOffsets: myOffsetB, chains };
        }
    }
    /**
     * Construct curves that are offset from a Path or Loop as viewed in xy-plane (ignoring z).
     * * The construction will remove "some" local effects of features smaller than the offset distance, but will not detect self intersection among widely separated edges.
     * @param curves base curves.
     * @param offsetDistanceOrOptions offset distance (positive to left of curve, negative to right) or options object.
     */
    static constructCurveXYOffset(curves, offsetDistanceOrOptions) {
        return PolygonOffsetContext_1.CurveChainWireOffsetContext.constructCurveXYOffset(curves, offsetDistanceOrOptions);
    }
    /**
     * Create the offset of a single curve primitive as viewed in the xy-plane (ignoring z).
     * @param curve primitive to offset
     * @param offsetDistanceOrOptions offset distance (positive to left of curve, negative to right) or options object
     */
    static createSingleOffsetPrimitiveXY(curve, offsetDistanceOrOptions) {
        return PolygonOffsetContext_1.CurveChainWireOffsetContext.createSingleOffsetPrimitiveXY(curve, offsetDistanceOrOptions);
    }
    /**
     * Restructure curve fragments as Paths and Loops.
     * * Chain formation is dependent upon input fragment order, as a greedy algorithm is employed.
     * @param fragments fragments to be chained
     * @param gapTolerance distance to be treated as "effectively zero" when assembling fragments head-to-tail
     * @param planeTolerance tolerance for considering a closed chain to be planar. If undefined, only create Path. If defined, create Loops for closed chains within tolerance of a plane.
     * @returns chains, possibly wrapped in a [[BagOfCurves]].
     */
    static collectChains(fragments, gapTolerance = Geometry_1.Geometry.smallMetricDistance, planeTolerance) {
        const collector = new MultiChainCollector_1.MultiChainCollector(gapTolerance, planeTolerance);
        for (const s of fragments) {
            collector.captureCurve(s);
        }
        return collector.grabResult(true);
    }
    /**
     * Restructure curve fragments, to be stroked and passed into the callback.
     * * Chain formation is dependent upon input fragment order, as a greedy algorithm is employed.
     * @param fragments fragments to be chained and stroked
     * @param announceChain callback to process each stroked Path and Loop
     * @param strokeOptions options for stroking the chains
     * @param gapTolerance distance to be treated as "effectively zero" when assembling fragments head-to-tail. Also used for removing duplicate points in the stroked chains.
     * @param _planeTolerance unused, pass undefined
     */
    static collectChainsAsLineString3d(fragments, announceChain, strokeOptions, gapTolerance = Geometry_1.Geometry.smallMetricDistance, _planeTolerance) {
        const collector = new MultiChainCollector_1.MultiChainCollector(gapTolerance); // no planarity tolerance needed
        for (const s of fragments) {
            collector.captureCurve(s);
        }
        collector.announceChainsAsLineString3d(announceChain, strokeOptions);
    }
    /**
     * Compute the range of the curves in the local coordinates of a constructed localToWorld frame.
     * @param curves input geometry: curves or points.
     * @param localRange pre-allocated object to populate with the computed local range.
     * @param localToWorld optional pre-allocated object to populate with the computed frame.
     * @returns whether the frame was successfully computed.
     */
    static computeLocalRange(curves, localRange, localToWorld) {
        const builderData = [curves];
        if (localToWorld)
            builderData.push(localToWorld);
        localToWorld = FrameBuilder_1.FrameBuilder.createRightHandedFrame(Point3dVector3d_1.Vector3d.unitZ(), ...builderData);
        if (!localToWorld)
            return false;
        const worldToLocal = localToWorld.inverse();
        localRange.setNull();
        if (Array.isArray(curves) || !(curves instanceof GeometryQuery_1.GeometryQuery))
            Range_1.Range3d.createFromVariantData(curves, worldToLocal, localRange);
        else
            this.extendRange(localRange, curves, worldToLocal);
        return true;
    }
    /**
     * Check whether or not the curves are planar, and if so, return a localToWorld frame.
     * @param curves input geometry: curves or points.
     * @param options bundle of options.
     * @returns localToWorld frame `T` for coplanar curves, or undefined if they are not coplanar.
     * `T` satisfies:
     * * `T.origin` is in the plane.
     * * `T.matrix.columnZ()` is the plane unit normal (or its negative).
     * * `T.matrix.isRigid()` returns true.
     * * `T.inverse()` is worldToLocal; apply to input geometry to rotate it into the xy-plane.
     */
    static isPlanar(curves, options) {
        const localRange = Range_1.Range3d.create();
        const localToWorld = options?.localToWorld ?? Transform_1.Transform.createIdentity();
        if (!this.computeLocalRange(curves, localRange, localToWorld))
            return undefined;
        const maxAltitude = options?.maxDeviation ?? Geometry_1.Geometry.smallMetricDistance;
        return (localRange.zLength() <= Math.abs(maxAltitude)) ? localToWorld : undefined;
    }
    /**
     * Check whether or not the curves lie in a straight line, and if so, return a colinear ray.
     * * This test does not take curve traversal or point order into account.
     * @param curves input geometry: curves or points.
     * @param options bundle of options.
     * @returns ray colinear with input, or undefined if input is not colinear.
     */
    static isColinear(curves, options) {
        const localRange = Range_1.Range3d.create();
        const localToWorld = options?.localToWorld ?? Transform_1.Transform.createIdentity();
        if (!this.computeLocalRange(curves, localRange, localToWorld))
            return undefined;
        const maxAltitude = Math.abs(options?.maxDeviation ?? Geometry_1.Geometry.smallMetricDistance);
        if (localRange.zLength() > maxAltitude)
            return undefined; // non-planar
        const ray = options?.colinearRay ?? Ray3d_1.Ray3d.createZero();
        ray.origin.setFrom(localToWorld.origin);
        const xLength = localRange.xLength();
        const yLength = localRange.yLength();
        if (xLength <= maxAltitude && yLength <= maxAltitude) {
            ray.direction.setZero();
            return ray; // the input is essentially a point
        }
        if (yLength <= maxAltitude) {
            localToWorld.matrix.columnX(ray.direction);
            return ray; // the input lies along local x-axis
        }
        if (xLength <= maxAltitude) {
            localToWorld.matrix.columnY(ray.direction);
            return ray; // the input lies along local y-axis
        }
        if (!options?.xyColinear)
            return undefined; // non-colinear
        const angleTolerance = Math.abs(options?.radianTolerance ?? Geometry_1.Geometry.smallAngleRadians);
        const verticalPlaneDeviation = Math.abs(localToWorld.matrix.columnDotXYZ(Geometry_1.AxisIndex.Z, 0, 0, 1));
        if (verticalPlaneDeviation > angleTolerance) // cos(t + pi/2) = -sin(t) ~ -t for small t
            return undefined; // non-xy-colinear
        if (xLength > yLength)
            localToWorld.matrix.columnX(ray.direction);
        else
            localToWorld.matrix.columnY(ray.direction);
        ray.direction.z = 0.0;
        return ray; // xy-colinear (plane is vertical)
    }
}
exports.CurveOps = CurveOps;
//# sourceMappingURL=CurveOps.js.map