polygonOffset.js


/**
 * Created by Alex Bol on 12/02/2018.
 */
"use strict";
import {Point, Segment, Vector, Line, Box, Arc, Polygon, Face} from "@flatten-js/core";
import {CW, CCW, INSIDE, OUTSIDE, ORIENTATION} from "@flatten-js/core";
import {point, segment, vector} from "@flatten-js/core";
import {unify,subtract} from "@flatten-js/boolean-op";

import  {BOOLEAN_UNION} from "@flatten-js/boolean-op";

import {addToIntPoints, getSortedArray, splitByIntersections} from "@flatten-js/boolean-op";
import {removeNotRelevantChains, removeOldFaces, restoreFaces} from "@flatten-js/boolean-op";

import {arcSE, arcStartSweep, arcEndSweep} from "./createArcs";

/**
 * Offset polygon by given value
 * @param {Polygon} polygon - input polygon
 * @param {number} value - offset value, may be positive or negative
 * @returns {Polygon} offsetPolygon
 */

/**
 *
 * @param polygon
 * @param value
 *
 */
export function offset(polygon, value) {
    let w = value;

    let edges = [...polygon.edges];
    let offsetPolygon = polygon.clone();
    let offsetEdge;

    if (w != 0) {

        for (let edge of edges) {
            if (edge.isSegment()) {
                offsetEdge = offsetSegment(edge.shape, w);
            }
            else {
                offsetEdge = offsetArc(edge.shape, w);
            }

            if (w > 0) {
                offsetPolygon = unify(offsetPolygon, offsetEdge);
            }
            else {
                offsetPolygon = subtract(offsetPolygon, offsetEdge);
            }
        }
    }

    return offsetPolygon;
}

function offsetArc(arc, value) {
    let edges = [];

    let w = Math.abs(value);

    // Define outline polygon
    let polygon = new Polygon();
    let arc_cap1,arc_cap2;

    let arc_outer = arc.clone();
    arc_outer.r = arc.r + w;

    arc_cap1 = arcStartSweep(arc.end, arc_outer.end, Math.PI, arc.counterClockwise);
    arc_cap2 = arcEndSweep(arc.start, arc_outer.start, Math.PI, arc.counterClockwise);

    let arc_inner = undefined;
    if (arc.r > w) {
        arc_inner = new Arc(arc.pc, arc.r - w, arc.endAngle, arc.startAngle,
            arc.counterClockwise === CW ? CCW : CW);
    }
    else {
        // arc_inner = new Arc(arc.pc, w - arc.r, arc.startAngle, arc.endAngle, arc.counterClockwise);
        arc_inner = new Segment(arc_cap1.end, arc_cap2.start);
    }

    polygon.addFace([arc_outer, arc_cap1, arc_inner, arc_cap2]);
    [...polygon.faces][0].setArcLength();

    // Create intersection points
    let ips = Face.getSelfIntersections([...polygon.faces][0], polygon.edges, false);

    // TODO: getSelfIntersections returns points with correspondent edges - avoid duplication
    ips = ips.slice(0,ips.length/2);    // for now slice array to avoid duplication in points

    let int_points = [];
    let edge_cap1;
    let edge_cap2;

    edge_cap1 = [...polygon.edges][1];
    edge_cap2 = [...polygon.edges][3];

    for (let pt of ips) {
        addToIntPoints(edge_cap1, pt, int_points);
        addToIntPoints(edge_cap2, pt, int_points);
    }

    // Sort intersection points and insert them as new vertices
    let int_points_sorted = getSortedArray(int_points);
    splitByIntersections(polygon, int_points_sorted);


    // Set BV flags
    let bv = OUTSIDE;
    for (let int_point of int_points_sorted) {
        int_point.edge_before.bv = bv;
        int_point.edge_after.bv = (bv == OUTSIDE ? INSIDE : OUTSIDE);
        bv = int_point.edge_after.bv;   // invert flag on each iteration
    }

    // Remove inner "chains"
    let op = BOOLEAN_UNION;
    removeNotRelevantChains(polygon, op, int_points_sorted, true);

    // return int_points_sorted;
    // Swap links
    let num = int_points.length;
    if (num > 0) {
        let edge_before;
        let edge_after;
        // 0 => 3
        edge_before = int_points_sorted[0].edge_before;
        edge_after = int_points_sorted[num-1].edge_after;
        edge_before.next = edge_after;
        edge_after.prev = edge_before;

        // Fill in missed links in intersection points
        int_points_sorted[0].edge_after = int_points_sorted[num-1].edge_after;
        int_points_sorted[num-1].edge_before = int_points_sorted[0].edge_before;

        if (num == 4) {
            // 2 => 1
            edge_before = int_points_sorted[2].edge_before;
            edge_after = int_points_sorted[1].edge_after;
            edge_before.next = edge_after;
            edge_after.prev = edge_before;

            // Fill in missed links in intersection points
            int_points_sorted[2].edge_after = int_points_sorted[1].edge_after;
            int_points_sorted[1].edge_before = int_points_sorted[2].edge_before;
        }

        // remove old faces
        removeOldFaces(polygon, int_points);
        // restore faces
        restoreFaces(polygon, int_points, int_points);
    }

    let face0 = [...polygon.faces][0];
    if (face0.orientation() === ORIENTATION.CCW) {
        polygon.reverse();
    }
    return polygon;
}

function offsetSegment(seg, value) {
    let w = Math.abs(value);

    let polygon = new Polygon();
    let v_seg = vector(seg.end.x-seg.start.x, seg.end.y-seg.start.y);
    let v_seg_unit = v_seg.normalize();
    let v_left = v_seg_unit.rotate90CCW().multiply(w);
    let v_right = v_seg_unit.rotate90CW().multiply(w);
    let seg_left = seg.translate(v_left);
    let seg_right = seg.translate(v_right).reverse();
    let cap1 = arcSE(seg.end, seg_left.end, seg_right.start, CW);
    let cap2 = arcSE(seg.start, seg_right.end, seg_left.start, CW);

    polygon.addFace([seg_left, cap1, seg_right, cap2]);
    return polygon;
}