diff --git a/dist/main.cjs b/dist/main.cjs index b9be8df..741c310 100644 --- a/dist/main.cjs +++ b/dist/main.cjs @@ -399,6 +399,271 @@ class LinkedList { } } +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() +} + +/** + * 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} + */ +class Multiline extends LinkedList { + constructor(...args) { + super(); + + if (args.length === 0) { + return; + } + + if (args.length === 1) { + if (args[0] instanceof Array) { + let shapes = args[0]; + if (shapes.length === 0) + return; + + // TODO: more strict validation: + // there may be only one line + // only first and last may be rays + shapes.every((shape) => { + return shape instanceof Flatten.Segment || + shape instanceof Flatten.Arc || + shape instanceof Flatten.Ray || + shape instanceof Flatten.Line + }); + + for (let shape of shapes) { + let edge = new Flatten.Edge(shape); + this.append(edge); + } + + this.setArcLength(); + } + } + } + + /** + * (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; + } + + /** + * 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 face. + * Arc_length of the edge it the arc length from the first edge of the face + */ + 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; + } + } + + /** + * 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; + } + + /** + * 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 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\n`; + return svgStr; + } +} + +Flatten.Multiline = Multiline; + +/** + * 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 @@ -454,11 +719,7 @@ function addToIntPoints(edge, pt, int_points) function sortIntersections(intersections) { - // if (intersections.int_points1.length === 0) return; - // augment intersections with new sorted arrays - // intersections.int_points1_sorted = intersections.int_points1.slice().sort(compareFn); - // intersections.int_points2_sorted = intersections.int_points2.slice().sort(compareFn); intersections.int_points1_sorted = getSortedArray(intersections.int_points1); intersections.int_points2_sorted = getSortedArray(intersections.int_points2); } @@ -502,18 +763,6 @@ function compareFn(ip1, ip2) return 0; } -// export function getSortedArrayOnLine(line, int_points) { -// return int_points.slice().sort( (int_point1, int_point2) => { -// if (line.coord(int_point1.pt) < line.coord(int_point2.pt)) { -// return -1; -// } -// if (line.coord(int_point1.pt) > line.coord(int_point2.pt)) { -// return 1; -// } -// return 0; -// }) -// } - function filterDuplicatedIntersections(intersections) { if (intersections.int_points1.length < 2) return; @@ -745,14 +994,10 @@ function splitByIntersections(polygon, int_points) int_point.is_vertex |= END_VERTEX$1; } - if (int_point.is_vertex & START_VERTEX$1) { // nothing to split + if (int_point.is_vertex & START_VERTEX$1) { // nothing to split + int_point.edge_before = edge.prev; if (edge.prev) { - int_point.edge_before = edge.prev; // polygon - int_point.is_vertex = END_VERTEX$1; - } - else { // multiline start vertex - int_point.edge_after = int_point.edge_before; - int_point.edge_before = edge.prev; + int_point.is_vertex = END_VERTEX$1; // polygon } continue; } @@ -768,6 +1013,11 @@ function splitByIntersections(polygon, int_points) if (int_point.edge_before) { int_point.edge_after = int_point.edge_before.next; } + else { + if (polygon instanceof Multiline && int_point.is_vertex & START_VERTEX$1) { + int_point.edge_after = polygon.first; + } + } } } @@ -2026,547 +2276,282 @@ function intersectCircle2Circle(circle1, circle2) { 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); - } + +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 line.sortPoints(ip); + return ips; } -function intersectCircle2Polygon(circle, polygon) { +function intersectArc2Arc(arc1, arc2) { let ip = []; - if (polygon.isEmpty()) { + if (arc1.box.not_intersect(arc2.box)) { return ip; } - for (let edge of polygon.edges) { - for (let pt of intersectEdge2Circle(edge, circle)) { + // 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); - } - } - return ip; -} + pt = arc1.end; + if (pt.on(arc2)) + ip.push(pt); -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 [] -} + pt = arc2.start; + if (pt.on(arc1)) ip.push(pt); -function intersectEdge2Polygon(edge, polygon) { - let ip = []; + pt = arc2.end; + if (pt.on(arc1)) ip.push(pt); - 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)]; + // 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 intersectPolygon2Polygon(polygon1, polygon2) { +function intersectArc2Circle(arc, circle) { let ip = []; - if (polygon1.isEmpty() || polygon2.isEmpty()) { + if (arc.box.not_intersect(circle.box)) { return ip; } - if (polygon1.box.not_intersect(polygon2.box)) { + // 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; } - for (let edge1 of polygon1.edges) { - ip = [...ip, ...intersectEdge2Polygon(edge1, polygon2)]; + // 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 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 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 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 intersectEdge2Segment(edge, segment) { + return edge.isSegment ? intersectSegment2Segment(edge.shape, segment) : intersectSegment2Arc(segment, edge.shape); } -function intersectRay2Polygon(ray, polygon) { - return intersectLine2Polygon(createLineFromRay(ray), polygon) - .filter(pt => ray.contains(pt)) +function intersectEdge2Arc(edge, arc) { + return edge.isSegment ? intersectSegment2Arc(edge.shape, arc) : intersectArc2Arc(edge.shape, arc); } -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 intersectEdge2Line(edge, line) { + return edge.isSegment ? intersectSegment2Line(edge.shape, line) : intersectLine2Arc(line, edge.shape); } -function convertToString(attrs) { - return new SVGAttributes(attrs).toAttributesString() +function intersectEdge2Ray(edge, ray) { + return edge.isSegment ? intersectRay2Segment(ray, edge.shape) : intersectRay2Arc(ray, edge.shape); } -/** - * 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} - */ -class Multiline extends LinkedList { - constructor(...args) { - super(); - - if (args.length === 0) { - return; - } - - if (args.length === 1) { - if (args[0] instanceof Array) { - let shapes = args[0]; - if (shapes.length === 0) - return; - - // TODO: more strict validation: - // there may be only one line - // only first and last may be rays - shapes.every((shape) => { - return shape instanceof Flatten.Segment || - shape instanceof Flatten.Arc || - shape instanceof Flatten.Ray || - shape instanceof Flatten.Line - }); - - for (let shape of shapes) { - let edge = new Flatten.Edge(shape); - this.append(edge); - } - - this.setArcLength(); - } - } - } - - /** - * (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; - } - - /** - * Return new cloned instance of Multiline - * @returns {Multiline} - */ - clone() { - return new Multiline(this.toShapes()); - } +function intersectEdge2Circle(edge, circle) { + return edge.isSegment ? intersectSegment2Circle(edge.shape, circle) : intersectArc2Circle(edge.shape, circle); +} - /** - * Set arc_length property for each of the edges in the face. - * Arc_length of the edge it the arc length from the first edge of the face - */ - setArcLength() { - for (let edge of this) { - this.setOneEdgeArcLength(edge); +function intersectSegment2Polygon(segment, polygon) { + let ip = []; + + for (let edge of polygon.edges) { + for (let pt of intersectEdge2Segment(edge, segment)) { + ip.push(pt); } } - setOneEdgeArcLength(edge) { - if (edge === this.first) { - edge.arc_length = 0.0; - } else { - edge.arc_length = edge.prev.arc_length + edge.prev.length; + return ip; +} + +function intersectArc2Polygon(arc, polygon) { + let ip = []; + + for (let edge of polygon.edges) { + for (let pt of intersectEdge2Arc(edge, arc)) { + ip.push(pt); } } - /** - * 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; + return ip; +} - if (shapes[0] === null) // point incident to edge start vertex, return previous edge - return edge.prev; +function intersectLine2Polygon(line, polygon) { + let ip = []; - if (shapes[1] === null) // point incident to edge end vertex, return edge itself - return edge; + if (polygon.isEmpty()) { + return ip; + } - let newEdge = new Flatten.Edge(shapes[0]); - let edgeBefore = edge.prev; + for (let edge of polygon.edges) { + for (let pt of intersectEdge2Line(edge, line)) { + if (!ptInIntPoints(pt, ip)) { + ip.push(pt); + } + } + } - /* Insert first split edge into linked list after edgeBefore */ - this.insert(newEdge, edgeBefore); // edge.face ? + return line.sortPoints(ip); +} - // Update edge shape with second split edge keeping links - edge.shape = shapes[1]; +function intersectCircle2Polygon(circle, polygon) { + let ip = []; - return newEdge; + if (polygon.isEmpty()) { + return ip; } - getChain(edgeFrom, edgeTo) { - let edges = []; - for (let edge = edgeFrom; edge !== edgeTo.next; edge = edge.next) { - edges.push(edge); + for (let edge of polygon.edges) { + for (let pt of intersectEdge2Circle(edge, circle)) { + ip.push(pt); } - 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; - } + return ip; +} - /** - * 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; +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 [] +} - /** - * 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))); +function intersectEdge2Polygon(edge, polygon) { + let ip = []; + + if (polygon.isEmpty() || edge.shape.box.not_intersect(polygon.box)) { + return ip; } - /** - * 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) )); + let resp_edges = polygon.edges.search(edge.shape.box); + + for (let resp_edge of resp_edges) { + ip = [...ip, ...intersectEdge2Edge(edge, resp_edge)]; } - /** - * 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))); + return ip; +} + +function intersectPolygon2Polygon(polygon1, polygon2) { + let ip = []; + + if (polygon1.isEmpty() || polygon2.isEmpty()) { + return ip; } - /** - * Transform multiline into array of shapes - * @returns {Shape[]} - */ - toShapes() { - return this.edges.map(edge => edge.shape.clone()) + if (polygon1.box.not_intersect(polygon2.box)) { + return ip; } - /** - * 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()); + for (let edge1 of polygon1.edges) { + ip = [...ip, ...intersectEdge2Polygon(edge1, polygon2)]; } - /** - * 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\n`; - return svgStr; + 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 []; } } -Flatten.Multiline = Multiline; +function ptInIntPoints(new_pt, ip) { + return ip.some( pt => pt.equalTo(new_pt) ) +} -/** - * Shortcut function to create multiline - * @param args - */ -const multiline = (...args) => new Flatten.Multiline(...args); -Flatten.multiline = multiline; +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)) +} /** * @module RayShoot diff --git a/dist/main.mjs b/dist/main.mjs index 3ba0f91..a30c8a8 100644 --- a/dist/main.mjs +++ b/dist/main.mjs @@ -395,6 +395,271 @@ class LinkedList { } } +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() +} + +/** + * 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} + */ +class Multiline extends LinkedList { + constructor(...args) { + super(); + + if (args.length === 0) { + return; + } + + if (args.length === 1) { + if (args[0] instanceof Array) { + let shapes = args[0]; + if (shapes.length === 0) + return; + + // TODO: more strict validation: + // there may be only one line + // only first and last may be rays + shapes.every((shape) => { + return shape instanceof Flatten.Segment || + shape instanceof Flatten.Arc || + shape instanceof Flatten.Ray || + shape instanceof Flatten.Line + }); + + for (let shape of shapes) { + let edge = new Flatten.Edge(shape); + this.append(edge); + } + + this.setArcLength(); + } + } + } + + /** + * (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; + } + + /** + * 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 face. + * Arc_length of the edge it the arc length from the first edge of the face + */ + 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; + } + } + + /** + * 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; + } + + /** + * 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 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\n`; + return svgStr; + } +} + +Flatten.Multiline = Multiline; + +/** + * 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 @@ -450,11 +715,7 @@ function addToIntPoints(edge, pt, int_points) function sortIntersections(intersections) { - // if (intersections.int_points1.length === 0) return; - // augment intersections with new sorted arrays - // intersections.int_points1_sorted = intersections.int_points1.slice().sort(compareFn); - // intersections.int_points2_sorted = intersections.int_points2.slice().sort(compareFn); intersections.int_points1_sorted = getSortedArray(intersections.int_points1); intersections.int_points2_sorted = getSortedArray(intersections.int_points2); } @@ -498,18 +759,6 @@ function compareFn(ip1, ip2) return 0; } -// export function getSortedArrayOnLine(line, int_points) { -// return int_points.slice().sort( (int_point1, int_point2) => { -// if (line.coord(int_point1.pt) < line.coord(int_point2.pt)) { -// return -1; -// } -// if (line.coord(int_point1.pt) > line.coord(int_point2.pt)) { -// return 1; -// } -// return 0; -// }) -// } - function filterDuplicatedIntersections(intersections) { if (intersections.int_points1.length < 2) return; @@ -741,14 +990,10 @@ function splitByIntersections(polygon, int_points) int_point.is_vertex |= END_VERTEX$1; } - if (int_point.is_vertex & START_VERTEX$1) { // nothing to split + if (int_point.is_vertex & START_VERTEX$1) { // nothing to split + int_point.edge_before = edge.prev; if (edge.prev) { - int_point.edge_before = edge.prev; // polygon - int_point.is_vertex = END_VERTEX$1; - } - else { // multiline start vertex - int_point.edge_after = int_point.edge_before; - int_point.edge_before = edge.prev; + int_point.is_vertex = END_VERTEX$1; // polygon } continue; } @@ -764,6 +1009,11 @@ function splitByIntersections(polygon, int_points) if (int_point.edge_before) { int_point.edge_after = int_point.edge_before.next; } + else { + if (polygon instanceof Multiline && int_point.is_vertex & START_VERTEX$1) { + int_point.edge_after = polygon.first; + } + } } } @@ -2022,547 +2272,282 @@ function intersectCircle2Circle(circle1, circle2) { 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); - } + +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 line.sortPoints(ip); + return ips; } -function intersectCircle2Polygon(circle, polygon) { +function intersectArc2Arc(arc1, arc2) { let ip = []; - if (polygon.isEmpty()) { + if (arc1.box.not_intersect(arc2.box)) { return ip; } - for (let edge of polygon.edges) { - for (let pt of intersectEdge2Circle(edge, circle)) { + // 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); - } - } - return ip; -} + pt = arc1.end; + if (pt.on(arc2)) + ip.push(pt); -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 [] -} + pt = arc2.start; + if (pt.on(arc1)) ip.push(pt); -function intersectEdge2Polygon(edge, polygon) { - let ip = []; + pt = arc2.end; + if (pt.on(arc1)) ip.push(pt); - 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)]; + // 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 intersectPolygon2Polygon(polygon1, polygon2) { +function intersectArc2Circle(arc, circle) { let ip = []; - if (polygon1.isEmpty() || polygon2.isEmpty()) { + if (arc.box.not_intersect(circle.box)) { return ip; } - if (polygon1.box.not_intersect(polygon2.box)) { + // 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; } - for (let edge1 of polygon1.edges) { - ip = [...ip, ...intersectEdge2Polygon(edge1, polygon2)]; + // 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 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 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 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 intersectEdge2Segment(edge, segment) { + return edge.isSegment ? intersectSegment2Segment(edge.shape, segment) : intersectSegment2Arc(segment, edge.shape); } -function intersectRay2Polygon(ray, polygon) { - return intersectLine2Polygon(createLineFromRay(ray), polygon) - .filter(pt => ray.contains(pt)) +function intersectEdge2Arc(edge, arc) { + return edge.isSegment ? intersectSegment2Arc(edge.shape, arc) : intersectArc2Arc(edge.shape, arc); } -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 intersectEdge2Line(edge, line) { + return edge.isSegment ? intersectSegment2Line(edge.shape, line) : intersectLine2Arc(line, edge.shape); } -function convertToString(attrs) { - return new SVGAttributes(attrs).toAttributesString() +function intersectEdge2Ray(edge, ray) { + return edge.isSegment ? intersectRay2Segment(ray, edge.shape) : intersectRay2Arc(ray, edge.shape); } -/** - * 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} - */ -class Multiline extends LinkedList { - constructor(...args) { - super(); - - if (args.length === 0) { - return; - } - - if (args.length === 1) { - if (args[0] instanceof Array) { - let shapes = args[0]; - if (shapes.length === 0) - return; - - // TODO: more strict validation: - // there may be only one line - // only first and last may be rays - shapes.every((shape) => { - return shape instanceof Flatten.Segment || - shape instanceof Flatten.Arc || - shape instanceof Flatten.Ray || - shape instanceof Flatten.Line - }); - - for (let shape of shapes) { - let edge = new Flatten.Edge(shape); - this.append(edge); - } - - this.setArcLength(); - } - } - } - - /** - * (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; - } - - /** - * Return new cloned instance of Multiline - * @returns {Multiline} - */ - clone() { - return new Multiline(this.toShapes()); - } +function intersectEdge2Circle(edge, circle) { + return edge.isSegment ? intersectSegment2Circle(edge.shape, circle) : intersectArc2Circle(edge.shape, circle); +} - /** - * Set arc_length property for each of the edges in the face. - * Arc_length of the edge it the arc length from the first edge of the face - */ - setArcLength() { - for (let edge of this) { - this.setOneEdgeArcLength(edge); +function intersectSegment2Polygon(segment, polygon) { + let ip = []; + + for (let edge of polygon.edges) { + for (let pt of intersectEdge2Segment(edge, segment)) { + ip.push(pt); } } - setOneEdgeArcLength(edge) { - if (edge === this.first) { - edge.arc_length = 0.0; - } else { - edge.arc_length = edge.prev.arc_length + edge.prev.length; + return ip; +} + +function intersectArc2Polygon(arc, polygon) { + let ip = []; + + for (let edge of polygon.edges) { + for (let pt of intersectEdge2Arc(edge, arc)) { + ip.push(pt); } } - /** - * 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; + return ip; +} - if (shapes[0] === null) // point incident to edge start vertex, return previous edge - return edge.prev; +function intersectLine2Polygon(line, polygon) { + let ip = []; - if (shapes[1] === null) // point incident to edge end vertex, return edge itself - return edge; + if (polygon.isEmpty()) { + return ip; + } - let newEdge = new Flatten.Edge(shapes[0]); - let edgeBefore = edge.prev; + for (let edge of polygon.edges) { + for (let pt of intersectEdge2Line(edge, line)) { + if (!ptInIntPoints(pt, ip)) { + ip.push(pt); + } + } + } - /* Insert first split edge into linked list after edgeBefore */ - this.insert(newEdge, edgeBefore); // edge.face ? + return line.sortPoints(ip); +} - // Update edge shape with second split edge keeping links - edge.shape = shapes[1]; +function intersectCircle2Polygon(circle, polygon) { + let ip = []; - return newEdge; + if (polygon.isEmpty()) { + return ip; } - getChain(edgeFrom, edgeTo) { - let edges = []; - for (let edge = edgeFrom; edge !== edgeTo.next; edge = edge.next) { - edges.push(edge); + for (let edge of polygon.edges) { + for (let pt of intersectEdge2Circle(edge, circle)) { + ip.push(pt); } - 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; - } + return ip; +} - /** - * 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; +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 [] +} - /** - * 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))); +function intersectEdge2Polygon(edge, polygon) { + let ip = []; + + if (polygon.isEmpty() || edge.shape.box.not_intersect(polygon.box)) { + return ip; } - /** - * 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) )); + let resp_edges = polygon.edges.search(edge.shape.box); + + for (let resp_edge of resp_edges) { + ip = [...ip, ...intersectEdge2Edge(edge, resp_edge)]; } - /** - * 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))); + return ip; +} + +function intersectPolygon2Polygon(polygon1, polygon2) { + let ip = []; + + if (polygon1.isEmpty() || polygon2.isEmpty()) { + return ip; } - /** - * Transform multiline into array of shapes - * @returns {Shape[]} - */ - toShapes() { - return this.edges.map(edge => edge.shape.clone()) + if (polygon1.box.not_intersect(polygon2.box)) { + return ip; } - /** - * 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()); + for (let edge1 of polygon1.edges) { + ip = [...ip, ...intersectEdge2Polygon(edge1, polygon2)]; } - /** - * 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\n`; - return svgStr; + 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 []; } } -Flatten.Multiline = Multiline; +function ptInIntPoints(new_pt, ip) { + return ip.some( pt => pt.equalTo(new_pt) ) +} -/** - * Shortcut function to create multiline - * @param args - */ -const multiline = (...args) => new Flatten.Multiline(...args); -Flatten.multiline = multiline; +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)) +} /** * @module RayShoot diff --git a/dist/main.umd.js b/dist/main.umd.js index d07c6ea..841cedd 100644 --- a/dist/main.umd.js +++ b/dist/main.umd.js @@ -401,6 +401,271 @@ } } + 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() + } + + /** + * 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} + */ + class Multiline extends LinkedList { + constructor(...args) { + super(); + + if (args.length === 0) { + return; + } + + if (args.length === 1) { + if (args[0] instanceof Array) { + let shapes = args[0]; + if (shapes.length === 0) + return; + + // TODO: more strict validation: + // there may be only one line + // only first and last may be rays + shapes.every((shape) => { + return shape instanceof Flatten.Segment || + shape instanceof Flatten.Arc || + shape instanceof Flatten.Ray || + shape instanceof Flatten.Line + }); + + for (let shape of shapes) { + let edge = new Flatten.Edge(shape); + this.append(edge); + } + + this.setArcLength(); + } + } + } + + /** + * (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; + } + + /** + * 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 face. + * Arc_length of the edge it the arc length from the first edge of the face + */ + 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; + } + } + + /** + * 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; + } + + /** + * 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 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\n`; + return svgStr; + } + } + + Flatten.Multiline = Multiline; + + /** + * 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 @@ -456,11 +721,7 @@ function sortIntersections(intersections) { - // if (intersections.int_points1.length === 0) return; - // augment intersections with new sorted arrays - // intersections.int_points1_sorted = intersections.int_points1.slice().sort(compareFn); - // intersections.int_points2_sorted = intersections.int_points2.slice().sort(compareFn); intersections.int_points1_sorted = getSortedArray(intersections.int_points1); intersections.int_points2_sorted = getSortedArray(intersections.int_points2); } @@ -504,18 +765,6 @@ return 0; } - // export function getSortedArrayOnLine(line, int_points) { - // return int_points.slice().sort( (int_point1, int_point2) => { - // if (line.coord(int_point1.pt) < line.coord(int_point2.pt)) { - // return -1; - // } - // if (line.coord(int_point1.pt) > line.coord(int_point2.pt)) { - // return 1; - // } - // return 0; - // }) - // } - function filterDuplicatedIntersections(intersections) { if (intersections.int_points1.length < 2) return; @@ -747,14 +996,10 @@ int_point.is_vertex |= END_VERTEX$1; } - if (int_point.is_vertex & START_VERTEX$1) { // nothing to split + if (int_point.is_vertex & START_VERTEX$1) { // nothing to split + int_point.edge_before = edge.prev; if (edge.prev) { - int_point.edge_before = edge.prev; // polygon - int_point.is_vertex = END_VERTEX$1; - } - else { // multiline start vertex - int_point.edge_after = int_point.edge_before; - int_point.edge_before = edge.prev; + int_point.is_vertex = END_VERTEX$1; // polygon } continue; } @@ -770,6 +1015,11 @@ if (int_point.edge_before) { int_point.edge_after = int_point.edge_before.next; } + else { + if (polygon instanceof Multiline && int_point.is_vertex & START_VERTEX$1) { + int_point.edge_after = polygon.first; + } + } } } @@ -2028,547 +2278,282 @@ 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); - } + + 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 line.sortPoints(ip); + return ips; } - function intersectCircle2Polygon(circle, polygon) { + function intersectArc2Arc(arc1, arc2) { let ip = []; - if (polygon.isEmpty()) { + if (arc1.box.not_intersect(arc2.box)) { return ip; } - for (let edge of polygon.edges) { - for (let pt of intersectEdge2Circle(edge, circle)) { + // 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); - } - } - return ip; - } + pt = arc1.end; + if (pt.on(arc2)) + ip.push(pt); - 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 [] - } + pt = arc2.start; + if (pt.on(arc1)) ip.push(pt); - function intersectEdge2Polygon(edge, polygon) { - let ip = []; + pt = arc2.end; + if (pt.on(arc1)) ip.push(pt); - 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)]; + // 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 intersectPolygon2Polygon(polygon1, polygon2) { + function intersectArc2Circle(arc, circle) { let ip = []; - if (polygon1.isEmpty() || polygon2.isEmpty()) { + if (arc.box.not_intersect(circle.box)) { return ip; } - if (polygon1.box.not_intersect(polygon2.box)) { + // 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; } - for (let edge1 of polygon1.edges) { - ip = [...ip, ...intersectEdge2Polygon(edge1, polygon2)]; + // 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 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 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 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 intersectEdge2Segment(edge, segment) { + return edge.isSegment ? intersectSegment2Segment(edge.shape, segment) : intersectSegment2Arc(segment, edge.shape); } - function intersectRay2Polygon(ray, polygon) { - return intersectLine2Polygon(createLineFromRay(ray), polygon) - .filter(pt => ray.contains(pt)) + function intersectEdge2Arc(edge, arc) { + return edge.isSegment ? intersectSegment2Arc(edge.shape, arc) : intersectArc2Arc(edge.shape, arc); } - 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 intersectEdge2Line(edge, line) { + return edge.isSegment ? intersectSegment2Line(edge.shape, line) : intersectLine2Arc(line, edge.shape); } - function convertToString(attrs) { - return new SVGAttributes(attrs).toAttributesString() + function intersectEdge2Ray(edge, ray) { + return edge.isSegment ? intersectRay2Segment(ray, edge.shape) : intersectRay2Arc(ray, edge.shape); } - /** - * 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} - */ - class Multiline extends LinkedList { - constructor(...args) { - super(); - - if (args.length === 0) { - return; - } - - if (args.length === 1) { - if (args[0] instanceof Array) { - let shapes = args[0]; - if (shapes.length === 0) - return; - - // TODO: more strict validation: - // there may be only one line - // only first and last may be rays - shapes.every((shape) => { - return shape instanceof Flatten.Segment || - shape instanceof Flatten.Arc || - shape instanceof Flatten.Ray || - shape instanceof Flatten.Line - }); - - for (let shape of shapes) { - let edge = new Flatten.Edge(shape); - this.append(edge); - } - - this.setArcLength(); - } - } - } - - /** - * (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; - } - - /** - * Return new cloned instance of Multiline - * @returns {Multiline} - */ - clone() { - return new Multiline(this.toShapes()); - } + function intersectEdge2Circle(edge, circle) { + return edge.isSegment ? intersectSegment2Circle(edge.shape, circle) : intersectArc2Circle(edge.shape, circle); + } - /** - * Set arc_length property for each of the edges in the face. - * Arc_length of the edge it the arc length from the first edge of the face - */ - setArcLength() { - for (let edge of this) { - this.setOneEdgeArcLength(edge); + function intersectSegment2Polygon(segment, polygon) { + let ip = []; + + for (let edge of polygon.edges) { + for (let pt of intersectEdge2Segment(edge, segment)) { + ip.push(pt); } } - setOneEdgeArcLength(edge) { - if (edge === this.first) { - edge.arc_length = 0.0; - } else { - edge.arc_length = edge.prev.arc_length + edge.prev.length; + return ip; + } + + function intersectArc2Polygon(arc, polygon) { + let ip = []; + + for (let edge of polygon.edges) { + for (let pt of intersectEdge2Arc(edge, arc)) { + ip.push(pt); } } - /** - * 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; + return ip; + } - if (shapes[0] === null) // point incident to edge start vertex, return previous edge - return edge.prev; + function intersectLine2Polygon(line, polygon) { + let ip = []; - if (shapes[1] === null) // point incident to edge end vertex, return edge itself - return edge; + if (polygon.isEmpty()) { + return ip; + } - let newEdge = new Flatten.Edge(shapes[0]); - let edgeBefore = edge.prev; + for (let edge of polygon.edges) { + for (let pt of intersectEdge2Line(edge, line)) { + if (!ptInIntPoints(pt, ip)) { + ip.push(pt); + } + } + } - /* Insert first split edge into linked list after edgeBefore */ - this.insert(newEdge, edgeBefore); // edge.face ? + return line.sortPoints(ip); + } - // Update edge shape with second split edge keeping links - edge.shape = shapes[1]; + function intersectCircle2Polygon(circle, polygon) { + let ip = []; - return newEdge; + if (polygon.isEmpty()) { + return ip; } - getChain(edgeFrom, edgeTo) { - let edges = []; - for (let edge = edgeFrom; edge !== edgeTo.next; edge = edge.next) { - edges.push(edge); + for (let edge of polygon.edges) { + for (let pt of intersectEdge2Circle(edge, circle)) { + ip.push(pt); } - 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; - } + return ip; + } - /** - * 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; + 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 [] + } - /** - * 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))); + function intersectEdge2Polygon(edge, polygon) { + let ip = []; + + if (polygon.isEmpty() || edge.shape.box.not_intersect(polygon.box)) { + return ip; } - /** - * 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) )); + let resp_edges = polygon.edges.search(edge.shape.box); + + for (let resp_edge of resp_edges) { + ip = [...ip, ...intersectEdge2Edge(edge, resp_edge)]; } - /** - * 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))); + return ip; + } + + function intersectPolygon2Polygon(polygon1, polygon2) { + let ip = []; + + if (polygon1.isEmpty() || polygon2.isEmpty()) { + return ip; } - /** - * Transform multiline into array of shapes - * @returns {Shape[]} - */ - toShapes() { - return this.edges.map(edge => edge.shape.clone()) + if (polygon1.box.not_intersect(polygon2.box)) { + return ip; } - /** - * 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()); + for (let edge1 of polygon1.edges) { + ip = [...ip, ...intersectEdge2Polygon(edge1, polygon2)]; } - /** - * 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\n`; - return svgStr; + 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 []; } } - Flatten.Multiline = Multiline; + function ptInIntPoints(new_pt, ip) { + return ip.some( pt => pt.equalTo(new_pt) ) + } - /** - * Shortcut function to create multiline - * @param args - */ - const multiline = (...args) => new Flatten.Multiline(...args); - Flatten.multiline = multiline; + 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)) + } /** * @module RayShoot diff --git a/package.json b/package.json index 1c5df0b..424715c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@flatten-js/core", - "version": "1.5.2", + "version": "1.5.3", "description": "Javascript library for 2d geometry", "main": "dist/main.cjs", "umd:main": "dist/main.umd.js", diff --git a/src/data_structures/smart_intersections.js b/src/data_structures/smart_intersections.js index ace7ccc..da43df6 100644 --- a/src/data_structures/smart_intersections.js +++ b/src/data_structures/smart_intersections.js @@ -4,6 +4,7 @@ */ import * as Utils from "../utils/utils"; import * as Constants from '../utils/constants'; +import {Multiline} from "../classes/multiline"; export function addToIntPoints(edge, pt, int_points) { @@ -55,11 +56,7 @@ export function addToIntPoints(edge, pt, int_points) export function sortIntersections(intersections) { - // if (intersections.int_points1.length === 0) return; - // augment intersections with new sorted arrays - // intersections.int_points1_sorted = intersections.int_points1.slice().sort(compareFn); - // intersections.int_points2_sorted = intersections.int_points2.slice().sort(compareFn); intersections.int_points1_sorted = getSortedArray(intersections.int_points1); intersections.int_points2_sorted = getSortedArray(intersections.int_points2); } @@ -103,18 +100,6 @@ function compareFn(ip1, ip2) return 0; } -// export function getSortedArrayOnLine(line, int_points) { -// return int_points.slice().sort( (int_point1, int_point2) => { -// if (line.coord(int_point1.pt) < line.coord(int_point2.pt)) { -// return -1; -// } -// if (line.coord(int_point1.pt) > line.coord(int_point2.pt)) { -// return 1; -// } -// return 0; -// }) -// } - export function filterDuplicatedIntersections(intersections) { if (intersections.int_points1.length < 2) return; @@ -346,14 +331,10 @@ export function splitByIntersections(polygon, int_points) int_point.is_vertex |= Constants.END_VERTEX; } - if (int_point.is_vertex & Constants.START_VERTEX) { // nothing to split + if (int_point.is_vertex & Constants.START_VERTEX) { // nothing to split + int_point.edge_before = edge.prev; if (edge.prev) { - int_point.edge_before = edge.prev; // polygon - int_point.is_vertex = Constants.END_VERTEX; - } - else { // multiline start vertex - int_point.edge_after = int_point.edge_before - int_point.edge_before = edge.prev + int_point.is_vertex = Constants.END_VERTEX; // polygon } continue; } @@ -369,6 +350,11 @@ export function splitByIntersections(polygon, int_points) if (int_point.edge_before) { int_point.edge_after = int_point.edge_before.next; } + else { + if (polygon instanceof Multiline && int_point.is_vertex & Constants.START_VERTEX) { + int_point.edge_after = polygon.first + } + } } } diff --git a/test/classes/polygon.js b/test/classes/polygon.js index bdf63dc..9e41e3a 100644 --- a/test/classes/polygon.js +++ b/test/classes/polygon.js @@ -933,6 +933,54 @@ describe('#Flatten.Polygon', function() { expect(newPoly.faces.size).to.equal(2); expect(newPoly.edges.size).to.equal(10) }) + it('Polygon.cut error #175', () => { + // Create polygon from json + let json = [ + [ + { + ps: {x: 641.64, y: 118.32, name: "point"}, + pe: {x: 641.64, y: 151.74, name: "point"}, + name: "segment" + }, + { + ps: {x: 641.64, y: 151.74, name: "point"}, + pe: {x: 504.66, y: 151.74, name: "point"}, + name: "segment" + }, + { + ps: {x: 504.66, y: 151.74, name: "point"}, + pe: {x: 504.66, y: 118.32, name: "point"}, + name: "segment" + }, + { + ps: {x: 504.66, y: 118.32, name: "point"}, + pe: {x: 641.64, y: 118.32, name: "point"}, + name: "segment" + } + ] + ]; + + let polygon = new Polygon(json); + + // Create Multiline + let mlj = [ + { + ps: {x: 576.48, y: 118.32, name: "point"}, + pe: {x: 576.48, y: 274.14, name: "point"}, + name: "segment" + }, + { + ps: {x: 576.48, y: 274.14, name: "point"}, + pe: {x: 641.64, y: 274.14, name: "point"}, + name: "segment" + } + ]; + let ml = multiline(mlj.map((s) => segment(s))); + const newPoly = polygon.cut(ml) + const a = newPoly.toArray() + expect(newPoly.faces.size).to.equal(2); + expect(newPoly.edges.size).to.equal(8) + }) describe('#Intersections', function () { it('Can perform intersection between polygons', function () { const poly1 = new Polygon(