多邊形的布爾運算(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)
    })
};

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章