计算矩形覆盖区域边框

需求背景

在canvas中绘制矩形时,当激活一个矩形时,需要将被覆盖区域的边框用虚线绘制,绘制效果图如下:
image.png

算法实现:

type Rectangle = { x: number, y: number, width: number, height: number }
type Edge = { x1: number, y1: number, x2: number, y2: number }

/**
 * 获取目标矩形的未被覆盖的边框和被覆盖的边框
 * @param targetRect - 目标矩形
 * @param rectangles - 矩形数组,表示遮挡矩形
 * @returns 一个对象,包含未被覆盖的边框和被覆盖的边框
 */
function getEdgeRanges(targetRect: Rectangle, rectangles: Rectangle[]): { uncoveredEdges: Edge[], coveredEdges: Edge[] } {
    // 计算未被覆盖的边框
    const uncoveredEdges = calculateEdges(targetRect, rectangles, false)
    // 计算被覆盖的边框
    const coveredEdges = calculateEdges(targetRect, rectangles, true)

    return {
        uncoveredEdges,
        coveredEdges
    }
}

/**
 * 计算目标矩形的边框坐标
 * @param targetRect - 目标矩形
 * @param rectangles - 矩形数组,表示遮挡矩形
 * @param isCovered - 是否计算被覆盖的边框(true)还是未被覆盖的边框(false)
 * @returns 边框坐标数组
 */
function calculateEdges(targetRect: Rectangle, rectangles: Rectangle[], isCovered: boolean): Edge[] {
    // 对每条边进行计算,合并所有边的结果
    const edges: Edge[] = [
        ...calculateEdge(targetRect, rectangles, 'top', isCovered),
        ...calculateEdge(targetRect, rectangles, 'right', isCovered),
        ...calculateEdge(targetRect, rectangles, 'bottom', isCovered),
        ...calculateEdge(targetRect, rectangles, 'left', isCovered)
    ]
    return edges
}

/**
 * 计算单条边框的坐标
 * @param targetRect - 目标矩形
 * @param rectangles - 矩形数组,表示遮挡矩形
 * @param edge - 边框类型(top, right, bottom, left)
 * @param isCovered - 是否计算被覆盖的边框(true)还是未被覆盖的边框(false)
 * @returns 边框坐标数组
 */
function calculateEdge(targetRect: Rectangle, rectangles: Rectangle[], edge: 'top' | 'right' | 'bottom' | 'left', isCovered: boolean): Edge[] {
    // 根据边框类型确定完整范围
    const fullRange: [number, number] = edge === 'top' || edge === 'bottom'
        ? [targetRect.x, targetRect.x + targetRect.width]
        : [targetRect.y, targetRect.y + targetRect.height]

    // 存储被遮挡的范围
    const coveredRanges: [number, number][] = []

    // 遍历所有遮挡矩形
    for (const rect of rectangles) {
        // 检查遮挡矩形是否与当前边框相交
        if (rectIntersectsEdge(targetRect, rect, edge)) {
            // 计算遮挡矩形与边框的交集范围
            const [rectStart, rectEnd] = getIntersectionRange(targetRect, rect, edge)

            // 只有在交集范围有效时才进行处理
            if (rectStart < rectEnd) {
                coveredRanges.push([rectStart, rectEnd])
            }
        }
    }

    // 计算目标矩形边框的被覆盖范围或未被覆盖范围
    const targetRanges: [number, number][] = isCovered 
        ? mergeRanges(coveredRanges) 
        : subtractRanges(fullRange, mergeRanges(coveredRanges))

    // 将计算出的范围转换为边框坐标对象
    return targetRanges.map(range => createEdge(edge, range[0], range[1], targetRect))
}

/**
 * 合并重叠的范围
 * @param ranges - 范围数组
 * @returns 合并后的范围数组
 */
function mergeRanges(ranges: [number, number][]): [number, number][] {
    if (ranges.length === 0) return []

    // 按开始点排序
    ranges.sort((a, b) => a[0] - b[0])

    const merged: [number, number][] = []
    let currentStart: number = ranges[0][0]
    let currentEnd: number = ranges[0][1]

    for (let i = 1; i < ranges.length; i++) {
        const [start, end] = ranges[i]
        if (start <= currentEnd) {
            // 合并重叠的范围
            currentEnd = Math.max(currentEnd, end)
        } else {
            // 记录当前合并的范围,并开始新的范围
            merged.push([currentStart, currentEnd])
            currentStart = start
            currentEnd = end
        }
    }

    // 添加最后一个合并的范围
    merged.push([currentStart, currentEnd])
    return merged
}

/**
 * 从完整范围中减去被覆盖的范围
 * @param fullRange - 完整范围
 * @param coveredRanges - 被覆盖的范围
 * @returns 未被覆盖的范围数组
 */
