package com.sundablog.clipper;

import com.sundablog.clipper.Clipper.ClipType;
import com.sundablog.clipper.Clipper.Direction;
import com.sundablog.clipper.Clipper.PolyFillType;
import com.sundablog.clipper.Clipper.PolyType;
import com.sundablog.clipper.Point.LongPoint;

import java.util.logging.Logger;

/**
 * Edge类表示多边形裁剪算法中的边对象
 * 包含边的几何信息、多边形类型、连接关系等属性
 * 以及各种用于处理边的方法,如排序、交点计算、边状态维护等
 */
class Edge {
    /**
     * 边在多边形中的位置方向枚举
     */
    enum Side {
        LEFT,  // 左边
        RIGHT  // 右边
    }

    /**
     * 判断边e2是否应该插入到e1之前
     * 用于活动边列表(AEL)的排序
     * @param e1 第一条边
     * @param e2 第二条边
     * @return 如果e2应该在e1前面则返回true
     */
    static boolean doesE2InsertBeforeE1(Edge e1, Edge e2 ) {
        if (e2.current.getX() == e1.current.getX()) {
            if (e2.top.getY() > e1.top.getY()) {
                return e2.top.getX() < topX( e1, e2.top.getY() );
            }
            else {
                return e1.top.getX() > topX( e2, e1.top.getY() );
            }
        }
        else {
            return e2.current.getX() < e1.current.getX();
        }
    }

    /**
     * 判断两条边是否具有相同的斜率
     * 用于检测平行线
     * @param e1 第一条边
     * @param e2 第二条边
     * @return 如果斜率相同则返回true
     */
    static boolean slopesEqual(Edge e1, Edge e2 ) {
        // 使用交叉相乘比较斜率,避免浮点精度问题
        return e1.getDelta().getY() * e2.getDelta().getX() == e1.getDelta().getX() * e2.getDelta().getY();

    }

    /**
     * 交换两条边的输出索引
     * @param edge1 第一条边
     * @param edge2 第二条边
     */
    static void swapPolyIndexes(Edge edge1, Edge edge2 ) {
        final int outIdx = edge1.outIdx;
        edge1.outIdx = edge2.outIdx;
        edge2.outIdx = outIdx;
    }

    /**
     * 交换两条边的方向
     * @param edge1 第一条边
     * @param edge2 第二条边
     */
    static void swapSides(Edge edge1, Edge edge2 ) {
        final Side side = edge1.side;
        edge1.side = edge2.side;
        edge2.side = side;
    }

    /**
     * 计算在指定Y坐标处的X坐标值
     * 用于扫描线算法中边的X坐标插值
     * @param edge 边对象
     * @param currentY 当前Y坐标
     * @return 对应的X坐标值
     */
    static long topX(Edge edge, long currentY ) {
        if (currentY == edge.getTop().getY()) {
            return edge.getTop().getX();
        }
        // 使用线性插值计算X坐标
        return edge.getBot().getX() + Math.round( edge.deltaX * (currentY - edge.getBot().getY()) );
    }

    // 边的底部端点
    private final LongPoint bot;

    // 当前点(在每个新的扫描线处更新)
    private final LongPoint current;

    // 边的顶部端点
    private final LongPoint top;

    // 边的方向向量(top - bot)
    private final LongPoint delta;

    // 斜率的倒数(dx/dy),用于扫描线算法中的X坐标计算
    double deltaX;

    // 多边形类型(SUBJECT或CLIP)
    PolyType polyTyp;

    // 边在解多边形中的方向(LEFT或RIGHT)
    Side side;

    // 环绕方向增量(1或-1,取决于环绕方向)
    int windDelta;

    // 环绕计数
    int windCnt;

    // 相反多边形类型的环绕计数
    int windCnt2;

    // 输出索引,用于标识边属于哪个输出多边形
    int outIdx;

    // 边在原始多边形中的下一条边
    Edge next;

    // 边在原始多边形中的前一条边
    Edge prev;

    // 局部最小值链表中的下一条边
    Edge nextInLML;

    // 活动边列表中的下一条边
    Edge nextInAEL;

    // 活动边列表中的前一条边
    Edge prevInAEL;

    // 排序边列表中的下一条边
    Edge nextInSEL;

    // 排序边列表中的前一条边
    Edge prevInSEL;

    // 跳过标记常量
    protected final static int SKIP = -2;

    // 未分配标记常量
    protected final static int UNASSIGNED = -1;

    // 水平线标记常量(特殊值)
    protected final static double HORIZONTAL = -3.4E+38;

    // 日志记录器
    private final static Logger LOGGER = Logger.getLogger( Edge.class.getName() );

    /**
     * 构造函数,初始化边的各种点坐标对象
     */
    public Edge() {
        delta = new LongPoint();
        top = new LongPoint();
        bot = new LongPoint();
        current = new LongPoint();
    }

