多边形的布尔运算(intersection交集, union并集, difference差异, xor 异或)

前言

这是工作中的一个需求,要求使用PixiJS来进行绘制,所以这里就不使用原生Canvas或是其他绘制工具了。但归根结底,原理都是一样的。

正文

多边形的布尔运算( boolean operation on polygons)包括: intersection交集, union并集, difference差异, xor 异或。

具体表现如,Photoshop中的选区操作
Photoshop中的**选区操作**
Sketch中的图形叠加效果
Sketch中的**图形叠加效果**

实现思路

根据两个多边形的所有顶点的坐标组成GeoJSON,然后通过martinez这个库计算出布尔运算之后的GeoJSON,再解析生成最终效果的多边形定点数组,最后绘制数组生成图像。(下面我使用了PIXI来绘制图形)

API:
.intersection(<Geometry>, <Geometry>) => <Geometry>
.union(<Geometry>, <Geometry>) => <Geometry>
.diff(<Geometry>, <Geometry>) => <Geometry>
.xor(<Geometry>, <Geometry>) => <Geometry>

关于由顶点数组绘制多边形,可参阅博文【绘制】HTML5 Canvas 实现任意圆角多边形

效果展示

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

缺点

只能应用于直线边,对于圆角/弧度边无法计算

代码实现

本身我是不打算贴出代码的,因为代码中复用了之前的文章的“圆角多边形”的radius效果,产生了一定的代码冗余,但是考虑到读者可能会在某些地方有所顾虑,还是贴上了,请大家有选择的翻阅。

index.js

import React, {useEffect, useState} from "react";
import * as PIXI from 'pixi.js'
import {intersection, union, xor, diff, drawAll} from './utils.js'

export default function PloygonsBooleanDemo() {
    const [json1, setJson1] = useState([
        // 正方形
        {
            x: 20,
            y: 20,
            radius: 0
        },
        {
            x: 110,
            y: 10,
            radius: 0
        },
        {
            x: 90,
            y: 90,
            radius: 0
        },
        {
            x: 20,
            y: 80,
            radius: 0
        }

    ]);
    const [json2, setJson2] = useState([
        {
            x: 50,
            y: 50,
            radius: 0
        },
        {
            x: 110,
            y: 50,
            radius: 0
        },
        {
            x: 110,
            y: 110,
            radius: 0
        },
        {
            x: 50,
            y: 110,
            radius: 0
        }
    ]);

    const app = new PIXI.Application({
        antialias: true,    // default: false
    });
    let graphics;

    useEffect(() => {
        document.body.appendChild(app.view);
        graphics = new PIXI.Graphics();
        app.stage.addChild(graphics);
    }, []);

    // 初始化图形
    let initGraphics = () => {
        app.stage.removeChild(graphics);
        graphics = new PIXI.Graphics();
        app.stage.addChild(graphics);
    };

    return (<div>
        <button onClick={() => {
            initGraphics();
            drawAll(intersection(json1, json2), graphics);
        }
        }>交集
        </button>
        <button onClick={() => {
            initGraphics();
            drawAll(union(json1, json2), graphics);

        }}>并集
        </button>
        <button onClick={() => {
            initGraphics();
            drawAll(diff(json1, json2), graphics);
        }}>除去一层1
        </button>
        <button onClick={() => {
            initGraphics();
            drawAll(diff(json2, json1), graphics);
        }}>除去一层2
        </button>
        <button onClick={() => {
            initGraphics();
            drawAll(xor(json1, json2), graphics);
        }}>差集
        </button>
    </div>)
}

utils.js

import * as martinez from "martinez-polygon-clipping";

export function geoJSON2RadiusShapeJSON(geoJSON) {
    let radiusShapeJSON = [];

    geoJSON.geometry.coordinates.forEach((element)=>{
        let childRadiusShapeJSON = [];
        element[0].forEach((element, index, array) => {
            if (index !== array.length - 1) {
                childRadiusShapeJSON.push({x: element[0], y: element[1], radius: 0});
            }
        });
        radiusShapeJSON.push(childRadiusShapeJSON)
    })
    return radiusShapeJSON;
}

