diff --git a/src/path.js b/src/path.js index 1789cbcad53..2f9e74878f6 100644 --- a/src/path.js +++ b/src/path.js @@ -2,6 +2,7 @@ import Attributes from "./attributes"; import Point from "./point"; import Bezier from "bezier-js"; import { round } from "./round"; +import { linesCross, curveCrossesLine, curveCrossesCurve } from "./utils"; function Path() { this.render = true; @@ -450,7 +451,7 @@ Path.prototype.reverse = function() { return rev; }; -/** Returns a reversed version of this */ +/** Returns the point at an edge of this path */ Path.prototype.edge = function(side) { this.boundary(); if (side === "topLeft") return this.topLeft; @@ -514,10 +515,144 @@ function edgeCurveAsBezier(op) { { x: op.to.x, y: op.to.y } ); } -///* Returns the edge of a single path operation */ -//function opEdge(op, side) { -// if(op.type === 'move' || op.type -// -//} + +/** Divides a path into atomic paths */ +Path.prototype.divide = function() { + let paths = []; + let current, start; + for (let i in this.ops) { + let op = this.ops[i]; + if (op.type === "move") { + current = op.to; + start = op.to; + } else if (op.type === "line") { + paths.push(new Path().move(current).line(op.to)); + } else if (op.type === "curve") { + paths.push(new Path().move(current).curve(op.cp1, op.cp2, op.to)); + } else if (op.type === "close") { + paths.push(new Path().move(current).line(start)); + } + if (op.to) current = op.to; + } + + return paths; +}; + +/** Finds intersections between this path and an X value */ +Path.prototype.crossesX = function(x) { + return this.crossesAxis(x, "x"); +}; + +/** Finds intersections between this path and an Y value */ +Path.prototype.crossesY = function(y) { + return this.crossesAxis(y, "y"); +}; + +/** Finds intersections between this path and a X or Y value */ +Path.prototype.crossesAxis = function(val = false, mode) { + if (val === false) throw "Path.crosses[X-Y] requires an value as parameter"; + let intersections = []; + let lineStart = + mode === "x" ? new Point(val, -100000) : new Point(-10000, val); + let lineEnd = mode === "x" ? new Point(val, 100000) : new Point(100000, val); + for (let path of this.divide()) { + if (path.ops[1].type === "line") { + addIntersectionsToArray( + linesCross(path.ops[0].to, path.ops[1].to, lineStart, lineEnd), + intersections + ); + } else if (path.ops[1].type === "curve") { + addIntersectionsToArray( + curveCrossesLine( + path.ops[0].to, + path.ops[1].cp1, + path.ops[1].cp2, + path.ops[1].to, + lineStart, + lineEnd + ), + intersections + ); + } + } + + return intersections; +}; + +/** Finds intersections between this path and another path */ +Path.prototype.intersects = function(path) { + if (this === path) + throw "Calculating intersections between two identical paths is bad idea"; + let intersections = []; + for (let pathA of this.divide()) { + for (let pathB of path.divide()) { + if (pathA.ops[1].type === "line") { + if (pathB.ops[1].type === "line") { + addIntersectionsToArray( + linesCross( + pathA.ops[0].to, + pathA.ops[1].to, + pathB.ops[0].to, + pathB.ops[1].to + ), + intersections + ); + } else if (pathB.ops[1].type === "curve") { + addIntersectionsToArray( + curveCrossesLine( + pathB.ops[0].to, + pathB.ops[1].cp1, + pathB.ops[1].cp2, + pathB.ops[1].to, + pathA.ops[0].to, + pathA.ops[1].to + ), + intersections + ); + } + } else if (pathA.ops[1].type === "curve") { + if (pathB.ops[1].type === "line") { + addIntersectionsToArray( + curveCrossesLine( + pathA.ops[0].to, + pathA.ops[1].cp1, + pathA.ops[1].cp2, + pathA.ops[1].to, + pathB.ops[0].to, + pathB.ops[1].to + ), + intersections + ); + } else if (pathB.ops[1].type === "curve") { + addIntersectionsToArray( + curveCrossesCurve( + pathA.ops[0].to, + pathA.ops[1].cp1, + pathA.ops[1].cp2, + pathA.ops[1].to, + pathB.ops[0].to, + pathB.ops[1].cp1, + pathB.ops[1].cp2, + pathB.ops[1].to + ), + intersections + ); + } + } + } + } + + return intersections; +}; + +function addIntersectionsToArray(candidates, intersections) { + if (!candidates) return; + if (typeof candidates === "object") { + if (typeof candidates.x === "number") intersections.push(candidates); + else { + for (let candidate of candidates) intersections.push(candidate); + } + } +} export default Path; diff --git a/tests/path.test.js b/tests/path.test.js index 50de2bd7131..9ca1d894f83 100644 --- a/tests/path.test.js +++ b/tests/path.test.js @@ -392,6 +392,176 @@ it("Should find the edges of a path for corner cases", () => { expect(round(a.paths.test.edge("right").y)).to.equal(60); }); +it("Should find where a path crosses an X value", () => { + let pattern = new freesewing.Pattern(); + pattern.parts.a = new pattern.Part(); + let a = pattern.parts.a; + a.points.A = new a.Point(95, 50); + a.points.B = new a.Point(10, 30); + a.points.BCp2 = new a.Point(40, 20); + a.points.C = new a.Point(90, 30); + a.points.CCp1 = new a.Point(50, -30); + a.points.D = new a.Point(50, 130); + a.points.DCp1 = new a.Point(150, 30); + a.paths.test = new a.Path() + .move(a.points.A) + .line(a.points.B) + .curve(a.points.BCp2, a.points.CCp1, a.points.C) + .curve(a.points.DCp1, a.points.DCp1, a.points.D) + .close(); + let intersections = a.paths.test.crossesX(60); + expect(intersections.length).to.equal(4); + expect(intersections[0].x).to.equal(60); + expect(intersections[0].y).to.equal(41.76); + expect(intersections[1].x).to.equal(60); + expect(intersections[1].y).to.equal(1.45); + expect(intersections[2].x).to.equal(60); + expect(intersections[2].y).to.equal(120); + expect(intersections[3].x).to.equal(60); + expect(intersections[3].y).to.equal(112.22); +}); + +it("Should find where a path crosses an Y value", () => { + let pattern = new freesewing.Pattern(); + pattern.parts.a = new pattern.Part(); + let a = pattern.parts.a; + a.points.A = new a.Point(95, 50); + a.points.B = new a.Point(10, 30); + a.points.BCp2 = new a.Point(40, 20); + a.points.C = new a.Point(90, 30); + a.points.CCp1 = new a.Point(50, -30); + a.points.D = new a.Point(50, 130); + a.points.DCp1 = new a.Point(150, 30); + a.paths.test = new a.Path() + .move(a.points.A) + .line(a.points.B) + .curve(a.points.BCp2, a.points.CCp1, a.points.C) + .curve(a.points.DCp1, a.points.DCp1, a.points.D) + .close(); + let intersections = a.paths.test.crossesY(60); + expect(intersections.length).to.equal(2); + expect(intersections[0].x).to.equal(117.83); + expect(intersections[0].y).to.equal(60); + expect(intersections[1].x).to.equal(89.38); + expect(intersections[1].y).to.equal(60); +}); + +it("Should throw an error when not passing a value to path.crossesX", () => { + let pattern = new freesewing.Pattern(); + pattern.parts.a = new pattern.Part(); + let a = pattern.parts.a; + a.paths.test = new a.Path(); + expect(() => a.paths.test.crossesX()).to.throw(); + expect(() => a.paths.test.crossesY()).to.throw(); +}); + +it("Should find the intersections between two paths", () => { + let pattern = new freesewing.Pattern(); + pattern.parts.a = new pattern.Part(); + let a = pattern.parts.a; + a.points.A = new a.Point(45, 60); + a.points.B = new a.Point(10, 30); + a.points.BCp2 = new a.Point(40, 20); + a.points.C = new a.Point(90, 30); + a.points.CCp1 = new a.Point(50, -30); + a.points.D = new a.Point(50, 130); + a.points.DCp1 = new a.Point(150, 30); + + a.points._A = new a.Point(55, 40); + a.points._B = new a.Point(0, 55); + a.points._BCp2 = new a.Point(40, -20); + a.points._C = new a.Point(90, 40); + a.points._CCp1 = new a.Point(50, -30); + a.points._D = new a.Point(40, 120); + a.points._DCp1 = new a.Point(180, 40); + + a.paths.example1 = new a.Path() + .move(a.points.A) + .line(a.points.B) + .curve(a.points.BCp2, a.points.CCp1, a.points.C) + .curve(a.points.DCp1, a.points.DCp1, a.points.D); + a.paths.example2 = new a.Path() + .move(a.points._A) + .line(a.points._B) + .curve(a.points._BCp2, a.points._CCp1, a.points._C) + .curve(a.points._DCp1, a.points._DCp1, a.points._D); + let intersections = a.paths.example1.intersects(a.paths.example2); + expect(intersections.length).to.equal(6); + expect(intersections[0].x).to.equal(29.71); + expect(intersections[0].y).to.equal(46.9); + expect(intersections[1].x).to.equal(12.48); + expect(intersections[1].y).to.equal(32.12); + expect(intersections[2].x).to.equal(14.84); + expect(intersections[2].y).to.equal(27.98); + expect(intersections[3].x).to.equal(66.33); + expect(intersections[3].y).to.equal(4.1); + expect(intersections[4].x).to.equal(130.65); + expect(intersections[4].y).to.equal(40.52); + expect(intersections[5].x).to.equal(86.52); + expect(intersections[5].y).to.equal(93.31); +}); + +it("Should throw an error when running path.intersect on an identical path", () => { + let pattern = new freesewing.Pattern(); + pattern.parts.a = new pattern.Part(); + let a = pattern.parts.a; + a.paths.test = new a.Path(); + expect(() => a.paths.test.intersects(a.paths.test)).to.throw(); +}); + +it("Should divide a path", () => { + let pattern = new freesewing.Pattern(); + pattern.parts.a = new pattern.Part(); + let a = pattern.parts.a; + a.points.A = new a.Point(45, 60); + a.points.B = new a.Point(10, 30); + a.points.BCp2 = new a.Point(40, 20); + a.points.C = new a.Point(90, 30); + a.points.CCp1 = new a.Point(50, -30); + a.points.D = new a.Point(-60, 90); + a.points.E = new a.Point(90, 190); + a.paths.test = new a.Path() + .move(a.points.A) + .line(a.points.B) + .curve(a.points.BCp2, a.points.CCp1, a.points.C) + .curve(a.points.E, a.points.D, a.points.A) + .close(); + let divided = a.paths.test.divide(); + expect(divided.length).to.equal(4); + expect(divided[0].ops[0].type).to.equal("move"); + expect(divided[0].ops[0].to.x).to.equal(45); + expect(divided[0].ops[0].to.y).to.equal(60); + expect(divided[0].ops[1].type).to.equal("line"); + expect(divided[0].ops[1].to.x).to.equal(10); + expect(divided[0].ops[1].to.y).to.equal(30); + expect(divided[1].ops[0].type).to.equal("move"); + expect(divided[1].ops[0].to.x).to.equal(10); + expect(divided[1].ops[0].to.y).to.equal(30); + expect(divided[1].ops[1].type).to.equal("curve"); + expect(divided[1].ops[1].cp1.x).to.equal(40); + expect(divided[1].ops[1].cp1.y).to.equal(20); + expect(divided[1].ops[1].cp2.x).to.equal(50); + expect(divided[1].ops[1].cp2.y).to.equal(-30); + expect(divided[1].ops[1].to.x).to.equal(90); + expect(divided[1].ops[1].to.y).to.equal(30); + expect(divided[2].ops[0].type).to.equal("move"); + expect(divided[2].ops[0].to.x).to.equal(90); + expect(divided[2].ops[0].to.y).to.equal(30); + expect(divided[2].ops[1].type).to.equal("curve"); + expect(divided[2].ops[1].cp1.x).to.equal(90); + expect(divided[2].ops[1].cp1.y).to.equal(190); + expect(divided[2].ops[1].cp2.x).to.equal(-60); + expect(divided[2].ops[1].cp2.y).to.equal(90); + expect(divided[2].ops[1].to.x).to.equal(45); + expect(divided[2].ops[1].to.y).to.equal(60); + expect(divided[3].ops[0].type).to.equal("move"); + expect(divided[3].ops[0].to.x).to.equal(45); + expect(divided[3].ops[0].to.y).to.equal(60); + expect(divided[3].ops[1].type).to.equal("line"); + expect(divided[3].ops[1].to.x).to.equal(45); + expect(divided[3].ops[1].to.y).to.equal(60); +}); + function round(value) { return Math.round(value * 1e2) / 1e2; } diff --git a/tests/utils.test.js b/tests/utils.test.js index bdd295f690b..e826e1469c5 100644 --- a/tests/utils.test.js +++ b/tests/utils.test.js @@ -60,11 +60,21 @@ it("Should find the intersection of two line segments", () => { let b = new freesewing.Point(90, 74); let c = new freesewing.Point(90, 19); let d = new freesewing.Point(11, 70); - let X = freesewing.utils.beamsCross(a, b, c, d); + let X = freesewing.utils.linesCross(a, b, c, d); expect(X.x).to.equal(51.95); expect(X.y).to.equal(43.56); }); +it("Should find the intersection of two line segments - round() edge case", () => { + let a = new freesewing.Point(45, 60); + let b = new freesewing.Point(10, 30); + let c = new freesewing.Point(55, 40); + let d = new freesewing.Point(0, 55); + let X = freesewing.utils.linesCross(a, b, c, d); + expect(X.x).to.equal(29.71); + expect(X.y).to.equal(46.9); +}); + it("Should find the intersection of an endles line and a give X-value", () => { let a = new freesewing.Point(10, 10); let b = new freesewing.Point(90, 74);