    /**
     * 查找下一个局部最小值点
     * 在扫描线算法中用于构建局部最小值链表
     * @return 包含局部最小值点的边
     */
    public Edge findNextLocMin() {
        Edge e = this;
        Edge e2;
        for (;;) {
            // 查找下一个局部最低点
            while (!e.bot.equals( e.prev.bot ) || e.current.equals( e.top )) {
                e = e.next;
            }
            // 如果找到非水平边的局部最低点
            if (e.deltaX != Edge.HORIZONTAL && e.prev.deltaX != Edge.HORIZONTAL) {
                break;
            }
            // 处理水平边的情况
            while (e.prev.deltaX == Edge.HORIZONTAL) {
                e = e.prev;
            }
            e2 = e;
            while (e.deltaX == Edge.HORIZONTAL) {
                e = e.next;
            }
            if (e.top.getY() == e.prev.bot.getY()) {
                continue; // 只是中间水平边
            }
            // 选择正确的局部最小值
            if (e2.prev.bot.getX() < e.bot.getX()) {
                e = e2;
            }
            break;
        }
        return e;
    }

    /**
     * 获取边的底部端点
     * @return 底部端点坐标
     */
    public LongPoint getBot() {
        return bot;
    }

    /**
     * 获取边的当前点
     * @return 当前点坐标
     */
    public LongPoint getCurrent() {
        return current;
    }

    /**
     * 获取边的方向向量
     * @return 方向向量
     */
    public LongPoint getDelta() {
        return delta;
    }

    /**
     * 获取共享同一个顶点的另一条边(最大值对)
     * @return 如果存在最大值对,则返回对应的边;否则返回null
     */
    public Edge getMaximaPair() {
        if (next.top.equals( top ) && next.nextInLML == null) {
            return next;
        }
        else if (prev.top.equals( top ) && prev.nextInLML == null) {
            return prev;
        }
        return null;
    }

    /**
     * 高级版本的getMaximaPair,还检查最大值对是否在活动边列表中
     * @return 如果存在有效的最大值对,则返回对应的边;否则返回null
     */
    Edge getMaximaPairEx() {
        // 与getMaximaPair类似,但如果最大值对不在活动边列表中则返回null(水平线除外)
        Edge result = getMaximaPair();
        if (result == null || result.outIdx == Edge.SKIP ||
                ((result.nextInAEL == result.prevInAEL) && !result.isHorizontal())) {
            return null;
        }
        return result;
    }

    /**
     * 根据指定方向获取活动边列表中的下一条边
     * @param direction 遍历方向(LEFT_TO_RIGHT或RIGHT_TO_LEFT)
     * @return 下一条边
     */
    public Edge getNextInAEL(Direction direction ) {
        return direction == Direction.LEFT_TO_RIGHT ? nextInAEL : prevInAEL;
    }

    /**
     * 获取边的顶部端点
     * @return 顶部端点坐标
     */
    public LongPoint getTop() {
        return top;
    }

    /**
     * 判断边是否对最终裁剪结果有贡献
     * 根据填充类型和裁剪操作类型确定边是否应该被包含在结果中
     * @param clipFillType 裁剪多边形的填充类型
     * @param subjFillType 主体多边形的填充类型
     * @param clipType 裁剪操作类型(INTERSECTION、UNION、DIFFERENCE、XOR)
     * @return 如果边对结果有贡献则返回true
     */
    public boolean isContributing(PolyFillType clipFillType, PolyFillType subjFillType, ClipType clipType ) {
        LOGGER.entering( Edge.class.getName(), "isContributing" );

        // 根据边的多边形类型确定使用哪种填充规则
        PolyFillType pft, pft2;
        if (polyTyp == PolyType.SUBJECT) {
            pft = subjFillType;
            pft2 = clipFillType;
        }
        else {
            pft = clipFillType;
            pft2 = subjFillType;
        }

        // 根据填充规则检查环绕计数
        switch (pft) {
            case EVEN_ODD:
                // 如果是内部的主题线则返回false
                if (windDelta == 0 && windCnt != 1) {
                    return false;
                }
                break;
            case NON_ZERO:
                if (Math.abs( windCnt ) != 1) {
                    return false;
                }
                break;
            case POSITIVE:
                if (windCnt != 1) {
                    return false;
                }
                break;
            default: // PolyFillType.pftNegative
                if (windCnt != -1) {
                    return false;
                }
                break;
        }

        // 根据裁剪操作类型和相反多边形的填充规则确定是否贡献
        switch (clipType) {
            case INTERSECTION:
                switch (pft2) {
                    case EVEN_ODD:
                    case NON_ZERO:
                        return windCnt2 != 0;
                    case POSITIVE:
                        return windCnt2 > 0;
                    default:
                        return windCnt2 < 0;
                }
            case UNION:
                switch (pft2) {
                    case EVEN_ODD:
                    case NON_ZERO:
                        return windCnt2 == 0;
                    case POSITIVE:
                        return windCnt2 <= 0;
                    default:
                        return windCnt2 >= 0;
                }
            case DIFFERENCE:
                if (polyTyp == PolyType.SUBJECT) {
                    switch (pft2) {
                        case EVEN_ODD:
                        case NON_ZERO:
                            return windCnt2 == 0;
                        case POSITIVE:
                            return windCnt2 <= 0;
                        default:
                            return windCnt2 >= 0;
                    }
                }
                else {
                    switch (pft2) {
                        case EVEN_ODD:
                        case NON_ZERO:
                            return windCnt2 != 0;
                        case POSITIVE:
                            return windCnt2 > 0;
                        default:
                            return windCnt2 < 0;
                    }
                }
            case XOR:
                if (windDelta == 0) {
                    switch (pft2) {
                        case EVEN_ODD:
                        case NON_ZERO:
                            return windCnt2 == 0;
                        case POSITIVE:
                            return windCnt2 <= 0;
                        default:
                            return windCnt2 >= 0;
                    }
                }
                else {
                    return true;
                }
        }
        return true;
    }

