diff --git a/src/path/CompoundPath.js b/src/path/CompoundPath.js index 14868b8a..68bd5aef 100644 --- a/src/path/CompoundPath.js +++ b/src/path/CompoundPath.js @@ -30,6 +30,9 @@ var CompoundPath = PathItem.extend(/** @lends CompoundPath# */{ _serializeFields: { children: [] }, + // Enforce creation of beans, as bean getters have hidden parameters. + // See #getPathData() and #getArea below. + beans: true, /** * Creates a new compound path item and places it in the active layer. @@ -248,11 +251,11 @@ var CompoundPath = PathItem.extend(/** @lends CompoundPath# */{ * @bean * @type Number */ - getArea: function() { + getArea: function(_closed) { var children = this._children, area = 0; for (var i = 0, l = children.length; i < l; i++) - area += children[i].getArea(); + area += children[i].getArea(_closed); return area; }, @@ -269,10 +272,7 @@ var CompoundPath = PathItem.extend(/** @lends CompoundPath# */{ for (var i = 0, l = children.length; i < l; i++) length += children[i].getLength(); return length; - } -}, /** @lends CompoundPath# */{ - // Enforce bean creation for getPathData(), as it has hidden parameters. - beans: true, + }, getPathData: function(_matrix, _precision) { // NOTE: #setPathData() is defined in PathItem. @@ -285,8 +285,8 @@ var CompoundPath = PathItem.extend(/** @lends CompoundPath# */{ ? _matrix.appended(mx) : _matrix, _precision)); } return paths.join(''); - } -}, /** @lends CompoundPath# */{ + }, + _hitTestChildren: function _hitTestChildren(point, options, viewMatrix) { return _hitTestChildren.base.call(this, point, // If we're not specifically asked to returns paths through diff --git a/src/path/Path.js b/src/path/Path.js index 1fff1b35..783b57d5 100644 --- a/src/path/Path.js +++ b/src/path/Path.js @@ -295,8 +295,8 @@ var Path = PathItem.extend(/** @lends Path# */{ } } }, /** @lends Path# */{ - // Enforce bean creation for getPathData() and getArea(), as they have - // hidden parameters. + // Enforce creation of beans, as bean getters have hidden parameters. + // See #getPathData() and #getArea below. beans: true, getPathData: function(_matrix, _precision) { @@ -829,24 +829,28 @@ var Path = PathItem.extend(/** @lends Path# */{ * @type Number */ getArea: function(_closed) { - // If the call overrides the 'closed' state, do not cache the result. - // This is used in tracePaths(). - var cached = _closed === undefined, - area = this._area; - if (!cached || area == null) { + // Cache the area for the open path, and the the final curve separately, + // so open and closed area can be returned at almost no additional cost. + var closed = Base.pick(_closed, this._closed), + cached = this._area; + if (cached == null) { var segments = this._segments, - count = segments.length, - closed = cached ? this._closed : _closed, - last = count - 1; - area = 0; - for (var i = 0, l = closed ? count : last; i < l; i++) { - area += Curve.getArea(Curve.getValues( - segments[i], segments[i < last ? i + 1 : 0])); + sum = 0, + close = 0; + for (var i = 0, l = segments.length; i < l; i++) { + var next = i + 1, + last = next >= l, + area = Curve.getArea(Curve.getValues( + segments[i], segments[last ? 0 : i + 1])); + if (last) { + close = area; + } else { + sum += area; + } } - if (cached) - this._area = area; + cached = this._area = [sum, close]; } - return area; + return cached[0] + (closed ? cached[1] : 0); }, /** diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index aa220225..af237081 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -50,52 +50,52 @@ PathItem.inject(new function() { * remove empty curves, #resolveCrossings() to resolve self-intersection * make sure all paths have correct winding direction. */ - function preparePath(path, closed) { + function preparePath(path, resolve) { var res = path.clone(false).reduce({ simplify: true }) .transform(null, true, true); - if (closed) - res.setClosed(true); - return closed + return resolve ? res.resolveCrossings().reorient(res.getFillRule() === 'nonzero') : res; } - function createResult(ctor, paths, reduce, path1, path2) { + function createResult(ctor, paths, reduce, path1, path2, options) { 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({ simplify: true }); - // 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); + if (!(options && options.insert === false)) { + // 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 input path attributes, excluding matrix and we're done. result.copyAttributes(path1, true); return result; } - function computeBoolean(path1, path2, operation) { - // Retrieve the operator lookup table for winding numbers. - var operator = operators[operation]; + function computeBoolean(path1, path2, operation, options) { + // Only support subtract and intersect operations when computing stroke + // based boolean operations. + if (options && options.stroke && + /^(subtract|intersect)$/.test(operation)) + return computeStrokeBoolean(path1, path2, operation === 'subtract'); + // We do not modify the operands themselves, but create copies instead, + // fas produced by the calls to preparePath(). + // NOTE: The result paths might not belong to the same type i.e. + // subtract(A:Path, B:Path):CompoundPath etc. + var _path1 = preparePath(path1, true), + _path2 = path2 && path1 !== path2 && preparePath(path2, true), + // Retrieve the operator lookup table for winding numbers. + operator = operators[operation]; // Add a simple boolean property to check for a given operation, // e.g. `if (operator.unite)` operator[operation] = true; - // If path1 is open, delegate to computeOpenBoolean(). - // NOTE: Do not access private _closed property here, since path1 may - // be a CompoundPath. - if (!path1.isClosed()) - return computeOpenBoolean(path1, path2, operator); - // 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, true), - _path2 = path2 && path1 !== path2 && preparePath(path2, true); // Give both paths the same orientation except for subtraction // and exclusion, where we need them at opposite orientation. if (_path2 && (operator.subtract || operator.exclude) - ^ (_path2.isClockwise() ^ _path1.isClockwise())) + ^ (_path2.isClockwise(true) ^ _path1.isClockwise(true))) _path2.reverse(); // Split curves at crossings on both paths. Note that for self- // intersection, path2 is null and getIntersections() handles it. @@ -183,26 +183,20 @@ PathItem.inject(new function() { paths = tracePaths(segments, operator); } - return createResult(CompoundPath, paths, true, path1, path2); + return createResult(CompoundPath, paths, true, path1, path2, options); } - function computeOpenBoolean(path1, path2, operator) { - // Only support subtract and intersect operations between an open - // and a closed path. - if (!path2 || !operator.subtract && !operator.intersect) { - throw new Error('Boolean operations on open paths only support ' + - 'subtraction and intersection with another path.'); - } - var _path1 = preparePath(path1, false), - _path2 = preparePath(path2, false), + function computeStrokeBoolean(path1, path2, subtract) { + var _path1 = preparePath(path1), + _path2 = preparePath(path2), crossings = _path1.getCrossings(_path2), - sub = operator.subtract, paths = []; function addPath(path) { // Simple see if the point halfway across the open path is inside // path2, and include / exclude the path based on the operator. - if (_path2.contains(path.getPointAt(path.getLength() / 2)) ^ sub) { + if (_path2.contains(path.getPointAt(path.getLength() / 2)) + ^ subtract) { paths.unshift(path); return true; } @@ -358,13 +352,17 @@ PathItem.inject(new function() { * {@link CompoundPath#getCurves()} * @param {Number} [dir=0] the direction in which to determine the * winding contribution, `0`: in x-direction, `1`: in y-direction + * @param {Boolean} [closed=false] determines how areas should be closed + * when a curve is part of an open path, `false`: area is closed with a + * straight line, `true`: area is closed taking the handles of the first + * and last segment into account * @param {Boolean} [dontFlip=false] controls whether the algorithm is * allowed to flip direction if it is deemed to produce better results * @return {Object} an object containing the calculated winding number, as * well as an indication whether the point was situated on the contour * @private */ - function getWinding(point, curves, dir, dontFlip) { + function getWinding(point, curves, dir, closed, dontFlip) { var epsilon = /*#=*/Numerical.WINDING_EPSILON, // Determine the index of the abscissa and ordinate values in the // curve values arrays, based on the direction: @@ -465,7 +463,7 @@ PathItem.inject(new function() { // again with flipped direction and return that result instead. return !dontFlip && a > paL && a < paR && Curve.getTangent(v, t)[dir ? 'x' : 'y'] === 0 - && getWinding(point, curves, dir ? 0 : 1, true); + && getWinding(point, curves, dir ? 0 : 1, closed, true); } function handleCurve(v) { @@ -505,14 +503,22 @@ PathItem.inject(new function() { // We're on a new (sub-)path, so we need to determine values of // the last non-horizontal curve on this path. vPrev = null; - // If the path is not closed, connect the end points with a - // straight curve, just like how filling open paths works. + // If the path is not closed, connect the first and last segment + // based on the value of `closed`: + // - `false`: Connect with a straight curve, just like how + // filling open paths works. + // - `true`: Connect with a curve that takes the segment handles + // into account, just like how closed paths behave. if (!path._closed) { - var p1 = path.getLastCurve().getPoint2(), - p2 = curve.getPoint1(), + var s1 = path.getLastCurve().getSegment2(), + s2 = curve.getSegment1(), + p1 = s1._point, + p2 = s2._point, x1 = p1._x, y1 = p1._y, x2 = p2._x, y2 = p2._y; - vClose = [x1, y1, x1, y1, x2, y2, x2, y2]; + vClose = closed + ? Curve.getValues(s1, s2) + : [x1, y1, x1, y1, x2, y2, x2, y2]; // This closing curve is a potential candidate for the last // non-horizontal curve. if (vClose[io] !== vClose[io + 6]) { @@ -555,7 +561,7 @@ PathItem.inject(new function() { // for clockwise and [-1,+1] for counter-clockwise paths. // If the ray is cast in y direction (dir == 1), the // windings always have opposite sign. - var add = path.isClockwise() ^ dir ? 1 : -1; + var add = path.isClockwise(closed) ^ dir ? 1 : -1; windingL += add; windingR -= add; onPathWinding += add; @@ -626,10 +632,12 @@ PathItem.inject(new function() { // contributing to the second operand and is outside the // first operand. winding = !(operator.subtract && path2 && ( - path === path1 && path2._getWinding(pt, dir).winding || - path === path2 && !path1._getWinding(pt, dir).winding)) - ? getWinding(pt, curves, dir) - : { winding: 0 }; + path === path1 && + path2._getWinding(pt, dir, true).winding || + path === path2 && + !path1._getWinding(pt, dir, true).winding)) + ? getWinding(pt, curves, dir, true) + : { winding: 0 }; break; } length -= curveLength; @@ -734,6 +742,7 @@ PathItem.inject(new function() { for (var i = 0, l = segments.length; i < l; i++) { var path = null, finished = false, + closed = true, seg = segments[i], inter = seg._intersection, handleIn; @@ -793,18 +802,21 @@ PathItem.inject(new function() { seg = other; } } - // Bail out if we're done, or if we encounter an already visited - // next segment. - if (finished || seg._visited) { - // It doesn't hurt to set again to share some code. + if (finished) { seg._visited = true; + // If we end up on the first or last segment of an operand, + // copy over its closed state, to support mixed open/closed + // scenarios as described in #1036 + if (seg.isFirst() || seg.isLast()) + closed = seg._path._closed; break; } - // If there are only valid overlaps and we encounter and invalid - // segment, bail out immediately. Otherwise we need to be more - // tolerant due to complex situations of crossing, - // see findBestIntersection() - if (seg._path._validOverlapsOnly && !isValid(seg)) + // If a visited or invalid segment is encountered, bail out + // immediately. But if there aren't only valid overlaps, be more + // tolerant due to complex crossing situations. + // See findBestIntersection() + if (seg._visited + || seg._path._validOverlapsOnly && !isValid(seg)) break; if (!path) { path = new Path(Item.NO_INSERT); @@ -830,7 +842,7 @@ PathItem.inject(new function() { // Finish with closing the paths, and carrying over the last // handleIn to the first segment. path.firstSegment.setHandleIn(handleIn); - path.setClosed(true); + path.setClosed(closed); } else if (path) { // Only complain about open paths if they would actually contain // an area when closed. Open paths that can silently discarded @@ -873,39 +885,58 @@ PathItem.inject(new function() { * winding contribution, `0`: in x-direction, `1`: in y-direction * @return {Number} the winding number */ - _getWinding: function(point, dir) { - return getWinding(point, this.getCurves(), dir); + _getWinding: function(point, dir, closed) { + return getWinding(point, this.getCurves(), dir, closed); }, /** * {@grouptitle Boolean Path Operations} * - * Merges the geometry of the specified path with this path's geometry + * Unites the geometry of the specified path with this path's geometry * and returns the result as a new path item. * + * @option [options.insert=true] {Boolean} whether the resulting item + * should be inserted back into the scene graph, above both paths + * involved in the operation + * * @param {PathItem} path the path to unite with + * @param {Object} [options] the boolean operation options * @return {PathItem} the resulting path item */ - unite: function(path) { - return computeBoolean(this, path, 'unite'); + unite: function(path, options) { + return computeBoolean(this, path, 'unite', options); }, /** * Intersects the geometry of the specified path with this path's * geometry and returns the result as a new path item. * + * @option [options.insert=true] {Boolean} whether the resulting item + * should be inserted back into the scene graph, above both paths + * involved in the operation + * @option [options.stroke=false] {Boolean} whether the operation should + * be performed on the stroke or on the fill of the first path + * * @param {PathItem} path the path to intersect with + * @param {Object} [options] the boolean operation options * @return {PathItem} the resulting path item */ - intersect: function(path) { - return computeBoolean(this, path, 'intersect'); + intersect: function(path, options) { + return computeBoolean(this, path, 'intersect', options); }, /** * Subtracts the geometry of the specified path from this path's * geometry and returns the result as a new path item. * + * @option [options.insert=true] {Boolean} whether the resulting item + * should be inserted back into the scene graph, above both paths + * involved in the operation + * @option [options.stroke=false] {Boolean} whether the operation should + * be performed on the stroke or on the fill of the first path + * * @param {PathItem} path the path to subtract + * @param {Object} [options] the boolean operation options * @return {PathItem} the resulting path item */ subtract: function(path) { @@ -916,11 +947,16 @@ PathItem.inject(new function() { * Excludes the intersection of the geometry of the specified path with * this path's geometry and returns the result as a new path item. * + * @option [options.insert=true] {Boolean} whether the resulting item + * should be inserted back into the scene graph, above both paths + * involved in the operation + * * @param {PathItem} path the path to exclude the intersection of + * @param {Object} [options] the boolean operation options * @return {PathItem} the resulting group item */ - exclude: function(path) { - return computeBoolean(this, path, 'exclude'); + exclude: function(path, options) { + return computeBoolean(this, path, 'exclude', options); }, /** @@ -929,12 +965,21 @@ PathItem.inject(new function() { * calling {@link #subtract(path)} and {@link #subtract(path)} and * putting the results into a new group. * + * @option [options.insert=true] {Boolean} whether the resulting item + * should be inserted back into the scene graph, above both paths + * involved in the operation + * @option [options.stroke=false] {Boolean} whether the operation should + * be performed on the stroke or on the fill of the first path + * * @param {PathItem} path the path to divide by + * @param {Object} [options] the boolean operation options * @return {Group} the resulting group item */ - divide: function(path) { - return createResult(Group, [this.subtract(path), - this.intersect(path)], true, this, path); + divide: function(path, options) { + return createResult(Group, [ + this.subtract(path, options), + this.intersect(path, options) + ], true, this, path, options); }, /* diff --git a/src/path/PathItem.js b/src/path/PathItem.js index 1d3133ca..a3f5b2ae 100644 --- a/src/path/PathItem.js +++ b/src/path/PathItem.js @@ -23,6 +23,9 @@ var PathItem = Item.extend(/** @lends PathItem# */{ _class: 'PathItem', _selectBounds: false, _canScaleStroke: true, + // Enforce creation of beans, as bean getters have hidden parameters. + // See #isClockwise() below. + beans: true, initialize: function PathItem() { // Do nothing. @@ -101,8 +104,8 @@ var PathItem = Item.extend(/** @lends PathItem# */{ * @see Path#getArea() * @see CompoundPath#getArea() */ - isClockwise: function() { - return this.getArea() >= 0; + isClockwise: function(_closed) { + return this.getArea(_closed) >= 0; }, setClockwise: function(clockwise) { diff --git a/test/tests/Path_Boolean.js b/test/tests/Path_Boolean.js index 190f7a37..2000be82 100644 --- a/test/tests/Path_Boolean.js +++ b/test/tests/Path_Boolean.js @@ -99,7 +99,7 @@ test('#719', function() { compareBoolean(result, expected); }); -test('#757 (support for open paths)', function() { +test('#757 (path1.intersect(pat2, { stroke: true }))', function() { var rect = new Path.Rectangle({ from: [100, 250], to: [350, 350] @@ -116,7 +116,7 @@ test('#757 (support for open paths)', function() { ] }); - var res = line.intersect(rect); + var res = line.intersect(rect, { stroke: true }); var children = res.removeChildren(); var first = children[0]; @@ -540,6 +540,37 @@ test('#973', function() { 'children orientation after calling path.resolveCrossings()'); }); +test('#1036', function() { + var line1 = new Path([ + [[305.10732,101.34786],[0,0],[62.9214,0]], + [[499.20274,169.42611],[-29.38716,-68.57004],[5.78922,13.50818]], + [[497.75426,221.57115],[2.90601,-13.5614],[-9.75434,45.52027]], + [[416.63976,331.65512],[31.9259,-30.40562],[-21.77284,20.73604]], + [[350.00999,391.04252],[23.92578,-18.22917],[-23.33885,17.78198]], + [[277.58633,431.59977],[27.45996,-10.67887],[-1.72805,0.67202]], + [[251.51381,437.39367],[0,5.7145],[0,0]] + ]); + + var line2 = new Path([ + [[547.00236,88.31161],[0,0],[-1.36563,0]], + [[544.10541,85.41466],[1.29555,0.43185],[-9.83725,-3.27908]], + [[509.34205,82.51771],[10.32634,-1.29079],[-10.20055,1.27507]], + [[444.16075,97.00245],[7.10741,-4.06138],[-4.93514,2.82008]], + [[431.12449,105.69328],[4.27047,-2.13524],[-14.94798,7.47399]], + [[407.94892,175.22],[1.27008,-13.33587],[-3.16966,33.28138]], + [[399.25808,279.51008],[-5.61644,-33.69865],[1.73417,10.40499]], + [[415.19129,307.03107],[-5.98792,-8.16534],[2.74694,3.74583]], + [[432.57297,328.75817],[-3.89061,-3.29206],[2.9716,2.51443]], + [[442.71228,334.55206],[-3.01275,-2.46498],[2.39275,1.95771]], + [[448.50617,341.79443],[-2.37502,-1.97918],[39.75954,33.13295]], + [[578.86877,378.00626],[-51.65429,10.33086],[11.28627,-2.25725]], + [[612.18365,366.41848],[-10.6547,4.26188],[3.10697,-1.24279]], + [[617.97755,362.07306],[-3.19904,0],[0,0]] + ]); + compareBoolean(function() { return line1.intersect(line2); }, + 'M424.54226,112.15158c32.89387,9.15202 61.28089,26.0555 74.66048,57.27453c5.78922,13.50818 1.45753,38.58364 -1.44848,52.14504c-8.75233,40.8442 -41.40003,72.54068 -71.07836,100.58668c-4.48065,-5.55963 -9.68976,-12.67924 -11.48461,-15.12676c-5.98792,-8.16534 -14.19904,-17.116 -15.93321,-27.52099c-5.61644,-33.69865 5.52118,-71.0087 8.69084,-104.29008c1.06648,-11.19805 6.14308,-47.34273 16.59334,-63.06842z'); +}); + test('#1054', function() { var p1 = new Path({ segments: [