import _ from 'lodash';
import '../../../assert/js/assert.js';
import '../../../globals.js';
// Copyright 2023-2024, University of Colorado Boulder
/**
 * A ClippableFace based on a set of line segment edges, where (a) it is contained within a bounding box, and (b)
 * line segments going along the full border of the bounding box (from one corner to another) will be counted
 * separately. This helps with performance, since EdgedFace on its own would build up large counts of these edges
 * that "undo" each other during recursive clipping operations.
 *
 * Should still represent multiple closed loops, but it is not explicit.
 *
 * "implicit" edges/vertices are those defined by the clipped counts (minXCount, etc.)
 * "explicit" edges/vertices are those in the edges list
 *
 * @author Jonathan Olson <jonathan.olson@colorado.edu>
 */
import { alpenglow, BinaryClipping, BoundsClipping, CircularClipping, EdgedFace, GridClipping, LinearEdge, PolygonBilinear, PolygonMitchellNetravali, StripeClipping } from '../imports.js';
import Bounds2 from '../../../dot/js/Bounds2.js';
import Range from '../../../dot/js/Range.js';
import Vector2 from '../../../dot/js/Vector2.js';
import Utils from '../../../dot/js/Utils.js';
const scratchVectorA = new Vector2(0, 0);
const scratchVectorB = new Vector2(0, 0);
const scratchVectorC = new Vector2(0, 0);
const scratchVectorD = new Vector2(0, 0);
const emptyArray = [];
export default class EdgedClippedFace {
    constructor(
    // Should contain only "internal" edges, not those clipped edges that are corner-to-corner along the edge of the
    // bounding box.
    edges, 
    // Bounding box
    minX, minY, maxX, maxY, 
    // Count of edges from (minX,minY) to (minX,maxY) minus count of edges from (minX,maxY) to (minX,minY)
    // (minX, minY=>maxY positive)
    minXCount, 
    // Count of edges from (minX,minY) to (maxX,minY) minus count of edges from (maxX,minY) to (minX,minY)
    // (minX=>maxX positive, minY)
    minYCount, 
    // Count of edges from (maxX,minY) to (maxX,maxY) minus count of edges from (maxX,maxY) to (maxX,minY)
    // (maxX, minY=>maxY positive)
    maxXCount, 
    // Count of edges from (minX,maxY) to (maxX,maxY) minus count of edges from (maxX,maxY) to (minX,maxY)
    // (minX=>maxX positive, maxY)
    maxYCount) {
        this.edges = edges;
        this.minX = minX;
        this.minY = minY;
        this.maxX = maxX;
        this.maxY = maxY;
        this.minXCount = minXCount;
        this.minYCount = minYCount;
        this.maxXCount = maxXCount;
        this.maxYCount = maxYCount;
        assert && assert(isFinite(minX) && isFinite(maxX) && minX <= maxX);
        assert && assert(isFinite(minY) && isFinite(maxY) && minY <= maxY);
        assert && assert(isFinite(minXCount) && Number.isInteger(minXCount));
        assert && assert(isFinite(minYCount) && Number.isInteger(minYCount));
        assert && assert(isFinite(maxXCount) && Number.isInteger(maxXCount));
        assert && assert(isFinite(maxYCount) && Number.isInteger(maxYCount));
        assertSlow && assertSlow(edges.every(edge => {
            return edge.startPoint.x >= minX - 1e-8 && edge.startPoint.x <= maxX + 1e-8 &&
                edge.startPoint.y >= minY - 1e-8 && edge.startPoint.y <= maxY + 1e-8 &&
                edge.endPoint.x >= minX - 1e-8 && edge.endPoint.x <= maxX + 1e-8 &&
                edge.endPoint.y >= minY - 1e-8 && edge.endPoint.y <= maxY + 1e-8;
        }));
        this.clippedEdgedFace = new EdgedFace(edges, true);
        // Check on validating edges, since our binary clips won't work well if things aren't matched up (can get extra
        // edges).
        assertSlow && LinearEdge.validateStartEndMatches(this.getAllEdges());
    }
    // TODO: also have FAST conversions to here, where we DO NOT scan those edges?
    /**
     * Converts the face to a polygonal face. Epsilon is used to determine whether start/end points match.
     *
     * NOTE: This is likely a low-performance method, and should only be used for debugging.
     */
    toPolygonalFace(epsilon = 1e-8) {
        // We'll need to add in our counted edges
        return this.toEdgedFace().toPolygonalFace(epsilon);
    }
    /**
     * Converts the face to an edged face.
     */
    toEdgedFace() {
        return new EdgedFace(this.getAllEdges());
    }
    /**
     * Converts the face to a edged-clipped face (scanning the edges, to convert bounds-side edges to counts)
     */
    toEdgedClippedFace(minX, minY, maxX, maxY) {
        if (this.minX === minX && this.minY === minY && this.maxX === maxX && this.maxY === maxY) {
            return this;
        }
        else {
            return EdgedClippedFace.fromEdges(this.getAllEdges(), minX, minY, maxX, maxY);
        }
    }
    /**
     * Converts the face to a edged-clipped face (without scanning the edges to see if they are bounds-side)
     */
    toEdgedClippedFaceWithoutCheck(minX, minY, maxX, maxY) {
        if (this.minX === minX && this.minY === minY && this.maxX === maxX && this.maxY === maxY) {
            return this;
        }
        else {
            return EdgedClippedFace.fromEdgesWithoutCheck(this.getAllEdges(), minX, minY, maxX, maxY);
        }
    }
    static fromEdges(edges, minX, minY, maxX, maxY) {
        // TODO: both of these clear some things, get rid of inefficiency?
        scratchAccumulator.reset();
        scratchAccumulator.setAccumulationBounds(minX, minY, maxX, maxY);
        for (let i = 0; i < edges.length; i++) {
            const edge = edges[i];
            scratchAccumulator.addEdge(edge.startPoint.x, edge.startPoint.y, edge.endPoint.x, edge.endPoint.y, edge.startPoint, edge.endPoint);
        }
        return scratchAccumulator.finalizeEnsureFace(minX, minY, maxX, maxY);
    }
    static fromEdgesWithoutCheck(edges, minX, minY, maxX, maxY) {
        return new EdgedClippedFace(edges, minX, minY, maxX, maxY, 0, 0, 0, 0);
    }
    static implicitEdge(startPoint, endPoint, count) {
        assert && assert(count !== 0);
        return new LinearEdge(count > 0 ? startPoint : endPoint, count > 0 ? endPoint : startPoint);
    }
    forEachImplicitEdge(callback) {
        const minXMinY = (this.minXCount || this.minYCount) ? this.getMinXMinY() : null;
        const minXMaxY = (this.minXCount || this.maxYCount) ? this.getMinXMaxY() : null;
        const maxXMinY = (this.maxXCount || this.minYCount) ? this.getMaxXMinY() : null;
        const maxXMaxY = (this.maxXCount || this.maxYCount) ? this.getMaxXMaxY() : null;
        for (let i = 0; i !== this.minXCount; i += Math.sign(this.minXCount)) {
            assert && assert(minXMinY && minXMaxY);
            this.minXCount > 0 ? callback(minXMinY, minXMaxY) : callback(minXMaxY, minXMinY);
        }
        for (let i = 0; i !== this.minYCount; i += Math.sign(this.minYCount)) {
            assert && assert(minXMinY && maxXMinY);
            this.minYCount > 0 ? callback(minXMinY, maxXMinY) : callback(maxXMinY, minXMinY);
        }
        for (let i = 0; i !== this.maxXCount; i += Math.sign(this.maxXCount)) {
            assert && assert(maxXMinY && maxXMaxY);
            this.maxXCount > 0 ? callback(maxXMinY, maxXMaxY) : callback(maxXMaxY, maxXMinY);
        }
        for (let i = 0; i !== this.maxYCount; i += Math.sign(this.maxYCount)) {
            assert && assert(minXMaxY && maxXMaxY);
            this.maxYCount > 0 ? callback(minXMaxY, maxXMaxY) : callback(maxXMaxY, minXMaxY);
        }
    }
    getImplicitEdges() {
        const edges = [];
        this.forEachImplicitEdge((startPoint, endPoint) => edges.push(new LinearEdge(startPoint, endPoint)));
        return edges;
    }
    getAllEdges() {
        return [
            ...this.edges,
            ...this.getImplicitEdges()
        ];
    }
    /**
     * Returns a Shape for the face.
     *
     * NOTE: This is likely a low-performance method, and should only be used for debugging.
     */
    getShape(epsilon = 1e-8) {
        return this.toPolygonalFace(epsilon).getShape();
    }
    getMinXMinY() {
        return new Vector2(this.minX, this.minY);
    }
    getMinXMaxY() {
        return new Vector2(this.minX, this.maxY);
    }
    getMaxXMinY() {
        return new Vector2(this.maxX, this.minY);
    }
    getMaxXMaxY() {
        return new Vector2(this.maxX, this.maxY);
    }
    /**
     * Returns whether this face has an implicit vertex at the minX-minY corner.
     */
    hasMinXMinY() {
        return this.minXCount !== 0 || this.minYCount !== 0;
    }
    /**
     * Returns whether this face has an implicit vertex at the minX-maxY corner.
     */
    hasMinXMaxY() {
        return this.minXCount !== 0 || this.maxYCount !== 0;
    }
    /**
     * Returns whether this face has an implicit vertex at the maxX-minY corner.
     */
    hasMaxXMinY() {
        return this.maxXCount !== 0 || this.minYCount !== 0;
    }
    /**
     * Returns whether this face has an implicit vertex at the maxX-maxY corner.
     */
    hasMaxXMaxY() {
        return this.maxXCount !== 0 || this.maxYCount !== 0;
    }
    /**
     * Returns whether this face has an implicit vertex with minX.
     */
    hasMinX() {
        return this.minXCount !== 0 || this.minYCount !== 0 || this.maxYCount !== 0;
    }
    /**
     * Returns whether this face has an implicit vertex with minY.
     */
    hasMinY() {
        return this.minYCount !== 0 || this.minXCount !== 0 || this.maxXCount !== 0;
    }
    /**
     * Returns whether this face has an implicit vertex with maxX.
     */
    hasMaxX() {
        return this.maxXCount !== 0 || this.minYCount !== 0 || this.maxYCount !== 0;
    }
    /**
     * Returns whether this face has an implicit vertex with maxY.
     */
    hasMaxY() {
        return this.maxYCount !== 0 || this.minXCount !== 0 || this.maxXCount !== 0;
    }
    /**
     * Returns the bounds of the face (ignoring any "fake" edges, if the type supports them)
     */
    getBounds() {
        const result = this.clippedEdgedFace.getBounds();
        this.hasMinX() && result.addX(this.minX);
        this.hasMinY() && result.addY(this.minY);
        this.hasMaxX() && result.addX(this.maxX);
        this.hasMaxY() && result.addY(this.maxY);
        return result;
    }
    /**
     * Returns the range of values for the dot product of the given normal with any point contained within the face
     * (for polygons, this is the same as the range of values for the dot product of the normal with any vertex).
     */
    getDotRange(normal) {
        const range = this.clippedEdgedFace.getDotRange(normal);
        this.hasMinXMinY() && range.addValue(normal.x * this.minX + normal.y * this.minY);
        this.hasMinXMaxY() && range.addValue(normal.x * this.minX + normal.y * this.maxY);
        this.hasMaxXMinY() && range.addValue(normal.x * this.maxX + normal.y * this.minY);
        this.hasMaxXMaxY() && range.addValue(normal.x * this.maxX + normal.y * this.maxY);
        return range;
    }
    /**
     * Returns the range of distances from the given point to every point along the edges of the face.
     * For instance, if the face was the unit cube, the range would be 1/2 to sqrt(2), for distances to the middles of
     * the edges and the corners respectively.
     */
    getDistanceRangeToEdges(point) {
        const range = this.clippedEdgedFace.getDistanceRangeToEdges(point);
        this.minXCount && LinearEdge.addDistanceRange(this.getMinXMinY(), this.getMinXMaxY(), point, range);
        this.minYCount && LinearEdge.addDistanceRange(this.getMinXMinY(), this.getMaxXMinY(), point, range);
        this.maxXCount && LinearEdge.addDistanceRange(this.getMaxXMinY(), this.getMaxXMaxY(), point, range);
        this.maxYCount && LinearEdge.addDistanceRange(this.getMinXMaxY(), this.getMaxXMaxY(), point, range);
        return range;
    }
    /**
     * Returns the range of distances from the given point to every point inside the face. The upper bound should be
     * the same as getDistanceRangeToEdges, however the lower bound may be 0 if the point is inside the face.
     */
    getDistanceRangeToInside(point) {
        const range = this.getDistanceRangeToEdges(point);
        if (this.containsPoint(point)) {
            return new Range(0, range.max);
        }
        else {
            return range;
        }
    }
    /**
     * Returns the signed area of the face (positive if the vertices are in counter-clockwise order, negative if clockwise)
     */
    getArea() {
        let area = this.clippedEdgedFace.getArea();
        // NOTE: This ASSUMES that we're using the specific shoelace formulation of ( p1.x + p0.x ) * ( p1.y - p0.y ) in
        // the super call.
        // Our minYCount/maxYCount won't contribute (since they have the same Y values, their shoelace contribution will be
        // zero.
        // ALSO: there is a doubling and non-doubling that cancel out here (1/2 from shoelace, 2* due to x+x).
        area += (this.maxY - this.minY) * (this.minXCount * this.minX + this.maxXCount * this.maxX);
        return area;
    }
    /**
     * Returns the partial for the centroid computation. These should be summed up, divided by 6, and divided by the area
     * to give the full centroid
     */
    getCentroidPartial() {
        const centroidPartial = this.clippedEdgedFace.getCentroidPartial();
        // NOTE: This ASSUMES we're using the compound formulation, based on
        // xc = ( p0.x - p1.x ) * ( p0.x * ( 2 * p0.y + p1.y ) + p1.x * ( p0.y + 2 * p1.y ) )
        // yc = ( p1.y - p0.y ) * ( p0.x * ( 2 * p0.y + p1.y ) + p1.x * ( p0.y + 2 * p1.y ) )
        if (this.minYCount || this.maxYCount) {
            centroidPartial.x += 3 * (this.minX - this.maxX) * (this.minX + this.maxX) * (this.minYCount * this.minY + this.maxYCount * this.maxY);
        }
        if (this.minXCount || this.maxXCount) {
            centroidPartial.y += 3 * (this.maxY - this.minY) * (this.minY + this.maxY) * (this.minXCount * this.minX + this.maxXCount * this.maxX);
        }
        return centroidPartial;
    }
    /**
     * Returns the centroid of the face (area is required for the typical integral required to evaluate)
     */
    getCentroid(area) {
        return this.getCentroidPartial().timesScalar(1 / (6 * area));
    }
    /**
     * Returns the evaluation of an integral that will be zero if the boundaries of the face are correctly closed.
     * It is designed so that if there is a "gap" and we have open boundaries, the result will likely be non-zero.
     *
     * NOTE: This is only used for debugging, so performance is not a concern.
     */
    getZero() {
        return _.sum(this.getAllEdges().map(e => e.getLineIntegralZero()));
    }
    /**
     * Returns the average distance from the given point to every point inside the face. The integral evaluation requires
     * the area (similarly to the centroid computation).
     */
    getAverageDistance(point, area) {
        let average = this.clippedEdgedFace.getAverageDistance(point, area);
        const minX = this.minX - point.x;
        const minY = this.minY - point.y;
        const maxX = this.maxX - point.x;
        const maxY = this.maxY - point.y;
        if (this.minXCount) {
            average += this.minXCount * LinearEdge.evaluateLineIntegralDistance(minX, minY, minX, maxY) / area;
        }
        if (this.minYCount) {
            average += this.minYCount * LinearEdge.evaluateLineIntegralDistance(minX, minY, maxX, minY) / area;
        }
        if (this.maxXCount) {
            average += this.maxXCount * LinearEdge.evaluateLineIntegralDistance(maxX, minY, maxX, maxY) / area;
        }
        if (this.maxYCount) {
            average += this.maxYCount * LinearEdge.evaluateLineIntegralDistance(minX, maxY, maxX, maxY) / area;
        }
        return average;
    }
    /**
     * Returns the average distance from the origin to every point inside the face transformed by the given matrix.
     */
    getAverageDistanceTransformedToOrigin(transform, area) {
        let average = this.clippedEdgedFace.getAverageDistanceTransformedToOrigin(transform, area);
        if (this.minXCount || this.minYCount || this.maxXCount || this.maxYCount) {
            const divisor = area * transform.getSignedScale();
            const minXMinY = transform.multiplyVector2(scratchVectorA.setXY(this.minX, this.minY));
            const minXMaxY = transform.multiplyVector2(scratchVectorB.setXY(this.minX, this.maxY));
            const maxXMinY = transform.multiplyVector2(scratchVectorC.setXY(this.maxX, this.minY));
            const maxXMaxY = transform.multiplyVector2(scratchVectorD.setXY(this.maxX, this.maxY));
            if (this.minXCount) {
                average += this.minXCount * LinearEdge.evaluateLineIntegralDistance(minXMinY.x, minXMinY.y, minXMaxY.x, minXMaxY.y) / divisor;
            }
            if (this.minYCount) {
                average += this.minYCount * LinearEdge.evaluateLineIntegralDistance(minXMinY.x, minXMinY.y, maxXMinY.x, maxXMinY.y) / divisor;
            }
            if (this.maxXCount) {
                average += this.maxXCount * LinearEdge.evaluateLineIntegralDistance(maxXMinY.x, maxXMinY.y, maxXMaxY.x, maxXMaxY.y) / divisor;
            }
            if (this.maxYCount) {
                average += this.maxYCount * LinearEdge.evaluateLineIntegralDistance(minXMaxY.x, minXMaxY.y, maxXMaxY.x, maxXMaxY.y) / divisor;
            }
        }
        return average;
    }
    /**
     * Returns a copy of the face that is clipped to be within the given axis-aligned bounding box.
     */
    getClipped(minX, minY, maxX, maxY) {
        // TODO: consider whether containment checks are worth it. Most cases, no.
        const edges = [];
        const centerX = (minX + maxX) / 2;
        const centerY = (minY + maxY) / 2;
        for (let i = 0; i < this.edges.length; i++) {
            const edge = this.edges[i];
            BoundsClipping.boundsClipEdge(edge.startPoint, edge.endPoint, minX, minY, maxX, maxY, centerX, centerY, edges);
        }
        this.forEachImplicitEdge((startPoint, endPoint) => {
            BoundsClipping.boundsClipEdge(startPoint, endPoint, minX, minY, maxX, maxY, centerX, centerY, edges);
        });
        // TODO: a more optimized form here! The clipping could output counts instead of us having to check here
        return EdgedClippedFace.fromEdges(edges, minX, minY, maxX, maxY);
    }
    /**
     * Returns two copies of the face, one that is clipped to be to the left of the given x value, and one that is
     * clipped to be to the right of the given x value.
     *
     * The fakeCornerY is used to determine the "fake" corner that is used for unsorted-edge clipping.
     */
    getBinaryXClip(x, fakeCornerY) {
        return BinaryClipping.binaryXClipEdgedClipped(this, x);
    }
    /**
     * Returns two copies of the face, one that is clipped to y values less than the given y value, and one that is
     * clipped to values greater than the given y value.
     *
     * The fakeCornerX is used to determine the "fake" corner that is used for unsorted-edge clipping.
     */
    getBinaryYClip(y, fakeCornerX) {
        return BinaryClipping.binaryYClipEdgedClipped(this, y);
    }
    /**
     * Returns two copies of the face, one that is clipped to contain points where dot( normal, point ) < value,
     * and one that is clipped to contain points where dot( normal, point ) > value.
     *
     * The fake corner perpendicular is used to determine the "fake" corner that is used for unsorted-edge clipping
     */
    getBinaryLineClip(normal, value, fakeCornerPerpendicular) {
        const minEdges = [];
        const maxEdges = [];
        for (let i = 0; i < this.edges.length; i++) {
            const edge = this.edges[i];
            BinaryClipping.binaryLineClipEdge(edge.startPoint, edge.endPoint, normal, value, fakeCornerPerpendicular, minEdges, maxEdges);
        }
        this.forEachImplicitEdge((startPoint, endPoint) => {
            BinaryClipping.binaryLineClipEdge(startPoint, endPoint, normal, value, fakeCornerPerpendicular, minEdges, maxEdges);
        });
        assert && assert(minEdges.every(e => normal.dot(e.startPoint) <= value + 1e-8 && normal.dot(e.endPoint) <= value + 1e-8));
        assert && assert(maxEdges.every(e => normal.dot(e.startPoint) >= value - 1e-8 && normal.dot(e.endPoint) >= value - 1e-8));
        // TODO: a more optimized form here! The clipping could output counts instead of us having to check here
        // NOTE: We can't really refine the bounds here.
        return {
            minFace: EdgedClippedFace.fromEdges(minEdges, this.minX, this.minY, this.maxX, this.maxY),
            maxFace: EdgedClippedFace.fromEdges(maxEdges, this.minX, this.minY, this.maxX, this.maxY)
        };
    }
    /**
     * Returns an array of faces, clipped similarly to getBinaryLineClip, but with more than one (parallel) split line at
     * a time. The first face will be the one with dot( normal, point ) < values[0], the second one with
     * values[ 0 ] < dot( normal, point ) < values[1], etc.
     */
    getStripeLineClip(normal, values, fakeCornerPerpendicular) {
        if (values.length === 0) {
            return [this];
        }
        const edgesCollection = _.range(values.length + 1).map(() => []);
        for (let i = 0; i < this.edges.length; i++) {
            const edge = this.edges[i];
            StripeClipping.binaryStripeClipEdge(edge.startPoint, edge.endPoint, normal, values, fakeCornerPerpendicular, edgesCollection);
        }
        this.forEachImplicitEdge((startPoint, endPoint) => {
            StripeClipping.binaryStripeClipEdge(startPoint, endPoint, normal, values, fakeCornerPerpendicular, edgesCollection);
        });
        if (assert) {
            for (let i = 0; i < edgesCollection.length; i++) {
                const edges = edgesCollection[i];
                const minValue = i > 0 ? values[i - 1] : Number.NEGATIVE_INFINITY;
                const maxValue = i < values.length ? values[i] : Number.POSITIVE_INFINITY;
                assert(edges.every(e => {
                    return normal.dot(e.startPoint) + 1e-8 >= minValue && normal.dot(e.startPoint) - 1e-8 <= maxValue &&
                        normal.dot(e.endPoint) + 1e-8 >= minValue && normal.dot(e.endPoint) - 1e-8 <= maxValue;
                }));
            }
        }
        return edgesCollection.map(edges => EdgedClippedFace.fromEdges(edges, this.minX, this.minY, this.maxX, this.maxY));
    }
    /**
     * Returns two copies of the face, one that is clipped to contain points inside the circle defined by the given
     * center and radius, and one that is clipped to contain points outside the circle.
     *
     * NOTE: maxAngleSplit is used to determine the polygonal approximation of the circle. The returned result will not
     * have a chord with an angle greater than maxAngleSplit.
     */
    getBinaryCircularClip(center, radius, maxAngleSplit) {
        const insideEdges = [];
        const outsideEdges = [];
        CircularClipping.binaryCircularClipEdges(this.getAllEdges(), center, radius, maxAngleSplit, insideEdges, outsideEdges);
        return {
            insideFace: EdgedClippedFace.fromEdges(insideEdges, this.minX, this.minY, this.maxX, this.maxY),
            outsideFace: EdgedClippedFace.fromEdges(outsideEdges, this.minX, this.minY, this.maxX, this.maxY)
        };
    }
    /**
     * Given an integral bounding box and step sizes (which define the grid), this will clip the face to each cell in the
     * grid, calling the callback for each cell's contributing edges (in order, if we are a PolygonalFace).
     * polygonCompleteCallback will be called whenever a polygon is completed (if we are a polygonal type of face).
     */
    gridClipIterate(minX, minY, maxX, maxY, stepX, stepY, stepWidth, stepHeight, callback, polygonCompleteCallback) {
        for (let i = 0; i < this.edges.length; i++) {
            const edge = this.edges[i];
            GridClipping.gridClipIterate(edge.startPoint, edge.endPoint, minX, minY, maxX, maxY, stepX, stepY, stepWidth, stepHeight, callback);
        }
        this.forEachImplicitEdge((startPoint, endPoint) => {
            GridClipping.gridClipIterate(startPoint, endPoint, minX, minY, maxX, maxY, stepX, stepY, stepWidth, stepHeight, callback);
        });
        if (this.edges.length) {
            polygonCompleteCallback();
        }
    }
    /**
     * Returns the evaluation of the bilinear (tent) filter integrals for the given point, ASSUMING that the face
     * is clipped to the transformed unit square of x: [minX,minX+1], y: [minY,minY+1].
     */
    getBilinearFiltered(pointX, pointY, minX, minY) {
        // TODO: optimization
        // TODO: we REALLY should have a ClippedFace primitive?
        return PolygonBilinear.evaluateLinearEdges(this.getAllEdges(), pointX, pointY, minX, minY);
    }
    /**
     * Returns the evaluation of the Mitchell-Netravali (1/3,1/3) filter integrals for the given point, ASSUMING that the
     * face is clipped to the transformed unit square of x: [minX,minX+1], y: [minY,minY+1].
     */
    getMitchellNetravaliFiltered(pointX, pointY, minX, minY) {
        // TODO: optimization
        return PolygonMitchellNetravali.evaluateLinearEdges(this.getAllEdges(), pointX, pointY, minX, minY);
    }
    /**
     * Returns whether the face contains the given point.
     */
    containsPoint(point) {
        let windingNumber = LinearEdge.getWindingNumberEdges(this.edges, point);
        this.forEachImplicitEdge((startPoint, endPoint) => {
            windingNumber += LinearEdge.windingContribution(startPoint.x, startPoint.y, endPoint.x, endPoint.y, point.x, point.y);
        });
        return windingNumber !== 0;
    }
    /**
     * Returns an affine-transformed version of the face.
     */
    getTransformed(transform) {
        if (transform.isIdentity()) {
            return this;
        }
        else {
            const transformedEdges = [];
            const allEdges = this.getAllEdges();
            for (let i = 0; i < allEdges.length; i++) {
                const edge = allEdges[i];
                const start = transform.timesVector2(edge.startPoint);
                const end = transform.timesVector2(edge.endPoint);
                if (!start.equals(end)) {
                    transformedEdges.push(new LinearEdge(start, end));
                }
            }
            const bounds = new Bounds2(this.minX, this.minY, this.maxX, this.maxY).transform(transform);
            return EdgedClippedFace.fromEdgesWithoutCheck(transformedEdges, bounds.minX, bounds.minY, bounds.maxX, bounds.maxY);
        }
    }
    /**
     * Returns a rounded version of the face, where [-epsilon/2, epsilon/2] rounds to 0, etc.
     */
    getRounded(epsilon) {
        const edges = [];
        for (let i = 0; i < this.edges.length; i++) {
            const edge = this.edges[i];
            const startPoint = new Vector2(Utils.roundSymmetric(edge.startPoint.x / epsilon) * epsilon, Utils.roundSymmetric(edge.startPoint.y / epsilon) * epsilon);
            const endPoint = new Vector2(Utils.roundSymmetric(edge.endPoint.x / epsilon) * epsilon, Utils.roundSymmetric(edge.endPoint.y / epsilon) * epsilon);
            if (!startPoint.equals(endPoint)) {
                edges.push(new LinearEdge(startPoint, endPoint, edge.containsFakeCorner));
            }
        }
        // TODO: more code sharing?
        this.forEachImplicitEdge((startPoint, endPoint) => {
            const roundedStartPoint = new Vector2(Utils.roundSymmetric(startPoint.x / epsilon) * epsilon, Utils.roundSymmetric(startPoint.y / epsilon) * epsilon);
            const roundedEndPoint = new Vector2(Utils.roundSymmetric(endPoint.x / epsilon) * epsilon, Utils.roundSymmetric(endPoint.y / epsilon) * epsilon);
            if (!roundedStartPoint.equals(roundedEndPoint)) {
                edges.push(new LinearEdge(roundedStartPoint, roundedEndPoint));
            }
        });
        return EdgedClippedFace.fromEdgesWithoutCheck(edges, Utils.roundSymmetric(this.minX / epsilon) * epsilon, Utils.roundSymmetric(this.minY / epsilon) * epsilon, Utils.roundSymmetric(this.maxX / epsilon) * epsilon, Utils.roundSymmetric(this.maxY / epsilon) * epsilon);
    }
    /**
     * Returns a version of the face with the orientation of all of the edges swapped.
     */
    withReversedEdges() {
        return new EdgedClippedFace(this.edges.map(edge => new LinearEdge(edge.endPoint, edge.startPoint)).reverse(), this.minX, this.minY, this.maxX, this.maxY, -this.maxXCount, -this.maxYCount, -this.minXCount, -this.minYCount);
    }
    /**
     * Calls the callback with points for each edge in the face.
     */
    forEachEdge(callback) {
        for (let i = 0; i < this.edges.length; i++) {
            const edge = this.edges[i];
            callback(edge.startPoint, edge.endPoint);
        }
        this.forEachImplicitEdge(callback);
    }
    /**
     * Returns a singleton accumulator for this type of face. This will always return the same instance, and should ONLY
     * be used if there will be no reentrant or co-occurring usage of this accumulator (i.e. only use it when you can
     * guarantee nothing else will be clipped at the same time). If two tasks try to use this at the same time, it will
     * likely break.
     *
     * This is a method that can be called on an unknown-type face, to reproduce the same type of face. This is
     * important, since we can't feed unsorted edge data directly to a PolygonalFace's accumulator, and in general this
     * is the safest way to get an accumulator for a face.
     */
    getScratchAccumulator() {
        return scratchAccumulator;
    }
    /**
     * Returns a singleton accumulator for this type of face. This will always return the same instance, and should ONLY
     * be used if there will be no reentrant or co-occurring usage of this accumulator (i.e. only use it when you can
     * guarantee nothing else will be clipped at the same time). If two tasks try to use this at the same time, it will
     * likely break.
     *
     * This should be used directly when you know you want an EdgedClippedFace as output.
     */
    static getScratchAccumulator() {
        return scratchAccumulator;
    }
    /**
     * Returns a new accumulator for this type of face. This should be used when concurrent clipping will need to happen.
     *
     * This is a method that can be called on an unknown-type face, to reproduce the same type of face. This is
     * important, since we can't feed unsorted edge data directly to a PolygonalFace's accumulator, and in general this
     * is the safest way to get an accumulator for a face.
     */
    getAccumulator() {
        return new EdgedClippedFaceAccumulator();
    }
    /**
     * Returns a debugging string.
     */
    toString() {
        return this.getAllEdges().map(e => `${e.startPoint.x},${e.startPoint.y} => ${e.endPoint.x},${e.endPoint.y}`).join('\n');
    }
    /**
     * Returns a serialized version of the face, that should be able to be deserialized into the same type of face.
     * See {FaceType}.deserialize.
     *
     * NOTE: If you don't know what type of face this is, use serializeClippableFace instead.
     */
    serialize() {
        return {
            edges: this.edges.map(edge => edge.serialize()),
            minX: this.minX,
            minY: this.minY,
            maxX: this.maxX,
            maxY: this.maxY,
            minXCount: this.minXCount,
            minYCount: this.minYCount,
            maxXCount: this.maxXCount,
            maxYCount: this.maxYCount
        };
    }
    static deserialize(serialized) {
        return new EdgedClippedFace(serialized.edges.map(edge => LinearEdge.deserialize(edge)), serialized.minX, serialized.minY, serialized.maxX, serialized.maxY, serialized.minXCount, serialized.minYCount, serialized.maxXCount, serialized.maxYCount);
    }
    static fromBounds(bounds) {
        return new EdgedClippedFace([], bounds.minX, bounds.minY, bounds.maxX, bounds.maxY, -1, 1, 1, -1);
    }
    static fromBoundsValues(minX, minY, maxX, maxY) {
        return new EdgedClippedFace([], minX, minY, maxX, maxY, -1, 1, 1, -1);
    }
}
alpenglow.register('EdgedClippedFace', EdgedClippedFace);
export class EdgedClippedFaceAccumulator {
    constructor() {
        this.edges = [];
        this.minX = 0;
        this.minY = 0;
        this.maxX = 0;
        this.maxY = 0;
        this.minXCount = 0;
        this.minYCount = 0;
        this.maxXCount = 0;
        this.maxYCount = 0;
        this.usesEndPoint = true;
    }
    addEdge(startX, startY, endX, endY, startPoint, endPoint) {
        if (assertSlow) {
            const start = startPoint || new Vector2(startX, startY);
            const end = endPoint || new Vector2(endX, endY);
            assertSlow(start.x >= this.minX - 1e-8 && start.x <= this.maxX + 1e-8 &&
                start.y >= this.minY - 1e-8 && start.y <= this.maxY + 1e-8 &&
                end.x >= this.minX - 1e-8 && end.x <= this.maxX + 1e-8 &&
                end.y >= this.minY - 1e-8 && end.y <= this.maxY + 1e-8);
        }
        if (
        // If all points are on a corner
        (startX === this.minX || startX === this.maxX) &&
            (startY === this.minY || startY === this.maxY) &&
            (endX === this.minX || endX === this.maxX) &&
            (endY === this.minY || endY === this.maxY) &&
            // And we're not on opposite corners
            (startX === endX || startY === endY)) {
            assert && assert(startX !== endX || startY !== endY, 'Points should not be identical');
            if (startX === endX) {
                const delta = (startY === this.minY ? 1 : -1);
                if (startX === this.minX) {
                    this.minXCount += delta;
                }
                else {
                    this.maxXCount += delta;
                }
            }
            else {
                const delta = (startX === this.minX ? 1 : -1);
                if (startY === this.minY) {
                    this.minYCount += delta;
                }
                else {
                    this.maxYCount += delta;
                }
            }
        }
        else {
            this.edges.push(new LinearEdge(startPoint || new Vector2(startX, startY), endPoint || new Vector2(endX, endY)));
        }
    }
    markNewPolygon() {
        // no-op, since we're storing unsorted edges!
    }
    setAccumulationBounds(minX, minY, maxX, maxY) {
        this.minX = minX;
        this.minY = minY;
        this.maxX = maxX;
        this.maxY = maxY;
        this.minXCount = 0;
        this.minYCount = 0;
        this.maxXCount = 0;
        this.maxYCount = 0;
    }
    // Will reset it to the initial state also
    finalizeFace() {
        if (this.edges.length === 0 && this.minXCount === 0 && this.minYCount === 0 && this.maxXCount === 0 && this.maxYCount === 0) {
            return null;
        }
        const result = new EdgedClippedFace(this.edges, this.minX, this.minY, this.maxX, this.maxY, this.minXCount, this.minYCount, this.maxXCount, this.maxYCount);
        this.edges = [];
        this.minXCount = 0;
        this.minYCount = 0;
        this.maxXCount = 0;
        this.maxYCount = 0;
        return result;
    }
    finalizeEnsureFace(minX, minY, maxX, maxY) {
        return this.finalizeFace() || new EdgedClippedFace(emptyArray, minX, minY, maxX, maxY, 0, 0, 0, 0);
    }
    // Will reset without creating a face
    reset() {
        this.edges.length = 0;
        this.minXCount = 0;
        this.minYCount = 0;
        this.maxXCount = 0;
        this.maxYCount = 0;
    }
}
const scratchAccumulator = new EdgedClippedFaceAccumulator();
//# sourceMappingURL=EdgedClippedFace.js.map