/**
* Created by Alex Bol on 12/02/2018.
*/
"use strict";
let Flatten = require("flatten-js");
let BooleanOp = require("flatten-boolean-op");
let {Point, Segment, Vector, Line, Box, Arc, Polygon, Face} = Flatten;
let {point, segment, arc, vector} = Flatten;
/**
* Class Offset implements offset of polygons
*/
class Offset {
/**
* Offset polygon by given value
* @param {Polygon} polygon - input polygon
* @param {number} value - offset value, may be positive or negative
*/
static 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.shape instanceof Flatten.Segment) {
offsetEdge = Offset.offsetSegment(edge.shape, w);
}
else {
offsetEdge = Offset.offsetArc(edge.shape, w);
}
if (w > 0) {
offsetPolygon = BooleanOp.unify(offsetPolygon, offsetEdge);
}
else {
offsetPolygon = BooleanOp.subtract(offsetPolygon, offsetEdge);
}
}
}
return offsetPolygon;
}
static offsetSegment(seg, value) {
let w = Math.abs(value);
let polygon = new Polygon();
let v_seg = vector(seg.start, seg.end);
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 = Offset.arcSE(seg.end, seg_left.end, seg_right.start, Flatten.CW);
let cap2 = Offset.arcSE(seg.start, seg_right.end, seg_left.start, Flatten.CW);
polygon.addFace([seg_left, cap1, seg_right, cap2]);
return polygon;
}
static 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 = Offset.arcStartSweep(arc.end, arc_outer.end, Math.PI, arc.counterClockwise);
arc_cap2 = Offset.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 === Flatten.CW ? Flatten.CCW : Flatten.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) {
BooleanOp.addToIntPoints(edge_cap1, pt, int_points);
BooleanOp.addToIntPoints(edge_cap2, pt, int_points);
}
// Sort intersection points and insert them as new vertices
let int_points_sorted = BooleanOp.getSortedArray(int_points);
BooleanOp.splitByIntersections(polygon, int_points_sorted);
// Set BV flags
let bv = Flatten.OUTSIDE;
for (let int_point of int_points_sorted) {
int_point.edge_before.bv = bv;
int_point.edge_after.bv = (bv == Flatten.OUTSIDE ? Flatten.INSIDE : Flatten.OUTSIDE);
bv = int_point.edge_after.bv; // invert flag on each iteration
}
// Remove inner "chains"
let op = Flatten.BOOLEAN_UNION;
BooleanOp.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
BooleanOp.removeOldFaces(polygon, int_points);
// restore faces
BooleanOp.restoreFaces(polygon, int_points, int_points);
}
let face0 = [...polygon.faces][0];
if (face0.orientation() === Flatten.ORIENTATION.CCW) {
polygon.reverse();
}
return polygon;
}
static arcSE(center, start, end, counterClockwise) {
let startAngle = vector(center,start).slope;
let endAngle = vector(center, end).slope;
if (Flatten.Utils.EQ(startAngle, endAngle)) {
endAngle += 2*Math.PI;
counterClockwise = true;
}
let r = vector(center, start).length;
return new Arc(center, r, startAngle, endAngle, counterClockwise);
}
static arcStartSweep(center, start, sweep, counterClockwise) {
let startAngle = vector(center,start).slope;
let endAngle = startAngle + sweep;
if (Flatten.Utils.EQ(startAngle, endAngle)) {
endAngle += 2*Math.PI;
counterClockwise = true;
}
let r = vector(center, start).length;
return new Arc(center, r, startAngle, endAngle, counterClockwise);
}
static arcEndSweep(center, end, sweep, counterClockwise) {
let {vector, Arc} = Flatten;
let endAngle = vector(center,end).slope;
let startAngle = endAngle - sweep;
if (Flatten.Utils.EQ(startAngle, endAngle)) {
endAngle += 2*Math.PI;
counterClockwise = true;
}
let r = vector(center, end).length;
return new Arc(center, r, startAngle, endAngle, counterClockwise);
}
}
Flatten.Polygon.prototype.offset = function(value) {
return Offset.offset(this, value);
};
module.exports = Offset;