@flatten-js/core
Version:
Javascript library for 2d geometry
1,594 lines (1,352 loc) • 346 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global["@flatten-js/core"] = {}));
})(this, (function (exports) { 'use strict';
/**
* Global constant CCW defines counterclockwise direction of arc
* @type {boolean}
*/
const CCW = true;
/**
* Global constant CW defines clockwise direction of arc
* @type {boolean}
*/
const CW = false;
/**
* Defines orientation for face of the polygon: clockwise, counterclockwise
* or not orientable in the case of self-intersection
* @type {{CW: number, CCW: number, NOT_ORIENTABLE: number}}
*/
const ORIENTATION = {CCW:-1, CW:1, NOT_ORIENTABLE: 0};
const PIx2 = 2 * Math.PI;
const INSIDE$2 = 1;
const OUTSIDE$1 = 0;
const BOUNDARY$1 = 2;
const CONTAINS = 3;
const INTERLACE = 4;
const OVERLAP_SAME$1 = 1;
const OVERLAP_OPPOSITE$1 = 2;
const NOT_VERTEX$1 = 0;
const START_VERTEX$1 = 1;
const END_VERTEX$1 = 2;
var Constants = /*#__PURE__*/Object.freeze({
__proto__: null,
BOUNDARY: BOUNDARY$1,
CCW: CCW,
CONTAINS: CONTAINS,
CW: CW,
END_VERTEX: END_VERTEX$1,
INSIDE: INSIDE$2,
INTERLACE: INTERLACE,
NOT_VERTEX: NOT_VERTEX$1,
ORIENTATION: ORIENTATION,
OUTSIDE: OUTSIDE$1,
OVERLAP_OPPOSITE: OVERLAP_OPPOSITE$1,
OVERLAP_SAME: OVERLAP_SAME$1,
PIx2: PIx2,
START_VERTEX: START_VERTEX$1
});
/**
* Created by Alex Bol on 2/18/2017.
*/
/**
* Floating point comparison tolerance.
* Default value is 0.000001 (10e-6)
* @type {number}
*/
let DP_TOL = 0.000001;
/**
* Set new floating point comparison tolerance
* @param {number} tolerance
*/
function setTolerance(tolerance) {DP_TOL = tolerance;}
/**
* Get floating point comparison tolerance
* @returns {number}
*/
function getTolerance() {return DP_TOL;}
const DECIMALS = 3;
/**
* Returns *true* if value comparable to zero
* @param {number} x
* @param {number} y
* @return {boolean}
*/
function EQ_0(x) {
return (x < DP_TOL && x > -DP_TOL);
}
/**
* Returns *true* if two values are equal up to DP_TOL
* @param {number} x
* @param {number} y
* @return {boolean}
*/
function EQ(x, y) {
return (x - y < DP_TOL && x - y > -DP_TOL);
}
/**
* Returns *true* if first argument greater than second argument up to DP_TOL
* @param {number} x
* @param {number} y
* @return {boolean}
*/
function GT(x, y) {
return (x - y > DP_TOL);
}
/**
* Returns *true* if first argument greater than or equal to second argument up to DP_TOL
* @param {number} x
* @param {number} y
* @returns {boolean}
*/
function GE(x, y) {
return (x - y > -DP_TOL);
}
/**
* Returns *true* if first argument less than second argument up to DP_TOL
* @param {number} x
* @param {number} y
* @return {boolean}
*/
function LT(x, y) {
return (x - y < -DP_TOL)
}
/**
* Returns *true* if first argument less than or equal to second argument up to DP_TOL
* @param {number} x
* @param {number} y
* @return {boolean}
*/
function LE(x, y) {
return (x - y < DP_TOL);
}
var Utils$1 = /*#__PURE__*/Object.freeze({
__proto__: null,
DECIMALS: DECIMALS,
EQ: EQ,
EQ_0: EQ_0,
GE: GE,
GT: GT,
LE: LE,
LT: LT,
getTolerance: getTolerance,
setTolerance: setTolerance
});
let Flatten = {
Utils: Utils$1,
Errors: undefined,
Matrix: undefined,
Planar_set: undefined,
Point: undefined,
Vector: undefined,
Line: undefined,
Circle: undefined,
Segment: undefined,
Arc: undefined,
Box: undefined,
Edge: undefined,
Face: undefined,
Ray: undefined,
Ray_shooting: undefined,
Multiline: undefined,
Polygon: undefined,
Distance: undefined,
Inversion: undefined
};
for (let c in Constants) {Flatten[c] = Constants[c];}
Object.defineProperty(Flatten, 'DP_TOL', {
get:function(){return getTolerance()},
set:function(value){setTolerance(value);}
});
/**
* Created by Alex Bol on 2/19/2017.
*/
/**
* Class of system errors
*/
class Errors {
/**
* Throw error ILLEGAL_PARAMETERS when cannot instantiate from given parameter
* @returns {ReferenceError}
*/
static get ILLEGAL_PARAMETERS() {
return new ReferenceError('Illegal Parameters');
}
/**
* Throw error ZERO_DIVISION to catch situation of zero division
* @returns {Error}
*/
static get ZERO_DIVISION() {
return new Error('Zero division');
}
/**
* Error to throw from BooleanOperations module in case when fixBoundaryConflicts not capable to fix it
* @returns {Error}
*/
static get UNRESOLVED_BOUNDARY_CONFLICT() {
return new Error('Unresolved boundary conflict in boolean operation');
}
/**
* Error to throw from LinkedList:testInfiniteLoop static method
* in case when circular loop detected in linked list
* @returns {Error}
*/
static get INFINITE_LOOP() {
return new Error('Infinite loop');
}
static get CANNOT_COMPLETE_BOOLEAN_OPERATION() {
return new Error('Cannot complete boolean operation')
}
static get CANNOT_INVOKE_ABSTRACT_METHOD() {
return new Error('Abstract method cannot be invoked');
}
static get OPERATION_IS_NOT_SUPPORTED() {
return new Error('Operation is not supported')
}
static get UNSUPPORTED_SHAPE_TYPE() {
return new Error('Unsupported shape type')
}
}
Flatten.Errors = Errors;
/**
* Class implements bidirectional non-circular linked list. <br/>
* LinkedListElement - object of any type that has properties next and prev.
*/
class LinkedList {
constructor(first, last) {
this.first = first;
this.last = last || this.first;
}
[Symbol.iterator]() {
let value = undefined;
return {
next: () => {
value = value ? value.next : this.first;
return {value: value, done: value === undefined};
}
};
};
/**
* Return number of elements in the list
* @returns {number}
*/
get size() {
let counter = 0;
for (let edge of this) {
counter++;
}
return counter;
}
/**
* Return array of elements from start to end,
* If start or end not defined, take first as start, last as end
* @returns {Array}
*/
toArray(start=undefined, end=undefined) {
let elements = [];
let from = start || this.first;
let to = end || this.last;
let element = from;
if (element === undefined) return elements;
do {
elements.push(element);
element = element.next;
} while (element !== to.next);
return elements;
}
/**
* Append new element to the end of the list
* @param {LinkedListElement} element
* @returns {LinkedList}
*/
append(element) {
if (this.isEmpty()) {
this.first = element;
} else {
element.prev = this.last;
this.last.next = element;
}
// update edge to be last
this.last = element;
// nullify non-circular links
this.last.next = undefined;
this.first.prev = undefined;
return this;
}
/**
* Insert new element to the list after elementBefore
* @param {LinkedListElement} newElement
* @param {LinkedListElement} elementBefore
* @returns {LinkedList}
*/
insert(newElement, elementBefore) {
if (this.isEmpty()) {
this.first = newElement;
this.last = newElement;
}
else if (elementBefore === null || elementBefore === undefined) {
newElement.next = this.first;
this.first.prev = newElement;
this.first = newElement;
}
else {
/* set links to new element */
let elementAfter = elementBefore.next;
elementBefore.next = newElement;
if (elementAfter) elementAfter.prev = newElement;
/* set links from new element */
newElement.prev = elementBefore;
newElement.next = elementAfter;
/* extend list if new element added after the last element */
if (this.last === elementBefore)
this.last = newElement;
}
// nullify non-circular links
this.last.next = undefined;
this.first.prev = undefined;
return this;
}
/**
* Remove element from the list
* @param {LinkedListElement} element
* @returns {LinkedList}
*/
remove(element) {
// special case if last edge removed
if (element === this.first && element === this.last) {
this.first = undefined;
this.last = undefined;
} else {
// update linked list
if (element.prev) element.prev.next = element.next;
if (element.next) element.next.prev = element.prev;
// update first if need
if (element === this.first) {
this.first = element.next;
}
// update last if need
if (element === this.last) {
this.last = element.prev;
}
}
return this;
}
/**
* Return true if list is empty
* @returns {boolean}
*/
isEmpty() {
return this.first === undefined;
}
/**
* Throw an error if circular loop detected in the linked list
* @param {LinkedListElement} first element to start iteration
* @throws {Errors.INFINITE_LOOP}
*/
static testInfiniteLoop(first) {
let edge = first;
let controlEdge = first;
do {
if (edge != first && edge === controlEdge) {
throw Errors.INFINITE_LOOP; // new Error("Infinite loop")
}
edge = edge.next;
controlEdge = controlEdge.next.next;
} while (edge != first)
}
}
const defaultAttributes = {
stroke: "black"
};
class SVGAttributes {
constructor(args = defaultAttributes) {
for(const property in args) {
this[property] = args[property];
}
this.stroke = args.stroke ?? defaultAttributes.stroke;
}
toAttributesString() {
return Object.keys(this)
.reduce( (acc, key) =>
acc + (this[key] !== undefined ? this.toAttrString(key, this[key]) : "")
, ``)
}
toAttrString(key, value) {
const SVGKey = key === "className" ? "class" : this.convertCamelToKebabCase(key);
return value === null ? `${SVGKey} ` : `${SVGKey}="${value.toString()}" `
}
convertCamelToKebabCase(str) {
return str
.match(/[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g)
.join('-')
.toLowerCase();
}
}
function convertToString(attrs) {
return new SVGAttributes(attrs).toAttributesString()
}
/**
* Intersection
*
* */
function intersectLine2Line(line1, line2) {
let ip = [];
let [A1, B1, C1] = line1.standard;
let [A2, B2, C2] = line2.standard;
/* Cramer's rule */
let det = A1 * B2 - B1 * A2;
let detX = C1 * B2 - B1 * C2;
let detY = A1 * C2 - C1 * A2;
if (!Flatten.Utils.EQ_0(det)) {
let x, y;
if (B1 === 0) { // vertical line x = C1/A1, where A1 == +1 or -1
x = C1/A1;
y = detY / det;
}
else if (B2 === 0) { // vertical line x = C2/A2, where A2 = +1 or -1
x = C2/A2;
y = detY / det;
}
else if (A1 === 0) { // horizontal line y = C1/B1, where B1 = +1 or -1
x = detX / det;
y = C1/B1;
}
else if (A2 === 0) { // horizontal line y = C2/B2, where B2 = +1 or -1
x = detX / det;
y = C2/B2;
}
else {
x = detX / det;
y = detY / det;
}
ip.push(new Flatten.Point(x, y));
}
return ip;
}
function intersectLine2Circle(line, circle) {
let ip = [];
let prj = circle.pc.projectionOn(line); // projection of circle center on a line
let dist = circle.pc.distanceTo(prj)[0]; // distance from circle center to projection
if (Flatten.Utils.EQ(dist, circle.r)) { // line tangent to circle - return single intersection point
ip.push(prj);
} else if (Flatten.Utils.LT(dist, circle.r)) { // return two intersection points
let delta = Math.sqrt(circle.r * circle.r - dist * dist);
let v_trans, pt;
v_trans = line.norm.rotate90CCW().multiply(delta);
pt = prj.translate(v_trans);
ip.push(pt);
v_trans = line.norm.rotate90CW().multiply(delta);
pt = prj.translate(v_trans);
ip.push(pt);
}
return ip;
}
function intersectLine2Box(line, box) {
let ips = [];
for (let seg of box.toSegments()) {
let ips_tmp = intersectSegment2Line(seg, line);
for (let pt of ips_tmp) {
if (!ptInIntPoints(pt, ips)) {
ips.push(pt);
}
}
}
return ips;
}
function intersectLine2Arc(line, arc) {
let ip = [];
if (intersectLine2Box(line, arc.box).length === 0) {
return ip;
}
let circle = new Flatten.Circle(arc.pc, arc.r);
let ip_tmp = intersectLine2Circle(line, circle);
for (let pt of ip_tmp) {
if (pt.on(arc)) {
ip.push(pt);
}
}
return ip;
}
function intersectSegment2Line(seg, line) {
let ip = [];
// Boundary cases
if (seg.ps.on(line)) {
ip.push(seg.ps);
}
// If both ends lay on line, return two intersection points
if (seg.pe.on(line) && !seg.isZeroLength()) {
ip.push(seg.pe);
}
if (ip.length > 0) {
return ip; // done, intersection found
}
// If zero-length segment and nothing found, return no intersections
if (seg.isZeroLength()) {
return ip;
}
// Not a boundary case, check if both points are on the same side and
// hence there is no intersection
if (seg.ps.leftTo(line) && seg.pe.leftTo(line) ||
!seg.ps.leftTo(line) && !seg.pe.leftTo(line)) {
return ip;
}
// Calculate intersection between lines
let line1 = new Flatten.Line(seg.ps, seg.pe);
return intersectLine2Line(line1, line);
}
function intersectSegment2Segment(seg1, seg2) {
let ip = [];
// quick reject
if (seg1.box.not_intersect(seg2.box)) {
return ip;
}
// Special case of seg1 zero length
if (seg1.isZeroLength()) {
if (seg1.ps.on(seg2)) {
ip.push(seg1.ps);
}
return ip;
}
// Special case of seg2 zero length
if (seg2.isZeroLength()) {
if (seg2.ps.on(seg1)) {
ip.push(seg2.ps);
}
return ip;
}
// Neither seg1 nor seg2 is zero length
let line1 = new Flatten.Line(seg1.ps, seg1.pe);
let line2 = new Flatten.Line(seg2.ps, seg2.pe);
// Check overlapping between segments in case of incidence
// If segments touching, add one point. If overlapping, add two points
if (line1.incidentTo(line2)) {
if (seg1.ps.on(seg2)) {
ip.push(seg1.ps);
}
if (seg1.pe.on(seg2)) {
ip.push(seg1.pe);
}
if (seg2.ps.on(seg1) && !seg2.ps.equalTo(seg1.ps) && !seg2.ps.equalTo(seg1.pe)) {
ip.push(seg2.ps);
}
if (seg2.pe.on(seg1) && !seg2.pe.equalTo(seg1.ps) && !seg2.pe.equalTo(seg1.pe)) {
ip.push(seg2.pe);
}
} else { /* not incident - parallel or intersect */
// Calculate intersection between lines
let new_ip = intersectLine2Line(line1, line2);
if (new_ip.length > 0) {
if (isPointInSegmentBox(new_ip[0], seg1) && isPointInSegmentBox(new_ip[0], seg2)) {
ip.push(new_ip[0]);
}
}
}
return ip;
}
function isPointInSegmentBox(point, segment) {
const box = segment.box;
return Flatten.Utils.LE(point.x, box.xmax) && Flatten.Utils.GE(point.x, box.xmin) &&
Flatten.Utils.LE(point.y, box.ymax) && Flatten.Utils.GE(point.y, box.ymin)
}
function intersectSegment2Circle(segment, circle) {
let ips = [];
if (segment.box.not_intersect(circle.box)) {
return ips;
}
// Special case of zero length segment
if (segment.isZeroLength()) {
let [dist, _] = segment.ps.distanceTo(circle.pc);
if (Flatten.Utils.EQ(dist, circle.r)) {
ips.push(segment.ps);
}
return ips;
}
// Non zero-length segment
let line = new Flatten.Line(segment.ps, segment.pe);
let ips_tmp = intersectLine2Circle(line, circle);
for (let ip of ips_tmp) {
if (ip.on(segment)) {
ips.push(ip);
}
}
return ips;
}
function intersectSegment2Arc(segment, arc) {
let ip = [];
if (segment.box.not_intersect(arc.box)) {
return ip;
}
// Special case of zero-length segment
if (segment.isZeroLength()) {
if (segment.ps.on(arc)) {
ip.push(segment.ps);
}
return ip;
}
// Non-zero length segment
let line = new Flatten.Line(segment.ps, segment.pe);
let circle = new Flatten.Circle(arc.pc, arc.r);
let ip_tmp = intersectLine2Circle(line, circle);
for (let pt of ip_tmp) {
if (pt.on(segment) && pt.on(arc)) {
ip.push(pt);
}
}
return ip;
}
function intersectSegment2Box(segment, box) {
let ips = [];
for (let seg of box.toSegments()) {
let ips_tmp = intersectSegment2Segment(seg, segment);
for (let ip of ips_tmp) {
ips.push(ip);
}
}
return ips;
}
function intersectCircle2Circle(circle1, circle2) {
let ip = [];
if (circle1.box.not_intersect(circle2.box)) {
return ip;
}
let vec = new Flatten.Vector(circle1.pc, circle2.pc);
let r1 = circle1.r;
let r2 = circle2.r;
// Degenerated circle
if (Flatten.Utils.EQ_0(r1) || Flatten.Utils.EQ_0(r2))
return ip;
// In case of equal circles return one leftmost point
if (Flatten.Utils.EQ_0(vec.x) && Flatten.Utils.EQ_0(vec.y) && Flatten.Utils.EQ(r1, r2)) {
ip.push(circle1.pc.translate(-r1, 0));
return ip;
}
let dist = circle1.pc.distanceTo(circle2.pc)[0];
if (Flatten.Utils.GT(dist, r1 + r2)) // circles too far, no intersections
return ip;
if (Flatten.Utils.LT(dist, Math.abs(r1 - r2))) // one circle is contained within another, no intersections
return ip;
// Normalize vector.
vec.x /= dist;
vec.y /= dist;
let pt;
// Case of touching from outside or from inside - single intersection point
// TODO: check this specifically not sure if correct
if (Flatten.Utils.EQ(dist, r1 + r2) || Flatten.Utils.EQ(dist, Math.abs(r1 - r2))) {
pt = circle1.pc.translate(r1 * vec.x, r1 * vec.y);
ip.push(pt);
return ip;
}
// Case of two intersection points
// Distance from first center to center of common chord:
// a = (r1^2 - r2^2 + d^2) / 2d
// Separate for better accuracy
let a = (r1 * r1) / (2 * dist) - (r2 * r2) / (2 * dist) + dist / 2;
let mid_pt = circle1.pc.translate(a * vec.x, a * vec.y);
let h = Math.sqrt(r1 * r1 - a * a);
// let norm;
// norm = vec.rotate90CCW().multiply(h);
pt = mid_pt.translate(vec.rotate90CCW().multiply(h));
ip.push(pt);
// norm = vec.rotate90CW();
pt = mid_pt.translate(vec.rotate90CW().multiply(h));
ip.push(pt);
return ip;
}
function intersectCircle2Box(circle, box) {
let ips = [];
for (let seg of box.toSegments()) {
let ips_tmp = intersectSegment2Circle(seg, circle);
for (let ip of ips_tmp) {
ips.push(ip);
}
}
return ips;
}
function intersectArc2Arc(arc1, arc2) {
let ip = [];
if (arc1.box.not_intersect(arc2.box)) {
return ip;
}
// Special case: overlapping arcs
// May return up to 4 intersection points
if (arc1.pc.equalTo(arc2.pc) && Flatten.Utils.EQ(arc1.r, arc2.r)) {
let pt;
pt = arc1.start;
if (pt.on(arc2))
ip.push(pt);
pt = arc1.end;
if (pt.on(arc2))
ip.push(pt);
pt = arc2.start;
if (pt.on(arc1)) ip.push(pt);
pt = arc2.end;
if (pt.on(arc1)) ip.push(pt);
return ip;
}
// Common case
let circle1 = new Flatten.Circle(arc1.pc, arc1.r);
let circle2 = new Flatten.Circle(arc2.pc, arc2.r);
let ip_tmp = circle1.intersect(circle2);
for (let pt of ip_tmp) {
if (pt.on(arc1) && pt.on(arc2)) {
ip.push(pt);
}
}
return ip;
}
function intersectArc2Circle(arc, circle) {
let ip = [];
if (arc.box.not_intersect(circle.box)) {
return ip;
}
// Case when arc center incident to circle center
// Return arc's end points as 2 intersection points
if (circle.pc.equalTo(arc.pc) && Flatten.Utils.EQ(circle.r, arc.r)) {
ip.push(arc.start);
ip.push(arc.end);
return ip;
}
// Common case
let circle1 = circle;
let circle2 = new Flatten.Circle(arc.pc, arc.r);
let ip_tmp = intersectCircle2Circle(circle1, circle2);
for (let pt of ip_tmp) {
if (pt.on(arc)) {
ip.push(pt);
}
}
return ip;
}
function intersectArc2Box(arc, box) {
let ips = [];
for (let seg of box.toSegments()) {
let ips_tmp = intersectSegment2Arc(seg, arc);
for (let ip of ips_tmp) {
ips.push(ip);
}
}
return ips;
}
function intersectEdge2Segment(edge, segment) {
return edge.isSegment ? intersectSegment2Segment(edge.shape, segment) : intersectSegment2Arc(segment, edge.shape);
}
function intersectEdge2Arc(edge, arc) {
return edge.isSegment ? intersectSegment2Arc(edge.shape, arc) : intersectArc2Arc(edge.shape, arc);
}
function intersectEdge2Line(edge, line) {
return edge.isSegment ? intersectSegment2Line(edge.shape, line) : intersectLine2Arc(line, edge.shape);
}
function intersectEdge2Ray(edge, ray) {
return edge.isSegment ? intersectRay2Segment(ray, edge.shape) : intersectRay2Arc(ray, edge.shape);
}
function intersectEdge2Circle(edge, circle) {
return edge.isSegment ? intersectSegment2Circle(edge.shape, circle) : intersectArc2Circle(edge.shape, circle);
}
function intersectSegment2Polygon(segment, polygon) {
let ip = [];
for (let edge of polygon.edges) {
for (let pt of intersectEdge2Segment(edge, segment)) {
ip.push(pt);
}
}
return ip;
}
function intersectArc2Polygon(arc, polygon) {
let ip = [];
for (let edge of polygon.edges) {
for (let pt of intersectEdge2Arc(edge, arc)) {
ip.push(pt);
}
}
return ip;
}
function intersectLine2Polygon(line, polygon) {
let ip = [];
if (polygon.isEmpty()) {
return ip;
}
for (let edge of polygon.edges) {
for (let pt of intersectEdge2Line(edge, line)) {
if (!ptInIntPoints(pt, ip)) {
ip.push(pt);
}
}
}
return line.sortPoints(ip);
}
function intersectCircle2Polygon(circle, polygon) {
let ip = [];
if (polygon.isEmpty()) {
return ip;
}
for (let edge of polygon.edges) {
for (let pt of intersectEdge2Circle(edge, circle)) {
ip.push(pt);
}
}
return ip;
}
function intersectEdge2Edge(edge1, edge2) {
if (edge1.isSegment) {
return intersectEdge2Segment(edge2, edge1.shape)
}
else if (edge1.isArc) {
return intersectEdge2Arc(edge2, edge1.shape)
}
else if (edge1.isLine) {
return intersectEdge2Line(edge2, edge1.shape)
}
else if (edge1.isRay) {
return intersectEdge2Ray(edge2, edge1.shape)
}
return []
}
function intersectEdge2Polygon(edge, polygon) {
let ip = [];
if (polygon.isEmpty() || edge.shape.box.not_intersect(polygon.box)) {
return ip;
}
let resp_edges = polygon.edges.search(edge.shape.box);
for (let resp_edge of resp_edges) {
ip = [...ip, ...intersectEdge2Edge(edge, resp_edge)];
}
return ip;
}
function intersectMultiline2Polygon(multiline, polygon) {
let ip = [];
if (polygon.isEmpty() || multiline.size === 0) {
return ip;
}
for (let edge of multiline) {
ip = [...ip, ...intersectEdge2Polygon(edge, polygon)];
}
return ip;
}
function intersectPolygon2Polygon(polygon1, polygon2) {
let ip = [];
if (polygon1.isEmpty() || polygon2.isEmpty()) {
return ip;
}
if (polygon1.box.not_intersect(polygon2.box)) {
return ip;
}
for (let edge1 of polygon1.edges) {
ip = [...ip, ...intersectEdge2Polygon(edge1, polygon2)];
}
return ip;
}
function intersectShape2Polygon(shape, polygon) {
if (shape instanceof Flatten.Line) {
return intersectLine2Polygon(shape, polygon);
}
else if (shape instanceof Flatten.Segment) {
return intersectSegment2Polygon(shape, polygon);
}
else if (shape instanceof Flatten.Arc) {
return intersectArc2Polygon(shape, polygon);
}
else {
return [];
}
}
function ptInIntPoints(new_pt, ip) {
return ip.some( pt => pt.equalTo(new_pt) )
}
function createLineFromRay(ray) {
return new Flatten.Line(ray.start, ray.norm)
}
function intersectRay2Segment(ray, segment) {
return intersectSegment2Line(segment, createLineFromRay(ray))
.filter(pt => ray.contains(pt));
}
function intersectRay2Arc(ray, arc) {
return intersectLine2Arc(createLineFromRay(ray), arc)
.filter(pt => ray.contains(pt))
}
function intersectRay2Circle(ray, circle) {
return intersectLine2Circle(createLineFromRay(ray), circle)
.filter(pt => ray.contains(pt))
}
function intersectRay2Box(ray, box) {
return intersectLine2Box(createLineFromRay(ray), box)
.filter(pt => ray.contains(pt))
}
function intersectRay2Line(ray, line) {
return intersectLine2Line(createLineFromRay(ray), line)
.filter(pt => ray.contains(pt))
}
function intersectRay2Ray(ray1, ray2) {
return intersectLine2Line(createLineFromRay(ray1), createLineFromRay(ray2))
.filter(pt => ray1.contains(pt))
.filter(pt => ray2.contains(pt))
}
function intersectRay2Polygon(ray, polygon) {
return intersectLine2Polygon(createLineFromRay(ray), polygon)
.filter(pt => ray.contains(pt))
}
function intersectShape2Shape(shape1, shape2) {
if (shape1.intersect && shape1.intersect instanceof Function) {
return shape1.intersect(shape2)
}
throw Errors.UNSUPPORTED_SHAPE_TYPE
}
function intersectShape2Multiline(shape, multiline) {
let ip = [];
for (let edge of multiline) {
ip = [...ip, ...intersectShape2Shape(shape, edge.shape)];
}
return ip;
}
function intersectMultiline2Multiline(multiline1, multiline2) {
let ip = [];
for (let edge1 of multiline1) {
for (let edge2 of multiline2) {
ip = [...ip, ...intersectShape2Shape(edge1, edge2)];
}
}
return ip;
}
/**
* Class Multiline represent connected path of [edges]{@link Flatten.Edge}, where each edge may be
* [segment]{@link Flatten.Segment}, [arc]{@link Flatten.Arc}, [line]{@link Flatten.Line} or [ray]{@link Flatten.Ray}
*/
let Multiline$1 = class Multiline extends LinkedList {
constructor(...args) {
super();
this.isInfinite = false;
if (args.length === 1 && args[0] instanceof Array && args[0].length > 0) {
// there may be only one line and
// only first and last may be rays
let validShapes = false;
const shapes = args[0];
const L = shapes.length;
const anyShape = (s) =>
s instanceof Flatten.Segment || s instanceof Flatten.Arc ||
s instanceof Flatten.Ray || s instanceof Flatten.Line;
const anyShapeExceptLine = (s) =>
s instanceof Flatten.Segment || s instanceof Flatten.Arc || s instanceof Flatten.Ray;
const shapeSegmentOrArc = (s) => s instanceof Flatten.Segment || s instanceof Flatten.Arc;
validShapes =
L === 1 && anyShape(shapes[0]) ||
L > 1 && anyShapeExceptLine(shapes[0]) && anyShapeExceptLine(shapes[L - 1]) &&
shapes.slice(1, L - 1).every(shapeSegmentOrArc);
if (validShapes) {
this.isInfinite = shapes.some(shape =>
shape instanceof Flatten.Ray ||
shape instanceof Flatten.Line
);
for (let shape of shapes) {
let edge = new Flatten.Edge(shape);
this.append(edge);
}
this.setArcLength();
} else {
throw Flatten.Errors.ILLEGAL_PARAMETERS;
}
}
}
/**
* (Getter) Return array of edges
* @returns {Edge[]}
*/
get edges() {
return [...this];
}
/**
* (Getter) Return bounding box of the multiline
* @returns {Box}
*/
get box() {
return this.edges.reduce( (acc,edge) => acc.merge(edge.box), new Flatten.Box() );
}
/**
* (Getter) Returns array of vertices
* @returns {Point[]}
*/
get vertices() {
let v = this.edges.map(edge => edge.start);
v.push(this.last.end);
return v;
}
/**
* (Getter) Returns length of the multiline, return POSITIVE_INFINITY if multiline is infinite
* @returns {number}
*/
get length() {
if (this.isEmpty()) return 0;
if (this.isInfinite) return Number.POSITIVE_INFINITY;
let len = 0;
for (let edge of this) {
len += edge.length;
}
return len
}
/**
* Return new cloned instance of Multiline
* @returns {Multiline}
*/
clone() {
return new Multiline(this.toShapes());
}
/**
* Set arc_length property for each of the edges in the multiline.
* Arc_length of the edge is the arc length from the multiline start vertex to the edge start vertex
*/
setArcLength() {
for (let edge of this) {
this.setOneEdgeArcLength(edge);
}
}
setOneEdgeArcLength(edge) {
if (edge === this.first) {
edge.arc_length = 0.0;
} else {
edge.arc_length = edge.prev.arc_length + edge.prev.length;
}
}
/**
* Return point on multiline at given length from the start of the multiline
* @param length
* @returns {Point | null}
*/
pointAtLength(length) {
if (length > this.length || length < 0) return null;
if (this.isInfinite) return null
let point = null;
for (let edge of this) {
if (length >= edge.arc_length &&
(edge === this.last || length < edge.next.arc_length)) {
point = edge.pointAtLength(length - edge.arc_length);
break;
}
}
return point;
}
/**
* Split edge and add new vertex, return new edge inserted
* @param {Point} pt - point on edge that will be added as new vertex
* @param {Edge} edge - edge to split
* @returns {Edge}
*/
addVertex(pt, edge) {
let shapes = edge.shape.split(pt);
// if (shapes.length < 2) return;
if (shapes[0] === null) // point incident to edge start vertex, return previous edge
return edge.prev;
if (shapes[1] === null) // point incident to edge end vertex, return edge itself
return edge;
let newEdge = new Flatten.Edge(shapes[0]);
let edgeBefore = edge.prev;
/* Insert first split edge into linked list after edgeBefore */
this.insert(newEdge, edgeBefore); // edge.face ?
// Update edge shape with second split edge keeping links
edge.shape = shapes[1];
return newEdge;
}
getChain(edgeFrom, edgeTo) {
let edges = [];
for (let edge = edgeFrom; edge !== edgeTo.next; edge = edge.next) {
edges.push(edge);
}
return edges
}
/**
* Split edges of multiline with intersection points and return mutated multiline
* @param {Point[]} ip - array of points to be added as new vertices
* @returns {Multiline}
*/
split(ip) {
for (let pt of ip) {
let edge = this.findEdgeByPoint(pt);
this.addVertex(pt, edge);
}
return this;
}
/**
* Returns edge which contains given point
* @param {Point} pt
* @returns {Edge}
*/
findEdgeByPoint(pt) {
let edgeFound;
for (let edge of this) {
if (edge.shape.contains(pt)) {
edgeFound = edge;
break;
}
}
return edgeFound;
}
/**
* Calculate distance and shortest segment from any shape to multiline
* @param shape
* @returns {[number,Flatten.Segment]}
*/
distanceTo(shape) {
if (shape instanceof Point) {
const [dist, shortest_segment] = Flatten.Distance.shape2multiline(shape, this);
return [dist, shortest_segment.reverse()];
}
if (shape instanceof Flatten.Line) {
const [dist, shortest_segment] = Flatten.Distance.shape2multiline(shape, this);
return [dist, shortest_segment.reverse()];
}
if (shape instanceof Flatten.Circle) {
const [dist, shortest_segment] = Flatten.Distance.shape2multiline(shape, this);
return [dist, shortest_segment.reverse()];
}
if (shape instanceof Flatten.Segment) {
const [dist, shortest_segment] = Flatten.Distance.shape2multiline(shape, this);
return [dist, shortest_segment.reverse()];
}
if (shape instanceof Flatten.Arc) {
const [dist, shortest_segment] = Flatten.Distance.shape2multiline(shape, this);
return [dist, shortest_segment.reverse()];
}
if (shape instanceof Flatten.Multiline) {
return Flatten.Distance.multiline2multiline(this, shape);
}
throw Flatten.Errors.UNSUPPORTED_SHAPE_TYPE;
}
/**
* Calculate intersection of multiline with other shape
* @param {Shape} shape
* @returns {Point[]}
*/
intersect(shape) {
if (shape instanceof Flatten.Multiline) {
return intersectMultiline2Multiline(this, shape);
}
else {
return intersectShape2Multiline(shape, this);
}
}
/**
* Return true if multiline contains the shape: no point of shape lies outside
* @param shape
* @returns {boolean}
*/
contains(shape) {
if (shape instanceof Flatten.Point) {
return this.edges.some(edge => edge.shape.contains(shape));
}
throw Flatten.Errors.UNSUPPORTED_SHAPE_TYPE;
}
/**
* Returns new multiline translated by vector vec
* @param {Vector} vec
* @returns {Multiline}
*/
translate(vec) {
return new Multiline(this.edges.map( edge => edge.shape.translate(vec)));
}
/**
* Return new multiline rotated by given angle around given point
* If point omitted, rotate around origin (0,0)
* Positive value of angle defines rotation counterclockwise, negative - clockwise
* @param {number} angle - rotation angle in radians
* @param {Point} center - rotation center, default is (0,0)
* @returns {Multiline} - new rotated polygon
*/
rotate(angle = 0, center = new Flatten.Point()) {
return new Multiline(this.edges.map( edge => edge.shape.rotate(angle, center) ));
}
/**
* Return new multiline transformed using affine transformation matrix
* Method does not support unbounded shapes
* @param {Matrix} matrix - affine transformation matrix
* @returns {Multiline} - new multiline
*/
transform(matrix = new Flatten.Matrix()) {
return new Multiline(this.edges.map( edge => edge.shape.transform(matrix)));
}
/**
* Transform multiline into array of shapes
* @returns {Shape[]}
*/
toShapes() {
return this.edges.map(edge => edge.shape.clone())
}
/**
* This method returns an object that defines how data will be
* serialized when called JSON.stringify() method
* @returns {Object}
*/
toJSON() {
return this.edges.map(edge => edge.toJSON());
}
/**
* Return string to be inserted into 'points' attribute of <polyline> element
* @returns {string}
*/
svgPoints() {
return this.vertices.map(p => `${p.x},${p.y}`).join(' ')
}
/**
* Return string to be assigned to 'd' attribute of <path> element
* @returns {*}
*/
dpath() {
let dPathStr = `M${this.first.start.x},${this.first.start.y}`;
for (let edge of this) {
dPathStr += edge.svg();
}
return dPathStr
}
/**
* Return string to draw multiline in svg
* @param attrs - an object with attributes for svg path element
* TODO: support semi-infinite Ray and infinite Line
* @returns {string}
*/
svg(attrs = {}) {
let svgStr = `\n<path ${convertToString({fill: "none", ...attrs})} d="`;
svgStr += `\nM${this.first.start.x},${this.first.start.y}`;
for (let edge of this) {
svgStr += edge.svg();
}
svgStr += `" >\n</path>`;
return svgStr;
}
};
Flatten.Multiline = Multiline$1;
/**
* Shortcut function to create multiline
* @param args
*/
const multiline = (...args) => new Flatten.Multiline(...args);
Flatten.multiline = multiline;
/*
Smart intersections describe intersection points that refers to the edges they intersect
This function are supposed for internal usage by morphing and relation methods between
*/
function addToIntPoints(edge, pt, int_points)
{
let id = int_points.length;
let shapes = edge.shape.split(pt);
// if (shapes.length < 2) return;
if (shapes.length === 0) return; // Point does not belong to edge ?
let len = 0;
if (shapes[0] === null) { // point incident to edge start vertex
len = 0;
}
else if (shapes[1] === null) { // point incident to edge end vertex
len = edge.shape.length;
}
else { // Edge was split into to edges
len = shapes[0].length;
}
let is_vertex = NOT_VERTEX$1;
if (EQ(len, 0)) {
is_vertex |= START_VERTEX$1;
}
if (EQ(len, edge.shape.length)) {
is_vertex |= END_VERTEX$1;
}
// Fix intersection point which is end point of the last edge
let arc_length;
if (len === Infinity) {
arc_length = shapes[0].coord(pt);
}
else {
arc_length = (is_vertex & END_VERTEX$1) && edge.next && edge.next.arc_length === 0 ?
0 :
edge.arc_length + len;
}
int_points.push({
id: id,
pt: pt,
arc_length: arc_length,
edge_before: edge,
edge_after: undefined,
face: edge.face,
is_vertex: is_vertex
});
}
function sortIntersections(intersections)
{
// augment intersections with new sorted arrays
intersections.int_points1_sorted = getSortedArray(intersections.int_points1);
intersections.int_points2_sorted = getSortedArray(intersections.int_points2);
}
function getSortedArray(int_points)
{
let faceMap = new Map;
let id = 0;
// Create integer id's for faces
for (let ip of int_points) {
if (!faceMap.has(ip.face)) {
faceMap.set(ip.face, id);
id++;
}
}
// Augment intersection points with face id's
for (let ip of int_points) {
ip.faceId = faceMap.get(ip.face);
}
// Clone and sort
let int_points_sorted = int_points.slice().sort(compareFn);
return int_points_sorted;
}
function compareFn(ip1, ip2)
{
// compare face id's
if (ip1.faceId < ip2.faceId) {
return -1;
}
if (ip1.faceId > ip2.faceId) {
return 1;
}
// same face - compare arc_length
if (ip1.arc_length < ip2.arc_length) {
return -1;
}
if (ip1.arc_length > ip2.arc_length) {
return 1;
}
return 0;
}
function filterDuplicatedIntersections(intersections)
{
if (intersections.int_points1.length < 2) return;
let do_squeeze = false;
let int_point_ref1;
let int_point_ref2;
let int_point_cur1;
let int_point_cur2;
for (let i = 0; i < intersections.int_points1_sorted.length; i++) {
if (intersections.int_points1_sorted[i].id === -1)
continue;
int_point_ref1 = intersections.int_points1_sorted[i];
int_point_ref2 = intersections.int_points2[int_point_ref1.id];
for (let j=i+1; j < intersections.int_points1_sorted.length; j++) {
int_point_cur1 = intersections.int_points1_sorted[j];
if (!EQ(int_point_cur1.arc_length, int_point_ref1.arc_length)) {
break;
}
if (int_point_cur1.id === -1)
continue;
int_point_cur2 = intersections.int_points2[int_point_cur1.id];
if (int_point_cur2.id === -1)
continue;
if (int_point_cur1.edge_before === int_point_ref1.edge_before &&
int_point_cur1.edge_after === int_point_ref1.edge_after &&
int_point_cur2.edge_before === int_point_ref2.edge_before &&
int_point_cur2.edge_after === int_point_ref2.edge_after) {
int_point_cur1.id = -1;
/* to be deleted */
int_point_cur2.id = -1;
/* to be deleted */
do_squeeze = true;
}
}
}
int_point_ref2 = intersections.int_points2_sorted[0];
int_point_ref1 = intersections.int_points1[int_point_ref2.id];
for (let i = 1; i < intersections.int_points2_sorted.length; i++) {
let int_point_cur2 = intersections.int_points2_sorted[i];
if (int_point_cur2.id === -1) continue;
/* already deleted */
if (int_point_ref2.id === -1 || /* can't be reference if already deleted */
!(EQ(int_point_cur2.arc_length, int_