UNPKG

@flatten-js/core

Version:

Javascript library for 2d geometry

1,594 lines (1,352 loc) 346 kB
(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_