// 将绘制所用的json转化为GoeJSON
export function radiusShapeJSON2GeoJSON(radiusShapeJSON) {
    let coordinatesArr = [];
    radiusShapeJSON.forEach((element) => {
        coordinatesArr.push([element.x, element.y]);
    });
    coordinatesArr.push([radiusShapeJSON[0].x, radiusShapeJSON[0].y]);


    return {
        "type": "Feature",
        "geometry": {
            "type": "Polygon", "coordinates": [
                coordinatesArr
            ]
        }
    }
}

//交集
export function intersection(radiusShapeJSON1, radiusShapeJSON2) {
    return geoJSON2RadiusShapeJSON(
        {
            "type": "Feature",
            "geometry": {
                "type": "Polygon", "coordinates":
                    martinez.intersection(
                        radiusShapeJSON2GeoJSON(radiusShapeJSON1).geometry.coordinates,
                        radiusShapeJSON2GeoJSON(radiusShapeJSON2).geometry.coordinates
                    )

            }
        }

    )
}

//差
export function diff(radiusShapeJSON1, radiusShapeJSON2) {
    return geoJSON2RadiusShapeJSON(
        {
            "type": "Feature",
            "geometry": {
                "type": "Polygon", "coordinates":
                    martinez.diff(
                        radiusShapeJSON2GeoJSON(radiusShapeJSON1).geometry.coordinates,
                        radiusShapeJSON2GeoJSON(radiusShapeJSON2).geometry.coordinates
                    )

            }
        }

    )
}

//并集
export function union(radiusShapeJSON1, radiusShapeJSON2) {
    return geoJSON2RadiusShapeJSON(
        {
            "type": "Feature",
            "geometry": {
                "type": "Polygon", "coordinates":
                    martinez.union(
                        radiusShapeJSON2GeoJSON(radiusShapeJSON1).geometry.coordinates,
                        radiusShapeJSON2GeoJSON(radiusShapeJSON2).geometry.coordinates
                    )

            }
        }

    )
}

//异或
export function xor(radiusShapeJSON1, radiusShapeJSON2) {
    return geoJSON2RadiusShapeJSON(
        {
            "type": "Feature",
            "geometry": {
                "type": "Polygon", "coordinates":
                    martinez.xor(
                        radiusShapeJSON2GeoJSON(radiusShapeJSON1).geometry.coordinates,
                        radiusShapeJSON2GeoJSON(radiusShapeJSON2).geometry.coordinates
                    )

            }
        }

    )
}

/**
 * 已知A、B、C三点坐标,求半径为r的内切圆与AC的切点坐标
 * @param A
 * @param B
 * @param C
 * @param r
 * @returns {{x: *, y: *}}
 */
export function _toOrigin(A, B, C, r) {
    let {pow, acos, tan} = Math;

    //ab的长度
    const ab = pow(pow(A.x - B.x, 2) + pow(A.y - B.y, 2), 0.5);
    //bc的长度
    const bc = pow(pow(C.x - B.x, 2) + pow(C.y - B.y, 2), 0.5);

    //B的角度
    const angleB = acos(((A.x - B.x) * (C.x - B.x) + (A.y - B.y) * (C.y - B.y)) / (ab * bc));

    //BM的长度,即B到切线的长度
    const M = r / tan(angleB / 2);


    const k = (C.y - B.y) / (C.x - B.x);


    if (k === '-Infinity') {
        return {
            x: B.x,
            y: B.y - M
        };
    } else if (k === 'Infinity') {
        return {
            x: B.x,
            y: B.y + M
        };
    } else {
        let i = 1;

        if (1 / k === '-Infinity') {
            i = -1;
        }

        if (k < 0) {
            if ((C.y - B.y) < 0) {
                i = 1;
            } else {
                i = -1;
            }
        }

        if (k > 0) {
            if (C.y - B.y < 0) {
                i = -1;
            }
        }


        return {
            x: B.x + (1 / pow(1 + k * k, 0.5)) * M * i,
            y: B.y + (k / pow(1 + k * k, 0.5)) * M * i
        };
    }


}

/**
 * 将json点集转化为可操作点handlePoints
 */