function subtractRanges(fullRange: [number, number], coveredRanges: [number, number][]): [number, number][] {
    const uncoveredRanges: [number, number][] = []
    let currentStart: number = fullRange[0]

    for (const [start, end] of coveredRanges) {
        if (start > currentStart) {
            // 记录未被覆盖的范围
            uncoveredRanges.push([currentStart, start])
        }
        currentStart = Math.max(currentStart, end)
    }

    if (currentStart < fullRange[1]) {
        // 记录最后的未被覆盖的范围
        uncoveredRanges.push([currentStart, fullRange[1]])
    }

    return uncoveredRanges
}

/**
 * 根据边框类型和范围创建边框坐标
 * @param edge - 边框类型(top, right, bottom, left)
 * @param start - 范围开始点
 * @param end - 范围结束点
 * @param targetRect - 目标矩形
 * @returns 边框坐标对象
 */
function createEdge(edge: 'top' | 'right' | 'bottom' | 'left', start: number, end: number, targetRect: Rectangle): Edge {
    switch (edge) {
        case 'top':
            // 上边框的坐标
            return { x1: start, y1: targetRect.y, x2: end, y2: targetRect.y }
        case 'right':
            // 右边框的坐标
            return { x1: targetRect.x + targetRect.width, y1: start, x2: targetRect.x + targetRect.width, y2: end }
        case 'bottom':
            // 下边框的坐标
            return { x1: start, y1: targetRect.y + targetRect.height, x2: end, y2: targetRect.y + targetRect.height }
        case 'left':
            // 左边框的坐标
            return { x1: targetRect.x, y1: start, x2: targetRect.x, y2: end }
    }
}

/**
 * 判断矩形是否与边框相交
 * @param targetRect - 目标矩形
 * @param rect - 遮挡矩形
 * @param edge - 边框类型(top, right, bottom, left)
 * @returns 是否相交
 */
function rectIntersectsEdge(targetRect: Rectangle, rect: Rectangle, edge: 'top' | 'right' | 'bottom' | 'left'): boolean {
    switch (edge) {
        case 'top':
            // 检查遮挡矩形的顶部是否与目标矩形的顶部相交
            return rect.y <= targetRect.y && rect.y + rect.height >= targetRect.y
        case 'right':
            // 检查遮挡矩形的右侧是否与目标矩形的右侧相交
            return rect.x <= targetRect.x + targetRect.width && rect.x + rect.width >= targetRect.x + targetRect.width
        case 'bottom':
            // 检查遮挡矩形的底部是否与目标矩形的底部相交
            return rect.y + rect.height >= targetRect.y + targetRect.height && rect.y <= targetRect.y + targetRect.height
        case 'left':
            // 检查遮挡矩形的左侧是否与目标矩形的左侧相交
            return rect.x <= targetRect.x && rect.x + rect.width >= targetRect.x
    }
}

/**
 * 获取矩形与边框的交集范围
 * @param targetRect - 目标矩形
 * @param rect - 遮挡矩形
 * @param edge - 边框类型(top, right, bottom, left)
 * @returns 交集范围
 */
function getIntersectionRange(targetRect: Rectangle, rect: Rectangle, edge: 'top' | 'right' | 'bottom' | 'left'): [number, number] {
    switch (edge) {
        case 'top':
            // 计算上边框的交集范围
            return [Math.max(rect.x, targetRect.x), Math.min(rect.x + rect.width, targetRect.x + targetRect.width)]
        case 'right':
            // 计算右边框的交集范围
            return [Math.max(rect.y, targetRect.y), Math.min(rect.y + rect.height, targetRect.y + targetRect.height)]
        case 'bottom':
            // 计算下边框的交集范围
            return [Math.max(rect.x, targetRect.x), Math.min(rect.x + rect.width, targetRect.x + targetRect.width)]
        case 'left':
            // 计算左边框的交集范围
            return [Math.max(rect.y, targetRect.y), Math.min(rect.y + rect.height, targetRect.y + targetRect.height)]
    }
}

// 示例数据
const targetRect: Rectangle = { x: 0, y: 0, width: 100, height: 10 }
const rectArray: Rectangle[] = [
    { x: 50, y: -5, width: 50, height: 20 }, // 覆盖上边框
    { x: 90, y: 5, width: 10, height: 10 },  // 覆盖右边框
    { x: 0, y: 10, width: 100, height: 10 }, // 覆盖下边框
    { x: -10, y: 0, width: 10, height: 10 }  // 覆盖左边框
]

const edgeRanges = getEdgeRanges(targetRect, rectArray)
console.log('Uncovered Edges:', edgeRanges.uncoveredEdges)
console.log('Covered Edges:', edgeRanges.coveredEdges)

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值