/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
 * @module Polyface
 */
// cspell:word internaldocs
import { assert } from "@itwin/core-bentley";
import { BagOfCurves } from "../curve/CurveCollection";
import { CurveOps } from "../curve/CurveOps";
import { MultiChainCollector } from "../curve/internalContexts/MultiChainCollector";
import { LineSegment3d } from "../curve/LineSegment3d";
import { LineString3d } from "../curve/LineString3d";
import { Loop } from "../curve/Loop";
import { StrokeOptions } from "../curve/StrokeOptions";
import { Geometry } from "../Geometry";
import { Angle } from "../geometry3d/Angle";
import { BarycentricTriangle } from "../geometry3d/BarycentricTriangle";
import { Point3dArrayCarrier } from "../geometry3d/Point3dArrayCarrier";
import { Point3d, Vector3d } from "../geometry3d/Point3dVector3d";
import { Point3dArray } from "../geometry3d/PointHelpers";
import { PolygonOps } from "../geometry3d/PolygonOps";
import { Range3d } from "../geometry3d/Range";
import { Ray3d } from "../geometry3d/Ray3d";
import { Transform } from "../geometry3d/Transform";
import { Matrix4d } from "../geometry4d/Matrix4d";
import { MomentData } from "../geometry4d/MomentData";
import { UnionFindContext } from "../numerics/UnionFind";
import { ChainMergeContext } from "../topology/ChainMerge";
import { HalfEdgeMask } from "../topology/Graph";
import { HalfEdgeGraphFromIndexedLoopsContext } from "../topology/HalfEdgeGraphFromIndexedLoopsContext";
import { HalfEdgeGraphSearch, HalfEdgeMaskTester } from "../topology/HalfEdgeGraphSearch";
import { HalfEdgeGraphMerge } from "../topology/Merging";
import { SpacePolygonTriangulation } from "../topology/SpaceTriangulation";
import { ConvexFacetLocationDetail, NonConvexFacetLocationDetail, TriangularFacetLocationDetail, } from "./FacetLocationDetail";
import { FacetOrientationFixup } from "./FacetOrientation";
import { IndexedEdgeMatcher, SortableEdge } from "./IndexedEdgeMatcher";
import { BuildAverageNormalsContext } from "./multiclip/BuildAverageNormalsContext";
import { OffsetMeshContext } from "./multiclip/OffsetMeshContext";
import { ClipSweptLineStringContext, EdgeClipData, SweepLineStringToFacetContext } from "./multiclip/SweepLineStringToFacetContext";
import { XYPointBuckets } from "./multiclip/XYPointBuckets";
import { IndexedPolyface, Polyface } from "./Polyface";
import { PolyfaceBuilder } from "./PolyfaceBuilder";
import { RangeLengthData } from "./RangeLengthData";
/**
 * Options carrier for sweeping linework onto meshes.
 * * The create method initializes all options.
 * @public
 */
export class SweepLineStringToFacetsOptions {
    /**
     * Vector towards the eye.
     * * In the common case of sweeping to an XY (e.g., ground or DTM) mesh, use the positive Z vector as an up vector.
     * * In general case, this is a vector from the mesh towards an eye at infinity.
     */
    vectorToEye;
    /** True to collect edges from facets that face towards the eye. */
    collectOnForwardFacets;
    /** True to collect facets that are "on the side", i.e., their outward vector is perpendicular to vectorToEye. */
    collectOnSideFacets;
    /** True to collect facets that face away from the eye. */
    collectOnRearFacets;
    /**
     * (Small) angle to use as tolerance for deciding if a facet is "on the side".
     * * Default (if given in degrees) is `Geometry.smallAngleDegrees`.
     */
    sideAngle;
    /** Option to assemble lines into chains. */
    assembleChains;
    /**
     * Optional searcher object for vertical sweep speedup. If provided, `vectorToEye` must be the positive Z vector.
     * @example To construct a 5x5 indexed search grid:
     * ````
     * const xyRange = Range2d.createFrom(myPolyface.range());
     * const searcher = GriddedRaggedRange2dSetWithOverflow.create<number>(xyRange, 5, 5)!;
     * for (const visitor = myPolyface.createVisitor(0); visitor.moveToNextFacet();) {
     *   searcher.addRange(visitor.point.getRange(), visitor.currentReadIndex());
     * }
     * ````
     */
    searcher;
    /** Constructor. Captures fully-checked parameters from static create method. */
    constructor(vectorToEye, sideAngle, assembleChains, collectOnForwardFacets, collectOnSideFacets, collectOnRearFacets, searcher) {
        this.vectorToEye = vectorToEye;
        this.sideAngle = sideAngle;
        this.assembleChains = assembleChains;
        this.collectOnForwardFacets = collectOnForwardFacets;
        this.collectOnSideFacets = collectOnSideFacets;
        this.collectOnRearFacets = collectOnRearFacets;
        this.searcher = searcher;
    }
    /**
     * Create an options structure.
     * * Default `vectorToEye` is (0,0,1).
     * * Default `sideAngle` is `Geometry.smallAngleRadians`.
     * * Default `assembleChains` is `true`.
     * * Default `collectOnForwardFacets`, `collectOnSideFacets`, `collectOnRearFacets` are all `true`.
     */
    static create(vectorToEye, sideAngle, assembleChains, collectOnForwardFacets, collectOnSideFacets, collectOnRearFacets, searcher) {
        return new SweepLineStringToFacetsOptions((!vectorToEye || searcher) ? Vector3d.unitZ() : vectorToEye.clone(), sideAngle?.clone() ?? Angle.createRadians(Geometry.smallAngleRadians), assembleChains ?? true, collectOnForwardFacets ?? true, collectOnSideFacets ?? true, collectOnRearFacets ?? true, searcher);
    }
    /** Return `true` if all outputs are requested. */
    get collectAll() {
        return this.collectOnForwardFacets && this.collectOnSideFacets && this.collectOnRearFacets;
    }
    /**
     * Decide if the instance collector flags accept a facet with the given normal.
     * * A facet whose facet normal has a positive, zero, or negative dot product with `vectorToEye` is classified
     * as forward, side, or rear, respectively.
     * * `undefined` facet normal returns `false`.
    */
    collectFromThisFacetNormal(facetNormal) {
        if (facetNormal === undefined)
            return false;
        const theta = facetNormal.angleFromPerpendicular(this.vectorToEye);
        if (theta.isMagnitudeLessThanOrEqual(this.sideAngle))
            return this.collectOnSideFacets;
        return facetNormal.dotProduct(this.vectorToEye) > 0 ? this.collectOnForwardFacets : this.collectOnRearFacets;
    }
}
/**
 * Options carrier for [[PolyfaceQuery.cloneOffset]].
 * * Default options are strongly recommended.
 * * The option most likely to be changed is `chamferAngleBetweenNormals`.
 * @public
 */
export class OffsetMeshOptions {
    /** Max angle between normals to be considered smooth. */
    smoothSingleAngleBetweenNormals;
    /** Max accumulation of angle between normals to be considered smooth. */
    smoothAccumulatedAngleBetweenNormals;
    /**
     * When facets meet and the turn angle (i.e., angle between facet normals) is larger than
     * `chamferTurnAngleBetweenNormals`, a chamfer edge may be added to prevent offset mesh from having
     *  facets that extend out too far away from the source mesh.
     */
    chamferAngleBetweenNormals;
    /**
     * Optional control structure for selective output.
     * * If `undefined`, output all expected offset facets.
     */
    outputSelector;
    /** Constructor -- CAPTURE parameters. */
    constructor(smoothSingleAngleBetweenNormals = Angle.createDegrees(25), smoothAccumulatedAngleBetweenNormals = Angle.createDegrees(60), chamferAngleBetweenNormals = Angle.createDegrees(90)) {
        this.smoothSingleAngleBetweenNormals = smoothSingleAngleBetweenNormals.clone();
        this.smoothAccumulatedAngleBetweenNormals = smoothAccumulatedAngleBetweenNormals.clone();
        this.chamferAngleBetweenNormals = chamferAngleBetweenNormals.clone();
    }
    /**
     * Construct and return an `OffsetMeshOptions` with given parameters.
     * * Angles are forced to minimum values.
     * * Clones of the angles are given to the constructor.
     * @param smoothSingleRadiansBetweenNormals an angle larger than this (between facets) is considered a sharp edge.
     * Default value is `25` degrees.
     * @param smoothAccumulatedAngleBetweenNormals angles that sum to this much may be consolidated for average normal.
     * Default value is `60` degrees.
     * @param chamferTurnAngleBetweenNormals when facets meet and the turn angle (i.e., angle between facet normals)
     * is larger than `chamferTurnAngleBetweenNormals`, a chamfer edge may be added to prevent offset mesh from having
     * facets that extend out too far away from the source mesh. Default value is `120` degrees.
     */
    static create(smoothSingleAngleBetweenNormals = Angle.createDegrees(25), smoothAccumulatedAngleBetweenNormals = Angle.createDegrees(60), chamferTurnAngleBetweenNormals = Angle.createDegrees(120)) {
        const mySmoothSingleRadiansBetweenNormals = smoothSingleAngleBetweenNormals.clone();
        const mySmoothAccumulatedRadiansBetweenNormals = smoothAccumulatedAngleBetweenNormals.clone();
        const myChamferTurnAngleBetweenNormals = chamferTurnAngleBetweenNormals.clone();
        if (mySmoothSingleRadiansBetweenNormals.degrees < 1)
            mySmoothAccumulatedRadiansBetweenNormals.setDegrees(1.0);
        if (mySmoothAccumulatedRadiansBetweenNormals.degrees < 1.0)
            mySmoothAccumulatedRadiansBetweenNormals.setDegrees(1.0);
        if (mySmoothAccumulatedRadiansBetweenNormals.degrees < 15.0)
            mySmoothAccumulatedRadiansBetweenNormals.setDegrees(15.0);
        return new OffsetMeshOptions(mySmoothSingleRadiansBetweenNormals, mySmoothAccumulatedRadiansBetweenNormals, myChamferTurnAngleBetweenNormals);
    }
}
/**
 * Enumeration of cases for retaining facets among duplicates.
 * @public
 */
export var DuplicateFacetClusterSelector;
(function (DuplicateFacetClusterSelector) {
    /** Retain none of the duplicates. */
    DuplicateFacetClusterSelector[DuplicateFacetClusterSelector["SelectNone"] = 0] = "SelectNone";
    /** Retain any one member among duplicates. */
    DuplicateFacetClusterSelector[DuplicateFacetClusterSelector["SelectAny"] = 1] = "SelectAny";
    /** Retain all members among duplicates. */
    DuplicateFacetClusterSelector[DuplicateFacetClusterSelector["SelectAll"] = 2] = "SelectAll";
    /** Retain one from any cluster with an odd number of faces. */
    DuplicateFacetClusterSelector[DuplicateFacetClusterSelector["SelectOneByParity"] = 3] = "SelectOneByParity";
})(DuplicateFacetClusterSelector || (DuplicateFacetClusterSelector = {}));
/**
 * PolyfaceQuery is a static class whose methods implement queries on a `Polyface` or `PolyfaceVisitor` provided as a
 * parameter to each method.
 * @public
 */