export function json2HandlePoint(json) {
    //矫正radius
    fixRadius(json);
    //将所有可操作点保存handlePoint变量中
    let handlePoints = [];

    //第一个点
    handlePoints.push({
        start: {x: json[json.length - 1].x, y: json[json.length - 1].y},
        middle: {x: json[0].x, y: json[0].y},
        end: {x: json[1].x, y: json[1].y},
        radius: json[0].radius
    });

    //中间所有点
    for (let i = 1; i < json.length - 1; i++) {
        handlePoints.push({
            start: {x: json[i - 1].x, y: json[i - 1].y},
            middle: {x: json[i].x, y: json[i].y},
            end: {x: json[i + 1].x, y: json[i + 1].y},
            radius: json[i].radius
        });
    }

    //最后一个点
    handlePoints.push({
        start: {x: json[json.length - 2].x, y: json[json.length - 2].y},
        middle: {x: json[json.length - 1].x, y: json[json.length - 1].y},
        end: {x: json[0].x, y: json[0].y},
        radius: json[json.length - 1].radius
    });


    return handlePoints;
}

/**
 * 将异常json的radius按照Sketch模式矫正
 * sketch圆角异常数据处理方法:

 用户可设定radius值,当出现不合法值的时候,原值不变,图像自行优化

 说明:如100*50的矩形,可以在50像素的边对应的角,各设置成radius=25,也可以其中一个设为0,另一个设为50,总之就是radius1+radius2=50。当radius1+radius2>50的时候,在绘制里,radius_min不变,radius_max = 50/2,而在json数据中不改变这些数据,已保存用户的数据记录
 */
export function fixRadius(json) {
    //第一个点
    _fixRadius(json[json.length - 1], json[0], json[1]);

    for (let i = 1; i < json.length - 1; i++) {
        _fixRadius(json[i - 1], json[i], json[i + 1]);
    }

    //最后一个点
    _fixRadius(json[json.length - 2], json[json.length - 1], json[0]);
}

export function _fixRadius(left, curr, right) {
    let {pow, acos, tan} = Math;

    const leftLine = pow(pow(left.x - curr.x, 2) + pow(left.y - curr.y, 2), 0.5);
    const rightLine = pow(pow(right.x - curr.x, 2) + pow(right.y - curr.y, 2), 0.5);

    //两边最短边
    const minLine = leftLine < rightLine ? leftLine : rightLine;
    //中间角度
    const angleCurr = acos(((left.x - curr.x) * (right.x - curr.x) + (left.y - curr.y) * (right.y - curr.y)) / (leftLine * rightLine));
    //理论上能实现的最大弧度半径
    let maxRadius = minLine * tan(angleCurr / 2);


    /*
        当左右都为0且,当前radius超过理论最大弧度半径时
        将radius缩减到最大弧度半径
     */
    if (left.radius === 0 && right.radius === 0) {
        if (curr.radius > maxRadius) {
            curr.radius = maxRadius;
        }
    }

    /*
        当左右任意一边有值,且radius超过理论最大弧度半径的一半时
        将radius缩减到最大弧度半径的一半
     */

    if (left.radius > 0 || right.radius > 0) {
        if (curr.radius > maxRadius / 2) {
            curr.radius = maxRadius / 2;
        }
    }

}

export function drawShape(handlePoints,graphics) {
    //重置路径
    graphics.beginFill(0x66CCFF);
    let currPoint = undefined;

    const origin = _toOrigin(handlePoints[0].start, handlePoints[0].middle, handlePoints[0].end, handlePoints[0].radius);

    graphics.moveTo(origin.x, origin.y);

    for (let i = 1; i < handlePoints.length; i++) {
        currPoint = handlePoints[i];

        //绘制圆弧
        graphics.arcTo(currPoint.middle.x, currPoint.middle.y, currPoint.end.x, currPoint.end.y, currPoint.radius);
    }

    currPoint = handlePoints[0];
    graphics.arcTo(currPoint.middle.x, currPoint.middle.y, currPoint.end.x, currPoint.end.y, currPoint.radius);

    graphics.endFill();
    graphics.closePath();
}

//绘制经布尔运算后的图形
export function drawAll(arr,graphics) {
    arr.forEach(element => {
        drawShape(json2HandlePoint(element),graphics)
    })
};

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值