diff --git a/examples/Animated/BooleanOperations.html b/examples/Animated/BooleanOperations.html index 9c271a99..83fd01c0 100644 --- a/examples/Animated/BooleanOperations.html +++ b/examples/Animated/BooleanOperations.html @@ -92,7 +92,7 @@ text.content = 'ring.' + operation + '(square)'; } result.selected = true; - result.fillColor = colors[curIndex % operations.length]; + result.fillColor = colors[curIndex % colors.length]; result.moveBelow(text); // If the result is a group, color each of its children differently: diff --git a/examples/Scripts/BooleanOperations.html b/examples/Scripts/BooleanOperations.html index e12356ff..9ed464f0 100644 --- a/examples/Scripts/BooleanOperations.html +++ b/examples/Scripts/BooleanOperations.html @@ -269,7 +269,7 @@ // // annotatePath(pathB) // // pathB.translate([ 300, 0 ]); // // pathB.segments.filter(function(a) { return a._ixPair; }).map( - // // function(a) { a._ixPair.getIntersection()._segment.selected = true; }); + // // function(a) { a._ixPair.intersection._segment.selected = true; }); // console.time('unite'); // var nup = unite(pathA, pathB); @@ -360,14 +360,12 @@ function disperse(path, distance) { distance = distance || 10; - if (! path instanceof CompoundPath || ! path instanceof Group) { return; } var center = path.bounds.center; var children = path.children, i ,len; - for (var i = 0, len = children.length; i < len; i++) { + for (var i = 0, len = children && children.length; i < len; i++) { var cCenter = children[i].bounds.center; var vec = cCenter.subtract(center); - vec = (vec.isClose([0,0], 0.5))? vec : vec.normalize(distance); - children[i].translate(vec); + children[i].translate(vec.length < 0.5 ? vec : vec.normalize(distance)); } } diff --git a/package.json b/package.json index 5ebfca0f..0fd06d12 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "devDependencies": { "gulp": "^3.9.0", "gulp-qunit": "^1.2.1", - "prepro": "~0.8.3", + "prepro": "~0.9.0", "qunitjs": "~1.15.0", "uglify-js": "~2.4.24" }, diff --git a/src/basic/Line.js b/src/basic/Line.js index 3d143109..7ed66852 100644 --- a/src/basic/Line.js +++ b/src/basic/Line.js @@ -48,30 +48,30 @@ var Line = Base.extend(/** @lends Line# */{ }, /** - * The starting point of the line + * The starting point of the line. * - * @name Line#point * @type Point + * @bean */ getPoint: function() { return new Point(this._px, this._py); }, /** - * The vector of the line + * The direction of the line as a vector. * - * @name Line#vector * @type Point + * @bean */ getVector: function() { return new Point(this._vx, this._vy); }, /** - * The length of the line + * The length of the line. * - * @name Line#length * @type Number + * @bean */ getLength: function() { return this.getVector().getLength(); @@ -113,35 +113,47 @@ var Line = Base.extend(/** @lends Line# */{ }, isCollinear: function(line) { - // TODO: Tests showed that 1e-10 might work well here, but we want to - // keep it in sync with Point#isCollinear() - return this._vx * line._vy - this._vy * line._vx - < /*#=*/Numerical.TOLERANCE; + return Point.isCollinear(this._vx, this._vy, line._vx, line._vy); + }, + + isOrthogonal: function(line) { + return Point.isOrthogonal(this._vx, this._vy, line._vx, line._vy); }, statics: /** @lends Line */{ - intersect: function(apx, apy, avx, avy, bpx, bpy, bvx, bvy, asVector, + intersect: function(p1x, p1y, v1x, v1y, p2x, p2y, v2x, v2y, asVector, isInfinite) { // Convert 2nd points to vectors if they are not specified as such. if (!asVector) { - avx -= apx; - avy -= apy; - bvx -= bpx; - bvy -= bpy; + v1x -= p1x; + v1y -= p1y; + v2x -= p2x; + v2y -= p2y; } - var cross = avx * bvy - avy * bvx; + var cross = v1x * v2y - v1y * v2x; // Avoid divisions by 0, and errors when getting too close to 0 if (!Numerical.isZero(cross)) { - var dx = apx - bpx, - dy = apy - bpy, - ta = (bvx * dy - bvy * dx) / cross, - tb = (avx * dy - avy * dx) / cross; - // Check the ranges of t parameters if the line is not allowed - // to extend beyond the definition points. - if (isInfinite || 0 <= ta && ta <= 1 && 0 <= tb && tb <= 1) + var dx = p1x - p2x, + dy = p1y - p2y, + u1 = (v2x * dy - v2y * dx) / cross, + u2 = (v1x * dy - v1y * dx) / cross, + // Check the ranges of the u parameters if the line is not + // allowed to extend beyond the definition points, but + // compare with EPSILON tolerance over the [0, 1] bounds. + epsilon = /*#=*/Numerical.EPSILON, + uMin = -epsilon, + uMax = 1 + epsilon; + if (isInfinite + || uMin < u1 && u1 < uMax && uMin < u2 && u2 < uMax) { + if (!isInfinite) { + // Address the tolerance at the bounds by clipping to + // the actual range. + u1 = u1 <= 0 ? 0 : u1 >= 1 ? 1 : u1; + } return new Point( - apx + ta * avx, - apy + ta * avy); + p1x + u1 * v1x, + p1y + u1 * v1y); + } } }, @@ -157,9 +169,7 @@ var Line = Base.extend(/** @lends Line# */{ ccw = v2x * vx + v2y * vy; // ccw = v2.dot(v1); if (ccw > 0) { // ccw = v2.subtract(v1).dot(v1); - v2x -= vx; - v2y -= vy; - ccw = v2x * vx + v2y * vy; + ccw = (v2x - vx) * vx + (v2y - vy) * vy; if (ccw < 0) ccw = 0; } @@ -172,11 +182,13 @@ var Line = Base.extend(/** @lends Line# */{ vx -= px; vy -= py; } - return Numerical.isZero(vx) - ? vy >= 0 ? px - x : x - px - : Numerical.isZero(vy) - ? vx >= 0 ? y - py : py - y - : (vx * (y - py) - vy * (x - px)) / Math.sqrt(vx * vx + vy * vy); + // Based on the error analysis by @iconexperience outlined in + // https://github.com/paperjs/paper.js/issues/799 + return vx === 0 + ? vy >= 0 ? px - x : x - px + : vy === 0 + ? vx >= 0 ? y - py : py - y + : (vx * (y - py) - vy * (x - px)) / Math.sqrt(vx * vx + vy * vy); } } }); diff --git a/src/basic/Point.js b/src/basic/Point.js index f9af8718..61304f51 100644 --- a/src/basic/Point.js +++ b/src/basic/Point.js @@ -460,11 +460,11 @@ var Point = Base.extend(/** @lends Point# */{ return this.clone(); angle = angle * Math.PI / 180; var point = center ? this.subtract(center) : this, - s = Math.sin(angle), - c = Math.cos(angle); + sin = Math.sin(angle), + cos = Math.cos(angle); point = new Point( - point.x * c - point.y * s, - point.x * s + point.y * c + point.x * cos - point.y * sin, + point.x * sin + point.y * cos ); return center ? point.add(center) : point; }, @@ -690,7 +690,9 @@ var Point = Base.extend(/** @lends Point# */{ * @param {Number} tolerance the maximum distance allowed * @return {Boolean} {@true if it is within the given distance} */ - isClose: function(point, tolerance) { + isClose: function(/* point, tolerance */) { + var point = Point.read(arguments), + tolerance = Base.read(arguments); return this.getDistance(point) < tolerance; }, @@ -701,11 +703,9 @@ var Point = Base.extend(/** @lends Point# */{ * @param {Point} point the vector to check against * @return {Boolean} {@true it is collinear} */ - isCollinear: function(point) { - // NOTE: Numerical.EPSILON is too small, breaking shape-path-shape - // conversion test. But tolerance is probably too large? - // TODO: Tests showed that 1e-10 might work well here. - return Math.abs(this.cross(point)) < /*#=*/Numerical.TOLERANCE; + isCollinear: function(/* point */) { + var point = Point.read(arguments); + return Point.isCollinear(this.x, this.y, point.x, point.y); }, // TODO: Remove version with typo after a while (deprecated June 2015) @@ -718,10 +718,9 @@ var Point = Base.extend(/** @lends Point# */{ * @param {Point} point the vector to check against * @return {Boolean} {@true it is orthogonal} */ - isOrthogonal: function(point) { - // NOTE: Numerical.EPSILON is too small, breaking shape-path-shape - // conversion test. - return Math.abs(this.dot(point)) < /*#=*/Numerical.TOLERANCE; + isOrthogonal: function(/* point */) { + var point = Point.read(arguments); + return Point.isOrthogonal(this.x, this.y, point.x, point.y); }, /** @@ -767,23 +766,19 @@ var Point = Base.extend(/** @lends Point# */{ }, /** - * Returns the projection of the point on another point. + * Returns the projection of the point onto another point. * Both points are interpreted as vectors. * * @param {Point} point - * @return {Point} the projection of the point on another point + * @return {Point} the projection of the point onto another point */ project: function(/* point */) { - var point = Point.read(arguments); - if (point.isZero()) { - return new Point(0, 0); - } else { - var scale = this.dot(point) / point.dot(point); - return new Point( - point.x * scale, - point.y * scale - ); - } + var point = Point.read(arguments), + scale = point.isZero() ? 0 : this.dot(point) / point.dot(point); + return new Point( + point.x * scale, + point.y * scale + ); }, /** @@ -920,6 +915,23 @@ var Point = Base.extend(/** @lends Point# */{ */ random: function() { return new Point(Math.random(), Math.random()); + }, + + isCollinear: function(x1, y1, x2, y2) { + // NOTE: We use normalized vectors so that the epsilon comparison is + // reliable. We could instead scale the epsilon based on the vector + // length. But instead of normalizing the vectors before calculating + // the cross product, we can scale the epsilon accordingly. + return Math.abs(x1 * y2 - y1 * x2) + <= Math.sqrt((x1 * x1 + y1 * y1) * (x2 * x2 + y2 * y2)) + * /*#=*/Numerical.TRIGONOMETRIC_EPSILON; + }, + + isOrthogonal: function(x1, y1, x2, y2) { + // See Point.isCollinear() + return Math.abs(x1 * x2 + y1 * y2) + <= Math.sqrt((x1 * x1 + y1 * y1) * (x2 * x2 + y2 * y2)) + * /*#=*/Numerical.TRIGONOMETRIC_EPSILON; } } }, Base.each(['round', 'ceil', 'floor', 'abs'], function(name) { diff --git a/src/basic/Rectangle.js b/src/basic/Rectangle.js index e26f27f7..75a43e30 100644 --- a/src/basic/Rectangle.js +++ b/src/basic/Rectangle.js @@ -475,7 +475,7 @@ var Rectangle = Base.extend(/** @lends Rectangle# */{ */ /** - * The area of the rectangle in square points. + * The area of the rectangle. * * @type Number * @bean @@ -857,7 +857,8 @@ var LinkedRectangle = Rectangle.extend({ this._owner[this._setter](this); return this; } -}, new function() { +}, +new function() { var proto = Rectangle.prototype; return Base.each(['x', 'y', 'width', 'height'], function(key) { diff --git a/src/core/PaperScope.js b/src/core/PaperScope.js index 4e154fa4..7b6a3b4f 100644 --- a/src/core/PaperScope.js +++ b/src/core/PaperScope.js @@ -120,7 +120,7 @@ var PaperScope = Base.extend(/** @lends PaperScope# */{ * * @type String */ - version: '/*#=*/__options.version', + version: /*#=*/__options.version, // DOCS: PaperScope#settings /** diff --git a/src/item/Item.js b/src/item/Item.js index 0dbb9364..2f9d2dde 100644 --- a/src/item/Item.js +++ b/src/item/Item.js @@ -1144,7 +1144,7 @@ var Item = Base.extend(Emitter, /** @lends Item# */{ /** * @bean - * @deprecated use {@link #getApplyMatrix()} instead. + * @deprecated use {@link #applyMatrix} instead. */ getTransformContent: '#getApplyMatrix', setTransformContent: '#setApplyMatrix', @@ -1638,10 +1638,10 @@ var Item = Base.extend(Emitter, /** @lends Item# */{ intersects: function(item, _matrix) { if (!(item instanceof Item)) return false; - // Tell _getIntersections to return as soon as some intersections are + // Tell getIntersections() to return as soon as some intersections are // found, because all we care for here is there are some or none: - return this._asPathItem()._getIntersections(item._asPathItem(), - _matrix || item._matrix, [], true).length > 0; + return this._asPathItem().getIntersections(item._asPathItem(), null, + _matrix || item._matrix, true).length > 0; }, /** @@ -1652,7 +1652,7 @@ var Item = Base.extend(Emitter, /** @lends Item# */{ * and may contain a combination of the following values: * * @option [options.tolerance={@link PaperScope#settings}.hitTolerance] - * {Number} the tolerance of the hit-test in points + * {Number} the tolerance of the hit-test * @option options.class {Function} only hit-test again a certain item class * and its sub-classes: {@code Group, Layer, Path, CompoundPath, * Shape, Raster, PlacedSymbol, PointText}, etc @@ -2618,6 +2618,16 @@ var Item = Base.extend(Emitter, /** @lends Item# */{ return item ? item.isDescendant(this) : false; }, + /** + * Checks if the item is an a sibling of the specified item. + * + * @param {Item} item the item to check against + * @return {Boolean} {@true if the item is aa sibling of the specified item} + */ + isSibling: function(item) { + return this._parent === item._parent; + }, + /** * Checks whether the item is grouped with the specified item. * diff --git a/src/item/Raster.js b/src/item/Raster.js index 57961a1f..a0ef9a41 100644 --- a/src/item/Raster.js +++ b/src/item/Raster.js @@ -207,7 +207,7 @@ var Raster = Item.extend(/** @lends Raster# */{ /** * @private * @bean - * @deprecated use {@link #getResolution()} instead. + * @deprecated use {@link #resolution} instead. */ getPpi: '#getResolution', diff --git a/src/item/Shape.js b/src/item/Shape.js index 69c033e3..a9c2c75d 100644 --- a/src/item/Shape.js +++ b/src/item/Shape.js @@ -64,7 +64,7 @@ var Shape = Item.extend(/** @lends Shape# */{ /** * @private * @bean - * @deprecated use {@link #getType()} instead. + * @deprecated use {@link #type} instead. */ getShape: '#getType', setShape: '#setType', @@ -277,7 +277,6 @@ var Shape = Item.extend(/** @lends Shape# */{ } }, new function() { // Scope for _contains() and _hitTestSelf() code. - // Returns the center of the quarter corner ellipse for rounded rectangle, // if the point lies within its bounding box. function getCornerCenter(that, point, expand) { diff --git a/src/path/CompoundPath.js b/src/path/CompoundPath.js index fb285478..aca47247 100644 --- a/src/path/CompoundPath.js +++ b/src/path/CompoundPath.js @@ -102,6 +102,15 @@ var CompoundPath = PathItem.extend(/** @lends CompoundPath# */{ }, insertChildren: function insertChildren(index, items, _preserve) { + // Convert CompoundPath items in the children list by adding their + // children to the list and removing their parent. + for (var i = items.length - 1; i >= 0; i--) { + var item = items[i]; + if (item instanceof CompoundPath) { + items.splice.apply(items, [i, 1].concat(item.removeChildren())); + item.remove(); + } + } // Pass on 'path' for _type, to make sure that only paths are added as // children. items = insertChildren.base.call(this, index, items, _preserve, Path); @@ -131,16 +140,23 @@ var CompoundPath = PathItem.extend(/** @lends CompoundPath# */{ this._children[i].smooth(); }, + // DOCS: reduce() + // TEST: reduce() reduce: function reduce() { - if (this._children.length === 0) { // Replace with a simple empty Path + var children = this._children; + for (var i = children.length - 1; i >= 0; i--) { + var path = children[i].reduce(); + if (path.isEmpty()) + children.splice(i, 1); + } + if (children.length === 0) { // Replace with a simple empty Path var path = new Path(Item.NO_INSERT); path.insertAbove(this); path.setStyle(this._style); this.remove(); return path; - } else { - return reduce.base.call(this); } + return reduce.base.call(this); }, /** @@ -220,8 +236,8 @@ var CompoundPath = PathItem.extend(/** @lends CompoundPath# */{ }, /** - * The area of the path in square points. Self-intersecting paths can - * contain sub-areas that cancel each other out. + * The area that the path's geometry is covering. Self-intersecting paths + * can contain sub-areas that cancel each other out. * * @type Number * @bean @@ -298,7 +314,8 @@ var CompoundPath = PathItem.extend(/** @lends CompoundPath# */{ : matrix.chain(mx)); } } -}, new function() { // Injection scope for PostScript-like drawing functions +}, +new function() { // Injection scope for PostScript-like drawing functions /** * Helper method that returns the current path and checks if a moveTo() * command is required first. diff --git a/src/path/Curve.js b/src/path/Curve.js index 5b0ed02d..e0b80985 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -58,43 +58,74 @@ var Curve = Base.extend(/** @lends Curve# */{ * @param {Number} y2 */ initialize: function Curve(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7) { - var count = arguments.length; + var count = arguments.length, + values, + seg1, seg2, + point1, point2, + handle1, handle2; + // The following code has to either set seg1 & seg2, + // or point1, point2, handle1 & handle2. At the end, the internal + // segments are created accordingly. if (count === 3) { // Undocumented internal constructor, used by Path#getCurves() // new Segment(path, segment1, segment2); this._path = arg0; - this._segment1 = arg1; - this._segment2 = arg2; + seg1 = arg1; + seg2 = arg2; } else if (count === 0) { - this._segment1 = new Segment(); - this._segment2 = new Segment(); + seg1 = new Segment(); + seg2 = new Segment(); } else if (count === 1) { // new Segment(segment); // Note: This copies from existing segments through bean getters - this._segment1 = new Segment(arg0.segment1); - this._segment2 = new Segment(arg0.segment2); + if ('segment1' in arg0) { + seg1 = new Segment(arg0.segment1); + seg2 = new Segment(arg0.segment2); + } else if ('point1' in arg0) { + // As printed by #toString() + point1 = arg0.point1; + handle1 = arg0.handle1; + handle2 = arg0.handle2; + point2 = arg0.point2; + } else if (Array.isArray(arg0)) { + // Convert getValues() array back to points and handles so we + // can create segments for those. + point1 = [arg0[0], arg0[1]]; + point2 = [arg0[6], arg0[7]]; + handle1 = [arg0[2] - arg0[0], arg0[3] - arg0[1]]; + handle2 = [arg0[4] - arg0[6], arg0[5] - arg0[7]]; + } } else if (count === 2) { // new Segment(segment1, segment2); - this._segment1 = new Segment(arg0); - this._segment2 = new Segment(arg1); - } else { - var point1, handle1, handle2, point2; - if (count === 4) { - point1 = arg0; - handle1 = arg1; - handle2 = arg2; - point2 = arg3; - } else if (count === 8) { - // Convert getValue() array back to points and handles so we - // can create segments for those. - point1 = [arg0, arg1]; - point2 = [arg6, arg7]; - handle1 = [arg2 - arg0, arg3 - arg1]; - handle2 = [arg4 - arg6, arg5 - arg7]; - } - this._segment1 = new Segment(point1, null, handle1); - this._segment2 = new Segment(point2, handle2, null); + seg1 = new Segment(arg0); + seg2 = new Segment(arg1); + } else if (count === 4) { + point1 = arg0; + handle1 = arg1; + handle2 = arg2; + point2 = arg3; + } else if (count === 8) { + // Convert getValues() array from arguments list back to points and + // handles so we can create segments for those. + // NOTE: This could be merged with the above code after the array + // check through the `arguments` object, but it would break JS + // optimizations. + point1 = [arg0, arg1]; + point2 = [arg6, arg7]; + handle1 = [arg2 - arg0, arg3 - arg1]; + handle2 = [arg4 - arg6, arg5 - arg7]; } + this._segment1 = seg1 || new Segment(point1, null, handle1); + this._segment2 = seg2 || new Segment(point2, handle2, null); + }, + + _serialize: function(options) { + // If it has no handles, only serialize points, otherwise handles too. + return Base.serialize(this.hasHandles() + ? [this.getPoint1(), this.getHandle1(), this.getHandle2(), + this.getPoint2()] + : [this.getPoint1(), this.getPoint2()], + options, true); }, _changed: function() { @@ -102,6 +133,45 @@ var Curve = Base.extend(/** @lends Curve# */{ this._length = this._bounds = undefined; }, + /** + * Returns a copy of the curve. + * + * @return {Curve} + */ + clone: function() { + return new Curve(this._segment1, this._segment2); + }, + + /** + * @return {String} a string representation of the curve + */ + toString: function() { + var parts = [ 'point1: ' + this._segment1._point ]; + if (!this._segment1._handleOut.isZero()) + parts.push('handle1: ' + this._segment1._handleOut); + if (!this._segment2._handleIn.isZero()) + parts.push('handle2: ' + this._segment2._handleIn); + parts.push('point2: ' + this._segment2._point); + return '{ ' + parts.join(', ') + ' }'; + }, + + /** + * Removes the curve from the path that it belongs to, by removing its + * second segment and merging its handle with the first segment. + * @return {Boolean} {@true if the curve was removed} + */ + remove: function() { + var removed = false; + if (this._path) { + var segment2 = this._segment2, + handleOut = segment2._handleOut; + removed = segment2.remove(); + if (removed) + this._segment1._handleOut.set(handleOut.x, handleOut.y); + } + return removed; + }, + /** * The first anchor point of the curve. * @@ -228,6 +298,26 @@ var Curve = Base.extend(/** @lends Curve# */{ || this._path._closed && curves[curves.length - 1]) || null; }, + /** + * Checks if the this is the first curve in the {@link Path#curves} array. + * + * @return {Boolean} {@true if this is the first curve} + */ + isFirst: function() { + return this._segment1._index === 0; + }, + + /** + * Checks if the this is the last curve in the {@link Path#curves} array. + * + * @return {Boolean} {@true if this is the last curve} + */ + isLast: function() { + var path = this._path; + return path && this._segment1._index === path._curves.length - 1 + || false; + }, + /** * Specifies whether the points and handles of the curve are selected. * @@ -248,10 +338,30 @@ var Curve = Base.extend(/** @lends Curve# */{ this.getPoint2().setSelected(selected); }, + /** + * An array of 8 float values, describing this curve's geometry in four + * absolute x/y pairs (point1, handle1, handle2, point2). This format is + * used internally for efficient processing of curve geometries, e.g. when + * calculating intersections or bounds. + * + * Note that the handles are converted to absolute coordinates. + * + * @type Number[] + * @bean + */ getValues: function(matrix) { return Curve.getValues(this._segment1, this._segment2, matrix); }, + /** + * An array of 4 point objects, describing this curve's geometry in absolute + * coordinates (point1, handle1, handle2, point2). + * + * Note that the handles are converted to absolute coordinates. + * + * @type Point[] + * @bean + */ getPoints: function() { // Convert to array of absolute points var coords = this.getValues(), @@ -262,25 +372,47 @@ var Curve = Base.extend(/** @lends Curve# */{ }, /** - * The approximated length of the curve in points. + * The approximated length of the curve. * * @type Number * @bean */ getLength: function() { - if (this._length == null) { - // Use simple point distance for linear curves - this._length = this.isLinear() - ? this._segment2._point.getDistance(this._segment1._point) - : Curve.getLength(this.getValues(), 0, 1); - } + if (this._length == null) + this._length = Curve.getLength(this.getValues(), 0, 1); return this._length; }, + /** + * The area that the curve's geometry is covering. + * + * @type Number + * @bean + */ getArea: function() { return Curve.getArea(this.getValues()); }, + /** + * @type Line + * @bean + * @private + */ + getLine: function() { + return new Line(this._segment1._point, this._segment2._point); + }, + + /** + * Creates a new curve as a sub-curve from this curve, its range defined by + * the given parameters. If {@code from} is larger than {@code to}, then + * the resulting curve will have its direction reversed. + * + * @param {Number} from the curve-time parameter at which the sub-curve + * starts + * @param {Number} to the curve-time parameter at which the sub-curve + * ends + * @return {Curve} the newly create sub-curve + */ getPart: function(from, to) { return new Curve(Curve.getPart(this.getValues(), from, to)); }, @@ -291,71 +423,19 @@ var Curve = Base.extend(/** @lends Curve# */{ }, /** - * Checks if this curve defines any curve handle. + * Returns all intersections between two {@link Curve} objects as an array + * of {@link CurveLocation} objects. * - * @return {Boolean} {@true if the curve has handles defined} - * @see Segment#hasHandles() - * @see Path#hasHandles() + * @param {Curve} curve the other curve to find the intersections with (if + * the curve itself or {@code null} is passed, the self intersection of the + * curve is returned, if it exists) + * @return {CurveLocation[]} the locations of all intersections between the + * curves */ - hasHandles: function() { - return !this.isStraight(); - }, - - /** - * Checks whether the curve is straight, meaning it has no curve handles - * defined and thus appears as a line. - * Note that this is not the same as {@link #isLinear()}, which performs a - * full linearity check that includes handles running collinear to the line - * direction. - * - * @return {Boolean} {@true if the curve is straight} - * @see Segment#isStraight() - */ - isStraight: function() { - return this._segment1._handleOut.isZero() - && this._segment2._handleIn.isZero(); - }, - - /** - * Checks if this curve appears as a line. This can mean that it has no - * handles defined, or that the handles run collinear with the line. - * - * @return {Boolean} {@true if the curve is linear} - * @see Segment#isLinear() - * @see Path#isLinear() - */ - isLinear: function() { - return Segment.isLinear(this._segment1, this._segment2); - }, - - /** - * Checks if the the two curves describe lines that are collinear, meaning - * they run in parallel. - * - * @param {Curve} the other curve to check against - * @return {Boolean} {@true if the two lines are collinear} - * @see Segment#isCollinear(segment) - */ - isCollinear: function(curve) { - return Segment.isCollinear(this._segment1, this._segment2, - curve._segment1, curve._segment2); - }, - - /** - * Checks if the curve describes an orthogonal arc, as used in the - * construction of circles and ellipses. - * - * @return {Boolean} {@true if the curve describes an orthogonal arc} - * @see Segment#isOrthogonalArc() - */ - isOrthogonalArc: function() { - return Segment.isOrthogonalArc(this._segment1, this._segment2); - }, - - // DOCS: Curve#getIntersections() getIntersections: function(curve) { - return Curve.filterIntersections(Curve.getIntersections( - this.getValues(), curve.getValues(), this, curve, [])); + return Curve._getIntersections(this.getValues(), + curve && curve !== this ? curve.getValues() : null, + this, curve, [], {}); }, // TODO: adjustThroughPoint @@ -392,54 +472,46 @@ var Curve = Base.extend(/** @lends Curve# */{ * is within the valid range, {code null} otherwise. */ // TODO: Rename to divideAt()? - divide: function(offset, isParameter, ignoreStraight) { + divide: function(offset, isParameter, _setHandles) { var parameter = this._getParameter(offset, isParameter), - tolerance = /*#=*/Numerical.TOLERANCE, + tMin = /*#=*/Numerical.CURVETIME_EPSILON, + tMax = 1 - tMin, res = null; // Only divide if not at the beginning or end. - if (parameter >= tolerance && parameter <= 1 - tolerance) { + if (parameter >= tMin && parameter <= tMax) { var parts = Curve.subdivide(this.getValues(), parameter), - setHandles = ignoreStraight || this.hasHandles(), left = parts[0], - right = parts[1]; - - // Write back the results: + right = parts[1], + setHandles = _setHandles || this.hasHandles(), + segment1 = this._segment1, + segment2 = this._segment2, + path = this._path; if (setHandles) { - this._segment1._handleOut.set(left[2] - left[0], - left[3] - left[1]); - // segment2 is the end segment. By inserting newSegment - // between segment1 and 2, 2 becomes the end segment. + // Adjust the handles on the existing segments. The new segment + // will be inserted between the existing segment1 and segment2: // Convert absolute -> relative - this._segment2._handleIn.set(right[4] - right[6], + segment1._handleOut.set(left[2] - left[0], + left[3] - left[1]); + segment2._handleIn.set(right[4] - right[6], right[5] - right[7]); } - - // Create the new segment, convert absolute -> relative: + // Create the new segment: var x = left[6], y = left[7], segment = new Segment(new Point(x, y), setHandles && new Point(left[4] - x, left[5] - y), setHandles && new Point(right[2] - x, right[3] - y)); - // Insert it in the segments list, if needed: - if (this._path) { - // Insert at the end if this curve is a closing curve of a - // closed path, since otherwise it would be inserted at 0. - if (this._segment1._index > 0 && this._segment2._index === 0) { - this._path.add(segment); - } else { - this._path.insert(this._segment2._index, segment); - } - // The way Path#_add handles curves, this curve will always - // become the owner of the newly inserted segment. - // TODO: I expect this.getNext() to produce the correct result, - // but since we're inserting differently in _add (something - // linked with CurveLocation#divide()), this is not the case... - res = this; // this.getNext(); + if (path) { + // By inserting at segment1.index + 1, we make sure to insert at + // the end if this curve is a closing curve of a closed path, + // as with segment2.index it would be inserted at 0. + path.insert(segment1._index + 1, segment); + // The newly inserted segment is the start of the next curve: + res = this.getNext(); } else { // otherwise create it from the result of split - var end = this._segment2; this._segment2 = segment; - res = new Curve(segment, end); + res = new Curve(segment, segment2); } } return res; @@ -473,50 +545,19 @@ var Curve = Base.extend(/** @lends Curve# */{ * * @return {Curve} a reversed version of the curve */ - reverse: function() { - return new Curve(this._segment2.reverse(), this._segment1.reverse()); + reversed: function() { + return new Curve(this._segment2.reversed(), this._segment1.reversed()); }, /** - * Removes the curve from the path that it belongs to, by removing its - * second segment and merging its handle with the first segment. - * @return {Boolean} {@true if the curve was removed} + * Clears the curve's handles by setting their coordinates to zero, + * turning the curve into a straight line. */ - remove: function() { - var removed = false; - if (this._path) { - var segment2 = this._segment2, - handleOut = segment2._handleOut; - removed = segment2.remove(); - if (removed) - this._segment1._handleOut.set(handleOut.x, handleOut.y); - } - return removed; + clearHandles: function() { + this._segment1._handleOut.set(0, 0); + this._segment2._handleIn.set(0, 0); }, - /** - * Returns a copy of the curve. - * - * @return {Curve} - */ - clone: function() { - return new Curve(this._segment1, this._segment2); - }, - - /** - * @return {String} a string representation of the curve - */ - toString: function() { - var parts = [ 'point1: ' + this._segment1._point ]; - if (!this._segment1._handleOut.isZero()) - parts.push('handle1: ' + this._segment1._handleOut); - if (!this._segment2._handleIn.isZero()) - parts.push('handle2: ' + this._segment2._handleIn); - parts.push('point2: ' + this._segment2._point); - return '{ ' + parts.join(', ') + ' }'; - }, - -// Mess with indentation in order to get more line-space below... statics: { getValues: function(segment1, segment2, matrix) { var p1 = segment1._point, @@ -572,57 +613,83 @@ statics: { return Numerical.solveCubic(a, b, c, p1 - val, roots, min, max); }, - getParameterOf: function(v, x, y) { - // Handle beginnings and end separately, as they are not detected - // sometimes. - var tolerance = /*#=*/Numerical.TOLERANCE, - abs = Math.abs; - if (abs(v[0] - x) < tolerance && abs(v[1] - y) < tolerance) - return 0; - if (abs(v[6] - x) < tolerance && abs(v[7] - y) < tolerance) - return 1; - var txs = [], - tys = [], - sx = Curve.solveCubic(v, 0, x, txs, 0, 1), - sy = Curve.solveCubic(v, 1, y, tys, 0, 1), - tx, ty; - // sx, sy === -1 means infinite solutions: - // Loop through all solutions for x and match with solutions for y, - // to see if we either have a matching pair, or infinite solutions - // for one or the other. - for (var cx = 0; sx === -1 || cx < sx;) { - if (sx === -1 || (tx = txs[cx++]) > 0 && tx < 1) { - for (var cy = 0; sy === -1 || cy < sy;) { - if (sy === -1 || (ty = tys[cy++]) > 0 && ty < 1) { - // Handle infinite solutions by assigning root of - // the other polynomial - if (sx === -1) { - tx = ty; - } else if (sy === -1) { - ty = tx; - } - // Use average if we're within tolerance - if (abs(tx - ty) < tolerance) - return (tx + ty) * 0.5; - } - } - // Avoid endless loops here: If sx is infinite and there was - // no fitting ty, there's no solution for this bezier - if (sx === -1) - break; + getParameterOf: function(v, point) { + // Before solving cubics, compare the beginning and end of the curve + // with zero epsilon: + var p1 = new Point(v[0], v[1]), + p2 = new Point(v[6], v[7]), + epsilon = /*#=*/Numerical.EPSILON, + t = point.isClose(p1, epsilon) ? 0 + : point.isClose(p2, epsilon) ? 1 + : null; + if (t !== null) + return t; + // Solve the cubic for both x- and y-coordinates and consider all found + // solutions, testing with the larger / looser geometric epsilon. + var coords = [point.x, point.y], + roots = [], + geomEpsilon = /*#=*/Numerical.GEOMETRIC_EPSILON; + for (var c = 0; c < 2; c++) { + var count = Curve.solveCubic(v, c, coords[c], roots, 0, 1); + for (var i = 0; i < count; i++) { + t = roots[i]; + if (point.isClose(Curve.getPoint(v, t), geomEpsilon)) + return t; } } - return null; + // For very short curves (length ~ 1e-13), the above code will not + // necessarily produce any valid roots. As a fall-back, just check the + // beginnings and ends at the end so we can still return a valid result. + return point.isClose(p1, geomEpsilon) ? 0 + : point.isClose(p2, geomEpsilon) ? 1 + : null; + }, + + getNearestParameter: function(v, point) { + var count = 100, + minDist = Infinity, + minT = 0; + + function refine(t) { + if (t >= 0 && t <= 1) { + var dist = point.getDistance(Curve.getPoint(v, t), true); + if (dist < minDist) { + minDist = dist; + minT = t; + return true; + } + } + } + + for (var i = 0; i <= count; i++) + refine(i / count); + + // Now iteratively refine solution until we reach desired precision. + var step = 1 / (count * 2); + while (step > /*#=*/Numerical.CURVETIME_EPSILON) { + if (!refine(minT - step) && !refine(minT + step)) + step /= 2; + } + return minT; }, // TODO: Find better name getPart: function(v, from, to) { + var flip = from > to; + if (flip) { + var tmp = from; + from = to; + to = tmp; + } if (from > 0) v = Curve.subdivide(v, from)[1]; // [1] right // Interpolate the parameter at 'to' in the new curve and cut there. if (to < 1) v = Curve.subdivide(v, (to - from) / (1 - from))[0]; // [0] left - return v; + // Return reversed curve if from / to were flipped: + return flip + ? [v[6], v[7], v[4], v[5], v[2], v[3], v[0], v[1]] + : v; }, hasHandles: function(v) { @@ -631,15 +698,6 @@ statics: { && isZero(v[4] - v[6]) && isZero(v[5] - v[7])); }, - isLinear: function(v) { - // See Segment#isLinear(): - var p1x = v[0], p1y = v[1], - p2x = v[6], p2y = v[7], - l = new Point(p2x - p1x, p2y - p1y); - return l.isCollinear(new Point(v[2] - p1x, v[3] - p1y)) - && l.isCollinear(new Point(v[4] - p2x, v[5] - p2y)); - }, - isFlatEnough: function(v, tolerance) { // Thanks to Kaspar Fischer and Roger Willcocks for the following: // http://hcklbrrfnn.files.wordpress.com/2012/08/bez.pdf @@ -656,29 +714,26 @@ statics: { }, getArea: function(v) { - var p1x = v[0], p1y = v[1], - c1x = v[2], c1y = v[3], - c2x = v[4], c2y = v[5], - p2x = v[6], p2y = v[7]; + // This is a combination of the methods to decide if a path is clockwise + // and to calculate the area, as described here: // http://objectmix.com/graphics/133553-area-closed-bezier-curve.html - return ( 3.0 * c1y * p1x - 1.5 * c1y * c2x - - 1.5 * c1y * p2x - 3.0 * p1y * c1x - - 1.5 * p1y * c2x - 0.5 * p1y * p2x - + 1.5 * c2y * p1x + 1.5 * c2y * c1x - - 3.0 * c2y * p2x + 0.5 * p2y * p1x - + 1.5 * p2y * c1x + 3.0 * p2y * c2x) / 10; - }, - - getEdgeSum: function(v) { - // Method derived from: // http://stackoverflow.com/questions/1165647 // We treat the curve points and handles as the outline of a polygon of // which we determine the orientation using the method of calculating // the sum over the edges. This will work even with non-convex polygons, - // telling you whether it's mostly clockwise - return (v[0] - v[2]) * (v[3] + v[1]) - + (v[2] - v[4]) * (v[5] + v[3]) - + (v[4] - v[6]) * (v[7] + v[5]); + // telling you whether it's mostly clockwise. + // With bezier curves, the trick appears to be to calculate edge sum + // with half the handles' lengths, and then: + // area = 6 * edge-sum / 10 + var p1x = v[0], p1y = v[1], + p2x = v[6], p2y = v[7], + h1x = (v[2] + p1x) / 2, + h1y = (v[3] + p1y) / 2, + h2x = (v[4] + v[6]) / 2, + h2y = (v[5] + v[7]) / 2; + return 6 * ((p1x - h1x) * (h1y + p1y) + + (h1x - h2x) * (h2y + h1y) + + (h2x - p2x) * (p2y + h2y)) / 10; }, getBounds: function(v) { @@ -718,7 +773,7 @@ statics: { // Add some tolerance for good roots, as t = 0, 1 are added // separately anyhow, and we don't want joins to be added with radii // in getStrokeBounds() - tMin = /*#=*/Numerical.TOLERANCE, + tMin = /*#=*/Numerical.CURVETIME_EPSILON, tMax = 1 - tMin; // Only add strokeWidth to bounds for points which lie within 0 < t < 1 // The corner cases for cap and join are handled in getStrokeBounds() @@ -751,14 +806,18 @@ statics: { if (!bounds) { // Calculate the curve bounds by passing a segment list for the // curve to the static Path.get*Boudns methods. - bounds = this._bounds[name] = Path[name]([this._segment1, - this._segment2], false, this._path.getStyle()); + var path = this._path; + bounds = this._bounds[name] = Path[name]( + [this._segment1, this._segment2], false, + path && path.getStyle()); } return bounds.clone(); }; }, /** @lends Curve# */{ /** + * {@grouptitle Bounding Boxes} + * * The bounding rectangle of the curve excluding stroke width. * * @name Curve#bounds @@ -787,6 +846,117 @@ statics: { * @type Rectangle * @ignore */ +}), Base.each({ // Injection scope for tests both as instance and static methods + isStraight: function(l, h1, h2) { + if (h1.isZero() && h2.isZero()) { + // No handles. + return true; + } else if (l.isZero()) { + // Zero-length line, with some handles defined. + return false; + } else if (h1.isCollinear(l) && h2.isCollinear(l)) { + // Collinear handles. Project them onto line to see if they are + // within the line's range: + var div = l.dot(l), + p1 = l.dot(h1) / div, + p2 = l.dot(h2) / div; + return p1 >= 0 && p1 <= 1 && p2 <= 0 && p2 >= -1; + } + return false; + }, + + isLinear: function(l, h1, h2) { + var third = l.divide(3); + return h1.equals(third) && h2.negate().equals(third); + } +}, function(test, name) { + // Produce the instance version that is called on curve object. + this[name] = function() { + var seg1 = this._segment1, + seg2 = this._segment2; + return test(seg2._point.subtract(seg1._point), + seg1._handleOut, seg2._handleIn); + }; + + // Produce the static version that handles a curve values array. + this.statics[name] = function(v) { + var p1x = v[0], p1y = v[1], + p2x = v[6], p2y = v[7]; + return test(new Point(p2x - p1x, p2y - p1y), + new Point(v[2] - p1x, v[3] - p1y), + new Point(v[4] - p2x, v[5] - p2y)); + }; +}, /** @lends Curve# */{ + statics: {}, // Filled in the Base.each loop above. + + /** + * {@grouptitle Curve Tests} + * + * Checks if this curve has any curve handles set. + * + * @return {Boolean} {@true if the curve has handles set} + * @see Curve#handle1 + * @see Curve#handle2 + * @see Segment#hasHandles() + * @see Path#hasHandles() + */ + hasHandles: function() { + return !this._segment1._handleOut.isZero() + || !this._segment2._handleIn.isZero(); + }, + + /** + * Checks if this curve appears as a straight line. This can mean that + * it has no handles defined, or that the handles run collinear with the + * line that connects the curve's start and end point, not falling + * outside of the line. + * + * @name Curve#isStraight + * @function + * @return {Boolean} {@true if the curve is straight} + */ + + /** + * Checks if this curve is parametrically linear, meaning that it is + * straight and its handles are positioned at 1/3 and 2/3 of the total + * length of the curve. + * + * @name Curve#isLinear + * @function + * @return {Boolean} {@true if the curve is parametrically linear} + */ + + /** + * Checks if the the two curves describe straight lines that are + * collinear, meaning they run in parallel. + * + * @param {Curve} curve the other curve to check against + * @return {Boolean} {@true if the two lines are collinear} + */ + isCollinear: function(curve) { + return curve && this.isStraight() && curve.isStraight() + && this.getLine().isCollinear(curve.getLine()); + }, + + /** + * Checks if the curve is a straight horizontal line. + * + * @return {Boolean} {@true if the line is horizontal} + */ + isHorizontal: function() { + return this.isStraight() && Math.abs(this.getTangentAt(0.5, true).y) + < /*#=*/Numerical.TRIGONOMETRIC_EPSILON; + }, + + /** + * Checks if the curve is a straight vertical line. + * + * @return {Boolean} {@true if the line is vertical} + */ + isVertical: function() { + return this.isStraight() && Math.abs(this.getTangentAt(0.5, true).x) + < /*#=*/Numerical.TRIGONOMETRIC_EPSILON; + } }), /** @lends Curve# */{ // Explicitly deactivate the creation of beans, as we have functions here // that look like bean getters but actually read arguments. @@ -794,6 +964,8 @@ statics: { beans: false, /** + * {@grouptitle Positions on Curves} + * * Calculates the curve time parameter of the specified offset on the path, * relative to the provided start parameter. If offset is a negative value, * the parameter is searched to the left of the start parameter. If no start @@ -816,8 +988,7 @@ statics: { * @return {Number} the curve time parameter of the specified point */ getParameterOf: function(/* point */) { - var point = Point.read(arguments); - return Curve.getParameterOf(this.getValues(), point.x, point.y); + return Curve.getParameterOf(this.getValues(), Point.read(arguments)); }, /** @@ -861,38 +1032,30 @@ statics: { return loc ? loc.getOffset() : null; }, + /** + * Returns the nearest location on the curve to the specified point. + * + * @function + * @param {Point} point the point for which we search the nearest location + * @return {CurveLocation} the location on the curve that's the closest to + * the specified point + */ getNearestLocation: function(/* point */) { var point = Point.read(arguments), values = this.getValues(), - count = 100, - minDist = Infinity, - minT = 0; - - function refine(t) { - if (t >= 0 && t <= 1) { - var dist = point.getDistance(Curve.getPoint(values, t), true); - if (dist < minDist) { - minDist = dist; - minT = t; - return true; - } - } - } - - for (var i = 0; i <= count; i++) - refine(i / count); - - // Now iteratively refine solution until we reach desired precision. - var step = 1 / (count * 2); - while (step > /*#=*/Numerical.TOLERANCE) { - if (!refine(minT - step) && !refine(minT + step)) - step /= 2; - } - var pt = Curve.getPoint(values, minT); - return new CurveLocation(this, minT, pt, null, null, null, - point.getDistance(pt)); + t = Curve.getNearestParameter(values, point), + pt = Curve.getPoint(values, t); + return new CurveLocation(this, t, pt, null, point.getDistance(pt)); }, + /** + * Returns the nearest point on the curve to the specified point. + * + * @function + * @param {Point} point the point for which we search the nearest point + * @return {Point} the point on the curve that's the closest to the + * specified point + */ getNearestPoint: function(/* point */) { return this.getNearestLocation.apply(this, arguments).getPoint(); } @@ -1037,12 +1200,13 @@ new function() { // Scope for methods that require private functions c1x = v[2], c1y = v[3], c2x = v[4], c2y = v[5], p2x = v[6], p2y = v[7], - tolerance = /*#=*/Numerical.TOLERANCE, + tMin = /*#=*/Numerical.CURVETIME_EPSILON, + tMax = 1 - tMin, x, y; // Handle special case at beginning / end of curve - if (type === 0 && (t < tolerance || t > 1 - tolerance)) { - var isZero = t < tolerance; + if (type === 0 && (t < tMin || t > tMax)) { + var isZero = t < tMin; x = isZero ? p1x : p2x; y = isZero ? p1y : p2y; } else { @@ -1066,10 +1230,10 @@ new function() { // Scope for methods that require private functions // the x and y coordinates: // Prevent tangents and normals of length 0: // http://stackoverflow.com/questions/10506868/ - if (t < tolerance) { + if (t < tMin) { x = cx; y = cy; - } else if (t > 1 - tolerance) { + } else if (t > tMax) { x = 3 * (p2x - c2x); y = 3 * (p2y - c2y); } else { @@ -1080,8 +1244,7 @@ new function() { // Scope for methods that require private functions // When the tangent at t is zero and we're at the beginning // or the end, we can use the vector between the handles, // but only when normalizing as its weighted length is 0. - if (x === 0 && y === 0 - && (t < tolerance || t > 1 - tolerance)) { + if (x === 0 && y === 0 && (t < tMin || t > tMax)) { x = c2x - c1x; y = c2y - c1y; } @@ -1110,20 +1273,15 @@ new function() { // Scope for methods that require private functions return type === 2 ? new Point(y, -x) : new Point(x, y); } - return { - statics: true, + return { statics: { getLength: function(v, a, b) { if (a === undefined) a = 0; if (b === undefined) b = 1; - var isZero = Numerical.isZero; - // See if the curve is linear by checking p1 == c1 and p2 == c2 - if (a === 0 && b === 1 - && isZero(v[0] - v[2]) && isZero(v[1] - v[3]) - && isZero(v[6] - v[4]) && isZero(v[7] - v[5])) { - // Straight line + if (a === 0 && b === 1 && Curve.isStraight(v)) { + // The length of straight curves can be calculated more easily. var dx = v[6] - v[0], // p2x - p1x dy = v[7] - v[1]; // p2y - p1y return Math.sqrt(dx * dx + dy * dy); @@ -1139,8 +1297,7 @@ new function() { // Scope for methods that require private functions return start; // See if we're going forward or backward, and handle cases // differently - var tolerance = /*#=*/Numerical.TOLERANCE, - abs = Math.abs, + var abs = Math.abs, forward = offset > 0, a = forward ? start : 0, b = forward ? 1 : start, @@ -1150,7 +1307,7 @@ new function() { // Scope for methods that require private functions // Get length of total range rangeLength = Numerical.integrate(ds, a, b, getIterations(a, b)); - if (abs(offset - rangeLength) < tolerance) { + if (abs(offset - rangeLength) < /*#=*/Numerical.EPSILON) { // Matched the end: return forward ? b : a; } else if (abs(offset) > rangeLength) { @@ -1174,8 +1331,8 @@ new function() { // Scope for methods that require private functions } // Start with out initial guess for x. // NOTE: guess is a negative value when not looking forward. - return Numerical.findRoot(f, ds, start + guess, a, b, 16, - tolerance); + return Numerical.findRoot(f, ds, start + guess, a, b, 32, + /*#=*/Numerical.EPSILON); }, getPoint: function(v, t) { @@ -1201,20 +1358,63 @@ new function() { // Scope for methods that require private functions getCurvature: function(v, t) { return evaluate(v, t, 3, false).x; } - }; -}, new function() { // Scope for intersection using bezier fat-line clipping - function addLocation(locations, include, curve1, t1, point1, curve2, t2, - point2) { - var loc = new CurveLocation(curve1, t1, point1, curve2, t2, point2); - if (!include || include(loc)) { - locations.push(loc); - } else { - loc = null; + }}; +}, +new function() { // Scope for intersection using bezier fat-line clipping + + function addLocation(locations, param, v1, c1, t1, p1, v2, c2, t2, p2, + overlap) { + var startConnected = param.startConnected, + endConnected = param.endConnected, + tMin = /*#=*/Numerical.CURVETIME_EPSILON, + tMax = 1 - tMin; + if (t1 == null) + t1 = Curve.getParameterOf(v1, p1); + // Check t1 and t2 against correct bounds, based on start-/endConnected: + // - startConnected means the start of c1 connects to the end of c2 + // - endConneted means the end of c1 connects to the start of c2 + // - If either c1 or c2 are at the end of the path, exclude their end, + // which connects back to the beginning, but only if it's not part of + // a found overlap. The normal intersection will already be found at + // the beginning, and would be added twice otherwise. + if (t1 !== null && t1 >= (startConnected ? tMin : 0) && + t1 <= (endConnected ? tMax : 1)) { + if (t2 == null) + t2 = Curve.getParameterOf(v2, p2); + if (t2 !== null && t2 >= (endConnected ? tMin : 0) && + t2 <= (startConnected ? tMax : 1)) { + // TODO: Don't we need to check the range of t2 as well? Does it + // also need startConnected / endConnected values? + var renormalize = param.renormalize; + if (renormalize) { + var res = renormalize(t1, t2); + t1 = res[0]; + t2 = res[1]; + } + var loc1 = new CurveLocation(c1, t1, + p1 || Curve.getPoint(v1, t1), overlap), + loc2 = new CurveLocation(c2, t2, + p2 || Curve.getPoint(v2, t2), overlap), + // For self-intersections, detect the case where the second + // curve wrapped around, and flip them so they can get + // matched to a potentially already existing intersection. + flip = loc1.getPath() === loc2.getPath() + && loc1.getIndex() > loc2.getIndex(), + loc = flip ? loc2 : loc1, + include = param.include; + // Link the two locations to each other. + loc1._intersection = loc2; + loc2._intersection = loc1; + // TODO: Remove this once debug logging is removed. + (flip ? loc1 : loc2)._other = true; + if (!include || include(loc)) { + CurveLocation.insert(locations, loc, true); + } + } } - return loc; } - function addCurveIntersections(v1, v2, curve1, curve2, locations, include, + function addCurveIntersections(v1, v2, c1, c2, locations, param, tMin, tMax, uMin, uMax, oldTDiff, reverse, recursion) { // Avoid deeper recursion. // NOTE: @iconexperience determined that more than 20 recursions are @@ -1222,16 +1422,16 @@ new function() { // Scope for methods that require private functions // below when determining which curve converges the least. He also // recommended a threshold of 0.5 instead of the initial 0.8 // See: https://github.com/paperjs/paper.js/issues/565 - if (recursion > 32) + if (++recursion >= 24) return; // Let P be the first curve and Q be the second var q0x = v2[0], q0y = v2[1], q3x = v2[6], q3y = v2[7], - tolerance = /*#=*/Numerical.TOLERANCE, + epsilon = /*#=*/(Numerical.CURVETIME_EPSILON / 10), getSignedDistance = Line.getSignedDistance, // Calculate the fat-line L for Q is the baseline l and two // offsets which completely encloses the curve P. - d1 = getSignedDistance(q0x, q0y, q3x, q3y, v2[2], v2[3]) || 0, - d2 = getSignedDistance(q0x, q0y, q3x, q3y, v2[4], v2[5]) || 0, + d1 = getSignedDistance(q0x, q0y, q3x, q3y, v2[2], v2[3]), + d2 = getSignedDistance(q0x, q0y, q3x, q3y, v2[4], v2[5]), factor = d1 * d2 > 0 ? 3 / 4 : 4 / 9, dMin = factor * Math.min(0, d1, d2), dMax = factor * Math.max(0, d1, d2), @@ -1242,35 +1442,25 @@ new function() { // Scope for methods that require private functions dp1 = getSignedDistance(q0x, q0y, q3x, q3y, v1[2], v1[3]), dp2 = getSignedDistance(q0x, q0y, q3x, q3y, v1[4], v1[5]), dp3 = getSignedDistance(q0x, q0y, q3x, q3y, v1[6], v1[7]), - tMinNew, tMaxNew, tDiff; - if (q0x === q3x && uMax - uMin < tolerance && recursion > 3) { - // The fatline of Q has converged to a point, the clipping is not - // reliable. Return the value we have even though we will miss the - // precision. - tMaxNew = tMinNew = (tMax + tMin) / 2; - tDiff = 0; - } else { // Get the top and bottom parts of the convex-hull - var hull = getConvexHull(dp0, dp1, dp2, dp3), - top = hull[0], - bottom = hull[1], - tMinClip, tMaxClip; - // Clip the convex-hull with dMin and dMax - tMinClip = clipConvexHull(top, bottom, dMin, dMax); - top.reverse(); - bottom.reverse(); - tMaxClip = clipConvexHull(top, bottom, dMin, dMax); - // No intersections if one of the tvalues are null or 'undefined' - if (tMinClip == null || tMaxClip == null) - return; - // Clip P with the fatline for Q - v1 = Curve.getPart(v1, tMinClip, tMaxClip); - tDiff = tMaxClip - tMinClip; + hull = getConvexHull(dp0, dp1, dp2, dp3), + top = hull[0], + bottom = hull[1], + tMinClip, + tMaxClip; + // Clip the convex-hull with dMin and dMax, taking into account that + // there will be no intersections if one of the tvalues are null. + if ((tMinClip = clipConvexHull(top, bottom, dMin, dMax)) == null || + (tMaxClip = clipConvexHull(top.reverse(), bottom.reverse(), + dMin, dMax)) == null) + return; + // Clip P with the fat-line for Q + v1 = Curve.getPart(v1, tMinClip, tMaxClip); + var tDiff = tMaxClip - tMinClip, // tMin and tMax are within the range (0, 1). We need to project it // to the original parameter range for v2. - tMinNew = tMax * tMinClip + tMin * (1 - tMinClip); - tMaxNew = tMax * tMaxClip + tMin * (1 - tMaxClip); - } + tMinNew = tMin + (tMax - tMin) * tMinClip, + tMaxNew = tMin + (tMax - tMin) * tMaxClip; // Check if we need to subdivide the curves if (oldTDiff > 0.5 && tDiff > 0.5) { // Subdivide the curve which has converged the least. @@ -1278,37 +1468,38 @@ new function() { // Scope for methods that require private functions var parts = Curve.subdivide(v1, 0.5), t = tMinNew + (tMaxNew - tMinNew) / 2; addCurveIntersections( - v2, parts[0], curve2, curve1, locations, include, - uMin, uMax, tMinNew, t, tDiff, !reverse, ++recursion); + v2, parts[0], c2, c1, locations, param, + uMin, uMax, tMinNew, t, tDiff, !reverse, recursion); addCurveIntersections( - v2, parts[1], curve2, curve1, locations, include, + v2, parts[1], c2, c1, locations, param, uMin, uMax, t, tMaxNew, tDiff, !reverse, recursion); } else { var parts = Curve.subdivide(v2, 0.5), t = uMin + (uMax - uMin) / 2; addCurveIntersections( - parts[0], v1, curve2, curve1, locations, include, - uMin, t, tMinNew, tMaxNew, tDiff, !reverse, ++recursion); + parts[0], v1, c2, c1, locations, param, + uMin, t, tMinNew, tMaxNew, tDiff, !reverse, recursion); addCurveIntersections( - parts[1], v1, curve2, curve1, locations, include, + parts[1], v1, c2, c1, locations, param, t, uMax, tMinNew, tMaxNew, tDiff, !reverse, recursion); } - } else if (Math.max(uMax - uMin, tMaxNew - tMinNew) < tolerance) { + } else if (Math.max(uMax - uMin, tMaxNew - tMinNew) < epsilon) { // We have isolated the intersection with sufficient precision var t1 = tMinNew + (tMaxNew - tMinNew) / 2, t2 = uMin + (uMax - uMin) / 2; - if (reverse) { - addLocation(locations, include, - curve2, t2, Curve.getPoint(v2, t2), - curve1, t1, Curve.getPoint(v1, t1)); - } else { - addLocation(locations, include, - curve1, t1, Curve.getPoint(v1, t1), - curve2, t2, Curve.getPoint(v2, t2)); - } - } else if (tDiff > 0) { // Iterate - addCurveIntersections(v2, v1, curve2, curve1, locations, include, - uMin, uMax, tMinNew, tMaxNew, tDiff, !reverse, ++recursion); + // Since we've been chopping up v1 and v2, we need to pass on the + // original full curves here again to match the parameter space of + // t1 and t2. + // TODO: Add two more arguments to addCurveIntersections after param + // to pass on the sub-curves. + v1 = c1.getValues(); + v2 = c2.getValues(); + addLocation(locations, param, + reverse ? v2 : v1, reverse ? c2 : c1, reverse ? t2 : t1, null, + reverse ? v1 : v2, reverse ? c1 : c2, reverse ? t1 : t2, null); + } else if (tDiff > /*#=*/Numerical.EPSILON) { // Iterate + addCurveIntersections(v2, v1, c2, c1, locations, param, + uMin, uMax, tMinNew, tMaxNew, tDiff, !reverse, recursion); } } @@ -1331,54 +1522,38 @@ new function() { // Scope for methods that require private functions p1 = [ 1 / 3, dq1 ], p2 = [ 2 / 3, dq2 ], p3 = [ 1, dq3 ], - // Find signed distance of p1 and p2 from line [ p0, p3 ] - getSignedDistance = Line.getSignedDistance, - dist1 = getSignedDistance(0, dq0, 1, dq3, 1 / 3, dq1), - dist2 = getSignedDistance(0, dq0, 1, dq3, 2 / 3, dq2), - flip = false, + // Find vertical signed distance of p1 and p2 from line [p0, p3] + dist1 = dq1 - (2 * dq0 + dq3) / 3, + dist2 = dq2 - (dq0 + 2 * dq3) / 3, hull; - // Check if p1 and p2 are on the same side of the line [ p0, p3 ] + // Check if p1 and p2 are on the opposite side of the line [p0, p3] if (dist1 * dist2 < 0) { - // p1 and p2 lie on different sides of [ p0, p3 ]. The hull is a - // quadrilateral and line [ p0, p3 ] is NOT part of the hull so we - // are pretty much done here. - // The top part includes p1, - // we will reverse it later if that is not the case + // p1 and p2 lie on different sides of [p0, p3]. The hull is a + // quadrilateral and line [p0, p3] is NOT part of the hull so we are + // pretty much done here. The top part includes p1, we will reverse + // it later if that is not the case. hull = [[p0, p1, p3], [p0, p2, p3]]; - flip = dist1 < 0; } else { - // p1 and p2 lie on the same sides of [ p0, p3 ]. The hull can be - // a triangle or a quadrilateral and line [ p0, p3 ] is part of the - // hull. Check if the hull is a triangle or a quadrilateral. - // Also, if at least one of the distances for p1 or p2, from line - // [p0, p3] is zero then hull must at most have 3 vertices. - var pmax, cross = 0, - distZero = dist1 === 0 || dist2 === 0; - if (Math.abs(dist1) > Math.abs(dist2)) { - pmax = p1; - // apex is dq3 and the other apex point is dq0 vector dqapex -> - // dqapex2 or base vector which is already part of the hull. - cross = (dq3 - dq2 - (dq3 - dq0) / 3) - * (2 * (dq3 - dq2) - dq3 + dq1) / 3; - } else { - pmax = p2; - // apex is dq0 in this case, and the other apex point is dq3 - // vector dqapex -> dqapex2 or base vector which is already part - // of the hull. - cross = (dq1 - dq0 + (dq0 - dq3) / 3) - * (-2 * (dq0 - dq1) + dq0 - dq2) / 3; - } - // Compare cross products of these vectors to determine if the point - // is in the triangle [ p3, pmax, p0 ], or if it is a quadrilateral. - hull = cross < 0 || distZero - // p2 is inside the triangle, hull is a triangle. - ? [[p0, pmax, p3], [p0, p3]] - // Convex hull is a quadrilateral and we need all lines in - // correct order where line [ p0, p3 ] is part of the hull. - : [[p0, p1, p2, p3], [p0, p3]]; - flip = dist1 ? dist1 < 0 : dist2 < 0; + // p1 and p2 lie on the same sides of [p0, p3]. The hull can be a + // triangle or a quadrilateral and line [p0, p3] is part of the + // hull. Check if the hull is a triangle or a quadrilateral. We have + // a triangle if the vertical distance of one of the middle points + // (p1, p2) is equal or less than half the vertical distance of the + // other middle point. + var distRatio = dist1 / dist2; + hull = [ + // p2 is inside, the hull is a triangle. + distRatio >= 2 ? [p0, p1, p3] + // p1 is inside, the hull is a triangle. + : distRatio <= .5 ? [p0, p2, p3] + // Hull is a quadrilateral, we need all lines in correct order. + : [p0, p1, p2, p3], + // Line [p0, p3] is part of the hull. + [p0, p3] + ]; } - return flip ? hull.reverse() : hull; + // Flip hull if dist1 is negative or if it is zero and dist2 is negative + return (dist1 || dist2) < 0 ? hull.reverse() : hull; } /** @@ -1405,8 +1580,10 @@ new function() { // Scope for methods that require private functions for (var i = 1, l = part.length; i < l; i++) { var qx = part[i][0], qy = part[i][1]; - if (top ? qy >= threshold : qy <= threshold) - return px + (threshold - py) * (qx - px) / (qy - py); + if (top ? qy >= threshold : qy <= threshold) { + return qy === threshold ? qx + : px + (threshold - py) * (qx - px) / (qy - py); + } px = qx; py = qy; } @@ -1420,9 +1597,8 @@ new function() { // Scope for methods that require private functions * line is on the X axis, and solve the implicit equations for the X axis * and the curve. */ - function addCurveLineIntersections(v1, v2, curve1, curve2, locations, - include) { - var flip = Curve.isLinear(v1), + function addCurveLineIntersections(v1, v2, c1, c2, locations, param) { + var flip = Curve.isStraight(v1), vc = flip ? v2 : v1, vl = flip ? v1 : v2, lx1 = vl[0], ly1 = vl[1], @@ -1435,9 +1611,6 @@ new function() { // Scope for methods that require private functions sin = Math.sin(angle), cos = Math.cos(angle), // (rlx1, rly1) = (0, 0) - rlx2 = ldx * cos - ldy * sin, - // The curve values for the rotated line. - rvl = [0, 0, 0, 0, rlx2, 0, rlx2, 0], // Calculate the curve values of the rotated curve. rvc = []; for(var i = 0; i < 8; i += 2) { @@ -1445,222 +1618,288 @@ new function() { // Scope for methods that require private functions y = vc[i + 1] - ly1; rvc.push( x * cos - y * sin, - y * cos + x * sin); + x * sin + y * cos); } + // Solve it for y = 0. We need to include t = 0, 1 and let addLocation() + // do the filtering, to catch important edge cases. var roots = [], count = Curve.solveCubic(rvc, 1, 0, roots, 0, 1); // NOTE: count could be -1 for infinite solutions, but that should only // happen with lines, in which case we should not be here. for (var i = 0; i < count; i++) { + // For each found solution on the rotated curve, get the point on + // the real curve and with that the location on the line. var tc = roots[i], - x = Curve.getPoint(rvc, tc).x; - // We do have a point on the infinite line. Check if it falls on - // the line *segment*. - if (x >= 0 && x <= rlx2) { - // Find the parameter of the intersection on the rotated line. - var tl = Curve.getParameterOf(rvl, x, 0), + pc = Curve.getPoint(vc, tc), + tl = Curve.getParameterOf(vl, pc); + if (tl !== null) { + var pl = Curve.getPoint(vl, tl), t1 = flip ? tl : tc, t2 = flip ? tc : tl; - addLocation(locations, include, - curve1, t1, Curve.getPoint(v1, t1), - curve2, t2, Curve.getPoint(v2, t2)); - } - } - } - - function addLineIntersection(v1, v2, curve1, curve2, locations, include) { - var point = Line.intersect( - v1[0], v1[1], v1[6], v1[7], - v2[0], v2[1], v2[6], v2[7]); - if (point) { - // We need to return the parameters for the intersection, - // since they will be used for sorting - var x = point.x, - y = point.y; - addLocation(locations, include, - curve1, Curve.getParameterOf(v1, x, y), point, - curve2, Curve.getParameterOf(v2, x, y), point); - } - } - - /** - * Code to detect overlaps of intersecting curves by @iconexperience: - * https://github.com/paperjs/paper.js/issues/648 - */ - function addOverlap(v1, v2, curve1, curve2, locations, include) { - var abs = Math.abs, - tolerance = /*#=*/Numerical.TOLERANCE, - epsilon = /*#=*/Numerical.EPSILON, - linear1 = Curve.isLinear(v1), - linear2 = Curve.isLinear(v2), - linear = linear1 && linear2; - if (linear) { - // Linear curves can only overlap if they are collinear, which means - // they must be are collinear and any point of curve 1 must be on - // curve 2 - var line1 = new Line(v1[0], v1[1], v1[6], v1[7], false), - line2 = new Line(v2[0], v2[1], v2[6], v2[7], false); - if (!line1.isCollinear(line2) || - line1.getDistance(line2.getPoint()) > epsilon) - return false; - } else if (linear1 ^ linear2) { - // If one curve is linear, the other curve must be linear, too, - // otherwise they cannot overlap. - return false; - } - var v = [v1, v2], - pairs = []; - // Iterate through all end points: First p1 and p2 of curve 1, - // then p1 and p2 of curve 2 - for (var i = 0, t1 = 0; - i < 2 && pairs.length < 2; - i += t1 === 0 ? 0 : 1, t1 = t1 ^ 1) { - var t2 = Curve.getParameterOf(v[i ^ 1], - v[i][t1 === 0 ? 0 : 6], - v[i][t1 === 0 ? 1 : 7]); - if (t2 != null) { // If point is on curve - var pair = i === 0 ? [t1, t2] : [t2, t1]; - if (pairs.length === 1 && pair[0] < pairs[0][0]) { - pairs.unshift(pair); - } else if (pairs.length === 0 - || abs(pair[0] - pairs[0][0]) > tolerance - || abs(pair[1] - pairs[0][1]) > tolerance) { - pairs.push(pair); + // If the two curves are connected and the 2nd is very short, + // (l < Numerical.GEOMETRIC_EPSILON), we need to filter out an + // invalid intersection at the beginning of this short curve. + if (!param.endConnected || t2 > Numerical.CURVETIME_EPSILON) { + addLocation(locations, param, + v1, c1, t1, flip ? pl : pc, + v2, c2, t2, flip ? pc : pl); } } - // If we checked 3 points but found no match, curves cannot overlap - if (i === 1 && pairs.length === 0) - return false; } - // If we found 2 pairs, the end points of v1 & v2 should be the same. - // We only have to check if the handles are the same, too. - if (pairs.length === 2) { - // create values for overlapping part of each curve - var c1 = Curve.getPart(v[0], pairs[0][0], pairs[1][0]), - c2 = Curve.getPart(v[1], Math.min(pairs[0][1], pairs[1][1]), - Math.max(pairs[0][1], pairs[1][1])); - // Reverse values of second curve if necessary - // if (abs(c1[0] - c2[6]) < epsilon && abs(c1[1] - c2[7]) < epsilon) { - if (pairs[0][1] > pairs[1][1]) { - c2 = [c2[6], c2[7], c2[4], c2[5], c2[2], c2[3], c2[0], c2[1]]; - } - // Check if handles of overlapping paths are similar enough. - // We could do another check for curve identity here if we find a - // better criteria. - if (linear || - abs(c2[0] - c1[0]) < epsilon && - abs(c2[1] - c1[1]) < epsilon && - abs(c2[1] - c1[1]) < epsilon && - abs(c2[3] - c1[3]) < epsilon && - abs(c2[2] - c1[2]) < epsilon && - abs(c2[5] - c1[5]) < epsilon && - abs(c2[3] - c1[3]) < epsilon && - abs(c2[7] - c1[7]) < epsilon) { - // Overlapping parts are identical - var t11 = pairs[0][0], - t12 = pairs[0][1], - t21 = pairs[1][0], - t22 = pairs[1][1], - loc1 = addLocation(locations, include, - curve1, t11, Curve.getPoint(v1, t11), - curve2, t12, Curve.getPoint(v2, t12), true), - loc2 = addLocation(locations, include, - curve1, t21, Curve.getPoint(v1, t21), - curve2, t22, Curve.getPoint(v2, t22), true); - if (loc1) - loc1._overlap = true; - if (loc2) - loc2._overlap = true; - return true; - } + } + + function addLineIntersection(v1, v2, c1, c2, locations, param) { + var pt = Line.intersect( + v1[0], v1[1], v1[6], v1[7], + v2[0], v2[1], v2[6], v2[7]); + if (pt) { + addLocation(locations, param, v1, c1, null, pt, v2, c2, null, pt); } - return false; } return { statics: /** @lends Curve */{ - // We need to provide the original left curve reference to the - // #getIntersections() calls as it is required to create the resulting - // CurveLocation objects. - getIntersections: function(v1, v2, c1, c2, locations, include) { - if (addOverlap(v1, v2, c1, c2, locations, include)) + _getIntersections: function(v1, v2, c1, c2, locations, param) { + if (!v2) { + // If v2 is not provided, search for self intersection on v1. + return Curve._getSelfIntersection(v1, c1, locations, param); + } + // Avoid checking curves if completely out of control bounds. As + // a little optimization, we can scale the handles with 0.75 + // before calculating the control bounds and still be sure that + // the curve is fully contained. + var c1p1x = v1[0], c1p1y = v1[1], + c1p2x = v1[6], c1p2y = v1[7], + c2p1x = v2[0], c2p1y = v2[1], + c2p2x = v2[6], c2p2y = v2[7], + // 's' stands for scaled handles... + c1s1x = (3 * v1[2] + c1p1x) / 4, + c1s1y = (3 * v1[3] + c1p1y) / 4, + c1s2x = (3 * v1[4] + c1p2x) / 4, + c1s2y = (3 * v1[5] + c1p2y) / 4, + c2s1x = (3 * v2[2] + c2p1x) / 4, + c2s1y = (3 * v2[3] + c2p1y) / 4, + c2s2x = (3 * v2[4] + c2p2x) / 4, + c2s2y = (3 * v2[5] + c2p2y) / 4, + min = Math.min, + max = Math.max; + if (!( max(c1p1x, c1s1x, c1s2x, c1p2x) >= + min(c2p1x, c2s1x, c2s2x, c2p2x) && + min(c1p1x, c1s1x, c1s2x, c1p2x) <= + max(c2p1x, c2s1x, c2s2x, c2p2x) && + max(c1p1y, c1s1y, c1s2y, c1p2y) >= + min(c2p1y, c2s1y, c2s2y, c2p2y) && + min(c1p1y, c1s1y, c1s2y, c1p2y) <= + max(c2p1y, c2s1y, c2s2y, c2p2y))) return locations; - var linear1 = Curve.isLinear(v1), - linear2 = Curve.isLinear(v2), - c1p1 = c1.getPoint1(), - c1p2 = c1.getPoint2(), - c2p1 = c2.getPoint1(), - c2p2 = c2.getPoint2(), - tolerance = /*#=*/Numerical.TOLERANCE; - // Handle a special case where if both curves start or end at the - // same point, the same end-point case will be handled after we - // calculate other intersections within the curve. - if (c1p1.isClose(c2p1, tolerance)) - addLocation(locations, include, c1, 0, c1p1, c2, 0, c1p1); - if (c1p1.isClose(c2p2, tolerance)) - addLocation(locations, include, c1, 0, c1p1, c2, 1, c1p1); - // Determine the correct intersection method based on values of - // linear1 & 2: - (linear1 && linear2 + // Now detect and handle overlaps: + if (!param.startConnected && !param.endConnected) { + var overlaps = Curve.getOverlaps(v1, v2); + if (overlaps) { + for (var i = 0; i < 2; i++) { + var overlap = overlaps[i]; + addLocation(locations, param, + v1, c1, overlap[0], null, + v2, c2, overlap[1], null, true); + } + return locations; + } + } + + var straight1 = Curve.isStraight(v1), + straight2 = Curve.isStraight(v2), + straight = straight1 && straight2, + // NOTE: Use smaller Numerical.EPSILON to compare beginnings and + // end points to avoid matching them on almost collinear lines, + // see: https://github.com/paperjs/paper.js/issues/777 + epsilon = /*#=*/Numerical.EPSILON, + before = locations.length; + // Determine the correct intersection method based on whether one or + // curves are straight lines: + (straight ? addLineIntersection - : linear1 || linear2 + : straight1 || straight2 ? addCurveLineIntersections : addCurveIntersections)( - v1, v2, c1, c2, locations, include, + v1, v2, c1, c2, locations, param, // Define the defaults for these parameters of // addCurveIntersections(): // tMin, tMax, uMin, uMax, oldTDiff, reverse, recursion 0, 1, 0, 1, 0, false, 0); - // Handle the special case where c1's end-point overlap with - // c2's points. - if (c1p2.isClose(c2p1, tolerance)) - addLocation(locations, include, c1, 1, c1p2, c2, 0, c1p2); - if (c1p2.isClose(c2p2, tolerance)) - addLocation(locations, include, c1, 1, c1p2, c2, 1, c1p2); + // We're done if we handle lines and found one intersection already: + // https://github.com/paperjs/paper.js/issues/805#issuecomment-148503018 + if (straight && locations.length > before) + return locations; + // Handle the special case where the first curve's start- or end- + // point overlaps with the second curve's start or end-point. + var c1p1 = new Point(c1p1x, c1p1y), + c1p2 = new Point(c1p2x, c1p2y), + c2p1 = new Point(c2p1x, c2p1y), + c2p2 = new Point(c2p2x, c2p2y); + if (c1p1.isClose(c2p1, epsilon)) + addLocation(locations, param, v1, c1, 0, c1p1, v2, c2, 0, c2p1); + if (!param.startConnected && c1p1.isClose(c2p2, epsilon)) + addLocation(locations, param, v1, c1, 0, c1p1, v2, c2, 1, c2p2); + if (!param.endConnected && c1p2.isClose(c2p1, epsilon)) + addLocation(locations, param, v1, c1, 1, c1p2, v2, c2, 0, c2p1); + if (c1p2.isClose(c2p2, epsilon)) + addLocation(locations, param, v1, c1, 1, c1p2, v2, c2, 1, c2p2); return locations; }, - filterIntersections: function(locations, expand) { - var last = locations.length - 1, - tMax = 1 - /*#=*/Numerical.TOLERANCE; - // Merge intersections very close to the end of a curve to the - // beginning of the next curve. - for (var i = last; i >= 0; i--) { - var loc = locations[i]; - if (loc._parameter >= tMax && (next = loc._curve.getNext())) { - loc._parameter = 0; - loc._curve = next; - } - if (loc._parameter2 >= tMax && (next = loc._curve2.getNext())) { - loc._parameter2 = 0; - loc._curve2 = next; - } + _getSelfIntersection: function(v1, c1, locations, param) { + // Read a detailed description of the approach used to handle self- + // intersection, developed by @iconexperience here: + // https://github.com/paperjs/paper.js/issues/773#issuecomment-144018379 + var p1x = v1[0], p1y = v1[1], + h1x = v1[2], h1y = v1[3], + h2x = v1[4], h2y = v1[5], + p2x = v1[6], p2y = v1[7]; + // Get the side of both control handles + var line = new Line(p1x, p1y, p2x, p2y, false), + side1 = line.getSide(h1x, h1y), + side2 = Line.getSide(h2x, h2y); + if (side1 === side2) { + var edgeSum = (p1x - h2x) * (h1y - p2y) + + (h1x - p2x) * (h2y - p1y); + // If both handles are on the same side, the curve can only have + // a self intersection if the edge sum and the handles' sides + // have different signs. If the handles are on the left side, + // the edge sum must be negative for a self intersection (and + // vice-versa). + if (edgeSum * side1 > 0) + return locations; } - - if (last > 0) { - CurveLocation.sort(locations); - // Filter out duplicate locations, but preserve _overlap setting - // among all duplicated (only one of them will have it defined). - var i = last, - loc = locations[i]; - while(--i >= 0) { - var prev = locations[i]; - if (prev.equals(loc)) { - locations.splice(i + 1, 1); // Remove loc. - // Preserve overlap setting. - var overlap = loc._overlap; - if (overlap) - prev._overlap = overlap; - last--; + // As a second condition we check if the curve has an inflection + // point. If an inflection point exists, the curve cannot have a + // self intersection. + var ax = p2x - 3 * h2x + 3 * h1x - p1x, + bx = h2x - 2 * h1x + p1x, + cx = h1x - p1x, + ay = p2y - 3 * h2y + 3 * h1y - p1y, + by = h2y - 2 * h1y + p1y, + cy = h1y - p1y, + // Condition for 1 or 2 inflection points: + // (ay*cx-ax*cy)^2 - 4*(ay*bx-ax*by)*(by*cx-bx*cy) >= 0 + ac = ay * cx - ax * cy, + ab = ay * bx - ax * by, + bc = by * cx - bx * cy; + if (ac * ac - 4 * ab * bc < 0) { + // The curve has no inflection points, so it may have a self + // intersection. Find the right parameter at which to split the + // curve. We search for the parameter where the velocity has an + // extremum by finding the roots of the cross product between + // the bezier curve's first and second derivative. + var roots = [], + tSplit, + count = Numerical.solveCubic( + ax * ax + ay * ay, + 3 * (ax * bx + ay * by), + 2 * (bx * bx + by * by) + ax * cx + ay * cy, + bx * cx + by * cy, + roots, 0, 1); + if (count > 0) { + // Select extremum with highest curvature. This is always on + // the loop in case of a self intersection. + for (var i = 0, maxCurvature = 0; i < count; i++) { + var curvature = Math.abs( + c1.getCurvatureAt(roots[i], true)); + if (curvature > maxCurvature) { + maxCurvature = curvature; + tSplit = roots[i]; + } } - loc = prev; + // Divide the curve in two and then apply the normal curve + // intersection code. + var parts = Curve.subdivide(v1, tSplit); + // After splitting, the end is always connected: + param.endConnected = true; + // Since the curve was split above, we need to adjust the + // parameters for both locations. + param.renormalize = function(t1, t2) { + return [t1 * tSplit, t2 * (1 - tSplit) + tSplit]; + }; + Curve._getIntersections(parts[0], parts[1], c1, c1, + locations, param); } } - if (expand) { - for (var i = last; i >= 0; i--) - locations.push(locations[i].getIntersection()); - CurveLocation.sort(locations); - } return locations; + }, + + /** + * Code to detect overlaps of intersecting curves by @iconexperience: + * https://github.com/paperjs/paper.js/issues/648 + */ + getOverlaps: function(v1, v2) { + var abs = Math.abs, + timeEpsilon = /*#=*/Numerical.CURVETIME_EPSILON, + geomEpsilon = /*#=*/Numerical.GEOMETRIC_EPSILON, + straight1 = Curve.isStraight(v1), + straight2 = Curve.isStraight(v2), + straight = straight1 && straight2; + + function getLineLengthSquared(v) { + var x = v[6] - v[0], + y = v[7] - v[1]; + return x * x + y * y; + } + + if (straight) { + // Linear curves can only overlap if they are collinear. Instead + // of using the #isCollinear() check, we pick the longer of the + // two lines and see how far the starting and end points of the + // other line are from this line (assumed as an infinite line). + var flip = getLineLengthSquared(v1) < getLineLengthSquared(v2), + l1 = flip ? v2 : v1, + l2 = flip ? v1 : v2, + line = new Line(l1[0], l1[1], l1[6], l1[7]); + if (line.getDistance(new Point(l2[0], l2[1])) > geomEpsilon || + line.getDistance(new Point(l2[6], l2[7])) > geomEpsilon) + return null; + } else if (straight1 ^ straight2) { + // If one curve is straight, the other curve must be straight, + // too, otherwise they cannot overlap. + return null; + } + + var v = [v1, v2], + pairs = []; + // Iterate through all end points: First p1 and p2 of curve 1, + // then p1 and p2 of curve 2 + for (var i = 0, t1 = 0; + i < 2 && pairs.length < 2; + i += t1 === 0 ? 0 : 1, t1 = t1 ^ 1) { + var t2 = Curve.getParameterOf(v[i ^ 1], new Point( + v[i][t1 === 0 ? 0 : 6], + v[i][t1 === 0 ? 1 : 7])); + if (t2 != null) { // If point is on curve + var pair = i === 0 ? [t1, t2] : [t2, t1]; + // Filter out tiny overlaps + // TODO: Compare distance of points instead of curve time? + if (pairs.length === 0 || + abs(pair[0] - pairs[0][0]) > timeEpsilon && + abs(pair[1] - pairs[0][1]) > timeEpsilon) + pairs.push(pair); + } + // If we checked 3 points but found no match, curves cannot + // overlap + if (i === 1 && pairs.length === 0) + break; + } + if (pairs.length !== 2) { + pairs = null; + } else if (!straight) { + // Straight pairs don't need further checks. If we found 2 pairs + // the end points on v1 & v2 should be the same. + var o1 = Curve.getPart(v1, pairs[0][0], pairs[1][0]), + o2 = Curve.getPart(v2, pairs[0][1], pairs[1][1]); + // Check if handles of the overlapping curves are the same too. + if (abs(o2[2] - o1[2]) > geomEpsilon || + abs(o2[3] - o1[3]) > geomEpsilon || + abs(o2[4] - o1[4]) > geomEpsilon || + abs(o2[5] - o1[5]) > geomEpsilon) + pairs = null; + } + return pairs; } }}; }); diff --git a/src/path/CurveLocation.js b/src/path/CurveLocation.js index 504f5a23..fccd561d 100644 --- a/src/path/CurveLocation.js +++ b/src/path/CurveLocation.js @@ -13,16 +13,16 @@ /** * @name CurveLocation * - * @class CurveLocation objects describe a location on {@link Curve} - * objects, as defined by the curve {@link #parameter}, a value between - * {@code 0} (beginning of the curve) and {@code 1} (end of the curve). If - * the curve is part of a {@link Path} item, its {@link #index} inside the + * @class CurveLocation objects describe a location on {@link Curve} objects, + * as defined by the curve-time {@link #parameter}, a value between {@code 0} + * (beginning of the curve) and {@code 1} (end of the curve). If the curve is + * part of a {@link Path} item, its {@link #index} inside the * {@link Path#curves} array is also provided. * * The class is in use in many places, such as - * {@link Path#getLocationAt(offset, isParameter)}, + * {@link Path#getLocationAt(offset)}, * {@link Path#getLocationOf(point)}, - * {@link Path#getNearestLocation(point), + * {@link Path#getNearestLocation(point)}, * {@link PathItem#getIntersections(path)}, * etc. */ @@ -41,22 +41,35 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ * @param {Number} parameter * @param {Point} [point] */ - initialize: function CurveLocation(curve, parameter, point, _curve2, - _parameter2, _point2, _distance) { + initialize: function CurveLocation(curve, parameter, point, + _overlap, _distance) { + // Merge intersections very close to the end of a curve with the + // beginning of the next curve. + if (parameter > /*#=*/(1 - Numerical.CURVETIME_EPSILON)) { + var next = curve.getNext(); + if (next) { + parameter = 0; + curve = next; + } + } // Define this CurveLocation's unique id. // NOTE: We do not use the same pool as the rest of the library here, // since this is only required to be unique at runtime among other // CurveLocation objects. this._id = UID.get(CurveLocation); + this._setCurve(curve); + this._parameter = parameter; + this._point = point || curve.getPointAt(parameter, true); + this._overlap = _overlap; + this._distance = _distance; + this._intersection = this._next = this._prev = null; + }, + + _setCurve: function(curve) { var path = curve._path; this._version = path ? path._version : 0; this._curve = curve; - this._parameter = parameter; - this._point = point || curve.getPointAt(parameter, true); - this._curve2 = _curve2; - this._parameter2 = _parameter2; - this._point2 = _point2; - this._distance = _distance; + this._segment = null; // To be determined, see #getSegment() // Also store references to segment1 and segment2, in case path // splitting / dividing is going to happen, in which case the segments // can be used to determine the new curves, see #getCurve(true) @@ -64,31 +77,40 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ this._segment2 = curve._segment2; }, + _setSegment: function(segment) { + this._setCurve(segment.getCurve()); + this._segment = segment; + this._parameter = segment === this._segment1 ? 0 : 1; + // To avoid issues with imprecision in getCurve() / trySegment() + this._point = segment._point.clone(); + }, + /** * The segment of the curve which is closer to the described location. * * @type Segment * @bean */ - getSegment: function(_preferFirst) { - if (!this._segment) { - var curve = this.getCurve(), - parameter = this.getParameter(); - if (parameter === 1) { - this._segment = curve._segment2; - } else if (parameter === 0 || _preferFirst) { - this._segment = curve._segment1; - } else if (parameter == null) { - return null; - } else { + getSegment: function() { + // Request curve first, so _segment gets invalidated if it's out of sync + var curve = this.getCurve(), + segment = this._segment; + if (!segment) { + var parameter = this.getParameter(); + if (parameter === 0) { + segment = curve._segment1; + } else if (parameter === 1) { + segment = curve._segment2; + } else if (parameter != null) { // Determine the closest segment by comparing curve lengths - this._segment = curve.getPartLength(0, parameter) + segment = curve.getPartLength(0, parameter) < curve.getPartLength(parameter, 1) ? curve._segment1 : curve._segment2; } + this._segment = segment; } - return this._segment; + return segment; }, /** @@ -99,29 +121,34 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ */ getCurve: function() { var curve = this._curve, - path = curve && curve._path; + path = curve && curve._path, + that = this; if (path && path._version !== this._version) { // If the path's segments have changed in the meantime, clear the // internal _parameter value and force refetching of the correct // curve again here. - curve = null; - this._parameter = null; + curve = this._parameter = this._curve = this._offset = null; } - if (!curve) { - // If we're asked to get the curve uncached, access current curve - // objects through segment1 / segment2. Since path splitting or - // dividing might have happened in the meantime, try segment1's - // curve, and see if _point lies on it still, otherwise assume it's - // the curve before segment2. - curve = this._segment1.getCurve(); - if (curve.getParameterOf(this._point) == null) - curve = this._segment2.getPrevious().getCurve(); - this._curve = curve; - // Fetch path again as it could be on a new one through split() - path = curve._path; - this._version = path ? path._version : 0; + + // If path is out of sync, access current curve objects through segment1 + // / segment2. Since path splitting or dividing might have happened in + // the meantime, try segment1's curve, and see if _point lies on it + // still, otherwise assume it's the curve before segment2. + function trySegment(segment) { + var curve = segment && segment.getCurve(); + if (curve && (that._parameter = curve.getParameterOf(that._point)) + != null) { + // Fetch path again as it could be on a new one through split() + that._setCurve(curve); + that._segment = segment; + return curve; + } } - return curve; + + return curve + || trySegment(this._segment) + || trySegment(this._segment1) + || trySegment(this._segment2.getPrevious()); }, /** @@ -136,8 +163,8 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ }, /** - * The index of the curve within the {@link Path#curves} list, if the - * curve is part of a {@link Path} item. + * The index of the {@link #curve} within the {@link Path#curves} list, if + * it is part of a {@link Path} item. * * @type Index * @bean @@ -148,9 +175,9 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ }, /** - * The curve parameter, as used by various bezier curve calculations. It is - * value between {@code 0} (beginning of the curve) and {@code 1} (end of - * the curve). + * The curve-time parameter, as used by various bezier curve calculations. + * It is value between {@code 0} (beginning of the curve) and {@code 1} + * (end of the curve). * * @type Number * @bean @@ -183,8 +210,13 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ * @bean */ getOffset: function() { - var path = this.getPath(); - return path ? path._getOffset(this) : this.getCurveOffset(); + var offset = this._offset; + if (offset == null) { + var path = this.getPath(); + offset = this._offset = path ? path._getOffset(this) + : this.getCurveOffset(); + } + return offset; }, /** @@ -209,37 +241,31 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ * @bean */ getIntersection: function() { - var intersection = this._intersection; - if (!intersection && this._curve2) { - // If we have the parameter on the other curve use that for - // intersection rather than the point. - this._intersection = intersection = new CurveLocation(this._curve2, - this._parameter2, this._point2 || this._point); - intersection._overlap = this._overlap; - intersection._intersection = this; - } - return intersection; + return this._intersection; }, /** * The tangential vector to the {@link #curve} at the given location. * - * @name Item#tangent + * @name CurveLocation#getTangent * @type Point + * @bean */ /** * The normal vector to the {@link #curve} at the given location. * - * @name Item#normal + * @name CurveLocation#getNormal * @type Point + * @bean */ /** * The curvature of the {@link #curve} at the given location. * - * @name Item#curvature + * @name CurveLocation#getCurvature * @type Number + * @bean */ /** @@ -274,23 +300,40 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ * @param {CurveLocation} location * @return {Boolean} {@true if the locations are equal} */ - equals: function(loc) { - var abs = Math.abs, - // Use the same tolerance for curve time parameter comparisons as - // in Curve.js when considering two locations the same. - tolerance = /*#=*/Numerical.TOLERANCE; - return this === loc - || loc instanceof CurveLocation - // Call getCurve() and getParameter() to keep in sync - && this.getCurve() === loc.getCurve() - && abs(this.getParameter() - loc.getParameter()) < tolerance - // _curve2/_parameter2 are only used for Boolean operations - // and don't need syncing there. - // TODO: That's not quite true though... Rework this! - && this._curve2 === loc._curve2 - && abs((this._parameter2 || 0) - (loc._parameter2 || 0)) - < tolerance - || false; + equals: function(loc, _ignoreOther) { + var res = this === loc, + epsilon = /*#=*/Numerical.GEOMETRIC_EPSILON; + // NOTE: We need to compare both by (index + parameter) and by proximity + // of points. See: + // https://github.com/paperjs/paper.js/issues/784#issuecomment-143161586 + if (!res && loc instanceof CurveLocation + && this.getPath() === loc.getPath() + && this.getPoint().isClose(loc.getPoint(), epsilon)) { + // The position is the same, but it could still be in a different + // location on the path. Perform more thorough checks now: + var c1 = this.getCurve(), + c2 = loc.getCurve(), + abs = Math.abs, + // We need to wrap diff around the path's beginning / end: + diff = abs( + ((c1.isLast() && c2.isFirst() ? -1 : c1.getIndex()) + + this.getParameter()) - + ((c2.isLast() && c1.isFirst() ? -1 : c2.getIndex()) + + loc.getParameter())); + res = (diff < /*#=*/Numerical.CURVETIME_EPSILON + // If diff isn't close enough, compare the actual offsets of + // both locations to determine if they're in the same spot, + // taking into account the wrapping around path ends too. + // This is necessary in order to handle very short consecutive + // curves (length ~< 1e-7), which would lead to diff > 1. + || ((diff = abs(this.getOffset() - loc.getOffset())) < epsilon + || abs(this.getPath().getLength() - diff) < epsilon)) + && (_ignoreOther + || (!this._intersection && !loc._intersection + || this._intersection && this._intersection.equals( + loc._intersection, true))); + } + return res; }, /** @@ -313,37 +356,207 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ return '{ ' + parts.join(', ') + ' }'; }, - statics: { - sort: function(locations) { - var tolerance = /*#=*/Numerical.TOLERANCE; - locations.sort(function compare(l1, l2) { - var curve1 = l1._curve, - curve2 = l2._curve, - path1 = curve1._path, - path2 = curve2._path; - // Sort by path-id, curve, parameter, curve2, parameter2 so we - // can easily remove duplicates with calls to equals() after. - return path1 === path2 - ? curve1 === curve2 - ? Math.abs(l1._parameter - l2._parameter) < tolerance - ? l1._curve2 === l2._curve2 - ? l1._parameter2 - l2._parameter2 - : l1._curve2.getIndex() - l2._curve2.getIndex() - : l1._parameter - l2._parameter - : curve1.getIndex() - curve2.getIndex() - // Sort by path id to group all locs on the same path. - : path1._id - path2._id; + + /** + * {@grouptitle Tests} + * Checks if the location is an intersection with another curve and is + * merely touching the other curve, as opposed to crossing it. + * + * @return {Boolean} {@true if the location is an intersection that is + * merely touching another curve} + * @see #isCrossing() + */ + isTouching: function() { + var inter = this._intersection; + if (inter && this.getTangent().isCollinear(inter.getTangent())) { + // Only consider two straight curves as touching if their lines + // don't intersect. + var curve1 = this.getCurve(), + curve2 = inter.getCurve(); + return !(curve1.isStraight() && curve2.isStraight() + && curve1.getLine().intersect(curve2.getLine())); + } + return false; + }, + + /** + * Checks if the location is an intersection with another curve and is + * crossing the other curve, as opposed to just touching it. + * + * @return {Boolean} {@true if the location is an intersection that is + * crossing another curve} + * @see #isTouching() + */ + isCrossing: function(_report) { + // Implementation based on work by Andy Finnell: + // http://losingfight.com/blog/2011/07/09/how-to-implement-boolean-operations-on-bezier-paths-part-3/ + // https://bitbucket.org/andyfinnell/vectorboolean + var inter = this._intersection; + if (!inter) + return false; + // TODO: Make getCurve() and getParameter() sync work in boolean ops + // before and after splitting!!! + var t1 = this._parameter, + t2 = inter._parameter, + tMin = /*#=*/Numerical.CURVETIME_EPSILON, + tMax = 1 - tMin; + // If the intersection is in the middle of the path, it is either a + // tangent or a crossing, no need for the detailed corner check below. + // But we do need a check for the edge case of tangents? + if (t1 >= tMin && t1 <= tMax || t2 >= tMin && t2 <= tMax) + return !this.isTouching(); + // Values for getTangentAt() that are almost 0 and 1. + // NOTE: Even though getTangentAt() has code to support 0 and 1 instead + // of tMin and tMax, we still need to use this instead, as other issues + // emerge from switching to 0 and 1 in edge cases. + // NOTE: VectorBoolean has code that slowly shifts these points inwards + // until the resulting tangents are not ambiguous. Do we need this too? + var c2 = this._curve, + c1 = c2.getPrevious(), + c4 = inter._curve, + c3 = c4.getPrevious(), + PI = Math.PI; + if (!c1 || !c3) + return false; + + if (_report) { + new Path.Circle({ + center: this.getPoint(), + radius: 10, + strokeColor: 'red' + }); + new Path({ + segments: [c1.getSegment1(), c1.getSegment2(), c2.getSegment2()], + strokeColor: 'red', + strokeWidth: 4 + }); + new Path({ + segments: [c3.getSegment1(), c3.getSegment2(), c4.getSegment2()], + strokeColor: 'orange', + strokeWidth: 4 }); } + + function isInRange(angle, min, max) { + return min < max + ? angle > min && angle < max + // The range wraps around -PI / PI: + : angle > min && angle <= PI || angle >= -PI && angle < max; + } + + // Calculate angles for all four tangents at the intersection point + var a1 = c1.getTangentAt(tMax, true).negate().getAngleInRadians(), + a2 = c2.getTangentAt(tMin, true).getAngleInRadians(), + a3 = c3.getTangentAt(tMax, true).negate().getAngleInRadians(), + a4 = c4.getTangentAt(tMin, true).getAngleInRadians(); + + // Count how many times curve2 angles appear between the curve1 angles + // If each pair of angles split the other two, then the edges cross. + return (isInRange(a3, a1, a2) ^ isInRange(a4, a1, a2)) + && (isInRange(a3, a2, a1) ^ isInRange(a4, a2, a1)); + }, + + /** + * Checks if the location is an intersection with another curve and is + * part of an overlap between the two involved paths. + * + * @return {Boolean} {@true if the location is an intersection that is + * part of an overlap between the two involved paths} + * @see #isCrossing() + * @see #isTouching() + */ + isOverlap: function() { + return !!this._overlap; } }, Base.each(Curve.evaluateMethods, function(name) { // Produce getters for #getTangent() / #getNormal() / #getCurvature() - if (name !== 'getPoint') { - var get = name + 'At'; - this[name] = function() { - var parameter = this.getParameter(), - curve = this.getCurve(); - return parameter != null && curve && curve[get](parameter, true); - }; + var get = name + 'At'; + this[name] = function() { + var parameter = this.getParameter(), + curve = this.getCurve(); + return parameter != null && curve && curve[get](parameter, true); + }; +}, { + // Do not override the existing #getPoint(): + preserve: true +}), +new function() { // Scope for statics + + function insert(locations, loc, merge) { + // Insert-sort by path-id, curve, parameter so we can easily merge + // duplicates with calls to equals() after. + var length = locations.length, + l = 0, + r = length - 1, + abs = Math.abs; + + function search(index, dir) { + // If we reach the beginning/end of the list, also compare with the + // location at the other end, as paths are circular lists. + // NOTE: When merging, the locations array will only contain + // locations on the same path, so it is fine that check for the end + // to address circularity. See PathItem#getIntersections() + for (var i = index + dir; i >= -1 && i <= length; i += dir) { + // Wrap the index around, to match the other ends: + var loc2 = locations[((i % length) + length) % length]; + // Once we're outside the spot, we can stop searching. + if (!loc.getPoint().isClose(loc2.getPoint(), + /*#=*/Numerical.GEOMETRIC_EPSILON)) + break; + if (loc.equals(loc2)) + return loc2; + } + return null; + } + + while (l <= r) { + var m = (l + r) >>> 1, + loc2 = locations[m], + found; + // See if the two locations are actually the same, and merge if + // they are. If they aren't check the other neighbors with search() + if (merge && (found = loc.equals(loc2) ? loc2 + : (search(m, -1) || search(m, 1)))) { + // We're done, don't insert, merge with the found location + // instead, and carry over overlap: + if (loc._overlap) { + found._overlap = found._intersection._overlap = true; + } + return found; + } + var path1 = loc.getPath(), + path2 = loc2.getPath(), + // NOTE: equals() takes the intersection location into account, + // while this calculation of diff doesn't! + diff = path1 === path2 + //Sort by both index and parameter. The two values added + // together provides a convenient sorting index. + ? (loc.getIndex() + loc.getParameter()) + - (loc2.getIndex() + loc2.getParameter()) + // Sort by path id to group all locs on same path. + : path1._id - path2._id; + if (diff < 0) { + r = m - 1; + } else { + l = m + 1; + } + } + // We didn't merge with a preexisting location, insert it now. + locations.splice(l, 0, loc); + return loc; } -}, {})); + + return { statics: { + insert: insert, + + expand: function(locations) { + // Create a copy since insert() keeps modifying the array and + // inserting at sorted indices. + var expanded = locations.slice(); + for (var i = 0, l = locations.length; i < l; i++) { + insert(expanded, locations[i]._intersection, false); + } + return expanded; + } + }}; +}); diff --git a/src/path/Path.js b/src/path/Path.js index b61c3c0d..0dbc3874 100644 --- a/src/path/Path.js +++ b/src/path/Path.js @@ -150,7 +150,9 @@ var Path = PathItem.extend(/** @lends Path# */{ if (parent) parent._currentPath = undefined; // Clockwise state becomes undefined as soon as geometry changes. - this._length = this._clockwise = undefined; + // Also clear cached mono curves used for winding calculations. + this._length = this._area = this._clockwise = this._monoCurves = + undefined; if (flags & /*#=*/ChangeFlag.SEGMENTS) { this._version++; // See CurveLocation } else if (this._curves) { @@ -159,10 +161,6 @@ var Path = PathItem.extend(/** @lends Path# */{ for (var i = 0, l = this._curves.length; i < l; i++) this._curves[i]._changed(); } - // Clear cached curves used for winding direction and containment - // calculation. - // NOTE: This is only needed with __options.booleanOperations - this._monoCurves = undefined; } else if (flags & /*#=*/ChangeFlag.STROKE) { // TODO: We could preserve the purely geometric bounds that are not // affected by stroke: _bounds.bounds and _bounds.handleBounds @@ -368,40 +366,6 @@ var Path = PathItem.extend(/** @lends Path# */{ return this._segments.length === 0; }, - /** - * Checks if this path consists of only linear curves. This can mean that - * the curves have no handles defined, or that the handles run collinear - * with the line. - * - * @return {Boolean} {@true if the path is entirely linear} - * @see Segment#isLinear() - * @see Curve#isLinear() - */ - isLinear: function() { - var segments = this._segments; - for (var i = 0, l = segments.length; i < l; i++) { - if (!segments[i].isLinear()) - return false; - } - return true; - }, - - /** - * Checks if none of the curves in the path define any curve handles. - * - * @return {Boolean} {@true if the path contains no curve handles} - * @see Segment#hasHandles() - * @see Curve#hasHandles() - */ - hasHandles: function() { - var segments = this._segments; - for (var i = 0, l = segments.length; i < l; i++) { - if (segments[i].hasHandles()) - return true; - } - return false; - }, - _transformContent: function(matrix) { var coords = new Array(6); for (var i = 0, l = this._segments.length; i < l; i++) @@ -410,10 +374,10 @@ var Path = PathItem.extend(/** @lends Path# */{ }, /** - * Private method that adds a segment to the segment list. It assumes that - * the passed object is a segment already and does not perform any checks. - * If a curves list was requested, it will kept in sync with the segments - * list automatically. + * Private method that adds segments to the segment list. It assumes that + * the passed object is an array of segments already and does not perform + * any checks. If a curves list was requested, it will be kept in sync with + * the segments list automatically. */ _add: function(segs, index) { // Local short-cuts: @@ -421,7 +385,7 @@ var Path = PathItem.extend(/** @lends Path# */{ curves = this._curves, amount = segs.length, append = index == null, - index = append ? segments.length : index; + from = append ? segments.length : index; // Scan through segments to add first, convert if necessary and set // _path and _index references on them. for (var i = 0; i < amount; i++) { @@ -431,7 +395,7 @@ var Path = PathItem.extend(/** @lends Path# */{ if (segment._path) segment = segs[i] = segment.clone(); segment._path = this; - segment._index = index + i; + segment._index = from + i; // If parts of this segment are selected, adjust the internal // _selectedSegmentState now if (segment._selectionState) @@ -442,20 +406,15 @@ var Path = PathItem.extend(/** @lends Path# */{ segments.push.apply(segments, segs); } else { // Insert somewhere else - segments.splice.apply(segments, [index, 0].concat(segs)); + segments.splice.apply(segments, [from, 0].concat(segs)); // Adjust the indices of the segments above. - for (var i = index + amount, l = segments.length; i < l; i++) + for (var i = from + amount, l = segments.length; i < l; i++) segments[i]._index = i; } // Keep the curves list in sync all the time in case it was requested // already. - if (curves || segs._curves) { - if (!curves) - curves = this._curves = []; - // We need to step one index down from the inserted segment to - // get its curve, except for the first segment. - var from = index > 0 ? index - 1 : index, - start = from, + if (curves) { + var start = from, to = Math.min(from + amount, this._countCurves()); if (segs._curves) { // Reuse removed curves. @@ -810,34 +769,91 @@ var Path = PathItem.extend(/** @lends Path# */{ clear: '#removeSegments', /** - * The approximate length of the path in points. + * Checks if any of the curves in the path have curve handles set. + * + * @return {Boolean} {@true if the path has curve handles set} + * @see Segment#hasHandles() + * @see Curve#hasHandles() + */ + hasHandles: function() { + var segments = this._segments; + for (var i = 0, l = segments.length; i < l; i++) { + if (segments[i].hasHandles()) + return true; + } + return false; + }, + + /** + * Clears the path's handles by setting their coordinates to zero, + * turning the path into a polygon (or a polyline if it isn't closed). + */ + clearHandles: function() { + var segments = this._segments; + for (var i = 0, l = segments.length; i < l; i++) + segments[i].clearHandles(); + }, + + /** + * The approximate length of the path. * * @type Number * @bean */ getLength: function() { if (this._length == null) { - var curves = this.getCurves(); - this._length = 0; + var curves = this.getCurves(), + length = 0; for (var i = 0, l = curves.length; i < l; i++) - this._length += curves[i].getLength(); + length += curves[i].getLength(); + this._length = length; } return this._length; }, /** - * The area of the path in square points. Self-intersecting paths can - * contain sub-areas that cancel each other out. + * The area that the path's geometry is covering. Self-intersecting paths + * can contain sub-areas that cancel each other out. * * @type Number * @bean */ getArea: function() { - var curves = this.getCurves(); - var area = 0; - for (var i = 0, l = curves.length; i < l; i++) - area += curves[i].getArea(); - return area; + if (this._area == null) { + var segments = this._segments, + count = segments.length, + last = count - 1, + area = 0; + for (var i = 0, l = this._closed ? count : last; i < l; i++) { + area += Curve.getArea(Curve.getValues( + segments[i], segments[i < last ? i + 1 : 0])); + } + this._area = area; + } + return this._area; + }, + + /** + * Specifies whether the path is oriented clock-wise. + * + * @type Boolean + * @bean + */ + isClockwise: function() { + if (this._clockwise !== undefined) + return this._clockwise; + return this.getArea() >= 0; + }, + + setClockwise: function(clockwise) { + // Only revers the path if its clockwise orientation is not the same + // as what it is now demanded to be. + // On-the-fly conversion to boolean: + if (this.isClockwise() != (clockwise = !!clockwise)) + this.reverse(); + // Reverse only flips _clockwise state if it was already set, so let's + // always set this here now. + this._clockwise = clockwise; }, /** @@ -1009,10 +1025,9 @@ var Path = PathItem.extend(/** @lends Path# */{ reduce: function() { var curves = this.getCurves(); for (var i = curves.length - 1; i >= 0; i--) { - var curve = curves[i], - next; - if (curve.isLinear() && (curve.getLength() === 0 - || (next = curve.getNext()) && curve.isCollinear(next))) + var curve = curves[i]; + if (!curve.hasHandles() && (curve.getLength() === 0 + || curve.isCollinear(curve.getNext()))) curve.remove(); } return this; @@ -1174,7 +1189,8 @@ var Path = PathItem.extend(/** @lends Path# */{ * * @param {Number} index the index of the curve in the {@link Path#curves} * array at which to split - * @param {Number} parameter the parameter at which the curve will be split + * @param {Number} parameter the curve-time parameter at which the curve + * will be split * @return {Path} the newly created path after splitting, if any */ split: function(index, parameter) { @@ -1191,7 +1207,7 @@ var Path = PathItem.extend(/** @lends Path# */{ index = arg.index; parameter = arg.parameter; } - var tMin = /*#=*/Numerical.TOLERANCE, + var tMin = /*#=*/Numerical.CURVETIME_EPSILON, tMax = 1 - tMin; if (parameter >= tMax) { // t == 1 is the same as t == 0 and index ++ @@ -1235,29 +1251,6 @@ var Path = PathItem.extend(/** @lends Path# */{ return null; }, - /** - * Specifies whether the path is oriented clock-wise. - * - * @type Boolean - * @bean - */ - isClockwise: function() { - if (this._clockwise !== undefined) - return this._clockwise; - return Path.isClockwise(this._segments); - }, - - setClockwise: function(clockwise) { - // Only revers the path if its clockwise orientation is not the same - // as what it is now demanded to be. - // On-the-fly conversion to boolean: - if (this.isClockwise() != (clockwise = !!clockwise)) - this.reverse(); - // Reverse only flips _clockwise state if it was already set, so let's - // always set this here now. - this._clockwise = clockwise; - }, - /** * Reverses the orientation of the path, by reversing all its segments. */ @@ -1414,15 +1407,47 @@ var Path = PathItem.extend(/** @lends Path# */{ topCenter; function isCollinear(i, j) { - return segments[i].isCollinear(segments[j]); + var seg1 = segments[i], + seg2 = seg1.getNext(), + seg3 = segments[j], + seg4 = seg3.getNext(); + return seg1._handleOut.isZero() && seg2._handleIn.isZero() + && seg3._handleOut.isZero() && seg4._handleIn.isZero() + && seg2._point.subtract(seg1._point).isCollinear( + seg4._point.subtract(seg3._point)); } function isOrthogonal(i) { - return segments[i].isOrthogonal(); + var seg2 = segments[i], + seg1 = seg2.getPrevious(), + seg3 = seg2.getNext(); + return seg1._handleOut.isZero() && seg2._handleIn.isZero() + && seg2._handleOut.isZero() && seg3._handleIn.isZero() + && seg2._point.subtract(seg1._point).isOrthogonal( + seg3._point.subtract(seg2._point)); } function isArc(i) { - return segments[i].isOrthogonalArc(); + var seg1 = segments[i], + seg2 = seg1.getNext(), + handle1 = seg1._handleOut, + handle2 = seg2._handleIn, + kappa = /*#=*/Numerical.KAPPA; + // Look at handle length and the distance to the imaginary corner + // point and see if it their relation is kappa. + if (handle1.isOrthogonal(handle2)) { + var pt1 = seg1._point, + pt2 = seg2._point, + // Find the corner point by intersecting the lines described + // by both handles: + corner = new Line(pt1, handle1, true).intersect( + new Line(pt2, handle2, true), true); + return corner && Numerical.isZero(handle1.getLength() / + corner.subtract(pt1).getLength() - kappa) + && Numerical.isZero(handle2.getLength() / + corner.subtract(pt2).getLength() - kappa); + } + return false; } function getDistance(i, j) { @@ -1974,7 +1999,7 @@ var Path = PathItem.extend(/** @lends Path# */{ * Returns the nearest location on the path to the specified point. * * @function - * @param point {Point} the point for which we search the nearest location + * @param {Point} point the point for which we search the nearest location * @return {CurveLocation} the location on the path that's the closest to * the specified point */ @@ -1997,7 +2022,7 @@ var Path = PathItem.extend(/** @lends Path# */{ * Returns the nearest point on the path to the specified point. * * @function - * @param point {Point} the point for which we search the nearest point + * @param {Point} point the point for which we search the nearest point * @return {Point} the point on the path that's the closest to the specified * point * @@ -2028,8 +2053,8 @@ var Path = PathItem.extend(/** @lends Path# */{ getNearestPoint: function(/* point */) { return this.getNearestLocation.apply(this, arguments).getPoint(); } -}), new function() { // Scope for drawing - +}), +new function() { // Scope for drawing // Note that in the code below we're often accessing _x and _y on point // objects that were read from segments. This is because the SegmentPoint // class overrides the plain x / y properties with getter / setters and @@ -2228,8 +2253,8 @@ var Path = PathItem.extend(/** @lends Path# */{ drawHandles(ctx, this._segments, matrix, paper.settings.handleSize); } }; -}, new function() { // Path Smoothing - +}, +new function() { // Path Smoothing /** * Solves a tri-diagonal system for one of coordinates (x or y) of first * bezier control points. @@ -2354,7 +2379,8 @@ var Path = PathItem.extend(/** @lends Path# */{ } } }; -}, new function() { // PostScript-style drawing commands +}, +new function() { // PostScript-style drawing commands /** * Helper method that returns the current segment and checks if a moveTo() * command is required first. @@ -2477,7 +2503,6 @@ var Path = PathItem.extend(/** @lends Path# */{ x = pt.x, y = pt.y, abs = Math.abs, - epsilon = /*#=*/Numerical.EPSILON, rx = abs(radius.width), ry = abs(radius.height), rxSq = rx * rx, @@ -2494,7 +2519,7 @@ var Path = PathItem.extend(/** @lends Path# */{ } factor = (rxSq * rySq - rxSq * ySq - rySq * xSq) / (rxSq * ySq + rySq * xSq); - if (abs(factor) < epsilon) + if (abs(factor) < /*#=*/Numerical.EPSILON) factor = 0; if (factor < 0) throw new Error( @@ -2664,21 +2689,6 @@ var Path = PathItem.extend(/** @lends Path# */{ // Mess with indentation in order to get more line-space below: statics: { - /** - * Determines whether the segments describe a path in clockwise or counter- - * clockwise orientation. - * - * @private - */ - isClockwise: function(segments) { - var sum = 0; - // TODO: Check if this works correctly for all open paths. - for (var i = 0, l = segments.length; i < l; i++) - sum += Curve.getEdgeSum(Curve.getValues( - segments[i], segments[i + 1 < l ? i + 1 : 0])); - return sum > 0; - }, - /** * Returns the bounding rectangle of the item excluding stroke width. * diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 8d44ed92..71ad94d7 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -13,23 +13,18 @@ /* * Boolean Geometric Path Operations * - * This is mostly written for clarity and compatibility, not optimised for - * performance, and has to be tested heavily for stability. - * * Supported * - Path and CompoundPath items * - Boolean Union * - Boolean Intersection * - Boolean Subtraction - * - Resolving a self-intersecting Path - * - * Not supported yet - * - Boolean operations on self-intersecting Paths + * - Boolean Exclusion + * - Resolving a self-intersecting Path items + * - Boolean operations on self-intersecting Paths items * * @author Harikrishnan Gopalakrishnan * http://hkrish.com/playground/paperjs/booleanStudy.html */ - PathItem.inject(new function() { var operators = { unite: function(w) { @@ -49,53 +44,90 @@ PathItem.inject(new function() { } }; + // Creates a cloned version of the path that we can modify freely, with its + // matrix applied to its geometry. Calls #reduce() to simplify compound + // paths and remove empty curves, and #reorient() to make sure all paths + // have correct winding direction. + function preparePath(path) { + return path.clone(false).reduce().resolveCrossings() + .transform(null, true, true); + } + + function finishBoolean(ctor, paths, path1, path2, reduce) { + var result = new ctor(Item.NO_INSERT); + result.addChildren(paths, true); + // See if the item can be reduced to just a simple Path. + if (reduce) + result = result.reduce(); + // Insert the resulting path above whichever of the two paths appear + // further up in the stack. + result.insertAbove(path2 && path1.isSibling(path2) + && path1.getIndex() < path2.getIndex() + ? path2 : path1); + // Copy over the left-hand item's style and we're done. + // TODO: Consider using Item#_clone() for this, but find a way to not + // clone children / name (content). + result.setStyle(path1._style); + return result; + } + + var scaleFactor = 1; + var textAngle = 0; + var fontSize = 5; + + var segmentOffset; + var pathIndices; + var pathIndex; + var pathCount; + // Boolean operators return true if a curve with the given winding // contribution contributes to the final result or not. They are called // for each curve in the graph after curves in the operands are // split at intersections. function computeBoolean(path1, path2, operation) { - // Creates a cloned version of the path that we can modify freely, with - // its matrix applied to its geometry. Calls #reduce() to simplify - // compound paths and remove empty curves, and #reorient() to make sure - // all paths have correct winding direction. - function preparePath(path) { - return path.clone(false).reduce().reorient().transform(null, true, - true); - } + scaleFactor = Base.pick(window.scaleFactor, scaleFactor); + textAngle = Base.pick(window.textAngle, 0); + segmentOffset = {}; + pathIndices = {}; + + var reportSegments = window.reportSegments; + var reportWindings = window.reportWindings; + var reportIntersections = window.reportIntersections; + if (path2) { + window.reportSegments = false; + window.reportWindings = false; + window.reportIntersections = false; + } // We do not modify the operands themselves, but create copies instead, // fas produced by the calls to preparePath(). // Note that the result paths might not belong to the same type // i.e. subtraction(A:Path, B:Path):CompoundPath etc. var _path1 = preparePath(path1), _path2 = path2 && path1 !== path2 && preparePath(path2); + window.reportSegments = reportSegments; + window.reportWindings = reportWindings; + window.reportIntersections = reportIntersections; // Give both paths the same orientation except for subtraction // and exclusion, where we need them at opposite orientation. if (_path2 && /^(subtract|exclude)$/.test(operation) ^ (_path2.isClockwise() !== _path1.isClockwise())) _path2.reverse(); - // Split curves at intersections on both paths. Note that for self - // intersection, _path2 will be null and getIntersections() handles it. - splitPath(Curve.filterIntersections( - _path1._getIntersections(_path2, null, []), true)); + // Split curves at crossings and overlaps on both paths. Note that for + // self-intersection, path2 is null and getIntersections() handles it. + // console.time('intersection'); + var intersections = CurveLocation.expand( + _path1.getIntersections(_path2, function(inter) { + // Only handle overlaps when not self-intersecting + return _path2 && inter.isOverlap() || inter.isCrossing(); + }) + ); + // console.timeEnd('intersection'); + splitPath(intersections); - /* - console.time('inter'); - var locations = _path1._getIntersections(_path2, null, []); - console.timeEnd('inter'); - if (_path2 && false) { - console.time('self'); - _path1._getIntersections(null, null, locations); - _path2._getIntersections(null, null, locations); - console.timeEnd('self'); - } - splitPath(Curve.filterIntersections(locations, true)); - */ - var chain = [], - segments = [], + var segments = [], // Aggregate of all curves in both operands, monotonic in y - monoCurves = [], - tolerance = /*#=*/Numerical.TOLERANCE; + monoCurves = []; function collect(paths) { for (var i = 0, l = paths.length; i < l; i++) { @@ -111,143 +143,109 @@ PathItem.inject(new function() { collect(_path2._children || [_path2]); // Propagate the winding contribution. Winding contribution of curves // does not change between two intersections. - // First, sort all segments with an intersection to the beginning. - segments.sort(function(a, b) { - var _a = a._intersection, - _b = b._intersection; - return !_a && !_b || _a && _b ? 0 : _a ? -1 : 1; - }); + // First, propagate winding contributions for curve chains starting in + // all intersections: + for (var i = 0, l = intersections.length; i < l; i++) { + propagateWinding(intersections[i]._segment, _path1, _path2, + monoCurves, operation); + } + // Now process the segments that are not part of any intersecting chains for (var i = 0, l = segments.length; i < l; i++) { var segment = segments[i]; - if (segment._winding != null) - continue; - // Here we try to determine the most probable winding number - // contribution for this curve-chain. Once we have enough confidence - // in the winding contribution, we can propagate it until the - // intersection or end of a curve chain. - chain.length = 0; - var startSeg = segment, - totalLength = 0, - windingSum = 0; - do { - var length = segment.getCurve().getLength(); - chain.push({ segment: segment, length: length }); - totalLength += length; - segment = segment.getNext(); - } while (segment && !segment._intersection && segment !== startSeg); - // Calculate the average winding among three evenly distributed - // points along this curve chain as a representative winding number. - // This selection gives a better chance of returning a correct - // winding than equally dividing the curve chain, with the same - // (amortised) time. - for (var j = 0; j < 3; j++) { - // Try the points at 1/4, 2/4 and 3/4 of the total length: - var length = totalLength * (j + 1) / 4; - for (var k = 0, m = chain.length; k < m; k++) { - var node = chain[k], - curveLength = node.length; - if (length <= curveLength) { - // If the selected location on the curve falls onto its - // beginning or end, use the curve's center instead. - if (length < tolerance - || curveLength - length < tolerance) - length = curveLength / 2; - var curve = node.segment.getCurve(), - pt = curve.getPointAt(length), - hor = isHorizontal(curve), - path = getMainPath(curve); - // While subtracting, we need to omit this curve if this - // curve is contributing to the second operand and is - // outside the first operand. - windingSum += operation === 'subtract' && _path2 - && (path === _path1 && _path2._getWinding(pt, hor) - || path === _path2 && !_path1._getWinding(pt, hor)) - ? 0 - : getWinding(pt, monoCurves, hor); - break; - } - length -= curveLength; - } - } - // Assign the average winding to the entire curve chain. - var winding = Math.round(windingSum / 3); - for (var j = chain.length - 1; j >= 0; j--) { - var seg = chain[j].segment, - inter = seg._intersection, - wind = winding; - if (inter && inter._overlap) { - switch (operation) { - case 'unite': - if (wind === 1) - wind = 2; - break; - case 'intersect': - if (wind === 2) - wind = 1; - break; - case 'subtract': - // When subtracting, we need to reverse the winding - // number along overlaps. - // Calculate the new winding number based on current - // number and role in the operation. - var path = getMainPath(seg), - newWind = wind === 0 && path === _path1 ? 1 - : wind === 1 && path === _path2 ? 2 - : null; - if (newWind != null) { - // Check against the winding of the intersecting - // path, to exclude islands in compound paths, where - // the reversal of winding numbers below in overlaps - // is not required: - var pt = inter._segment._path.getInteriorPoint(); - if (getWinding(pt, monoCurves) === 1) - wind = newWind; - } - break; - } - } - seg._winding = wind; + if (segment._winding == null) { + propagateWinding(segment, _path1, _path2, monoCurves, + operation); } } - // Trace closed contours and insert them into the result. - var result = new CompoundPath(Item.NO_INSERT); - result.addChildren(tracePaths(segments, monoCurves, operation, !_path2), - true); - // See if the CompoundPath can be reduced to just a simple Path. - result = result.reduce(); - result.insertAbove(path1); - // Copy over the left-hand item's style and we're done. - // TODO: Consider using Item#_clone() for this, but find a way to not - // clone children / name (content). - result.setStyle(path1._style); - return result; + return finishBoolean(CompoundPath, tracePaths(segments, operation), + path1, path2, true); + } + + function logIntersection(inter) { + var other = inter._intersection; + var log = ['Intersection', inter._id, 'id', inter.getPath()._id, + 'i', inter.getIndex(), 't', inter.getParameter(), + 'o', inter.isOverlap(), 'p', inter.getPoint(), + 'Other', other._id, 'id', other.getPath()._id, + 'i', other.getIndex(), 't', other.getParameter(), + 'o', other.isOverlap(), 'p', other.getPoint()]; + console.log(log.map(function(v) { + return v == null ? '-' : v + }).join(' ')); + } + + /* + * Creates linked lists between intersections through their _next property. + * + * @private + */ + function linkIntersections(from, to) { + // Only create the link if it's not already in the existing chain, to + // avoid endless recursions. First walk to the beginning of the chain, + // and abort if we find `to`. + var prev = from; + while (prev) { + if (prev === to) + return; + prev = prev._prev; + } + // Now walk to the end of the existing chain to find an empty spot, but + // stop if we find `to`, to avoid adding it again. + while (from._next && from._next !== to) + from = from._next; + // If we're reached the end of the list, we can add it. + if (!from._next) { + // Go back to beginning of the other chain, and link the two up. + while (to._prev) + to = to._prev; + from._next = to; + to._prev = from; + } } /** - * Private method for splitting a PathItem at the given intersections. - * The routine works for both self intersections and intersections - * between PathItems. + * Splits a path-item at the given locations. * - * @param {CurveLocation[]} intersections Array of CurveLocation objects + * @param {CurveLocation[]} locations an array of the locations to split the + * path-item at. + * @private */ - function splitPath(intersections) { - // TODO: Make public in API, since useful! - var tMin = /*#=*/Numerical.TOLERANCE, - tMax = 1 - tMin, - isStraight = false, - straightSegments = []; + function splitPath(locations) { + if (window.reportIntersections) { + console.log('Crossings', locations.length / 2); + locations.forEach(function(inter) { + if (inter._other) + return; + logIntersection(inter); + new Path.Circle({ + center: inter.point, + radius: 2 * scaleFactor, + strokeColor: 'red', + strokeScaling: false + }); + }); + } - for (var i = intersections.length - 1, curve, prev; i >= 0; i--) { - var loc = intersections[i], - t = loc._parameter; - // Check if we are splitting same curve multiple times, but avoid - // dividing with zero. - if (prev && prev._curve === loc._curve && prev._parameter > 0) { - // Scale parameter after previous split. - t /= prev._parameter; - } else { - curve = loc._curve; - isStraight = curve.isStraight(); + // TODO: Make public in API, since useful! + var tMin = /*#=*/Numerical.CURVETIME_EPSILON, + tMax = 1 - tMin, + noHandles = false, + clearSegments = [], + prevCurve, + prevT; + + for (var i = locations.length - 1; i >= 0; i--) { + var loc = locations[i], + curve = loc._curve, + t = loc._parameter, + origT = t; + if (curve !== prevCurve) { + // This is a new curve, update noHandles setting. + noHandles = !curve.hasHandles(); + } else if (prevT > 0) { + // Scale parameter when we are splitting same curve multiple + // times, but avoid dividing by zero. + t /= prevT; } var segment; if (t < tMin) { @@ -255,43 +253,52 @@ PathItem.inject(new function() { } else if (t > tMax) { segment = curve._segment2; } else { - // Split the curve at t, passing true for ignoreStraight to not - // force the result of splitting straight curves straight. - var newCurve = curve.divide(t, true, true); - segment = newCurve._segment1; - curve = newCurve.getPrevious(); - // Keep track of segments of once straight curves, so they can - // be set back straight at the end. - if (isStraight) - straightSegments.push(segment); + // Split the curve at t, passing true for _setHandles to always + // set the handles on the sub-curves even if the original curve + // had no handles. + segment = curve.divide(t, true, true)._segment1; + // Keep track of segments of curves without handles, so they can + // be cleared again at the end. + if (noHandles) + clearSegments.push(segment); } - // Link the new segment with the intersection on the other curve - segment._intersection = loc.getIntersection(); - loc._segment = segment; - prev = loc; - } - // Reset linear segments if they were part of a linear curve - // and if we are done with the entire curve. - for (var i = 0, l = straightSegments.length; i < l; i++) { - var segment = straightSegments[i]; - // TODO: Implement Segment#makeStraight(), - // or #adjustHandles({ straight: true })) - segment._handleIn.set(0, 0); - segment._handleOut.set(0, 0); - } - } + loc._setSegment(segment); - function getMainPath(item) { - var path = item._path, - parent = path._parent; - return parent instanceof CompoundPath ? parent : path; - } + // Create links from the new segment to the intersection on the + // other curve, as well as from there back. If there are multiple + // intersections on the same segment, we create linked lists between + // the intersections through linkIntersections(), linking both ways. + var inter = segment._intersection, + dest = loc._intersection; + if (inter) { + linkIntersections(inter, dest); + // Each time we add a new link to the linked list, we need to + // add links from all the other entries to the new entry. + var other = inter; + while (other) { + linkIntersections(other._intersection, inter); + other = other._next; + } + } else { + segment._intersection = dest; + } + prevCurve = curve; + prevT = origT; + } + // Clear segment handles if they were part of a curve with no handles, + // once we are done with the entire curve. + for (var i = 0, l = clearSegments.length; i < l; i++) { + clearSegments[i].clearHandles(); + } - function isHorizontal(curve) { - // Determine if the curve is a horizontal linear curve by checking the - // slope of it's tangent. - return curve.isLinear() && Math.abs(curve.getTangentAt(0.5, true).y) - < /*#=*/Numerical.TOLERANCE; + if (window.reportIntersections) { + console.log('Split Crossings'); + locations.forEach(function(inter) { + if (!inter._other) { + logIntersection(inter); + } + }); + } } /** @@ -299,8 +306,8 @@ PathItem.inject(new function() { * with respect to a given set of monotone curves. */ function getWinding(point, curves, horizontal, testContains) { - var tolerance = /*#=*/Numerical.TOLERANCE, - tMin = tolerance, + var epsilon = /*#=*/Numerical.WINDING_EPSILON, + tMin = /*#=*/Numerical.CURVETIME_EPSILON, tMax = 1 - tMin, px = point.x, py = point.y, @@ -314,8 +321,8 @@ PathItem.inject(new function() { if (horizontal) { var yTop = -Infinity, yBottom = Infinity, - yBefore = py - tolerance, - yAfter = py + tolerance; + yBefore = py - epsilon, + yAfter = py + epsilon; // Find the closest top and bottom intercepts for the same vertical // line. for (var i = 0, l = curves.length; i < l; i++) { @@ -335,14 +342,15 @@ PathItem.inject(new function() { // half of closest top and bottom intercepts. yTop = (yTop + py) / 2; yBottom = (yBottom + py) / 2; - // TODO: Don't we need to pass on testContains here? if (yTop > -Infinity) - windLeft = getWinding(new Point(px, yTop), curves); + windLeft = getWinding(new Point(px, yTop), curves, false, + testContains); if (yBottom < Infinity) - windRight = getWinding(new Point(px, yBottom), curves); + windRight = getWinding(new Point(px, yBottom), curves, false, + testContains); } else { - var xBefore = px - tolerance, - xAfter = px + tolerance; + var xBefore = px - epsilon, + xAfter = px + epsilon; // Find the winding number for right side of the curve, inclusive of // the curve itself, while tracing along its +-x direction. var startCounted = false, @@ -383,7 +391,7 @@ PathItem.inject(new function() { // curve merely touches the ray towards +-x direction, // but proceeds to the same side of the ray. // This essentially is not a crossing. - if (Numerical.isZero(slope) && !Curve.isLinear(values) + if (Numerical.isZero(slope) && !Curve.isStraight(values) // Does the slope over curve beginning change? || t < tMin && slope * Curve.getTangent( curve.previous.values, 1).y < 0 @@ -416,6 +424,65 @@ PathItem.inject(new function() { return Math.max(abs(windLeft), abs(windRight)); } + function propagateWinding(segment, path1, path2, monoCurves, operation) { + // Here we try to determine the most probable winding number + // contribution for the curve-chain starting with this segment. Once we + // have enough confidence in the winding contribution, we can propagate + // it until the next intersection or end of a curve chain. + var epsilon = /*#=*/Numerical.GEOMETRIC_EPSILON, + chain = [], + start = segment, + totalLength = 0, + windingSum = 0; + do { + var curve = segment.getCurve(), + length = curve.getLength(); + chain.push({ segment: segment, curve: curve, length: length }); + totalLength += length; + segment = segment.getNext(); + } while (segment && !segment._intersection && segment !== start); + // Calculate the average winding among three evenly distributed + // points along this curve chain as a representative winding number. + // This selection gives a better chance of returning a correct + // winding than equally dividing the curve chain, with the same + // (amortised) time. + for (var i = 0; i < 3; i++) { + // Try the points at 1/4, 2/4 and 3/4 of the total length: + var length = totalLength * (i + 1) / 4; + for (var k = 0, m = chain.length; k < m; k++) { + var node = chain[k], + curveLength = node.length; + if (length <= curveLength) { + // If the selected location on the curve falls onto its + // beginning or end, use the curve's center instead. + if (length < epsilon || curveLength - length < epsilon) + length = curveLength / 2; + var curve = node.curve, + path = curve._path, + parent = path._parent, + pt = curve.getPointAt(length), + hor = curve.isHorizontal(); + if (parent instanceof CompoundPath) + path = parent; + // While subtracting, we need to omit this curve if this + // curve is contributing to the second operand and is + // outside the first operand. + windingSum += operation === 'subtract' && path2 + && (path === path1 && path2._getWinding(pt, hor) + || path === path2 && !path1._getWinding(pt, hor)) + ? 0 + : getWinding(pt, monoCurves, hor); + break; + } + length -= curveLength; + } + } + // Assign the average winding to the entire curve chain. + var winding = Math.round(windingSum / 3); + for (var j = chain.length - 1; j >= 0; j--) + chain[j].segment._winding = winding; + } + /** * Private method to trace closed contours from a set of segments according * to a set of constraints-winding contribution and a custom operator. @@ -428,128 +495,345 @@ PathItem.inject(new function() { * not * @return {Path[]} the contours traced */ - function tracePaths(segments, monoCurves, operation) { - var paths = [], - operator = operators[operation], - tolerance = /*#=*/Numerical.TOLERANCE, - // Values for getTangentAt() that are almost 0 and 1. - // NOTE: Even though getTangentAt() supports 0 and 1 instead of - // tMin and tMax, we still need to use this instead, as other issues - // emerge from switching to 0 and 1 in edge cases. - tMin = tolerance, - tMax = 1 - tMin; - for (var i = 0, seg, startSeg, l = segments.length; i < l; i++) { - seg = startSeg = segments[i]; - if (seg._visited || !operator(seg._winding)) - continue; - var path = new Path(Item.NO_INSERT), + function tracePaths(segments, operation) { + pathIndex = 0; + pathCount = 1; + + function labelSegment(seg, text, color) { + var point = seg.point; + var key = Math.round(point.x / (10 * scaleFactor)) + + ',' + Math.round(point.y / (10 * scaleFactor)); + var offset = segmentOffset[key] || 0; + segmentOffset[key] = offset + 1; + var size = fontSize * scaleFactor; + var text = new PointText({ + point: point.add( + new Point(size, size / 2).add(0, offset * size * 1.2) + .rotate(textAngle)), + content: text, + justification: 'left', + fillColor: color, + fontSize: fontSize + }); + // TODO! PointText should have pivot in #point by default! + text.pivot = text.globalToLocal(text.point); + text.scale(scaleFactor); + text.rotate(textAngle); + new Path.Line({ + from: text.point, + to: seg.point, + strokeColor: color, + strokeScaling: false + }); + return text; + } + + function drawSegment(seg, other, text, index, color) { + if (!window.reportSegments) + return; + new Path.Circle({ + center: seg.point, + radius: fontSize / 2 * scaleFactor, + strokeColor: color, + strokeScaling: false + }); + labelSegment(seg, '#' + pathCount + '.' + + (path ? path._segments.length + 1 : 1) + + ' (' + (index + 1) + '): ' + text + + ' id: ' + seg._path._id + '.' + seg._index + + (other ? ' -> ' + other._path._id + '.' + other._index : '') + + ' v: ' + (seg._visited ? 1 : 0) + + ' p: ' + seg._point + + ' op: ' + isValid(seg) + + ' ov: ' + !!(inter && inter.isOverlap()) + + ' wi: ' + seg._winding + + ' mu: ' + !!(inter && inter._next) + , color); + } + + for (var i = 0, j = 0; + i < (window.reportWindings ? segments.length : 0); + i++, j++) { + var seg = segments[i]; + path = seg._path, + id = path._id, + point = seg.point, inter = seg._intersection, - startInterSeg = inter && inter._segment, - added = false, // Whether a first segment as added already - dir = 1; - do { - var handleIn = dir > 0 ? seg._handleIn : seg._handleOut, - handleOut = dir > 0 ? seg._handleOut : seg._handleIn, - interSeg; - // If the intersection segment is valid, try switching to - // it, with an appropriate direction to continue traversal. - // Else, stay on the same contour. - if (added && (!operator(seg._winding)) - && (inter = seg._intersection) - && (interSeg = inter._segment) - && interSeg !== startSeg) { - if (interSeg._path === seg._path) { - // Switch to the intersection segment, if we are - // resolving self-Intersections. - seg._visited = interSeg._visited; - seg = interSeg; - dir = 1; - } else if (inter._overlap && operation !== 'intersect') { - // Switch to the overlapping intersection segment - // if its winding number along the curve is 1, to - // leave the overlapping area. - // NOTE: We cannot check the next (overlapping) - // segment since its winding number will always be 2 - var curve = interSeg.getCurve(); - if (getWinding(curve.getPointAt(0.5, true), - monoCurves, isHorizontal(curve)) === 1) { - seg._visited = interSeg._visited; - seg = interSeg; - dir = 1; - } + ix = inter, + ixs = ix && ix._segment, + n1x = inter && inter._next, + n1xs = n1x && n1x._segment, + n2x = n1x && n1x._next, + n2xs = n2x && n2x._segment, + n3x = n2x && n2x._next, + n3xs = n3x && n3x._segment, + item = path._parent instanceof CompoundPath ? path._parent : path; + if (!(id in pathIndices)) { + pathIndices[id] = ++pathIndex; + j = 0; + } + labelSegment(seg, '#' + pathIndex + '.' + (j + 1) + + ' id: ' + seg._path._id + '.' + seg._index + + ' ix: ' + (ixs && ixs._path._id + '.' + ixs._index + + '(' + ix._id + ')' || '--') + + ' n1x: ' + (n1xs && n1xs._path._id + '.' + n1xs._index + + '(' + n1x._id + ')' || '--') + + ' n2x: ' + (n2xs && n2xs._path._id + '.' + n2xs._index + + '(' + n2x._id + ')' || '--') + + ' n3x: ' + (n3xs && n3xs._path._id + '.' + n3xs._index + + '(' + n3x._id + ')' || '--') + + ' pt: ' + seg._point + + ' ov: ' + !!(inter && inter.isOverlap()) + + ' wi: ' + seg._winding + , item.strokeColor || item.fillColor || 'black'); + } + + var paths = [], + start, + otherStart, + operator = operators[operation], + // Adjust winding contributions for specific operations on overlaps: + overlapWinding = { + unite: { 1: 2 }, + intersect: { 2: 1 } + }[operation]; + + function isValid(seg, unadjusted) { + if (seg._visited) + return false; + if (!operator) // For self-intersection, we're always valid! + return true; + var winding = seg._winding, + inter = seg._intersection; + if (inter && !unadjusted && overlapWinding && inter.isOverlap()) + winding = overlapWinding[winding] || winding; + return operator(winding); + } + + function isStart(seg) { + return seg === start || seg === otherStart; + } + + // If there are multiple possible intersections, find the one + // that's either connecting back to start or is not visited yet, + // and will be part of the boolean result: + function findBestIntersection(inter, strict) { + if (!inter._next) + return inter; + while (inter) { + var seg = inter._segment, + nextSeg = seg.getNext(), + nextInter = nextSeg._intersection; + if (window.reportSegments) { + console.log('getIntersection(' + strict + ')' + + ', seg: ' + seg._path._id + '.' + seg._index + + ', next: ' + nextSeg._path._id + '.' + + nextSeg._index + + ', seg vis:' + !!seg._visited + + ', next vis:' + !!nextSeg._visited + + ', next start:' + isStart(nextSeg) + + ', seg wi:' + seg._winding + + ', next wi:' + nextSeg._winding + + ', seg op:' + isValid(seg, true) + + ', next op:' + (!(strict && nextInter + && nextInter.isOverlap()) + && isValid(nextSeg, true) + || !strict && nextInter + && isValid(nextInter._segment, true)) + + ', seg ov: ' + !!(seg._intersection + && seg._intersection.isOverlap()) + + ', next ov: ' + !!(nextSeg._intersection + && nextSeg._intersection.isOverlap()) + + ', more: ' + (!!inter._next)); + } + // See if this segment and the next are both not visited yet, or + // are bringing us back to the beginning, and are both part of + // the boolean result. + // Handling overlaps correctly here is a bit tricky business, + // and requires two passes, first with `strict = true`, then + // `false`: In strict mode, the current segment and the next + // segment are both checked for validity, and only the current + // one is allowed to be an overlap (passing true for + // `unadjusted` in isValid()). If this pass does not yield a + // result, the non-strict mode is used, in which invalid current + // segments are tolerated, and overlaps for the next segment are + // allowed as long as they are valid when not adjusted. + if (isStart(nextSeg) + || !seg._visited && !nextSeg._visited + // Self-intersections (!operator) don't need isValid() calls + && (!operator + // We need to use the unadjusted winding here since an + // overlap crossing might have brought us here, in which + // case isValid(seg, false) might be false. + || (!strict || isValid(seg, true)) + // Do not consider nextSeg in strict mode if it is part + // of an overlap, in order to give non-overlapping + // options that might follow the priority over overlaps. + && (!(strict && nextInter && nextInter.isOverlap()) + && isValid(nextSeg, true) + // If the next segment isn't valid, its intersection + // to which we may switch might be, so check that. + || !strict && nextInter + && isValid(nextInter._segment, true)) + )) + return inter; + // If it's no match, continue with the next linked intersection. + inter = inter._next; + } + return null; + } + + function findStartSegment(inter, next) { + while (inter) { + var seg = inter._segment; + if (isStart(seg)) + return seg; + inter = inter[next ? '_next' : '_prev']; + } + } + + for (var i = 0, l = segments.length; i < l; i++) { + var seg = segments[i], + path = null, + finished = false; + // Do not start a chain with already visited segments, and segments + // that are not going to be part of the resulting operation. + if (!isValid(seg)) + continue; + start = otherStart = null; + while (!finished) { + var inter = seg._intersection; + // Once we started a chain, see if there are multiple + // intersections, and if so, pick the best one: + if (inter && window.reportSegments) { + console.log('-----\n' + + '#' + pathCount + '.' + + (path ? path._segments.length + 1 : 1) + + ', Before getIntersection()' + + ', seg: ' + seg._path._id + '.' + seg._index + + ', other: ' + inter._segment._path._id + '.' + + inter._segment._index); + } + inter = inter && (findBestIntersection(inter, true) + || findBestIntersection(inter, false)) || inter; + var other = inter && inter._segment; + // A switched intersection means we may have changed the segment + // Point to the other segment in the selected intersection. + if (inter && window.reportSegments) { + console.log('After getIntersection()' + + ', seg: ' + + seg._path._id + '.' + seg._index + + ', other: ' + inter._segment._path._id + '.' + + inter._segment._index); + } + var handleIn = path && seg._handleIn; + if (!path || !other) { + // Just add the first segment and all segments that have no + // intersection. + drawSegment(seg, null, 'add', i, 'black'); + } else if (isValid(other)) { + // The other segment is part of the boolean result, and we + // are at crossing, switch over. + drawSegment(seg, other, 'cross', i, 'green'); + seg = other; + } else if (inter.isOverlap() && operation !== 'intersect') { + // Switch to the overlapping intersecting segment if it is + // part of the boolean result. Do not adjust for overlap! + if (isValid(other, true)) { + drawSegment(seg, other, 'overlap-cross', i, 'orange'); + seg = other; } else { - var c1 = seg.getCurve(); - if (dir > 0) - c1 = c1.getPrevious(); - var t1 = c1.getTangentAt(dir < 0 ? tMin : tMax, true), - // Get both curves at the intersection - // (except the entry curves). - c4 = interSeg.getCurve(), - c3 = c4.getPrevious(), - // Calculate their winding values and tangents. - t3 = c3.getTangentAt(tMax, true), - t4 = c4.getTangentAt(tMin, true), - // Cross product of the entry and exit tangent - // vectors at the intersection, will let us select - // the correct contour to traverse next. - w3 = t1.cross(t3), - w4 = t1.cross(t4); - if (Math.abs(w3 * w4) > /*#=*/Numerical.EPSILON) { - // Do not attempt to switch contours if we aren't - // sure that there is a possible candidate. - var curve = w3 < w4 ? c3 : c4, - nextCurve = operator(curve._segment1._winding) - ? curve - : w3 < w4 ? c4 : c3, - nextSeg = nextCurve._segment1; - dir = nextCurve === c3 ? -1 : 1; - // If we didn't find a suitable direction for next - // contour to traverse, stay on the same contour. - if (nextSeg._visited && seg._path !== nextSeg._path - || !operator(nextSeg._winding)) { - dir = 1; - } else { - // Switch to the intersection segment. - seg._visited = interSeg._visited; - seg = interSeg; - if (nextSeg._visited) - dir = 1; - } - } else { - dir = 1; + drawSegment(seg, other, 'overlap-stay', i, 'orange'); + } + } else if (operation === 'exclude') { + // We need to handle exclusion separately, as we want to + // switch at each crossing. + drawSegment(seg, other, 'exclude-cross', i, 'green'); + seg = other; + } else { + // Keep on truckin' + drawSegment(seg, null, 'stay', i, 'blue'); + } + if (seg._visited) { + if (isStart(seg)) { + finished = true; + drawSegment(seg, null, 'done', i, 'red'); + } else if (inter) { + // See if any of the intersections is the start segment, + // and if so finish the path. + var found = findStartSegment(inter, true) + || findStartSegment(inter, false); + if (found) { + seg = found; + finished = true; + drawSegment(seg, null, 'done multiple', i, 'red'); } } - handleOut = dir > 0 ? seg._handleOut : seg._handleIn; + if (!finished) { + // We didn't manage to switch, so stop right here. + console.error('Visited segment encountered, aborting #' + + pathCount + '.' + + (path ? path._segments.length + 1 : 1) + + ', id: ' + seg._path._id + '.' + seg._index + + ', multiple: ' + !!(inter && inter._next)); + } + break; } - // Add the current segment to the path, and mark the added - // segment as visited. - path.add(new Segment(seg._point, added && handleIn, handleOut)); - added = true; + if (!path) { + path = new Path(Item.NO_INSERT); + start = seg; + otherStart = other; + } + if (window.reportSegments) { + console.log('Adding', seg._path._id + '.' + seg._index); + } + // Add the segment to the path, and mark it as visited. + path.add(new Segment(seg._point, handleIn, seg._handleOut)); seg._visited = true; - // Move to the next segment according to the traversal direction - seg = dir > 0 ? seg.getNext() : seg. getPrevious(); - } while (seg && !seg._visited - && seg !== startSeg && seg !== startInterSeg - && (seg._intersection || operator(seg._winding))); + seg = seg.getNext(); + if (isStart(seg)) { + drawSegment(seg, null, 'done', i, 'red'); + finished = true; + } + } + if (!path) + continue; // Finish with closing the paths if necessary, correctly linking up // curves etc. - if (seg && (seg === startSeg || seg === startInterSeg)) { - path.firstSegment.setHandleIn((seg === startInterSeg - ? startInterSeg : seg)._handleIn); + if (finished) { + path.firstSegment.setHandleIn(seg._handleIn); path.setClosed(true); + if (window.reportSegments) { + console.log('Boolean operation completed', + '#' + pathCount + '.' + + (path ? path._segments.length + 1 : 1)); + } } else { - // Boolean operation results in open path, removing for now. - // TODO: We shouldn't even get here maybe? How can this be - // prevented? + var colors = ['cyan', 'green', 'orange', 'yellow']; + var color = new Color(colors[pathCount % (colors.length - 1)]); + console.error('%cBoolean operation results in open path', + 'background: ' + color.toCSS() + '; color: #fff;', + 'segs =', + path._segments.length, 'length = ', path.getLength(), + '#' + pathCount + '.' + + (path ? path._segments.length + 1 : 1)); + paper.project.activeLayer.addChild(path); + color.alpha = 0.5; + path.strokeColor = color; + path.strokeWidth = 3; + path.strokeScaling = false; path = null; } // Add the path to the result, while avoiding stray segments and - // incomplete paths. The amount of segments for valid paths depend - // on their geometry: - // - Closed paths with only straight lines need more than 2 segments - // - Closed paths with curves can consist of only one segment - if (path && path._segments.length > path.isLinear() ? 2 : 0) + // paths that are incomplete or cover no area. + // As an optimization, only check paths with 4 or less segments + // for their area, and assume that they cover an area when more. + if (path && (path._segments.length > 4 + || !Numerical.isZero(path.getArea()))) { paths.push(path); + path = null; + } + pathCount++; } return paths; } @@ -619,6 +903,9 @@ PathItem.inject(new function() { */ exclude: function(path) { return computeBoolean(this, path, 'exclude'); + // return finishBoolean(CompoundPath, + // [this.subtract(path), path.subtract(this)], + // this, path, true); }, /** @@ -629,7 +916,23 @@ PathItem.inject(new function() { * @return {Group} the resulting group item */ divide: function(path) { - return new Group([this.subtract(path), this.intersect(path)]); + return finishBoolean(Group, + [this.subtract(path), this.intersect(path)], + this, path, true); + }, + + resolveCrossings: function() { + var crossings = this.getCrossings(); + if (!crossings.length) + return this.reorient(); + splitPath(CurveLocation.expand(crossings)); + var paths = this._children || [this], + segments = []; + for (var i = 0, l = paths.length; i < l; i++) { + segments.push.apply(segments, paths[i]._segments); + } + return finishBoolean(CompoundPath, tracePaths(segments), + this, null, false).reorient(); } }; }); @@ -676,8 +979,8 @@ Path.inject(/** @lends Path# */{ y1 = v[3], y2 = v[5], y3 = v[7]; - if (Curve.isLinear(v)) { - // Handling linear curves is easy. + if (Curve.isStraight(v)) { + // Handling straight curves is easy. insertCurve(v); } else { // Split the curve at y extrema, to get bezier curves with clear @@ -685,20 +988,20 @@ Path.inject(/** @lends Path# */{ var a = 3 * (y1 - y2) - y0 + y3, b = 2 * (y0 + y2) - 4 * y1, c = y1 - y0, - tolerance = /*#=*/Numerical.TOLERANCE, - roots = []; - // Keep then range to 0 .. 1 (excluding) in the search for y - // extrema. - var count = Numerical.solveQuadratic(a, b, c, roots, tolerance, - 1 - tolerance); - if (count === 0) { + tMin = /*#=*/Numerical.CURVETIME_EPSILON, + tMax = 1 - tMin, + roots = [], + // Keep then range to 0 .. 1 (excluding) in the search for y + // extrema. + n = Numerical.solveQuadratic(a, b, c, roots, tMin, tMax); + if (n === 0) { insertCurve(v); } else { roots.sort(); var t = roots[0], parts = Curve.subdivide(v, t); insertCurve(parts[0]); - if (count > 1) { + if (n > 1) { // If there are two extrema, renormalize t to the range // of the second range and split again. t = (roots[1] - t) / (1 - t); diff --git a/src/path/PathItem.js b/src/path/PathItem.js index b44c23b9..442468f5 100644 --- a/src/path/PathItem.js +++ b/src/path/PathItem.js @@ -32,10 +32,12 @@ var PathItem = Item.extend(/** @lends PathItem# */{ * supported. * * @param {PathItem} path the other item to find the intersections with - * @param {Boolean} [sorted=false] specifies whether the returned - * {@link CurveLocation} objects should be sorted by path and offset + * @param {Function} [include] a callback function that can be used to + * filter out undesired locations right while they are collected. + * When defined, it shall return {@true to include a location}. * @return {CurveLocation[]} the locations of all intersection between the * paths + * @see #getCrossings(path) * @example {@paperscript} // Finding the intersections between two paths * var path = new Path.Rectangle(new Point(30, 25), new Size(50, 50)); * path.strokeColor = 'black'; @@ -59,88 +61,100 @@ var PathItem = Item.extend(/** @lends PathItem# */{ * } * } */ - getIntersections: function(path, _matrix) { + getIntersections: function(path, include, _matrix, _returnFirst) { // NOTE: For self-intersection, path is null. This means you can also // just call path.getIntersections() without an argument to get self // intersections. // NOTE: The hidden argument _matrix is used internally to override the // passed path's transformation matrix. - return Curve.filterIntersections(this._getIntersections( - this !== path ? path : null, _matrix, [])); - }, - - _getIntersections: function(path, matrix, locations, returnFirst) { - var curves1 = this.getCurves(), - curves2 = path ? path.getCurves() : curves1, + var self = this === path || !path, // self-intersections? matrix1 = this._matrix.orNullIfIdentity(), - matrix2 = path ? (matrix || path._matrix).orNullIfIdentity() - : matrix1, - length1 = curves1.length, - length2 = path ? curves2.length : length1, - values2 = [], - tMin = /*#=*/Numerical.TOLERANCE, - tMax = 1 - tMin; + matrix2 = self ? matrix1 + : (_matrix || path._matrix).orNullIfIdentity(); // First check the bounds of the two paths. If they don't intersect, // we don't need to iterate through their curves. - if (path && !this.getBounds(matrix1).touches(path.getBounds(matrix2))) + if (!self && !this.getBounds(matrix1).touches(path.getBounds(matrix2))) return []; + var curves1 = this.getCurves(), + curves2 = self ? curves1 : path.getCurves(), + length1 = curves1.length, + length2 = self ? length1 : curves2.length, + values2 = [], + arrays = [], + locations, + path; + // Cache values for curves2 as we re-iterate them for each in curves1. for (var i = 0; i < length2; i++) values2[i] = curves2[i].getValues(matrix2); for (var i = 0; i < length1; i++) { var curve1 = curves1[i], - values1 = path ? curve1.getValues(matrix1) : values2[i]; - if (!path) { - // First check for self-intersections within the same curve - var seg1 = curve1.getSegment1(), - seg2 = curve1.getSegment2(), - h1 = seg1._handleOut, - h2 = seg2._handleIn; - // Check if extended handles of endpoints of this curve - // intersects each other. We cannot have a self intersection - // within this curve if they don't intersect due to convex-hull - // property. - if (new Line(seg1._point.subtract(h1), h1.multiply(2), true) - .intersect(new Line(seg2._point.subtract(h2), - h2.multiply(2), true), false)) { - // Self intersecting is found by dividing the curve in two - // and and then applying the normal curve intersection code. - var parts = Curve.subdivide(values1); - Curve.getIntersections( - parts[0], parts[1], curve1, curve1, locations, - function(loc) { - if (loc._parameter <= tMax) { - // Since the curve was split above, we need to - // adjust the parameters for both locations. - loc._parameter /= 2; - loc._parameter2 = 0.5 + loc._parameter2 / 2; - return true; - } - } - ); - } + values1 = self ? values2[i] : curve1.getValues(matrix1), + path1 = curve1.getPath(); + // NOTE: Due to the nature of Curve._getIntersections(), we need to + // use separate location arrays per path1, to make sure the + // circularity checks are not getting confused by locations on + // separate paths. We are flattening the separate arrays at the end. + if (path1 !== path) { + path = path1; + locations = []; + arrays.push(locations); + } + if (self) { + // First check for self-intersections within the same curve. + Curve._getSelfIntersection(values1, curve1, locations, { + include: include, + // Only possible if there is only one closed curve: + startConnected: length1 === 1 && + curve1.getPoint1().equals(curve1.getPoint2()) + }); } // Check for intersections with other curves. For self intersection, // we can start at i + 1 instead of 0 - for (var j = path ? 0 : i + 1; j < length2; j++) { + for (var j = self ? i + 1 : 0; j < length2; j++) { // There might be already one location from the above // self-intersection check: - if (returnFirst && locations.length) - break; - Curve.getIntersections( - values1, values2[j], curve1, curves2[j], locations, - // Avoid end point intersections on consecutive curves when - // self intersecting. - !path && (j === i + 1 || j === length2 - 1 && i === 0) - && function(loc) { - var t = loc._parameter; - return t >= tMin && t <= tMax; - } + if (_returnFirst && locations.length) + return locations; + var curve2 = curves2[j]; + // Avoid end point intersections on consecutive curves when + // self intersecting. + Curve._getIntersections( + values1, values2[j], curve1, curve2, locations, + { + include: include, + // Do not compare indices here to determine connection, + // since one array of curves can contain curves from + // separate sup-paths of a compound path. + startConnected: self && curve1.getPrevious() === curve2, + endConnected: self && curve1.getNext() === curve2 + } ); } } + // Now flatten the list of location arrays to one array and return it. + locations = []; + for (var i = 0, l = arrays.length; i < l; i++) { + locations.push.apply(locations, arrays[i]); + } return locations; }, + /** + * Returns all crossings between two {@link PathItem} items as an array + * of {@link CurveLocation} objects. {@link CompoundPath} items are also + * supported. + * Crossings are intersections where the paths actually are crossing each + * other, as opposed to simply touching. + * + * @param {PathItem} path the other item to find the crossings with + * @see #getIntersections(path) + */ + getCrossings: function(path) { + return this.getIntersections(path, function(inter) { + return inter.isCrossing(); + }); + }, + _asPathItem: function() { // See Item#_asPathItem() return this; diff --git a/src/path/PathIterator.js b/src/path/PathIterator.js index 1a997798..e610cf53 100644 --- a/src/path/PathIterator.js +++ b/src/path/PathIterator.js @@ -58,7 +58,7 @@ var PathIterator = Base.extend({ // appears to offer a good trade-off between speed and // precision for display purposes. && !Curve.isFlatEnough(curve, tolerance || 0.25)) { - var split = Curve.subdivide(curve), + var split = Curve.subdivide(curve, 0.5), halfT = (minT + maxT) / 2; // Recursively subdivide and compute parts again. computeParts(split[0], index, minT, halfT); diff --git a/src/path/Segment.js b/src/path/Segment.js index 3690b02c..c42bbb76 100644 --- a/src/path/Segment.js +++ b/src/path/Segment.js @@ -119,7 +119,7 @@ var Segment = Base.extend(/** @lends Segment# */{ // Nothing } else if (count === 1) { // Note: This copies from existing segments through accessors. - if (arg0.point) { + if ('point' in arg0) { point = arg0.point; handleIn = arg0.handleIn; handleOut = arg0.handleOut; @@ -145,9 +145,10 @@ var Segment = Base.extend(/** @lends Segment# */{ }, _serialize: function(options) { - // If it is straight, only serialize point, otherwise handles too. - return Base.serialize(this.isStraight() ? this._point - : [this._point, this._handleIn, this._handleOut], + // If it is has no handles, only serialize point, otherwise handles too. + return Base.serialize(this.hasHandles() + ? [this._point, this._handleIn, this._handleOut] + : this._point, options, true); }, @@ -229,87 +230,27 @@ var Segment = Base.extend(/** @lends Segment# */{ }, /** - * Checks whether the segment has curve handles defined, meaning it is not - * a straight segment. + * Checks if the segment has any curve handles set. * - * @return {Boolean} {@true if the segment has handles defined} + * @return {Boolean} {@true if the segment has handles set} + * @see Segment#getHandleIn() + * @see Segment#getHandleOut() * @see Curve#hasHandles() * @see Path#hasHandles() */ hasHandles: function() { - return !this.isStraight(); + return !this._handleIn.isZero() || !this._handleOut.isZero(); }, /** - * Checks whether the segment is straight, meaning it has no curve handles - * defined. If two straight segments follow each each other in a path, the - * curve between them will appear as a straight line. - * Note that this is not the same as {@link #isLinear()}, which performs a - * full linearity check that includes handles running collinear to the line - * direction. - * - * @return {Boolean} {@true if the segment is straight} - * @see Curve#isStraight() + * Clears the segment's handles by setting their coordinates to zero, + * turning the segment into a corner. */ - isStraight: function() { - return this._handleIn.isZero() && this._handleOut.isZero(); + clearHandles: function() { + this._handleIn.set(0, 0); + this._handleOut.set(0, 0); }, - /** - * Checks if the curve that starts in this segment appears as a line. This - * can mean that it has no handles defined, or that the handles run - * collinear with the line. - * - * @return {Boolean} {@true if the curve is linear} - * @see Curve#isLinear() - * @see Path#isLinear() - */ - isLinear: function() { - return Segment.isLinear(this, this.getNext()); - }, - - /** - * Checks if the the two segments are the beginning of two lines that are - * collinear, meaning they run in parallel. - * - * @param {Segment} the other segment to check against - * @return {Boolean} {@true if the two lines are collinear} - * @see Curve#isCollinear(curve) - */ - isCollinear: function(segment) { - return Segment.isCollinear(this, this.getNext(), - segment, segment.getNext()); - }, - - // TODO: Remove version with typo after a while (deprecated June 2015) - isColinear: '#isCollinear', - - /** - * Checks if the segment is connecting two lines that are orthogonal, - * meaning they connect at an 90° angle. - * - * @return {Boolean} {@true if the two lines connected by this segment are - * orthogonal} - */ - isOrthogonal: function() { - return Segment.isOrthogonal(this.getPrevious(), this, this.getNext()); - }, - - /** - * Checks if the segment is the beginning of an orthogonal arc, as used in - * the construction of circles and ellipses. - * - * @return {Boolean} {@true if the segment is the beginning of an orthogonal - * arc} - * @see Curve#isOrthogonalArc() - */ - isOrthogonalArc: function() { - return Segment.isOrthogonalArc(this, this.getNext()); - }, - - // TODO: Remove a while (deprecated August 2015) - isArc: '#isOrthogonalArc', - _selectionState: 0, /** @@ -452,10 +393,45 @@ var Segment = Base.extend(/** @lends Segment# */{ }, /** - * Returns the reversed the segment, without modifying the segment itself. + * Checks if the this is the first segment in the {@link Path#segments} + * array. + * + * @return {Boolean} {@true if this is the first segment} + */ + isFirst: function() { + return this._index === 0; + }, + + /** + * Checks if the this is the last segment in the {@link Path#segments} + * array. + * + * @return {Boolean} {@true if this is the last segment} + */ + isLast: function() { + var path = this._path; + return path && this._index === path._segments.length - 1 || false; + }, + + /** + * Reverses the {@link #handleIn} and {@link #handleOut} vectors of this + * segment. Note: the actual segment is modified, no copy is created. * @return {Segment} the reversed segment */ reverse: function() { + var handleIn = this._handleIn, + handleOut = this._handleOut, + inX = handleIn._x, + inY = handleIn._y; + handleIn.set(handleOut._x, handleOut._y); + handleOut.set(inX, inY); + }, + + /** + * Returns the reversed the segment, without modifying the segment itself. + * @return {Segment} the reversed segment + */ + reversed: function() { return new Segment(this._point, this._handleOut, this._handleIn); }, @@ -564,53 +540,5 @@ var Segment = Base.extend(/** @lends Segment# */{ } } return coords; - }, - - statics: { - // These statics are shared between Segment and Curve, for versions of - // these methods that are implemented in both places. - - isLinear: function(seg1, seg2) { - var l = seg2._point.subtract(seg1._point); - return l.isCollinear(seg1._handleOut) - && l.isCollinear(seg2._handleIn); - }, - - isCollinear: function(seg1, seg2, seg3, seg4) { - // TODO: This assumes isStraight(), while isLinear() allows handles! - return seg1._handleOut.isZero() && seg2._handleIn.isZero() - && seg3._handleOut.isZero() && seg4._handleIn.isZero() - && seg2._point.subtract(seg1._point).isCollinear( - seg4._point.subtract(seg3._point)); - }, - - isOrthogonal: function(seg1, seg2, seg3) { - // TODO: This assumes isStraight(), while isLinear() allows handles! - return seg1._handleOut.isZero() && seg2._handleIn.isZero() - && seg2._handleOut.isZero() && seg3._handleIn.isZero() - && seg2._point.subtract(seg1._point).isOrthogonal( - seg3._point.subtract(seg2._point)); - }, - - isOrthogonalArc: function(seg1, seg2) { - var handle1 = seg1._handleOut, - handle2 = seg2._handleIn, - kappa = /*#=*/Numerical.KAPPA; - // Look at handle length and the distance to the imaginary corner - // point and see if it their relation is kappa. - if (handle1.isOrthogonal(handle2)) { - var pt1 = seg1._point, - pt2 = seg2._point, - // Find the corner point by intersecting the lines described - // by both handles: - corner = new Line(pt1, handle1, true).intersect( - new Line(pt2, handle2, true), true); - return corner && Numerical.isZero(handle1.getLength() / - corner.subtract(pt1).getLength() - kappa) - && Numerical.isZero(handle2.getLength() / - corner.subtract(pt2).getLength() - kappa); - } - return false; - }, } }); diff --git a/src/project/Project.js b/src/project/Project.js index b5a96710..e9316366 100644 --- a/src/project/Project.js +++ b/src/project/Project.js @@ -304,7 +304,7 @@ var Project = PaperScopeItem.extend(/** @lends Project# */{ * and may contain a combination of the following values: * * @option [options.tolerance={@link PaperScope#settings}.hitTolerance] - * {Number} the tolerance of the hit-test in points + * {Number} the tolerance of the hit-test * @option options.class {Function} only hit-test again a certain item class * and its sub-classes: {@code Group, Layer, Path, CompoundPath, * Shape, Raster, PlacedSymbol, PointText}, etc diff --git a/src/project/Symbol.js b/src/project/Symbol.js index 6e8d25f7..736b53df 100644 --- a/src/project/Symbol.js +++ b/src/project/Symbol.js @@ -133,7 +133,7 @@ var Symbol = Base.extend(/** @lends Symbol# */{ /** * Places in instance of the symbol in the project. * - * @param [position] The position of the placed symbol + * @param {Point} [position] the position of the placed symbol * @return {PlacedSymbol} */ place: function(position) { diff --git a/src/style/Color.js b/src/style/Color.js index 3e1155f2..07d9966d 100644 --- a/src/style/Color.js +++ b/src/style/Color.js @@ -1112,7 +1112,8 @@ var Color = Base.extend(new function() { } } }); -}, new function() { +}, +new function() { var operators = { add: function(a, b) { return a + b; diff --git a/src/style/Style.js b/src/style/Style.js index 0ebe5bff..d4d0db31 100644 --- a/src/style/Style.js +++ b/src/style/Style.js @@ -309,7 +309,7 @@ var Style = Base.extend(new function() { /** * @private * @bean - * @deprecated use {@link #getFontFamily()} instead. + * @deprecated use {@link #fontFamily} instead. */ getFont: '#getFontFamily', setFont: '#setFontFamily', @@ -582,7 +582,7 @@ var Style = Base.extend(new function() { */ /** - * The font size of text content, as {@Number} in pixels, or as {@String} + * The font size of text content, as a number in pixels, or as a string * with optional units {@code 'px'}, {@code 'pt'} and {@code 'em'}. * * @name Style#fontSize @@ -592,12 +592,12 @@ var Style = Base.extend(new function() { /** * - * The font-family to be used in text content, as one {@String}. - * @deprecated use {@link #fontFamily} instead. + * The font-family to be used in text content, as one string. * * @name Style#font * @default 'sans-serif' * @type String + * @deprecated use {@link #fontFamily} instead. */ /** diff --git a/src/text/TextItem.js b/src/text/TextItem.js index 824bf561..8aeb6168 100644 --- a/src/text/TextItem.js +++ b/src/text/TextItem.js @@ -120,7 +120,7 @@ var TextItem = Item.extend(/** @lends TextItem# */{ */ /** - * The font size of text content, as {@Number} in pixels, or as {@String} + * The font size of text content, as a number in pixels, or as a string * with optional units {@code 'px'}, {@code 'pt'} and {@code 'em'}. * * @name TextItem#fontSize @@ -130,7 +130,7 @@ var TextItem = Item.extend(/** @lends TextItem# */{ /** * - * The font-family to be used in text content, as one {@String}. + * The font-family to be used in text content, as one string. * @deprecated use {@link #fontFamily} instead. * * @name TextItem#font @@ -159,7 +159,7 @@ var TextItem = Item.extend(/** @lends TextItem# */{ /** * @private * @bean - * @deprecated use {@link #getStyle()} instead. + * @deprecated use {@link #style} instead. */ getCharacterStyle: '#getStyle', setCharacterStyle: '#setStyle', @@ -167,7 +167,7 @@ var TextItem = Item.extend(/** @lends TextItem# */{ /** * @private * @bean - * @deprecated use {@link #getStyle()} instead. + * @deprecated use {@link #style} instead. */ getParagraphStyle: '#getStyle', setParagraphStyle: '#setStyle' diff --git a/src/util/Numerical.js b/src/util/Numerical.js index a970267a..fc0f7159 100644 --- a/src/util/Numerical.js +++ b/src/util/Numerical.js @@ -60,12 +60,15 @@ var Numerical = new function() { var abs = Math.abs, sqrt = Math.sqrt, pow = Math.pow, - TOLERANCE = 1e-6, EPSILON = 1e-12, MACHINE_EPSILON = 1.12e-16; + function clip(value, min, max) { + return value < min ? min : value > max ? max : value; + } + return /** @lends Numerical */{ - TOLERANCE: TOLERANCE, + TOLERANCE: 1e-6, // Precision when comparing against 0 // References: // http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html @@ -78,28 +81,49 @@ var Numerical = new function() { */ EPSILON: EPSILON, /** - * MACHINE_EPSILON for a double precision (Javascript Number) is - * 2.220446049250313e-16. (try this in the js console) + * The machine epsilon for a double precision (Javascript Number) is + * 2.220446049250313e-16. (try this in the js console: * (function(){for(var e=1;1<1+e/2;)e/=2;return e}()) * - * Here the constant MACHINE_EPSILON refers to the constants 'δ' and 'ε' - * such that, the error introduced by addition, multiplication - * on a 64bit float (js Number) will be less than δ and ε. That is to - * say, for all X and Y representable by a js Number object, S and P - * be their 'exact' sum and product respectively, then + * The constant MACHINE_EPSILON here refers to the constants δ and ε + * such that, the error introduced by addition, multiplication on a + * 64bit float (js Number) will be less than δ and ε. That is to say, + * for all X and Y representable by a js Number object, S and P be their + * 'exact' sum and product respectively, then * |S - (x+y)| <= δ|S|, and |P - (x*y)| <= ε|P|. - * This amounts to about half of the actual MACHINE_EPSILON + * This amounts to about half of the actual machine epsilon. */ MACHINE_EPSILON: MACHINE_EPSILON, + /** + * The epsilon to be used when handling curve-time parameters. This + * cannot be smaller, because errors add up to around 8e-7 in the bezier + * fat-line clipping code as a result of recursive sub-division. + */ + CURVETIME_EPSILON: 8e-7, + /** + * The epsilon to be used when performing "geometric" checks, such as + * point distances and examining cross products to check for + * collinearity. + */ + GEOMETRIC_EPSILON: 4e-7, // NOTE: 1e-7 doesn't work in some edge-cases! + /** + * The epsilon to be used when performing winding contribution checks. + */ + WINDING_EPSILON: 2e-7, // NOTE: 1e-7 doesn't work in some edge-cases! + /** + * The epsilon to be used when performing "trigonometric" checks, such + * as examining cross products to check for collinearity. + */ + TRIGONOMETRIC_EPSILON: 1e-7, // Kappa, see: http://www.whizkidtech.redprince.net/bezier/circle/kappa/ KAPPA: 4 * (sqrt(2) - 1) / 3, /** - * Check if the value is 0, within a tolerance defined by + * Checks if the value is 0, within a tolerance defined by * Numerical.EPSILON. */ isZero: function(val) { - return abs(val) <= EPSILON; + return val >= -EPSILON && val <= EPSILON; }, /** @@ -158,7 +182,7 @@ var Numerical = new function() { * * References: * Kahan W. - "To Solve a Real Cubic Equation" - * http://www.cs.berkeley.edu/~wkahan/Math128/Cubic.pdf + * http://www.cs.berkeley.edu/~wkahan/Math128/Cubic.pdf * Blinn J. - "How to solve a Quadratic Equation" * * @param {Number} a the quadratic term @@ -174,10 +198,14 @@ var Numerical = new function() { */ solveQuadratic: function(a, b, c, roots, min, max) { var count = 0, + eMin = min - EPSILON, + eMax = max + EPSILON, x1, x2 = Infinity, B = b, D; - b /= 2; + // a, b, c are expected to be the coefficients of the equation: + // Ax² - 2Bx + C == 0, so we take b = -B/2: + b /= -2; D = b * b - a * c; // Discriminant // If the discriminant is very small, we can try to pre-condition // the coefficients, so that we may get better accuracy @@ -188,8 +216,8 @@ var Numerical = new function() { // We multiply with a factor to normalize the coefficients. // The factor is just the nearest exponent of 10, big enough // to raise all the coefficients to nearly [-1, +1] range. - var mult = pow(10, abs( - Math.floor(Math.log(gmC) * Math.LOG10E))); + var mult = pow(10, + abs(Math.floor(Math.log(gmC) * Math.LOG10E))); if (!isFinite(mult)) mult = 0; a *= mult; @@ -204,29 +232,25 @@ var Numerical = new function() { if (abs(B) < EPSILON) return abs(c) < EPSILON ? -1 : 0; x1 = -c / B; - } else { - // No real roots if D < 0 - if (D >= -MACHINE_EPSILON) { - D = D < 0 ? 0 : D; - var R = sqrt(D); - // Try to minimise floating point noise. - if (b >= MACHINE_EPSILON && b <= MACHINE_EPSILON) { - x1 = abs(a) >= abs(c) ? R / a : -c / R; - x2 = -x1; - } else { - var q = -(b + (b < 0 ? -1 : 1) * R); - x1 = q / a; - x2 = c / q; - } - // Do we actually have two real roots? - // count = D > MACHINE_EPSILON ? 2 : 1; + } else if (D >= -MACHINE_EPSILON) { // No real roots if D < 0 + var Q = D < 0 ? 0 : sqrt(D), + R = b + (b < 0 ? -Q : Q); + // Try to minimize floating point noise. + if (R === 0) { + x1 = c / a; + x2 = -x1; + } else { + x1 = R / a; + x2 = c / R; } } - if (isFinite(x1) && (min == null || x1 >= min && x1 <= max)) - roots[count++] = x1; + // We need to include EPSILON in the comparisons with min / max, + // as some solutions are ever so lightly out of bounds. + if (isFinite(x1) && (min == null || x1 > eMin && x1 < eMax)) + roots[count++] = min == null ? x1 : clip(x1, min, max); if (x2 !== x1 - && isFinite(x2) && (min == null || x2 >= min && x2 <= max)) - roots[count++] = x2; + && isFinite(x2) && (min == null || x2 > eMin && x2 < eMax)) + roots[count++] = min == null ? x2 : clip(x2, min, max); return count; }, @@ -321,8 +345,8 @@ var Numerical = new function() { // The cubic has been deflated to a quadratic. var count = Numerical.solveQuadratic(a, b1, c2, roots, min, max); if (isFinite(x) && (count === 0 || x !== roots[count - 1]) - && (min == null || x >= min && x <= max)) - roots[count++] = x; + && (min == null || x > min - EPSILON && x < max + EPSILON)) + roots[count++] = min == null ? x : clip(x, min, max); return count; } }; diff --git a/src/view/CanvasView.js b/src/view/CanvasView.js index 996ad97e..910ffbe0 100644 --- a/src/view/CanvasView.js +++ b/src/view/CanvasView.js @@ -144,8 +144,8 @@ var CanvasView = View.extend(/** @lends CanvasView# */{ project._needsUpdate = false; return true; } -}, new function() { // Item based mouse handling: - +}, +new function() { // Item based mouse handling: var downPoint, lastPoint, overPoint, diff --git a/src/view/View.js b/src/view/View.js index f4708ea8..c40d8769 100644 --- a/src/view/View.js +++ b/src/view/View.js @@ -675,8 +675,8 @@ var View = Base.extend(Emitter, /** @lends View# */{ return new CanvasView(project, element); } } -}, new function() { - // Injection scope for mouse events on the browser +}, +new function() { // Injection scope for mouse events on the browser /*#*/ if (__options.environment == 'browser') { var tool, prevFocus, diff --git a/test/js/helpers.js b/test/js/helpers.js index c483fb27..cb2d5b03 100644 --- a/test/js/helpers.js +++ b/test/js/helpers.js @@ -286,7 +286,7 @@ function asyncTest(testName, expected) { var project = new Project(); expected(function() { project.remove(); - start(); + QUnit.start(); }); }); } diff --git a/test/tests/Curve.js b/test/tests/Curve.js index f125f1c3..87383ec5 100644 --- a/test/tests/Curve.js +++ b/test/tests/Curve.js @@ -161,7 +161,7 @@ test('Curve#getParameterAt()', function() { var t2 = curve.getParameterAt(o2); equals(t1, t2, 'Curve parameter at offset ' + o1 + ' should be the same value as at offset' + o2, - Numerical.TOLERANCE); + Numerical.CURVETIME_EPSILON); } equals(curve.getParameterAt(curve.length + 1), null, @@ -178,3 +178,48 @@ test('Curve#getLocationAt()', function() { 'Should return null when offset is out of range.'); // 'Should return null when point is not on the curve.'); }); + +test('Curve#isStraight()', function() { + equals(function() { + return new Curve([100, 100], null, null, [200, 200]).isStraight(); + }, true); + equals(function() { + return new Curve([100, 100], [-50, -50], null, [200, 200]).isStraight(); + }, false); + equals(function() { + return new Curve([100, 100], [50, 50], null, [200, 200]).isStraight(); + }, true); + equals(function() { + return new Curve([100, 100], [50, 50], [-50, -50], [200, 200]).isStraight(); + }, true); + equals(function() { + return new Curve([100, 100], [50, 50], [50, 50], [200, 200]).isStraight(); + }, false); + equals(function() { + return new Curve([100, 100], null, [-50, -50], [200, 200]).isStraight(); + }, true); + equals(function() { + return new Curve([100, 100], null, [50, 50], [200, 200]).isStraight(); + }, false); + equals(function() { + return new Curve([100, 100], null, null, [100, 100]).isStraight(); + }, true); + equals(function() { + return new Curve([100, 100], [50, 50], null, [100, 100]).isStraight(); + }, false); + equals(function() { + return new Curve([100, 100], null, [-50, -50], [100, 100]).isStraight(); + }, false); +}); + +test('Curve#isLinear()', function() { + equals(function() { + return new Curve([100, 100], [100 / 3, 100 / 3], [-100 / 3, -100 / 3], [200, 200]).isLinear(); + }, true); + equals(function() { + return new Curve([100, 100], null, null, [100, 100]).isLinear(); + }, true); + equals(function() { + return new Curve([100, 100], null, null, [200, 200]).isLinear(); + }, false); +}); diff --git a/test/tests/Matrix.js b/test/tests/Matrix.js index 350a1c3d..6a5c1967 100644 --- a/test/tests/Matrix.js +++ b/test/tests/Matrix.js @@ -16,8 +16,7 @@ test('Decomposition: rotate()', function() { var m = new Matrix().rotate(a), s = 'new Matrix().rotate(' + a + ')'; equals(m.getRotation(), Base.pick(ea, a), - s + '.getRotation()', - Numerical.TOLERANCE); + s + '.getRotation()'); equals(m.getScaling(), new Point(1, 1), s + '.getScaling()'); } @@ -42,8 +41,7 @@ test('Decomposition: scale()', function() { var m = new Matrix().scale(sx, sy), s = 'new Matrix().scale(' + sx + ', ' + sy + ')'; equals(m.getRotation(), ea || 0, - s + '.getRotation()', - Numerical.TOLERANCE); + s + '.getRotation()'); equals(m.getScaling(), new Point(Base.pick(ex, sx), Base.pick(ey, sy)), s + '.getScaling()'); } @@ -64,8 +62,7 @@ test('Decomposition: rotate() & scale()', function() { var m = new Matrix().scale(sx, sy).rotate(a), s = 'new Matrix().scale(' + sx + ', ' + sy + ').rotate(' + a + ')'; equals(m.getRotation(), ea || a, - s + '.getRotation()', - Numerical.TOLERANCE); + s + '.getRotation()'); equals(m.getScaling(), new Point(Base.pick(ex, sx), Base.pick(ey, sy)), s + '.getScaling()'); } diff --git a/test/tests/Path_Boolean.js b/test/tests/Path_Boolean.js index 4293c6b0..4967693b 100644 --- a/test/tests/Path_Boolean.js +++ b/test/tests/Path_Boolean.js @@ -51,5 +51,5 @@ test('ring.subtract(square); #610', function() { var ring = outer.subtract(inner); var result = ring.subtract(square); - equals(result.pathData, 'M-10,131.62689c-68.2302,-5.11075 -122,-62.08951 -122,-131.62689c0,-69.53737 53.7698,-126.51614 122,-131.62689l0,32.12064c-50.53323,5.01724 -90,47.65277 -90,99.50625c0,51.85348 39.46677,94.489 90,99.50625z'); + equals(result.pathData, 'M-132,0c0,-69.53737 53.7698,-126.51614 122,-131.62689l0,32.12064c-50.53323,5.01724 -90,47.65277 -90,99.50625c0,51.85348 39.46677,94.489 90,99.50625l0,32.12064c-68.2302,-5.11075 -122,-62.08951 -122,-131.62689z'); }); diff --git a/test/tests/Path_Curves.js b/test/tests/Path_Curves.js index c697e9b1..faebbd47 100644 --- a/test/tests/Path_Curves.js +++ b/test/tests/Path_Curves.js @@ -107,10 +107,10 @@ test('Curve list after removing a segment - 2', function() { }, 1, 'After removing the last segment, we should be left with one curve'); }); -test('Splitting a straight path should produce straight segments', function() { - var path = new Path.Line([0, 0], [50, 50]); - var path2 = path.split(0, 0.5); +test('Splitting a straight path should produce segments without handles', function() { + var path1 = new Path.Line([0, 0], [50, 50]); + var path2 = path1.split(0, 0.5); equals(function() { - return path2.firstSegment.isStraight(); + return !path1.lastSegment.hasHandles() && !path2.firstSegment.hasHandles(); }, true); }); diff --git a/test/tests/Segment.js b/test/tests/Segment.js index 3c8eb957..b28de7b7 100644 --- a/test/tests/Segment.js +++ b/test/tests/Segment.js @@ -43,7 +43,7 @@ test('new Segment(size)', function() { test('segment.reverse()', function() { var segment = new Segment(new Point(10, 10), new Point(5, 5), new Point(15, 15)); - segment = segment.reverse(); + segment.reverse(); equals(segment.toString(), '{ point: { x: 10, y: 10 }, handleIn: { x: 15, y: 15 }, handleOut: { x: 5, y: 5 } }'); }); diff --git a/test/tests/TextItem.js b/test/tests/TextItem.js index b3d218b4..77cbc5c9 100644 --- a/test/tests/TextItem.js +++ b/test/tests/TextItem.js @@ -14,7 +14,7 @@ module('TextItem'); test('PointText', function() { var text = new PointText({ - fontFamily: 'Arial', + fontFamily: 'Helvetica, Arial', fontSize: 14, point: [100, 100], content: 'Hello World!' @@ -22,7 +22,7 @@ test('PointText', function() { equals(text.fillColor, new Color(0, 0, 0), 'text.fillColor should be black by default'); equals(text.point, new Point(100, 100), 'text.point'); equals(text.bounds.point, new Point(100, 87.4), 'text.bounds.point'); - equals(text.bounds.size, new Size(77, 16.8), 'text.bounds.size', { tolerance: 1.0 }); + equals(text.bounds.size, new Size(76, 16.8), 'text.bounds.size', { tolerance: 1.0 }); equals(function() { return text.hitTest(text.bounds.center) != null; }, true);