    /**
     * 判断相反多边形类型是否使用奇偶填充规则
     * @param clipFillType 裁剪多边形的填充类型
     * @param subjFillType 主体多边形的填充类型
     * @return 如果相反多边形使用奇偶规则则返回true
     */
    public boolean isEvenOddAltFillType(PolyFillType clipFillType, PolyFillType subjFillType ) {
        if (polyTyp == PolyType.SUBJECT) {
            return clipFillType == PolyFillType.EVEN_ODD;
        }
        else {
            return subjFillType == PolyFillType.EVEN_ODD;
        }
    }

    /**
     * 判断当前多边形类型是否使用奇偶填充规则
     * @param clipFillType 裁剪多边形的填充类型
     * @param subjFillType 主体多边形的填充类型
     * @return 如果当前多边形使用奇偶规则则返回true
     */
    public boolean isEvenOddFillType(PolyFillType clipFillType, PolyFillType subjFillType ) {
        if (polyTyp == PolyType.SUBJECT) {
            return subjFillType == PolyFillType.EVEN_ODD;
        }
        else {
            return clipFillType == PolyFillType.EVEN_ODD;
        }
    }

    /**
     * 判断边是否为水平线
     * @return 如果是水平线则返回true
     */
    public boolean isHorizontal() {
        return delta.getY() == 0;
    }

    /**
     * 判断边是否为中间边(非局部最小值和最大值的边)
     * @param y 当前扫描线Y坐标
     * @return 如果是中间边则返回true
     */
    public boolean isIntermediate( double y ) {
        return top.getY() == y && nextInLML != null;
    }

    /**
     * 判断边是否为最大值边
     * @param Y 当前Y坐标
     * @return 如果是最大值边则返回true
     */
    public boolean isMaxima( double Y ) {
        return top.getY() == Y && nextInLML == null;
    }

    /**
     * 反转水平边的方向
     * 交换顶部和底部的X坐标,使它们符合边界的自然进展
     * 这样它们的底部X坐标将与相邻的下部边对齐
     */
    public void reverseHorizontal() {
        // 交换水平边的顶部和底部X坐标
        // 使其遵循边界的自然进展
        long temp = top.getX();
        top.setX( bot.getX() );
        bot.setX( temp );

        // 交换Z坐标(可能用于3D操作)
        temp = top.getZ();
        top.setZ( bot.getZ() );
        bot.setZ( temp );

    }

    /**
     * 设置边的底部端点
     * @param bot 新的底部端点
     */
    public void setBot( LongPoint bot ) {
        this.bot.set( bot );
    }

    /**
     * 设置边的当前点
     * @param current 新的当前点
     */
    public void setCurrent( LongPoint current ) {
        this.current.set( current );
    }

    /**
     * 设置边的顶部端点
     * @param top 新的顶部端点
     */
    public void setTop( LongPoint top ) {
        this.top.set( top );
    }

    /**
     * 重写toString方法,返回边的详细信息
     * @return 边的字符串表示
     */
    @Override
    public String toString() {
        return "TEdge [Bot=" + bot + ", Curr=" + current + ", Top=" + top + ", Delta=" + delta + ", Dx=" + deltaX + ", PolyTyp=" + polyTyp + ", Side=" + side
                + ", WindDelta=" + windDelta + ", WindCnt=" + windCnt + ", WindCnt2=" + windCnt2 + ", OutIdx=" + outIdx + ", Next=" + next + ", Prev="
                + prev + ", NextInLML=" + nextInLML + ", NextInAEL=" + nextInAEL + ", PrevInAEL=" + prevInAEL + ", NextInSEL=" + nextInSEL
                + ", PrevInSEL=" + prevInSEL + "]";
    }

    /**
     * 更新边的方向向量和斜率
     * 计算deltaX(dx/dy)用于扫描线算法中的X坐标计算
     */
    public void updateDeltaX() {
        // 计算方向向量(top - bot)
        delta.setX( top.getX() - bot.getX() );
        delta.setY( top.getY() - bot.getY() );
        // 如果是水平线,使用特殊标记值
        if (delta.getY() == 0) {
            deltaX = Edge.HORIZONTAL;
        }
        else {
            // 计算斜率的倒数(dx/dy)
            deltaX = (double) delta.getX() / delta.getY();
        }
    }

}
最后修改:2025 年 12 月 03 日
如果觉得我的文章对你有用,请随意赞赏