export class PolyfaceQuery {
    /** Copy the points from a visitor into a linestring loop. */
    static visitorToLoop(visitor) {
        const ls = LineString3d.createPoints(visitor.point.getPoint3dArray());
        return Loop.create(ls);
    }
    /** Create a linestring loop for each facet of the polyface. */
    static indexedPolyfaceToLoops(polyface) {
        const result = BagOfCurves.create();
        const visitor = polyface.createVisitor(1);
        while (visitor.moveToNextFacet()) {
            const loop = PolyfaceQuery.visitorToLoop(visitor);
            result.tryAddChild(loop);
        }
        return result;
    }
    /**
     * Sum all facet areas.
     * @param source polyface or visitor.
     * @param vectorToEye compute sum of (signed) facet areas projected to a view plane perpendicular to `vectorToEye`.
     * If `vectorToEye` is not provided, actual facet areas are calculated (without any projection).
     * @returns the sum of all facet areas.
     */
    static sumFacetAreas(source, vectorToEye) {
        let sum = 0;
        if (source instanceof Polyface)
            source = source.createVisitor(1);
        else
            source.setNumWrap(1);
        let unitVectorToEye;
        if (vectorToEye !== undefined)
            unitVectorToEye = vectorToEye.normalize();
        source.reset();
        while (source.moveToNextFacet()) {
            const areaNormal = PolygonOps.areaNormal(source.point.getPoint3dArray());
            sum += unitVectorToEye ? areaNormal.dotProduct(unitVectorToEye) : areaNormal.magnitude();
        }
        return sum;
    }
    /**
     * Sum volumes of tetrahedra from origin to all facets.
     * * If origin is `undefined`, the first point encountered (by the visitor) is used as origin.
     * * If the mesh is closed, this sum is the volume.
     * * If the mesh is not closed, this sum is the volume of a mesh with various additional facets from the origin
     * to facets.
     */
    static sumTetrahedralVolumes(source, origin) {
        let sum = 0;
        if (source instanceof Polyface)
            return PolyfaceQuery.sumTetrahedralVolumes(source.createVisitor(0), origin);
        let myOrigin = origin;
        const facetOrigin = Point3d.create();
        const targetA = Point3d.create();
        const targetB = Point3d.create();
        source.reset();
        while (source.moveToNextFacet()) {
            if (myOrigin === undefined)
                myOrigin = source.point.getPoint3dAtUncheckedPointIndex(0);
            source.point.getPoint3dAtUncheckedPointIndex(0, facetOrigin);
            for (let i = 1; i + 1 < source.point.length; i++) {
                source.point.getPoint3dAtUncheckedPointIndex(i, targetA);
                source.point.getPoint3dAtUncheckedPointIndex(i + 1, targetB);
                sum += myOrigin.tripleProductToPoints(facetOrigin, targetA, targetB);
            }
        }
        return sum / 6.0;
    }
    /**
     * Sum (signed) volumes between facets and a plane.
     * @param source facet set
     * @param plane infinite plane bounding volume between the input facets and (virtual) side facets perpendicular to the plane.
     * @param skipMoments whether to skip computation of the area moments. Set to `true` if only volume is needed. Default is `false`.
     * @returns a structure with multiple sums:
     * * volume: the sum of (signed) volumes between facets and the plane.
     * * positiveProjectedFacetAreaMoments, negativeProjectedFacetAreaMoments: area moment data (centroid, signed area,
     * and second moments with respect to the centroid), separately computed for the input facets that project with
     * positive/negative area onto the plane.
     */
    static sumVolumeBetweenFacetsAndPlane(source, plane, skipMoments) {
        const visitor = source instanceof Polyface ? source.createVisitor(0) : source;
        visitor.setNumWrap(0);
        const workPoint0 = Point3d.create();
        const workPoint1 = Point3d.create();
        const workVector = Vector3d.create();
        const workMatrix = Matrix4d.createZero();
        let signedVolumeTimes6 = 0.0;
        const posSums = skipMoments ? undefined : MomentData.create(undefined, true);
        const negSums = skipMoments ? undefined : MomentData.create(undefined, true);
        // For each facet:
        // - form triangles from facet origin to each far edge.
        // - sum signed area and volume contributions (for non-convex facet, signs can be mixed).
        // - each projected area contribution is twice the area of a triangle.
        // - each volume contribution is 3 times the actual volume -- a third of the altitude sum is the centroid altitude.
        const options = { skipMoments, p0: workPoint0, p1: workPoint1, v0: workVector, m0: workMatrix };
        for (visitor.reset(); visitor.moveToNextFacet();) {
            const facetData = PolygonOps.volumeBetweenPolygonAndPlane(visitor.point, plane, options);
            signedVolumeTimes6 += facetData.volume6;
            if (!skipMoments) {
                assert(posSums !== undefined && negSums !== undefined && facetData.origin !== undefined && facetData.products !== undefined);
                if (facetData.area2 > 0)
                    posSums.accumulateProductsFromOrigin(facetData.origin, facetData.products, 1.0);
                else
                    negSums.accumulateProductsFromOrigin(facetData.origin, facetData.products, 1.0);
            }
        }
        return {
            volume: signedVolumeTimes6 / 6.0,
            positiveProjectedFacetAreaMoments: posSums ? MomentData.inertiaProductsToPrincipalAxes(posSums.origin, posSums.sums) : undefined,
            negativeProjectedFacetAreaMoments: negSums ? MomentData.inertiaProductsToPrincipalAxes(negSums.origin, negSums.sums) : undefined,
        };
    }
    /** Return the inertia products [xx,xy,xz,xw,yw, etc] integrated over all all facets as viewed from origin. */
    static sumFacetSecondAreaMomentProducts(source, origin) {
        if (source instanceof Polyface)
            return PolyfaceQuery.sumFacetSecondAreaMomentProducts(source.createVisitor(0), origin);
        const products = Matrix4d.createZero();
        source.reset();
        while (source.moveToNextFacet()) {
            PolygonOps.addSecondMomentAreaProducts(source.point, origin, products);
        }
        return products;
    }
    /** Return the inertia products [xx,xy,xz,xw,yw, etc] integrated over all tetrahedral volumes from origin. */
    static sumFacetSecondVolumeMomentProducts(source, origin) {
        if (source instanceof Polyface)
            return PolyfaceQuery.sumFacetSecondVolumeMomentProducts(source.createVisitor(0), origin);
        const products = Matrix4d.createZero();
        source.reset();
        while (source.moveToNextFacet()) {
            PolygonOps.addSecondMomentVolumeProducts(source.point, origin, products);
        }
        return products;
    }
    /**
     * Compute area moments for the mesh. In the returned `MomentData`:
     * * `origin` is the centroid.
     * * `localToWorldMap` has the origin and principal directions.
     * * `radiiOfGyration` radii for rotation around the x,y,z axes.
     */
    static computePrincipalAreaMoments(source) {
        const origin = source.data.getPoint(0);
        if (!origin)
            return undefined;
        const inertiaProducts = PolyfaceQuery.sumFacetSecondAreaMomentProducts(source, origin);
        return MomentData.inertiaProductsToPrincipalAxes(origin, inertiaProducts);
    }
    /**
     * Compute area moments for the mesh. In the returned MomentData:
     * * `origin` is the centroid.
     * * `localToWorldMap` has the origin and principal directions.
     * * `radiiOfGyration` radii for rotation around the x,y,z axes.
     * * The result is only valid if the mesh is closed.
     * * There is no test for closure. Use `PolyfaceQuery.isPolyfaceClosedByEdgePairing(polyface)` to test for closure.
     */
    static computePrincipalVolumeMoments(source) {
        const origin = source.data.getPoint(0);
        if (!origin)
            return undefined;
        const inertiaProducts = PolyfaceQuery.sumFacetSecondVolumeMomentProducts(source, origin);
        return MomentData.inertiaProductsToPrincipalAxes(origin, inertiaProducts);
    }
    /**
     * Determine whether all facets are convex.
     * @param source polyface or visitor.
     * @returns `true` if all facets are convex; `false` otherwise.
     */
    static areFacetsConvex(source) {
        if (source instanceof Polyface)
            return this.areFacetsConvex(source.createVisitor(0));
        source.setNumWrap(0);
        source.reset();
        while (source.moveToNextFacet()) {
            if (source.pointCount > 3) {
                if (!PolygonOps.isConvex(source.point))
                    return false;
            }
        }
        return true;
    }
    /**
     * Compute a number summarizing the dihedral angles in the mesh.
     * * A dihedral angle is the signed angle between adjacent facets' normals. This angle is positive when the cross
     * product `normalA x normalB` has the same direction as facetA's traversal of the facets' shared edge.
     * @param source facets.
     * @param ignoreBoundaries if `true` ignore simple boundary edges, i.e., allow unclosed meshes. Default is `false`.
     * See [[isConvexByDihedralAngleCount]] for comments about passing true when there are multiple
     * connected components.
     * * Return `0` if all dihedral angles are zero (and `ignoreBoundaries === true`). The mesh is planar.
     * * Otherwise, return `1` if all dihedral angles are non-negative. The mesh probably encloses a convex volume and
     * has outward normals.
     * * Otherwise, return `-1` if all dihedral angles are non-positive. The mesh probably encloses a convex volume and
     * has inward normals.
     * * Otherwise, return `-2`. Also return `-2` if a non-manifold condition was detected, or a facet normal could not
     * be computed. A non-manifold condition is a positive-length edge adjacent to more than 2 facets or (if
     * `ignoreBoundaries` is false) adjacent to exactly one facet.
     */
    static dihedralAngleSummary(source, ignoreBoundaries = false) {
        // more info can be found at geometry/internaldocs/Polyface.md
        const edges = new IndexedEdgeMatcher();
        const vertices = source instanceof Polyface ? source.data.point : undefined;
        const visitor = source instanceof Polyface ? source.createVisitor(1) : source;
        visitor.setNumWrap(1);
        const centroidNormal = [];
        let normalCounter = 0;
        for (visitor.reset(); visitor.moveToNextFacet();) {
            const numEdges = visitor.pointCount - 1;
            const normal = PolygonOps.centroidAreaNormal(visitor.point);
            if (normal === undefined)
                return -2;
            centroidNormal.push(normal);
            for (let i = 0; i < numEdges; i++) {
                const edge = edges.addEdge(visitor.clientPointIndex(i), visitor.clientPointIndex(i + 1), normalCounter);
                if (!vertices) // decorate if we don't have vertices to query later
                    edge.edgeVector = Vector3d.createStartEnd(visitor.point.getPoint3dAtUncheckedPointIndex(i), visitor.point.getPoint3dAtUncheckedPointIndex(i + 1));
            }
            normalCounter++;
        }
        // find "manifold clusters" and "bad clusters"
        // manifold clusters are edges adjacent to 2 facets
        // bad clusters are edges adjacent to more than 2 facets or (if ignoreBoundaries is false) adjacent to 1 facet
        const badClusters = [];
        const manifoldClusters = [];
        edges.sortAndCollectClusters(manifoldClusters, ignoreBoundaries ? undefined : badClusters, undefined, badClusters);
        if (badClusters.length > 0)
            return -2;
        // find angle between facet centroid normals (dihedral angles)
        let numPositive = 0;
        let numPlanar = 0;
        let numNegative = 0;
        const edgeVector = Vector3d.create();
        for (const cluster of manifoldClusters) {
            if (Array.isArray(cluster) && cluster.length === 2) {
                const sideA = cluster[0];
                const sideB = cluster[1];
                if (vertices)
                    vertices.vectorIndexIndex(sideA.startVertex, sideA.endVertex, edgeVector);
                else
                    edgeVector.setFrom(sideA.edgeVector);
                const facetNormalA = centroidNormal[sideA.facetIndex].direction;
                const facetNormalB = centroidNormal[sideB.facetIndex].direction;
                const dihedralAngle = facetNormalA.signedAngleTo(facetNormalB, edgeVector);
                if (dihedralAngle.isAlmostZero)
                    numPlanar++;
                else if (dihedralAngle.radians > 0.0)
                    numPositive++;
                else
                    numNegative++;
            }
        }
        // categorize the mesh
        if (numPositive > 0 && numNegative === 0)
            return 1;
        if (numNegative > 0 && numPositive === 0)
            return -1;
        if (numPlanar > 0 && numPositive === 0 && numNegative === 0) // planar mesh
            return 0;
        return -2;
    }
    /**
   * Test for convex volume by dihedral angle tests on all edges.
   * * This tests if all dihedral angles of the mesh are positive.
   * * In a closed solid, this is a strong test for overall mesh convexity with outward facing normals.
   * * See [[dihedralAngleSummary]] for the definition of "dihedral angle".
   * * With `ignoreBoundaries` true, this may be a useful test when all the facets are in a single edge-connected
   * component, such as a pyramid with no underside.
   * * It is not a correct test if there are multiple, disjoint components.
   * * Take the above-mentioned pyramid with no underside.
   * * Within the same mesh, have a second pyramid placed to the side, still facing upward.
   * * The angles will pass the dihedral convexity test, but the composite thing surely is not convex.
   * @param source mesh.
   * @param ignoreBoundaries if `true` ignore simple boundary edges, i.e., allow unclosed meshes. Default is `false`.
   * @returns true if all dihedral angles of the mesh are positive.
   */
    static isConvexByDihedralAngleCount(source, ignoreBoundaries = false) {
        return this.dihedralAngleSummary(source, ignoreBoundaries) > 0;
    }
    /** Helper function to detect a subset visitor. */
    static isSubsetVisitor(visitor) {
        if (visitor instanceof Polyface)
            return false;
        const parentFacetCount = visitor.clientPolyface()?.facetCount;
        const visitableFacetCount = visitor.getVisitableFacetCount?.();
        if (parentFacetCount === undefined || visitableFacetCount === undefined)
            return false;
        return parentFacetCount > visitableFacetCount;
    }
    /**
     * Faster version of isPolyfaceManifold for specific input.
     * @returns whether the mesh is manifold, or undefined if unsuccessful.
     */
    static isPolyfaceManifoldFast(source, allowSimpleBoundaries) {
        if (allowSimpleBoundaries)
            return undefined; // edgeMateIndex does not distinguish boundary edges from non-manifold edges so we can only speed things up if we search for both
        if (this.isSubsetVisitor(source))
            return false; // edgeMateIndex doesn't capture the facet subset boundary
        const parentData = IndexedPolyface.hasEdgeMateIndex(source);
        if (!parentData)
            return undefined;
        for (const edgeMate of parentData.edgeMateIndex) {
            if (edgeMate === undefined)
                return false; // found a boundary or non-manifold edge
        }
        return true; // this is a 2-manifold closed surface
    }
    /**
     * Test edge pairing in `source` mesh.
     * * Any edge with 3 or more adjacent facets triggers `false` return.
     * * Any edge with 2 adjacent facets in the same direction triggers `false` return.
     * * Null edges are ignored.
     * @param source facet set to examine
     * @param allowSimpleBoundaries if `false` (default), a return value of `true` means the facets form a closed
     * 2-manifold surface; if `true`, a return value of `true` means the facets form a 2-manifold surface which may
     * have a boundary, but is still properly matched internally.
    */
    static isPolyfaceManifold(source, allowSimpleBoundaries = false) {
        const isManifold = this.isPolyfaceManifoldFast(source, allowSimpleBoundaries);
        if (isManifold !== undefined)
            return isManifold;
        const badClusters = [];
        this.createIndexedEdges(source).sortAndCollectClusters(undefined, allowSimpleBoundaries ? undefined : badClusters, undefined, badClusters);
        return badClusters.length === 0;
    }
    /** Test if the facets in `source` occur in perfectly mated pairs, as is required for a closed manifold volume. */
    static isPolyfaceClosedByEdgePairing(source) {
        return this.isPolyfaceManifold(source, false);
    }
    /**
     * Faster version of announceBoundaryEdges for specific input.
     * @returns true if successfully announced boundary edges.
     */
    static announceBoundaryEdgesFast(source, announceEdge, includeTypical, includeMismatch, includeNull) {
        if (includeTypical !== includeMismatch)
            return false; // edgeMateIndex does not distinguish boundary edges from non-manifold edges
        if (!includeTypical && !includeNull)
            return true;
        if (this.isSubsetVisitor(source))
            return false; // edgeMateIndex doesn't capture the facet subset boundary
        const parentData = IndexedPolyface.hasEdgeMateIndex(source);
        if (!parentData)
            return false;
        const pointA = Point3d.create();
        const pointB = Point3d.create();
        for (let edgeIndex = 0; edgeIndex < parentData.edgeMateIndex.length; edgeIndex++) {
            const edgeMate = parentData.edgeMateIndex[edgeIndex];
            const announceBoundaryOrNonManifoldEdge = includeTypical && edgeMate === undefined;
            const announceNullEdge = includeNull && edgeMate === edgeIndex;
            if (announceBoundaryOrNonManifoldEdge || announceNullEdge) {
                const facetIndex = parentData.parent.edgeIndexToFacetIndex(edgeIndex);
                if (facetIndex !== undefined) { // should always be defined
                    const pointIndexA = parentData.parent.data.pointIndex[edgeIndex];
                    let nextEdgeIndex = edgeIndex + 1;
                    if (nextEdgeIndex >= parentData.parent.facetIndex1(facetIndex))
                        nextEdgeIndex = parentData.parent.facetIndex0(facetIndex);
                    const pointIndexB = parentData.parent.data.pointIndex[nextEdgeIndex];
                    parentData.parent.data.getPoint(pointIndexA, pointA);
                    parentData.parent.data.getPoint(pointIndexB, pointB);
                    announceEdge(pointA, pointB, pointIndexA, pointIndexB, facetIndex);
                }
            }
        }
        return true;
    }
    /**
     * Announce boundary edges of the facet set as line segments.
     * @param source polyface or visitor.
     * @param announceEdge function to be called with each boundary edge. The announcement is start and end points,
     * start and end vertex indices, and facet index.
     * @param includeTypical true to announce typical boundary edges with a single adjacent facet. Default is true.
     * @param includeMismatch true to announce non-manifold edges (more than 2 adjacent facets, or mismatched
     * orientations). Default is true.
     * @param includeNull true to announce edges with identical start and end vertex indices. Default is true.
     * @see [[announceBoundaryChainsAsLineString3d]] for boundary linestring announcement
     * @see [[collectBoundaryEdges]] for boundary chain collection
     * @see [[boundaryEdges]] for boundary edge collection
     */
    static announceBoundaryEdges(source, announceEdge, includeTypical = true, includeMismatch = true, includeNull = true) {
        if (this.announceBoundaryEdgesFast(source, announceEdge, includeTypical, includeMismatch, includeNull))
            return;
        if (!includeTypical && !includeMismatch && !includeNull)
            return;
        const vertices = source instanceof Polyface ? source.data.point : undefined;
        const visitor = source instanceof Polyface ? source.createVisitor(1) : source;
        visitor.setNumWrap(1);
        const edges = new IndexedEdgeMatcher();
        for (visitor.reset(); visitor.moveToNextFacet();) {
            const numEdges = visitor.pointCount - 1;
            for (let i = 0; i < numEdges; i++) {
                const edge = edges.addEdge(visitor.clientPointIndex(i), visitor.clientPointIndex(i + 1), visitor.currentReadIndex());
                if (!vertices) { // decorate if we don't have vertices to query later
                    edge.pointA = visitor.point.getPoint3dAtUncheckedPointIndex(i);
                    edge.pointB = visitor.point.getPoint3dAtUncheckedPointIndex(i + 1);
                }
            }
        }
        const boundaryEdges = [];
        const typicalEdges = includeTypical ? boundaryEdges : undefined;
        const nullEdges = includeNull ? boundaryEdges : undefined;
        const mismatchEdges = includeMismatch ? boundaryEdges : undefined;
        edges.sortAndCollectClusters(undefined, typicalEdges, nullEdges, mismatchEdges);
        if (boundaryEdges.length === 0)
            return;
        const pointA = Point3d.create();
        const pointB = Point3d.create();
        for (const e of boundaryEdges) {
            const e1 = e instanceof SortableEdge ? e : e[0]; // only report the first edge in a cluster!
            const indexA = e1.startVertex;
            const indexB = e1.endVertex;
            if (vertices) {
                vertices.getPoint3dAtUncheckedPointIndex(indexA, pointA);
                vertices.getPoint3dAtUncheckedPointIndex(indexB, pointB);
            }
            else {
                pointA.setFrom(e1.pointA);
                pointB.setFrom(e1.pointB);
            }
            announceEdge(pointA, pointB, indexA, indexB, e1.facetIndex);
        }
    }
    /**
     * Collect boundary edges of the facet set as an unordered collection of line segments.
     * @param source polyface or visitor.
     * @param includeTypical true to include typical boundary edges with a single adjacent facet. Default is true.
     * @param includeMismatch true to include non-manifold edges (more than 2 adjacent facets, or mismatched
     * orientations). Default is true.
     * @param includeNull true to include edges with identical start and end vertex indices. Default is true.
     * @see [[announceBoundaryChainsAsLineString3d]] for boundary linestring announcement
     * @see [[collectBoundaryEdges]] for boundary chain collection
     * @see [[announceBoundaryEdges]] for boundary edge announcement
     */
    static boundaryEdges(source, includeTypical = true, includeMismatch = true, includeNull = true) {
        const result = new BagOfCurves();
        const announceEdge = (pointA, pointB, _indexA, _indexB, _readIndex) => {
            result.tryAddChild(LineSegment3d.create(pointA, pointB));
        };
        PolyfaceQuery.announceBoundaryEdges(source, announceEdge, includeTypical, includeMismatch, includeNull);
        if (result.children.length === 0)
            return undefined;
        return result;
    }
    /**
     * Collect boundary edges of the facet set as the simplest collection of chains of line segments.
     * @param source polyface or visitor.
     * @param includeTypical true to include typical boundary edges with a single adjacent facet. Default is true.
     * @param includeMismatch true to include non-manifold edges (more than 2 adjacent facets, or mismatched
     * orientations). Default is true.
     * @param includeNull true to include edges with identical start and end vertex indices. Default is true.
     * @see [[announceBoundaryChainsAsLineString3d]] for boundary linestring announcement
     * @see [[announceBoundaryEdges]] for boundary edge announcement
     * @see [[boundaryEdges]] for boundary edge collection
     */
    static collectBoundaryEdges(source, includeTypical = true, includeMismatch = true, includeNull = true) {
        const collector = new MultiChainCollector(Geometry.smallMetricDistance, Geometry.smallMetricDistance);
        const announceEdge = (ptA, ptB) => collector.captureCurve(LineSegment3d.create(ptA, ptB));
        PolyfaceQuery.announceBoundaryEdges(source, announceEdge, includeTypical, includeMismatch, includeNull);
        return collector.grabResult(true);
    }
    /**
     * Load an IndexedEdgeMatcher from the edges of a mesh.
     * @param polyface a mesh or visitor
     */
    static createIndexedEdges(polyface) {
        const edges = new IndexedEdgeMatcher();
        const visitor = polyface instanceof Polyface ? polyface.createVisitor(1) : polyface;
        visitor.setNumWrap(1);
        for (visitor.reset(); visitor.moveToNextFacet();)
            edges.addPath(visitor.pointIndex, visitor.currentReadIndex(), false);
        return edges;
    }
    /**
     * Invoke the callback on each manifold edge whose adjacent facet normals form vectorToEye dot products
     * with opposite sign.
     * * The callback is not called on boundary edges.
     * @param source polyface or visitor.
     * @param announce callback function invoked on manifold silhouette edges.
     * @param vectorToEye normal of plane in which to compute silhouette edges.
     * @param sideAngle angular tolerance for perpendicularity test.
     */
    static announceSilhouetteEdges(source, announce, vectorToEye, sideAngle = Angle.createSmallAngle()) {
        if (source instanceof Polyface)
            return this.announceSilhouetteEdges(source.createVisitor(1), announce, vectorToEye, sideAngle);
        const mesh = source.clientPolyface();
        if (undefined === mesh)
            return;
        source.setNumWrap(1);
        const allEdges = this.createIndexedEdges(source);
        const manifoldEdges = [];
        allEdges.sortAndCollectClusters(manifoldEdges);
        const sideAngleTol = sideAngle.radians < 0.0 ? -sideAngle.radians : sideAngle.radians;
        const pointA = Point3d.create();
        const pointB = Point3d.create();
        const normal = Vector3d.create();
        const analyzeFace = (iFacet) => {
            if (!PolyfaceQuery.computeFacetUnitNormal(source, iFacet, normal))
                return { isSideFace: false, perpAngle: 0.0 };
            const perpAngle = normal.radiansFromPerpendicular(vectorToEye);
            const isSideFace = Math.abs(perpAngle) <= sideAngleTol;
            return { isSideFace, perpAngle };
        };
        for (const pair of manifoldEdges) {
            if (!Array.isArray(pair) || pair.length !== 2)
                continue;
            const indexA = pair[0].startVertex;
            const indexB = pair[0].endVertex;
            if (!mesh.data.getPoint(indexA, pointA) || !mesh.data.getPoint(indexB, pointB))
                continue;
            const face0 = analyzeFace(pair[0].facetIndex);
            if (face0.isSideFace) {
                announce(pointA, pointB, indexA, indexB, pair[0].facetIndex);
                continue;
            }
            const face1 = analyzeFace(pair[1].facetIndex);
            if (face1.isSideFace) {
                announce(pointB, pointA, indexB, indexA, pair[1].facetIndex);
                continue;
            }
            if (face0.perpAngle * face1.perpAngle < 0.0) { // normals straddle plane
                announce(pointA, pointB, indexA, indexB, pair[0].facetIndex);
                continue;
            }
        }
    }
    /**
     * Collect manifold edges whose adjacent facet normals form vectorToEye dot products with opposite sign.
     * * Does not return boundary edges.
     * * Return the edges as chains of line segments.
     * @param source polyface or visitor.
     * @param vectorToEye normal of plane in which to compute silhouette edges.
     * @param sideAngle angular tolerance for perpendicularity test.
     */
    static collectSilhouetteEdges(source, vectorToEye, sideAngle = Angle.createSmallAngle()) {
        const collector = new MultiChainCollector(Geometry.smallMetricDistance, Geometry.smallMetricDistance);
        PolyfaceQuery.announceSilhouetteEdges(source, (ptA, ptB) => collector.captureCurve(LineSegment3d.create(ptA, ptB)), vectorToEye, sideAngle);
        return collector.grabResult(true);
    }
    /**
     * Find segments (within the linestring) which project to facets.
     * * Announce each pair of linestring segment and on-facet segment through a callback.
     * * Facets are ASSUMED to be convex and planar, and not overlap in the z direction.
     */
    static announceSweepLinestringToConvexPolyfaceXY(linestringPoints, polyface, announce) {
        const context = SweepLineStringToFacetContext.create(linestringPoints);
        if (context) {
            const visitor = polyface.createVisitor(0);
            for (visitor.reset(); visitor.moveToNextFacet();) {
                context.projectToPolygon(visitor.point, announce, polyface, visitor.currentReadIndex());
            }
        }
    }
    // amount of computation to do per step of async methods.
    static _asyncWorkLimit = 1.e06;
    /**
     * Set the limit on work during an async time blocks, and return the old value.
     * * This should be a large number -- default is 1.0e6
     * @internal
     */
    static setAsyncWorkLimit(value) {
        const oldValue = this._asyncWorkLimit;
        this._asyncWorkLimit = value;
        return oldValue;
    }
    /**
     * Query the current limit on work during an async time block.
     * @internal
     */
    static get asyncWorkLimit() {
        return this._asyncWorkLimit;
    }
    /**
     * Number of "await" steps executed in recent async calls.
     * @internal
     */
    static awaitBlockCount = 0;
    /** Execute `context.projectToPolygon` until its work estimates accumulate to workLimit.  */
    static async continueAnnounceSweepLinestringToConvexPolyfaceXY(context, visitor, announce) {
        let workCount = 0;
        while ((workCount < this.asyncWorkLimit) && visitor.moveToNextFacet()) {
            workCount += context.projectToPolygon(visitor.point, announce, visitor.clientPolyface(), visitor.currentReadIndex());
        }
        return workCount;
    }
    /**
     * Find segments (within the linestring) which project to facets.
     * * Announce each pair of linestring segment and on-facet segment through a callback.
     * * Facets are ASSUMED to be convex and planar, and not overlap in the z direction.
     * * REMARK: Although this is public, the usual use is via slightly higher level public methods, viz:
     *   * asyncSweepLinestringToFacetsXYReturnChains
     * @internal
     */
    static async asyncAnnounceSweepLinestringToConvexPolyfaceXY(linestringPoints, polyface, announce) {
        const context = SweepLineStringToFacetContext.create(linestringPoints);
        this.awaitBlockCount = 0;
        let workTotal = 0;
        if (context) {
            const visitor = polyface.createVisitor(0);
            let workCount;
            while (0 < (workCount = await Promise.resolve(PolyfaceQuery.continueAnnounceSweepLinestringToConvexPolyfaceXY(context, visitor, announce)))) {
                workTotal += workCount;
                this.awaitBlockCount++;
                // GeometryCoreTestIO.consoleLog({ myWorkCount: workCount, myBlockCount: this.awaitBlockCount });
            }
        }
        // GeometryCoreTestIO.consoleLog({ myWorkTotal: workTotal, myBlockCount: this.awaitBlockCount });
        return workTotal;
    }
    /**
     * Search the facets for facet subsets that are connected with at least vertex contact.
     * * Return array of arrays of facet indices.
     */
    static partitionFacetIndicesByVertexConnectedComponent(polyface) {
        if (polyface instanceof Polyface) {
            return this.partitionFacetIndicesByVertexConnectedComponent(polyface.createVisitor(0));
        }
        // The polyface is really a visitor
        const context = new UnionFindContext(this.visitorClientPointCount(polyface));
        for (polyface.reset(); polyface.moveToNextFacet();) {
            const firstVertexIndexOnThisFacet = polyface.pointIndex[0];
            for (const vertexIndex of polyface.pointIndex)
                context.mergeSubsets(firstVertexIndexOnThisFacet, vertexIndex);
        }
        const roots = context.collectRootIndices();
        const facetsInComponent = [];
        const numRoots = roots.length;
        for (let i = 0; i < numRoots; i++) {
            facetsInComponent.push([]);
        }
        for (polyface.reset(); polyface.moveToNextFacet();) {
            const firstVertexIndexOnThisFacet = polyface.pointIndex[0];
            const rootVertexForThisFacet = context.findRoot(firstVertexIndexOnThisFacet);
            for (let rootIndex = 0; rootIndex < numRoots; rootIndex++) {
                if (roots[rootIndex] === rootVertexForThisFacet) {
                    facetsInComponent[rootIndex].push(polyface.currentReadIndex());
                    break;
                }
            }
        }
        return facetsInComponent;
    }
    /**
     * * Examine the normal orientation for each faces.
     * * Separate to 3 partitions:
     *    * Facets with normal in the positive direction of the vectorToEye (partition 0).
     *    * Facets with normal in the negative direction of the vectorToEye (partition 1).
     *    * Facets nearly perpendicular to the view vector  (partition 2).
     * * Return array of arrays of facet indices.
     */
    static partitionFacetIndicesByVisibilityVector(polyface, vectorToEye, sideAngleTolerance) {
        if (polyface instanceof Polyface) {
            return this.partitionFacetIndicesByVisibilityVector(polyface.createVisitor(0), vectorToEye, sideAngleTolerance);
        }
        const facetsInComponent = [];
        for (let i = 0; i < 3; i++) {
            facetsInComponent.push([]);
        }
        const forwardComponent = facetsInComponent[0];
        const rearComponent = facetsInComponent[1];
        const sideComponent = facetsInComponent[2];
        const radiansTol = Math.max(sideAngleTolerance.radians, 1.0e-8);
        for (polyface.reset(); polyface.moveToNextFacet();) {
            const areaNormal = PolygonOps.areaNormalGo(polyface.point);
            const index = polyface.currentReadIndex();
            if (areaNormal) {
                const angle = areaNormal.angleFromPerpendicular(vectorToEye);
                if (Math.abs(angle.radians) < radiansTol) {
                    sideComponent.push(index);
                }
                else if (areaNormal.dotProduct(vectorToEye) < 0) {
                    rearComponent.push(index);
                }
                else {
                    forwardComponent.push(index);
                }
            }
        }
        return facetsInComponent;
    }
    /**
     * Return the boundary of facets that are facing the eye.
     * @param source polyface or visitor. Must be capable of constructing a subset visitor.
     * @param visibilitySubset selector among the visible facet sets extracted by partitionFacetIndicesByVisibilityVector
     *   * 0 ==> forward facing
     *   * 1 ==> rear facing
     *   * 2 ==> side facing
     * @param vectorToEye the vector to eye
     * @param sideAngleTolerance the tolerance of side angle
     */
    static boundaryOfVisibleSubset(source, visibilitySelect, vectorToEye, sideAngleTolerance = Angle.createDegrees(1.0e-3)) {
        const visitor = source instanceof Polyface ? source.createVisitor(0) : source;
        if (!visitor.createSubsetVisitor)
            return undefined;
        const partitionedIndices = this.partitionFacetIndicesByVisibilityVector(source, vectorToEye, sideAngleTolerance);
        if (partitionedIndices[visibilitySelect].length === 0)
            return undefined;
        const subsetVisitor = visitor.createSubsetVisitor(partitionedIndices[visibilitySelect], 1);
        return this.boundaryEdges(subsetVisitor, true, false, false);
    }
    /**
     * Announce boundary edges of the facet set as linestrings.
     * * Ignores non-manifold interior edges and null edges.
     * @param source polyface or visitor.
     * @param announceChain function to be called with each chain of boundary edges.
     * @see [[collectBoundaryEdges]] for boundary chain collection
     * @see [[announceBoundaryEdges]] for boundary edge announcement
     * @see [[boundaryEdges]] for boundary edge collection
     */
    static announceBoundaryChainsAsLineString3d(source, announceChain) {
        const collector = new MultiChainCollector(Geometry.smallMetricDistance); // no planarity tolerance needed
        const announceEdge = (ptA, ptB) => collector.captureCurve(LineSegment3d.create(ptA, ptB));
        PolyfaceQuery.announceBoundaryEdges(source, announceEdge, true, false, false);
        collector.announceChainsAsLineString3d(announceChain);
    }
    /**
     * Return a mesh with
     *  * clusters of adjacent, coplanar facets merged into larger (possibly non-convex) facets.
     *  * other facets are unchanged.
     * @param mesh existing mesh or visitor.
     * @param maxSmoothEdgeAngle maximum dihedral angle across an edge between facets deemed coplanar. If undefined,
     * uses `Geometry.smallAngleRadians`.
     */
    static cloneWithMaximalPlanarFacets(mesh, maxSmoothEdgeAngle) {
        if (mesh instanceof Polyface)
            return this.cloneWithMaximalPlanarFacets(mesh.createVisitor(0), maxSmoothEdgeAngle);
        const numFacets = PolyfaceQuery.visitorClientFacetCount(mesh);
        const smoothEdges = PolyfaceQuery.collectEdgesByDihedralAngle(mesh, maxSmoothEdgeAngle);
        const partitions = PolyfaceQuery.partitionFacetIndicesBySortableEdgeClusters(smoothEdges, numFacets);
        const builder = PolyfaceBuilder.create();
        const visitor = mesh;
        const planarPartitions = [];
        const partitionNormals = []; // average normal in each nontrivial partition
        const normal = Vector3d.createZero();
        for (const partition of partitions) {
            if (partition.length === 1) {
                if (visitor.moveToReadIndex(partition[0]))
                    builder.addFacetFromVisitor(visitor);
            }
            else if (partition.length > 1) { // nontrivial set of contiguous coplanar facets
                const averageNormal = Vector3d.createZero();
                const point0 = Point3d.createZero();
                if (visitor.moveToReadIndex(partition[0]))
                    visitor.point.getPoint3dAtCheckedPointIndex(0, point0);
                for (const facetIndex of partition) {
                    if (visitor.moveToReadIndex(facetIndex))
                        if (PolygonOps.areaNormalGo(visitor.point, normal))
                            averageNormal.addInPlace(normal);
                }
                partitionNormals.push(Ray3d.createCapture(point0, averageNormal));
                planarPartitions.push(partition);
            }
        }
        const fragmentPolyfaces = PolyfaceQuery.clonePartitions(mesh, planarPartitions);
        assert(planarPartitions.length === partitionNormals.length);
        assert(planarPartitions.length === fragmentPolyfaces.length);
        const gapTolerance = 1.0e-4;
        const planarityTolerance = 1.0e-4;
        const localToWorld = Transform.createIdentity();
        const worldToLocal = Transform.createIdentity();
        for (let i = 0; i < fragmentPolyfaces.length; ++i) {
            const fragment = fragmentPolyfaces[i];
            const edges = [];
            const edgeStrings = [];
            PolyfaceQuery.announceBoundaryEdges(fragment, (pointA, pointB, _indexA, _indexB) => {
                edges.push(LineSegment3d.create(pointA, pointB));
                edgeStrings.push([pointA.clone(), pointB.clone()]);
            }, true, false, false);
            const chains = CurveOps.collectChains(edges, gapTolerance, planarityTolerance);
            if (chains) {
                // avoid FrameBuilder: it can flip the normal of a nonconvex facet!
                partitionNormals[i].toRigidZFrame(localToWorld);
                if (localToWorld.inverse(worldToLocal)) {
                    worldToLocal.multiplyPoint3dArrayArrayInPlace(edgeStrings);
                    // Regularize adds bridge edges to holes, and adds other edges to aid triangulation.
                    // But we aren't triangulating here. So if we don't have holes, we can skip regularization
                    // to avoid splitting the loop.
                    const regularize = !(chains instanceof Loop);
                    const graph = HalfEdgeGraphMerge.formGraphFromChains(edgeStrings, regularize, HalfEdgeMask.BOUNDARY_EDGE);
                    if (graph) {
                        HalfEdgeGraphSearch.collectConnectedComponentsWithExteriorParityMasks(graph, new HalfEdgeMaskTester(HalfEdgeMask.BOUNDARY_EDGE), HalfEdgeMask.EXTERIOR);
                        // this.purgeNullFaces(HalfEdgeMask.EXTERIOR);
                        const polyface1 = PolyfaceBuilder.graphToPolyface(graph);
                        builder.addIndexedPolyface(polyface1, false, localToWorld);
                    }
                }
            }
        }
        return builder.claimPolyface(true);
    }
    /**
     * Return a mesh with "some" holes filled in with new facets.
     *  * Candidate chains are computed by [[announceBoundaryChainsAsLineString3d]].
     *  * Unclosed chains are rejected.
     *  * Closed chains are triangulated and returned as a mesh.
     *  * The options structure enforces restrictions on how complicated the hole filling can be:
     *     * maxEdgesAroundHole -- holes with more edges are skipped.
     *     * maxPerimeter -- holes with larger summed edge lengths are skipped.
     *     * upVector -- holes that do not have positive area along this view are skipped.
     *     * includeOriginalMesh -- includes the original mesh in the output mesh, so the composite mesh is a
     * clone with holes filled.
     * @param mesh existing mesh.
     * @param options options controlling the hole fill.
     * @param unfilledChains optional array to receive the points around holes that were not filled.
     */
    static fillSimpleHoles(mesh, options, unfilledChains) {
        if (mesh instanceof Polyface)
            return this.fillSimpleHoles(mesh.createVisitor(0), options, unfilledChains);
        const builder = PolyfaceBuilder.create();
        const chains = [];
        PolyfaceQuery.announceBoundaryChainsAsLineString3d(mesh, (ls) => { ls.reverseInPlace(); chains.push(ls); });
        for (const c of chains) {
            const points = c.points;
            let rejected = false;
            if (!c.isPhysicallyClosed)
                rejected = true;
            else if (options.maxEdgesAroundHole !== undefined && points.length > options.maxEdgesAroundHole)
                rejected = true;
            else if (options.maxPerimeter !== undefined && Point3dArray.sumEdgeLengths(points, false) > options.maxPerimeter)
                rejected = true;
            else if (options.upVector !== undefined && PolygonOps.sumTriangleAreasPerpendicularToUpVector(points, options.upVector) <= 0.0)
                rejected = true;
            if (!rejected && SpacePolygonTriangulation.triangulateSimplestSpaceLoop(points, (_loop, triangles) => {
                for (const t of triangles)
                    builder.addPolygon(t);
            })) {
            }
            else {
                rejected = true;
            }
            if (rejected && unfilledChains !== undefined)
                unfilledChains.push(c); // yes, capture it -- this scope owns the chains and has no further use for it.
        }
        if (options.includeOriginalMesh !== undefined && options.includeOriginalMesh) {
            for (mesh.reset(); mesh.moveToNextFacet();)
                builder.addFacetFromVisitor(mesh);
        }
        return builder.claimPolyface(true);
    }
    /** Clone the facets in each partition to a separate polyface. */
    static clonePartitions(polyface, partitions) {
        if (polyface instanceof Polyface) {
            return this.clonePartitions(polyface.createVisitor(0), partitions);
        }
        polyface.setNumWrap(0);
        const polyfaces = [];
        const options = StrokeOptions.createForFacets();
        options.needNormals = polyface.normal !== undefined;
        options.needParams = polyface.param !== undefined;
        options.needColors = polyface.color !== undefined;
        options.needTwoSided = polyface.twoSided;
        for (const partition of partitions) {
            const builder = PolyfaceBuilder.create(options);
            polyface.reset();
            for (const facetIndex of partition) {
                polyface.moveToReadIndex(facetIndex);
                builder.addFacetFromVisitor(polyface);
            }
            polyfaces.push(builder.claimPolyface(true));
        }
        return polyfaces;
    }
    /** Clone facets that pass a filter function. */
    static cloneFiltered(source, filter) {
        if (source instanceof Polyface) {
            return this.cloneFiltered(source.createVisitor(0), filter);
        }
        source.setNumWrap(0);
        const options = StrokeOptions.createForFacets();
        options.needNormals = source.normal !== undefined;
        options.needParams = source.param !== undefined;
        options.needColors = source.color !== undefined;
        options.needTwoSided = source.twoSided;
        const builder = PolyfaceBuilder.create(options);
        source.reset();
        for (; source.moveToNextFacet();) {
            if (filter(source))
                builder.addFacetFromVisitor(source);
        }
        return builder.claimPolyface(true);
    }
    /** Clone the facets with in-facet dangling edges removed. */
    static cloneWithDanglingEdgesRemoved(source) {
        if (source instanceof Polyface)
            return this.cloneWithDanglingEdgesRemoved(source.createVisitor(0));
        const options = StrokeOptions.createForFacets();
        options.needNormals = source.normal !== undefined;
        options.needParams = source.param !== undefined;
        options.needColors = source.color !== undefined;
        options.needTwoSided = source.twoSided;
        const builder = PolyfaceBuilder.create(options);
        // Finds an odd palindrome in data as indexed by indices.
        // An odd palindrome in a face loop corresponds to dangling edges in the face.
        // If one is found, indices is mutated to excise the palindrome (data is untouched).
        // @returns whether indices array was mutated
        const removeFirstOddPalindrome = (indices, data) => {
            const n = indices.length;
            for (let i = 0; i < n; ++i) {
                let palLength = 1;
                let i0 = i; // look for odd palindrome centered at i
                let i1 = i; // and with extents i0..i1
                while (palLength + 2 <= n) {
                    const iPrev = (i0 === 0) ? n - 1 : i0 - 1;
                    const iNext = (i1 === n - 1) ? 0 : i1 + 1;
                    if (data[indices[iPrev]] !== data[indices[iNext]])
                        break; // the maximal odd palindrome centered at i has length palLength and spans [i0,i1]
                    i0 = iPrev;
                    i1 = iNext;
                    palLength += 2;
                }
                if (palLength > 1) { // excise the palindrome (but keep i1)
                    if (i0 < i1) {
                        indices.splice(i0, palLength - 1); // remove entries [i0,i1)
                    }
                    else if (i0 > i1) {
                        indices.splice(i0); // remove entries [i0,n)
                        indices.splice(0, i1); // remove entries [0,i1)
                    }
                    return true;
                }
            }
            return false;
        };
        source.setNumWrap(0);
        source.reset();
        while (source.moveToNextFacet()) {
            const localIndices = [...Array(source.pointIndex.length).keys()]; // 0, 1, ... n-1;
            while (removeFirstOddPalindrome(localIndices, source.pointIndex)) { }
            builder.addFacetFromIndexedVisitor(source, localIndices);
        }
        return builder.claimPolyface(true);
    }
    /**
     * Return the point count of the `source`.
     * * If `source` is a visitor, this is an upper bound on the number of addressed mesh vertices.
     */
    static visitorClientPointCount(source) {
        if (source instanceof Polyface)
            return source.data.point.length;
        const polyface = source.clientPolyface();
        if (polyface !== undefined)
            return polyface.data.point.length;
        const saveReadIndex = source.currentReadIndex();
        source.reset();
        let maxIndex = -1;
        while (source.moveToNextFacet()) {
            for (const pointIndex of source.pointIndex)
                if (pointIndex > maxIndex)
                    maxIndex = pointIndex;
        }
        source.moveToReadIndex(saveReadIndex);
        return maxIndex + 1;
    }
    /**
     * Return the facet count of the `source`.
     * * If `source` is a visitor, this is the number of facets it can visit.
     */
    static visitorClientFacetCount(source) {
        if (source instanceof Polyface) {
            if (source.facetCount !== undefined)
                return source.facetCount;
            source = source.createVisitor(0);
        }
        if (source.getVisitableFacetCount)
            return source.getVisitableFacetCount();
        const polyface = source.clientPolyface();
        if (polyface !== undefined && polyface.facetCount !== undefined)
            return polyface.facetCount;
        const saveReadIndex = source.currentReadIndex();
        let facetCount = 0;
        source.reset();
        while (source.moveToNextFacet())
            ++facetCount;
        source.moveToReadIndex(saveReadIndex);
        return facetCount;
    }
    /**
     * Partition the facet set into connected components such that two adjacent facets are in the same component if and
     * only if they are adjacent across a clustered edge.
     * @param edgeClusters sorted and clustered edges (cf. `IndexedEdgeMatcher.sortAndCollectClusters`).
     * @param numFacets facet count in the parent mesh. In particular, `edge.facetIndex < numFacets` for every input edge.
     * @return collection of facet index arrays, one array per connected component.
     */
    static partitionFacetIndicesBySortableEdgeClusters(edgeClusters, numFacets) {
        const context = new UnionFindContext(numFacets);
        for (const cluster of edgeClusters) {
            if (cluster instanceof SortableEdge) {
                // this edge does not connect anywhere.  Ignore it!!
            }
            else {
                const edge0 = cluster[0];
                for (let i = 1; i < cluster.length; i++)
                    context.mergeSubsets(edge0.facetIndex, cluster[i].facetIndex);
            }
        }
        const roots = context.collectRootIndices();
        const facetsInComponent = [];
        const numRoots = roots.length;
        for (let i = 0; i < numRoots; i++) {
            facetsInComponent.push([]);
        }
        for (let facetIndex = 0; facetIndex < numFacets; facetIndex++) {
            const rootOfFacet = context.findRoot(facetIndex);
            for (let rootIndex = 0; rootIndex < numRoots; rootIndex++) {
                if (roots[rootIndex] === rootOfFacet) {
                    facetsInComponent[rootIndex].push(facetIndex);
                    break;
                }
            }
        }
        return facetsInComponent;
    }
    /**
     * Partition the facet set into connected components. Each facet in a given component shares an edge only with
     * other facets in the component (or is a boundary edge).
     * @param polyface facets to partition.
     * @param stopAtVisibleEdges whether to further split connected components by visible edges of the polyface.
     * @return collection of facet index arrays, one per connected component.
     */
    static partitionFacetIndicesByEdgeConnectedComponent(polyface, stopAtVisibleEdges = false) {
        if (polyface instanceof Polyface) {
            return this.partitionFacetIndicesByEdgeConnectedComponent(polyface.createVisitor(0), stopAtVisibleEdges);
        }
        polyface.setNumWrap(1);
        const matcher = new IndexedEdgeMatcher();
        polyface.reset();
        let numFacets = 0;
        while (polyface.moveToNextFacet()) {
            const numEdges = polyface.pointCount - 1;
            numFacets++;
            for (let i = 0; i < numEdges; i++) {
                if (stopAtVisibleEdges && polyface.edgeVisible[i]) {
                }
                else {
                    matcher.addEdge(polyface.clientPointIndex(i), polyface.clientPointIndex(i + 1), polyface.currentReadIndex());
                }
            }
        }
        const allEdges = [];
        matcher.sortAndCollectClusters(allEdges, allEdges, allEdges, allEdges);
        return this.partitionFacetIndicesBySortableEdgeClusters(allEdges, numFacets);
    }
    /**
     * Find segments (within the line string) which project to facets.
     * * Assemble each input segment paired with its projected segment/point as a quad/triangle facet in a new polyface.
     * * Input facets are ASSUMED to be convex and planar, and not overlap in the z direction.
     */
    static sweepLineStringToFacetsXYReturnSweptFacets(lineStringPoints, polyface) {
        const builder = PolyfaceBuilder.create();
        this.announceSweepLinestringToConvexPolyfaceXY(lineStringPoints, polyface, (_linestring, _segmentIndex, _polyface, _facetIndex, points) => {
            if (points.length === 4)
                builder.addQuadFacet(points);
            else if (points.length === 3)
                builder.addTriangleFacet(points);
        });
        return builder.claimPolyface(true);
    }
    /** @deprecated in 4.7.0 - will not be removed until after 2026-06-13. Use [[sweepLineStringToFacetsXYReturnSweptFacets]] instead. */
    static sweepLinestringToFacetsXYreturnSweptFacets(linestringPoints, polyface) {
        return this.sweepLineStringToFacetsXYReturnSweptFacets(linestringPoints, polyface);
    }
    /**
     * Sweep the line string to intersections with a mesh and return collected line segments.
     * * Facets are ASSUMED to be convex and planar, and not overlap in the sweep direction.
     * @param points the linestring to drape onto the mesh.
     * @param source target facet set. For best results, facets should be convex and planar.
     * @param options input, filtering, search, and output options.
     * * If `undefined`, the default sweep direction is the positive z-axis, and chains are assembled and returned.
     * * For faster _vertical_ sweep, a pre-computed range tree can be supplied in `options.searcher`.
     * * For faster _non-vertical_ sweep, first transform inputs with the inverse of the transform
     * `T = Transform.createRigidFromOriginAndVector(undefined, options.vectorToEye)`, construct the searcher on these
     * local facets, call `sweepLineStringToFacets/XY` with these local inputs (and default sweep direction), and lastly,
     * transform the returned draped linework back to world coordinates with `T`.
     */
    static sweepLineStringToFacets(points, source, options) {
        let result = [];
        if (options === undefined)
            options = SweepLineStringToFacetsOptions.create();
        let chainContext;
        if (options.assembleChains)
            chainContext = ChainMergeContext.create();
        const addSegment = chainContext ? (ptA, ptB) => chainContext.addSegment(ptA, ptB) : (ptA, ptB) => result.push(LineSegment3d.create(ptA, ptB));
        const workNormal = Vector3d.createZero();
        const visitor = source instanceof Polyface ? source.createVisitor(0) : source;
        visitor.setNumWrap(0);
        if (options.searcher && options.vectorToEye.isParallelTo(Vector3d.unitZ())) {
            const searchRange = Range3d.createNull();
            const workPoint0 = Point3d.createZero();
            const workPoint1 = Point3d.createZero();
            let edgeClipper;
            const clipEdgeToConvexPolygon = (_facetRange, readIndex) => {
                if (visitor.moveToReadIndex(readIndex) && (options.collectAll || options.collectFromThisFacetNormal(PolygonOps.areaNormalGo(visitor.point, workNormal))))
                    edgeClipper?.processPolygon(visitor.point, (ptA, ptB) => addSegment(ptA, ptB));
                return true;
            };
            for (let i = 1; i < points.length; i++) {
                points.getPoint3dAtUncheckedPointIndex(i - 1, workPoint0);
                points.getPoint3dAtUncheckedPointIndex(i, workPoint1);
                if (edgeClipper = EdgeClipData.createPointPointSweep(workPoint0, workPoint1, options.vectorToEye)) {
                    searchRange.setNull();
                    searchRange.extend(workPoint0, workPoint1);
                    options.searcher.searchRange2d(searchRange, clipEdgeToConvexPolygon);
                }
            }
        }
        else {
            const context = ClipSweptLineStringContext.create(points, options.vectorToEye);
            if (context) {
                for (visitor.reset(); visitor.moveToNextFacet();) {
                    if (options.collectAll || options.collectFromThisFacetNormal(PolygonOps.areaNormalGo(visitor.point, workNormal)))
                        context.processPolygon(visitor.point.getArray(), addSegment);
                }
            }
        }
        if (chainContext) {
            chainContext.clusterAndMergeVerticesXYZ();
            result = chainContext.collectMaximalChains();
        }
        return result;
    }
    /**
     * Sweep the line string in the z-direction to intersections with a mesh, using a search object for speedup.
     * @param points the linestring to drape onto the mesh.
     * @param source target facet set. For best results, facets should be convex and planar.
     * @param searcher object for searching facet 2D ranges tagged by mesh read index.
     * @returns the collected line strings.
     * @see [[sweepLineStringToFacets]] for further options.
     */
    static sweepLineStringToFacetsXY(points, source, searcher) {
        const chainContext = ChainMergeContext.create();
        const vectorToEye = Vector3d.unitZ();
        const searchRange = Range3d.create();
        const workPoint0 = Point3d.createZero();
        const workPoint1 = Point3d.createZero();
        const visitor = source instanceof Polyface ? source.createVisitor(0) : source;
        visitor.setNumWrap(0);
        let edgeClipper;
        const clipEdgeToConvexPolygon = (_facetRange, readIndex) => {
            if (visitor.moveToReadIndex(readIndex))
                edgeClipper?.processPolygon(visitor.point, (ptA, ptB) => chainContext.addSegment(ptA, ptB));
            return true;
        };
        const lineStringSource = Array.isArray(points) ? new Point3dArrayCarrier(points) : points;
        for (let i = 1; i < lineStringSource.length; i++) {
            lineStringSource.getPoint3dAtUncheckedPointIndex(i - 1, workPoint0);
            lineStringSource.getPoint3dAtUncheckedPointIndex(i, workPoint1);
            if (edgeClipper = EdgeClipData.createPointPointSweep(workPoint0, workPoint1, vectorToEye)) {
                searchRange.setNull();
                searchRange.extend(workPoint0, workPoint1);
                searcher.searchRange2d(searchRange, clipEdgeToConvexPolygon);
            }
        }
        chainContext.clusterAndMergeVerticesXYZ();
        return chainContext.collectMaximalChains();
    }
    /**
     * Find segments (within the linestring) which project to facets.
      * * Return collected line segments.
      * * This calls [[sweepLineStringToFacets]] with options created by
      *   `const options = SweepLineStringToFacetsOptions.create(Vector3d.unitZ(), Angle.createSmallAngle(), false, true, true, true);`
      * @deprecated in 4.7.0 - will not be removed until after 2026-06-13. Use [[PolyfaceQuery.sweepLineStringToFacets]] to get further options.
      */
    static sweepLinestringToFacetsXYReturnLines(linestringPoints, polyface) {
        const options = SweepLineStringToFacetsOptions.create(Vector3d.unitZ(), Angle.createSmallAngle(), false, true, true, true);
        return PolyfaceQuery.sweepLineStringToFacets(linestringPoints, polyface, options);
    }
    /**
     * Find segments (within the linestring) which project to facets.
     * * Return chains.
     * * This calls [[sweepLineStringToFacets]] with default options.
     * @deprecated in 4.7.0 - will not be removed until after 2026-06-13. Use [[PolyfaceQuery.sweepLineStringToFacets]] to get further options.
     */
    static sweepLinestringToFacetsXYReturnChains(linestringPoints, polyface) {
        return PolyfaceQuery.sweepLineStringToFacets(linestringPoints, polyface);
    }
    /**
     * Find segments (within the linestring) which project to facets.
     * * This is done as a sequence of "await" steps.
     * * Each "await" step deals with approximately PolyfaceQuery.asyncWorkLimit pairings of "linestring edge"
     * with "facet edge".
     * * PolyfaceQuery.setAsyncWorkLimit() to change work blocks from default.
     * * Return chains.
     * * Facets are ASSUMED to be convex and planar, and not overlap in the z direction.
     */
    static async asyncSweepLinestringToFacetsXYReturnChains(linestringPoints, polyface) {
        const chainContext = ChainMergeContext.create();
        await Promise.resolve(this.asyncAnnounceSweepLinestringToConvexPolyfaceXY(linestringPoints, polyface, (_linestring, _segmentIndex, _polyface, _facetIndex, points, indexA, indexB) => {
            chainContext.addSegment(points[indexA], points[indexB]);
        }));
        chainContext.clusterAndMergeVerticesXYZ();
        const chains = chainContext.collectMaximalChains();
        return chains;
    }
    /**
     * Examine ranges of facets.
     * * Return statistical summary of x,y,z ranges.
     */
    static collectRangeLengthData(polyface) {
        if (polyface instanceof Polyface) {
            return this.collectRangeLengthData(polyface.createVisitor(0));
        }
        const rangeData = new RangeLengthData();
        // polyface is a visitor
        for (polyface.reset(); polyface.moveToNextFacet();)
            rangeData.accumulateGrowableXYZArrayRange(polyface.point);
        return rangeData;
    }
    /**
     * Clone the facets, inserting vertices (within edges) where points not part of each facet's vertex indices
     * impinge within edges.
     */
    static cloneWithTVertexFixup(polyface) {
        const oldFacetVisitor = polyface.createVisitor(1); // this is to visit the existing facets
        const newFacetVisitor = polyface.createVisitor(0); // this is to build the new facets
        const rangeSearcher = XYPointBuckets.create(polyface.data.point, 30);
        const builder = PolyfaceBuilder.create();
        const edgeRange = Range3d.createNull();
        const point0 = Point3d.create();
        const point1 = Point3d.create();
        const spacePoint = Point3d.create();
        const segment = LineSegment3d.create(point0, point1);
        for (oldFacetVisitor.reset(); oldFacetVisitor.moveToNextFacet();) {
            newFacetVisitor.clearArrays();
            for (let i = 0; i + 1 < oldFacetVisitor.point.length; i++) {
                // each base vertex is part of the result
                oldFacetVisitor.point.getPoint3dAtUncheckedPointIndex(i, point0);
                oldFacetVisitor.point.getPoint3dAtUncheckedPointIndex(i + 1, point1);
                newFacetVisitor.pushDataFrom(oldFacetVisitor, i);
                edgeRange.setNull();
                LineSegment3d.create(point0, point1, segment);
                let detailArray;
                edgeRange.extend(point0);
                edgeRange.extend(point1);
                edgeRange.ensureMinLengths(Geometry.smallMetricDistance); // add some slop in case segment is axis-aligned
                rangeSearcher.announcePointsInRange(edgeRange, (index, _x, _y, _z) => {
                    // x,y,z has x,y within the range of the search; test for exact on (in full 3d)
                    polyface.data.point.getPoint3dAtUncheckedPointIndex(index, spacePoint);
                    const detail = segment.closestPoint(spacePoint, false);
                    if (undefined !== detail) {
                        if (detail.fraction > 0.0 && detail.fraction < 1.0 && !detail.point.isAlmostEqual(point0) &&
                            !detail.point.isAlmostEqual(point1) && spacePoint.isAlmostEqual(detail.point)) {
                            if (detailArray === undefined)
                                detailArray = [];
                            detail.a = index;
                            detailArray.push(detail);
                        }
                    }
                    return true;
                });
                if (detailArray !== undefined) {
                    detailArray.sort((a, b) => (a.fraction - b.fraction));
                    for (const d of detailArray) {
                        newFacetVisitor.pushInterpolatedDataFrom(oldFacetVisitor, i, d.fraction, i + 1);
                    }
                }
            }
            builder.addFacetFromGrowableArrays(newFacetVisitor.point, newFacetVisitor.normal, newFacetVisitor.param, newFacetVisitor.color, newFacetVisitor.edgeVisible);
        }
        return builder.claimPolyface();
    }
    /**
     * Compare index arrays formatted as follows. Return 0 if arrays are the same.
     * * Each array input structure is: [facetIndex, vertexIndex0, vertexIndex1, ....]
     * * Vertex indices assumed reversed so:
     *   * vertexIndex0 is the lowest index on the facet.
     *   * vertexIndex1 is the lowest neighbor of vertex0.
     *   * first different entry among vertex indices determines lexical result.
     *   * hence facets with duplicate indices (whether forward or reversed) are considered equal.
     */
    static compareFacetIndexAndVertexIndices(arrayA, arrayB) {
        if (arrayA.length !== arrayB.length)
            return arrayA.length - arrayB.length;
        for (let i = 1; i < arrayA.length; i++) {
            if (arrayA[i] !== arrayB[i]) {
                return arrayA[i] - arrayB[i];
            }
        }
        return 0;
    }
    /**
     * Announce facet duplicates.
     * @returns an array of arrays describing facet duplication. Each array `entry` in the output contains read
     * indices of a cluster of facets with the same vertex indices.
     */
    static announceDuplicateFacetIndices(polyface, announceCluster) {
        const visitor = polyface.createVisitor(0); // this is to visit the existing facets
        const facetIndexAndVertexIndices = [];
        for (visitor.reset(); visitor.moveToNextFacet();) {
            const facetIndex = visitor.currentReadIndex();
            const entry = [facetIndex];
            const pointIndex = visitor.pointIndex;
            const numPointsThisFacet = pointIndex.length;
            let lowIndex = 0;
            // find the lowest point index
            for (let i = 1; i < visitor.pointIndex.length; i++) {
                if (pointIndex[i] < pointIndex[lowIndex])
                    lowIndex = i;
            }
            // find its lowest neighbor; assemble sort array in that direction
            if (pointIndex[(lowIndex + 1) % numPointsThisFacet] < pointIndex[(lowIndex + numPointsThisFacet - 1) % numPointsThisFacet]) {
                for (let i = 0; i < numPointsThisFacet; i++) {
                    entry.push(pointIndex[(lowIndex + i) % numPointsThisFacet]);
                }
            }
            else {
                for (let i = 0; i < numPointsThisFacet; i++) {
                    entry.push(pointIndex[(lowIndex + numPointsThisFacet - i) % numPointsThisFacet]);
                }
            }
            facetIndexAndVertexIndices.push(entry);
        }
        facetIndexAndVertexIndices.sort((arrayA, arrayB) => this.compareFacetIndexAndVertexIndices(arrayA, arrayB));
        let i0, i1;
        const n = facetIndexAndVertexIndices.length;
        const clusterArray = [];
        for (i0 = 0; i0 < n; i0 = i1) {
            i1 = i0 + 1;
            clusterArray.length = 0;
            clusterArray.push(facetIndexAndVertexIndices[i0][0]);
            while (i1 < n && 0 === this.compareFacetIndexAndVertexIndices(facetIndexAndVertexIndices[i0], facetIndexAndVertexIndices[i1])) {
                clusterArray.push(facetIndexAndVertexIndices[i1][0]);
                i1++;
            }
            announceCluster(clusterArray);
        }
    }
    /**
     * Collect facet duplicates.
     * @param polyface the polyface.
     * @param includeSingletons if true, non-duplicated facets are included in the output.
     * @returns an array of arrays describing facet duplication. Each array `entry` in the output contains read
     * indices of a cluster of facets with the same vertex indices.
     */
    static collectDuplicateFacetIndices(polyface, includeSingletons = false) {
        const result = [];
        this.announceDuplicateFacetIndices(polyface, (clusterFacetIndices) => {
            if (includeSingletons || clusterFacetIndices.length > 1)
                result.push(clusterFacetIndices.slice());
        });
        return result;
    }
    /**
     * Return a new facet set from the source facets, specifying how to copy duplicate facets.
     * @param source the polyface.
     * @param includeSingletons true to copy facets that only appear once
     * @param clusterSelector indicates whether to copy 0, 1, or all facets in each cluster of duplicate facets.
     */
    static cloneByFacetDuplication(source, includeSingletons, clusterSelector) {
        const builder = PolyfaceBuilder.create();
        const visitor = source.createVisitor(0);
        this.announceDuplicateFacetIndices(source, (clusterFacetIndices) => {
            let numToSelect = 0;
            if (clusterFacetIndices.length === 1) {
                if (includeSingletons)
                    numToSelect = 1;
            }
            else if (clusterFacetIndices.length > 1) {
                if (clusterSelector === DuplicateFacetClusterSelector.SelectAny)
                    numToSelect = 1;
                else if (clusterSelector === DuplicateFacetClusterSelector.SelectAll)
                    numToSelect = clusterFacetIndices.length;
                else if (clusterSelector === DuplicateFacetClusterSelector.SelectOneByParity)
                    numToSelect = (clusterFacetIndices.length & 0x01) === 0x01 ? 1 : 0;
            }
            for (let i = 0; i < numToSelect; i++) {
                visitor.moveToReadIndex(clusterFacetIndices[i]);
                builder.addFacetFromVisitor(visitor);
            }
        });
        return builder.claimPolyface();
    }
    /** Clone the facets, removing points that are simply within colinear edges. */
    static cloneWithColinearEdgeFixup(polyface) {
        const oldFacetVisitor = polyface.createVisitor(2); // this is to visit the existing facets
        const newFacetVisitor = polyface.createVisitor(0); // this is to build the new facets
        const builder = PolyfaceBuilder.create();
        const vector01 = Vector3d.create();
        const vector12 = Vector3d.create();
        const numPoint = polyface.data.point.length;
        const pointState = new Int32Array(numPoint);
        // FIRST PASS: in each sector of each facet, determine if the sector has colinear incoming and outgoing vectors.
        // Mark each point as
        //    0 unvisited
        //   -1 adjacent to a non-colinear sector
        //    n adjacent to n colinear sectors
        for (oldFacetVisitor.reset(); oldFacetVisitor.moveToNextFacet();) {
            for (let i = 0; i + 2 < oldFacetVisitor.point.length; i++) {
                // each base vertex is part of the result
                oldFacetVisitor.point.vectorIndexIndex(i, i + 1, vector01);
                oldFacetVisitor.point.vectorIndexIndex(i + 1, i + 2, vector12);
                const pointIndex = oldFacetVisitor.clientPointIndex(i + 1);
                if (pointState[pointIndex] >= 0) {
                    const theta = vector01.angleTo(vector12);
                    if (theta.isAlmostZero) {
                        pointState[pointIndex]++;
                    }
                    else {
                        pointState[pointIndex] = -1;
                    }
                }
            }
        }
        // SECOND PASS: make copies, omitting references to points at colinear sectors.
        for (oldFacetVisitor.reset(); oldFacetVisitor.moveToNextFacet();) {
            newFacetVisitor.clearArrays();
            for (let i = 0; i + 2 < oldFacetVisitor.point.length; i++) {
                const pointIndex = oldFacetVisitor.clientPointIndex(i);
                if (pointState[pointIndex] < 0) {
                    newFacetVisitor.pushDataFrom(oldFacetVisitor, i);
                }
            }
            if (newFacetVisitor.point.length > 2)
                builder.addFacetFromGrowableArrays(newFacetVisitor.point, newFacetVisitor.normal, newFacetVisitor.param, newFacetVisitor.color, newFacetVisitor.edgeVisible);
        }
        return builder.claimPolyface();
    }
    /**
     * Set the edge visibility for specified edges in the polyface.
     * @param polyface mesh to be edited.
     * @param clusters array of edge references.
     * @param value visibility value (true or false).
     */
    static setEdgeVisibility(polyface, clusters, value) {
        for (const cluster of clusters) {
            if (cluster instanceof SortableEdge) {
                this.setSingleEdgeVisibility(polyface, cluster.facetIndex, cluster.startVertex, value);
            }
            else if (Array.isArray(cluster)) {
                for (const e1 of cluster)
                    this.setSingleEdgeVisibility(polyface, e1.facetIndex, e1.startVertex, value);
            }
        }
    }
    /**
     * Set the visibility of a particular edge of a particular facet.
     * @param polyface containing polyface.
     * @param facetIndex facet index.
     * @param vertexIndex vertex index (in vertex array) at which the edge starts.
     * @param value visibility value.
     */
    static setSingleEdgeVisibility(polyface, facetIndex, vertexIndex, value) {
        const data = polyface.data;
        const index0 = polyface.facetIndex0(facetIndex);
        const index1 = polyface.facetIndex1(facetIndex);
        for (let i = index0; i < index1; i++)
            if (data.pointIndex[i] === vertexIndex)
                data.edgeVisible[i] = value; // actually sets visibility on all edges in the face that start at this vertex
    }
    /**
     * Get the visibility of a particular edge of a particular facet.
     * @param polyface containing polyface.
     * @param facetIndex facet index.
     * @param vertexIndex vertex index (in vertex array) at which the edge starts.
     */
    static getSingleEdgeVisibility(polyface, facetIndex, vertexIndex) {
        const data = polyface.data;
        const index0 = polyface.facetIndex0(facetIndex);
        const index1 = polyface.facetIndex1(facetIndex);
        for (let i = index0; i < index1; i++)
            if (data.pointIndex[i] === vertexIndex)
                return data.edgeVisible[i]; // return visibility of first edge in the face that starts at this vertex
        return undefined;
    }
    /**
     * Return manifold edge pairs whose dihedral angle is bounded by the given angle.
     * * The dihedral angle of a manifold edge is measured between the normals of its two adjacent faces.
     * * Boundary edges are not returned as they are not manifold.
     * @param mesh existing polyface or visitor.
     * @param maxSmoothEdgeAngle maximum dihedral angle of a smooth edge. If `undefined`, uses `Geometry.smallAngleRadians`.
     * @param sharpEdges true to reverse the angle threshold test and return sharp edges; otherwise return smooth edges (default).
     */
    static collectEdgesByDihedralAngle(mesh, maxSmoothEdgeAngle, sharpEdges = false) {
        if (mesh instanceof Polyface)
            return this.collectEdgesByDihedralAngle(mesh.createVisitor(1), maxSmoothEdgeAngle, sharpEdges);
        mesh.setNumWrap(1);
        const allEdges = this.createIndexedEdges(mesh);
        const manifoldEdges = [];
        allEdges.sortAndCollectClusters(manifoldEdges);
        if (undefined === maxSmoothEdgeAngle || maxSmoothEdgeAngle.radians < 0)
            maxSmoothEdgeAngle = Angle.createRadians(Geometry.smallAngleRadians);
        const outEdges = [];
        const normal0 = Vector3d.create();
        const normal1 = Vector3d.create();
        for (const pair of manifoldEdges) {
            if (Array.isArray(pair) && pair.length === 2) {
                const e0 = pair[0];
                const e1 = pair[1];
                if (undefined !== PolyfaceQuery.computeFacetUnitNormal(mesh, e0.facetIndex, normal0)
                    && undefined !== PolyfaceQuery.computeFacetUnitNormal(mesh, e1.facetIndex, normal1)) {
                    const edgeAngle = normal0.smallerUnorientedAngleTo(normal1);
                    if (sharpEdges) {
                        if (edgeAngle.radians > maxSmoothEdgeAngle.radians)
                            outEdges.push(pair);
                    }
                    else {
                        if (edgeAngle.radians <= maxSmoothEdgeAngle.radians)
                            outEdges.push(pair);
                    }
                }
            }
        }
        return outEdges;
    }
    /**
     * Make paired edges invisible.
    * * Find mated pairs among facet edges.
    * * Mated pairs have the same vertex indices appearing in opposite order.
    * * Mark all non-mated pairs visible.
    * * At mated pairs
    *    * if angle across the edge is larger than `sharpEdgeAngle`, mark visible
    *    * otherwise mark invisible.
    */
    static markPairedEdgesInvisible(mesh, sharpEdgeAngle) {
        const visitor = mesh.createVisitor(1);
        const edges = this.createIndexedEdges(visitor);
        const pairedEdges = [];
        const boundaryEdges = [];
        edges.sortAndCollectClusters(pairedEdges, boundaryEdges, boundaryEdges, boundaryEdges);
        this.markAllEdgeVisibility(mesh, false);
        this.setEdgeVisibility(mesh, boundaryEdges, true);
        if (sharpEdgeAngle !== undefined) {
            const normal0 = Vector3d.create();
            const normal1 = Vector3d.create();
            for (const pair of pairedEdges) {
                if (Array.isArray(pair) && pair.length === 2) {
                    const e0 = pair[0];
                    const e1 = pair[1];
                    if (undefined !== PolyfaceQuery.computeFacetUnitNormal(visitor, e0.facetIndex, normal0)
                        && undefined !== PolyfaceQuery.computeFacetUnitNormal(visitor, e1.facetIndex, normal1)) {
                        const edgeAngle = normal0.smallerUnorientedAngleTo(normal1);
                        if (edgeAngle.radians > sharpEdgeAngle.radians) {
                            this.setSingleEdgeVisibility(mesh, e0.facetIndex, e0.startVertex, true);
                            this.setSingleEdgeVisibility(mesh, e1.facetIndex, e1.startVertex, true);
                        }
                    }
                }
            }
        }
    }
    /**
     * Try to compute a unit normal for a facet accessible through a visitor.
     * * Unit normal is computed by `PolygonOps.unitNormal` with the points around the facet.
     */
    static computeFacetUnitNormal(visitor, facetIndex, result) {
        if (!result)
            result = Vector3d.create();
        if (visitor.moveToReadIndex(facetIndex)) {
            if (PolygonOps.unitNormal(visitor.point, result))
                return result;
        }
        return undefined;
    }
    /**
     * * Mark all edge visibilities in the IndexedPolyface.
     * @param mesh mesh to be marked.
     * @param value true for visible, false for hidden.
     */
    static markAllEdgeVisibility(mesh, value) {
        const data = mesh.data;
        for (let i = 0; i < data.edgeVisible.length; i++)
            data.edgeVisible[i] = value;
    }
    /**
     * Create a HalfEdgeGraph with a face for each facet of the IndexedPolyface.
     * @param mesh mesh to convert.
     * @internal
     */
    static convertToHalfEdgeGraph(mesh) {
        const builder = new HalfEdgeGraphFromIndexedLoopsContext();
        const visitor = mesh.createVisitor(0);
        const xyz = Point3d.create();
        for (visitor.reset(); visitor.moveToNextFacet();) {
            builder.insertLoop(visitor.pointIndex, (edge) => {
                mesh.data.getPoint(edge.i, xyz);
                edge.setXYZ(xyz);
                mesh.data.getPoint(edge.edgeMate.i, xyz);
                edge.edgeMate.setXYZ(xyz);
            });
        }
        return builder.graph;
    }
    /** Examine adjacent facet orientations throughout the mesh. If possible, reverse a subset to achieve proper pairing. */
    static reorientVertexOrderAroundFacetsForConsistentOrientation(mesh) {
        return FacetOrientationFixup.doFixup(mesh);
    }
    /** Set up indexed normals with one normal in the plane of each facet of the mesh. */
    static buildPerFaceNormals(polyface) {
        BuildAverageNormalsContext.buildPerFaceNormals(polyface);
    }
    /**
    * * At each vertex of the mesh:
    *   * Find clusters of almost parallel normals.
    *   * Compute simple average of those normals.
    *   * Index to the averages.
    * * For typical meshes, this correctly clusters adjacent normals.
    * * One can imagine a vertex with multiple "smooth cone-like" sets of adjacent facets such that averaging occurs
    * among two nonadjacent cones. But this does not seem to be a problem in practice.
    * @param polyface polyface to update.
    * @param toleranceAngle averaging is done between normals up to this angle.
    */
    static buildAverageNormals(polyface, toleranceAngle = Angle.createDegrees(31.0)) {
        BuildAverageNormalsContext.buildFastAverageNormals(polyface, toleranceAngle);
    }
    /**
     * Offset the faces of the mesh.
     * @param source original mesh.
     * @param signedOffsetDistance distance to offset
     * @param offsetOptions angle options. The default options are recommended.
     * @returns shifted mesh.
     */
    static cloneOffset(source, signedOffsetDistance, offsetOptions = OffsetMeshOptions.create()) {
        const strokeOptions = StrokeOptions.createForFacets();
        const offsetBuilder = PolyfaceBuilder.create(strokeOptions);
        OffsetMeshContext.buildOffsetMeshWithEdgeChamfers(source, offsetBuilder, signedOffsetDistance, offsetOptions);
        return offsetBuilder.claimPolyface();
    }
    static _workTriangle;
    static _workTriDetail;
    static _workPolyDetail;
    static _workFacetDetail3;
    static _workFacetDetailC;
    static _workFacetDetailNC;
    /**
     * Search facets for the first one that intersects the infinite line.
     * * To process _all_ intersections, callers can supply an `options.acceptIntersection` callback that always
     * returns `false`. In this case, `intersectRay3d` will return `undefined`, but the callback will be invoked for
     * each intersection.
     * * Example callback logic:
     *    * Accept the first found facet that intersects the half-line specified by the ray: `return detail.a >= 0.0;`
     *    * Collect all intersections: `myIntersections.push(detail.clone()); return false;` Then after `intersectRay3d`
     * returns, sort along `ray` with `myIntersections.sort((d0, d1) => d0.a - d1.a);`
     * @param visitor facet iterator.
     * @param ray infinite line parameterized as a ray. The returned `detail.a` is the intersection parameter on the
     * ray, e.g., zero at `ray.origin` and increasing in `ray.direction`.
     * @param options options for computing and populating an intersection detail, and an optional callback for
     * accepting one.
     * @return detail for the (accepted) intersection with `detail.IsInsideOrOn === true`, or `undefined` if no
     * (accepted) intersection.
     * @see PolygonOps.intersectRay3d
    */
    static intersectRay3d(visitor, ray, options) {
        if (visitor instanceof Polyface)
            return PolyfaceQuery.intersectRay3d(visitor.createVisitor(0), ray, options);
        let detail;
        const tol = options ? [options.distanceTolerance, options.parameterTolerance] : undefined;
        visitor.setNumWrap(0);
        while (visitor.moveToNextFacet()) {
            const numEdges = visitor.pointCount; // #vertices = #edges since numWrap is zero
            const vertices = visitor.point;
            if (3 === numEdges) {
                const tri = this._workTriangle = BarycentricTriangle.create(vertices.getPoint3dAtUncheckedPointIndex(0), vertices.getPoint3dAtUncheckedPointIndex(1), vertices.getPoint3dAtUncheckedPointIndex(2), this._workTriangle);
                const detail3 = this._workTriDetail = tri.intersectRay3d(ray, this._workTriDetail);
                tri.snapLocationToEdge(detail3, options?.distanceTolerance, options?.parameterTolerance);
                detail = this._workFacetDetail3 = TriangularFacetLocationDetail.create(visitor.currentReadIndex(), detail3, this._workFacetDetail3);
            }
            else {
                const detailN = this._workPolyDetail = PolygonOps.intersectRay3d(vertices, ray, tol, this._workPolyDetail);
                if (PolygonOps.isConvex(vertices))
                    detail = this._workFacetDetailC = ConvexFacetLocationDetail.create(visitor.currentReadIndex(), numEdges, detailN, this._workFacetDetailC);
                else
                    detail = this._workFacetDetailNC = NonConvexFacetLocationDetail.create(visitor.currentReadIndex(), numEdges, detailN, this._workFacetDetailNC);
            }
            if (detail.isInsideOrOn) { // set optional caches, process the intersection
                if (options?.needNormal && visitor.normal)
                    detail.getNormal(visitor.normal, vertices, options?.distanceTolerance);
                if (options?.needParam && visitor.param)
                    detail.getParam(visitor.param, vertices, options?.distanceTolerance);
                if (options?.needColor && visitor.color)
                    detail.getColor(visitor.color, vertices, options?.distanceTolerance);
                if (options?.needBarycentricCoordinates)
                    detail.getBarycentricCoordinates(vertices, options?.distanceTolerance);
                if (options?.acceptIntersection && !options.acceptIntersection(detail, visitor))
                    continue;
                return detail;
            }
        }
        return undefined; // no intersection
    }
}
//# sourceMappingURL=PolyfaceQuery.js.map