diff --git a/src/path/Curve.js b/src/path/Curve.js index 80ed67e2..dddfa14e 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1203,8 +1203,12 @@ new function() { // Scope for methods that require private functions function addLocation(locations, include, curve1, t1, point1, curve2, t2, point2) { var loc = new CurveLocation(curve1, t1, point1, curve2, t2, point2); - if (!include || include(loc)) + if (!include || include(loc)) { locations.push(loc); + } else { + loc = null; + } + return loc; } function addCurveIntersections(v1, v2, curve1, curve2, locations, include, @@ -1476,11 +1480,86 @@ new function() { // Scope for methods that require private functions } } + /** + * 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, + isLinear = Curve.isLinear(v1) || Curve.isLinear(v2); + if (isLinear) { + // If one curve is linear, the other curve must be linear, too. Otherwise they cannot overlap. + // Linear curves can only overlap if they are collinear, which means they must be are parallel and + // any point of curve 1 must be on curve 2 + if (!Curve.isLinear(v1) || !Curve.isLinear(v2) || + abs((v1[0] - v1[6]) * (v2[1] - v2[7]) - (v1[1] - v1[7]) * (v2[0] - v2[6])) > tolerance || + abs(Line.getSignedDistance(v2[0], v2[1], v2[6], v2[7], v1[0], v1[1], false)) > tolerance) { + return false; + } + } + var v = [v1, v2], + matches = []; + // Iterate through all end points. First p1 and p2 of curve 1, then p1 and p2 of curve 2 + for (var vIdx = 0, t1 = 0; vIdx < 2 && matches.length < 2; vIdx += t1 === 0 ? 0 : 1, t1 = t1 ^ 1) { + var t2 = Curve.getParameterOf(v[vIdx^1], v[vIdx][t1 === 0 ? 0 : 6], v[vIdx][t1 === 0 ? 1 : 7]); + if (t2 != null) { // if point is on curve + var match = vIdx === 0 ? [t1, t2] : [t2, t1]; + if (matches.length === 1 && match[0] < matches[0][0]) { + matches.unshift(match); + } else if (matches.length === 0 || match[0] != matches[0][0] || match[1] != matches[0][1]) { + matches.push(match); + } + } + if (vIdx === 1 && matches.length == 0) { + return false; // if we checked three points but found no match then curves cannot overlap + } + } + // if we found two matches, the end points of v1 and v2 should be the same. We only have to check if the + // handles are the same, too. + var overlap = 1; + if (matches.length == 2) { + // create values for overlapping part of each curve + v[0] = Curve.getPart(v[0], matches[0][0], matches[1][0]); + v[1] = Curve.getPart(v[1], Math.min(matches[0][1], matches[1][1]), Math.max(matches[0][1], matches[1][1])); + // reverse values of second curve if necessary + if (abs(v[0][0] - v[1][6]) < tolerance && abs(v[0][1] - v[1][7]) < tolerance) { + overlap = -1; + v[1] = [v[1][6], v[1][7], v[1][4], v[1][5], v[1][2], v[1][3], v[1][0], v[1][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 (isLinear || + abs(v[0][2] - v[1][2]) < tolerance && abs(v[0][3] - v[1][3]) < tolerance && + abs(v[0][4] - v[1][4]) < tolerance && abs(v[0][5] - v[1][5]) < tolerance) { + // overlapping parts are identical + var t1 = matches[0][0], + t2 = matches[0][1], + loc = addLocation(locations, include, + curve1, t1, Curve.getPoint(v1, t1), + curve2, t2, Curve.getPoint(v2, t2), true); + if (loc) + loc._overlap = overlap; + var t1 = matches[1][0], + t2 = matches[1][1]; + loc = addLocation(locations, include, + curve1, t1, Curve.getPoint(v1, t1), + curve2, t2, Curve.getPoint(v2, t2), true); + if (loc) + loc._overlap = overlap; + return true; + } + } + 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)) + return locations; var linear1 = Curve.isLinear(v1), linear2 = Curve.isLinear(v2), c1p1 = c1.getPoint1(), @@ -1550,12 +1629,20 @@ new function() { // Scope for methods that require private functions if (last > 0) { locations.sort(compare); - // Filter out duplicate locations. - for (var i = last; i > 0; i--) { - if (locations[i].equals(locations[i - 1])) { - locations.splice(i, 1); + // 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)) { + var overlap = loc._overlap; + if (overlap) + prev._overlap = overlap; + locations.splice(i + 1, 1); // Remove loc last--; } + loc = prev; } } if (_expand) { diff --git a/src/path/CurveLocation.js b/src/path/CurveLocation.js index 7a47ede0..80610f19 100644 --- a/src/path/CurveLocation.js +++ b/src/path/CurveLocation.js @@ -214,7 +214,8 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ // 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, this); + this._parameter2, this._point2 || this._point); + intersection._overlap = this._overlap; intersection._intersection = this; } return intersection; @@ -246,6 +247,8 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ * * @type Number * @bean + * @see Curve#getNearestLocation(point) + * @see Path#getNearestLocation(point) */ getDistance: function() { return this._distance; diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 04b8d8e0..0ec57694 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -25,7 +25,7 @@ * * Not supported yet * - Boolean operations on self-intersecting Paths - * - Paths are clones of each other that ovelap exactly on top of each other! + * - Paths are clones of each other that overlap exactly on top of each other! * * @author Harikrishnan Gopalakrishnan * http://hkrish.com/playground/paperjs/booleanStudy.html @@ -158,6 +158,18 @@ PathItem.inject(new function() { || path === _path2 && !_path1._getWinding(pt, hor)) ? 0 : getWinding(pt, monoCurves, hor); + /* + new Path.Circle({ + center: pt, + radius: 3, + strokeColor: 'red' + }); + new PointText({ + point: pt, + content: getWinding(pt, monoCurves, hor), + fillColor: 'red' + }); + */ break; } length -= curveLength; @@ -165,15 +177,27 @@ PathItem.inject(new function() { } // 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; + for (var j = chain.length - 1; j >= 0; j--) { + var seg = chain[j].segment, + inter = seg._intersection; + seg._winding = winding; + if (inter && inter._overlap && winding === 1) + seg._winding = 2; + /* + new PointText({ + point: seg.point, + content: seg._winding, + fillColor: 'green' + }); + */ + } } // Trace closed contours and insert them into the result. var result = new CompoundPath(Item.NO_INSERT); - result.insertAbove(path1); result.addChildren(tracePaths(segments, operator), 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). @@ -189,6 +213,7 @@ PathItem.inject(new function() { * @param {CurveLocation[]} intersections Array of CurveLocation objects */ function splitPath(intersections) { + // TODO: Make public in API, since useful! var tMin = /*#=*/Numerical.TOLERANCE, tMax = 1 - tMin, isStraight = false, @@ -373,15 +398,66 @@ PathItem.inject(new function() { * @return {Path[]} the contours traced */ function tracePaths(segments, operator, selfOp) { + var segmentCount = 0; + var segmentOffset = {}; + + function labelSegment(seg, text, color) { + var textAngle = 45; + var point = seg.point; + var key = Math.round(point.x * 1000) + ',' + Math.round(point.y * 1000); + var offset = segmentOffset[key] || 0; + segmentOffset[key] = offset + 1; + var text = new PointText({ + point: point.add(new Point(8, 4).rotate(textAngle).add(0, offset * 14)), + content: text, + justification: 'left', + fillColor: color + }); + text.pivot = text.globalToLocal(text.point); + text.rotation = textAngle; + } + + function drawSegment(seg, text, index, color) { + if (false) + return; + // return; + new Path.Circle({ + center: seg.point, + radius: 3, + strokeColor: color + }); + var inter = seg._intersection; + labelSegment(seg, (segmentCount++) + '/' + index + ': ' + text + + ' v: ' + !!seg._visited + + ' op: ' + operator(seg._winding) + + ' o: ' + (inter ? inter._overlap : 0) + + ' w: ' + seg._winding + , color); + } + + for (var i = 0; i < 0 && segments.length; i++) { + var seg = segments[i]; + var point = seg.point; + var inter = seg._intersection; + labelSegment(seg, i + + ' i: ' + !!inter + + ' o: ' + (inter ? inter._overlap : 0) + + ' w: ' + seg._winding + , 'green'); + } + var paths = []; for (var i = 0, seg, startSeg, l = segments.length; i < l; i++) { seg = startSeg = segments[i]; - if (seg._visited || !operator(seg._winding)) + if (seg._visited || !operator(seg._winding)) { + drawSegment(seg, 'ignore', i, 'red'); continue; + } var path = new Path(Item.NO_INSERT), inter = seg._intersection, startInterSeg = inter && inter._segment, added = false, // Whether a first segment as added already + firstOverlap = true, dir = 1; do { var handleIn = dir > 0 ? seg._handleIn : seg._handleOut, @@ -404,7 +480,7 @@ PathItem.inject(new function() { var c1 = seg.getCurve(); if (dir > 0) c1 = c1.getPrevious(); - var t1 = c1.getTangentAt(dir < 1 ? 0 : 1, true), + var t1 = c1.getTangentAt(dir < 0 ? 0 : 1, true), // Get both curves at the intersection (except the // entry curves). c4 = interSeg.getCurve(), @@ -417,7 +493,21 @@ PathItem.inject(new function() { // the correct contour to traverse next. w3 = t1.cross(t3), w4 = t1.cross(t4); - if (w3 * w4 !== 0) { + var signature = (w3 * w4).toPrecision(1) + ' (' + w3.toPrecision(1) + ' * ' + w4.toPrecision(1) + ')'; + var overlap = inter._overlap; + if (overlap) { + // Switch to the overlapping intersection segment. + if (firstOverlap && overlap === 1) { + drawSegment(seg, '1st overlap ' + signature, i, 'orange'); + firstOverlap = false; + } else { + drawSegment(seg, '2nd overlap ' + signature, i, 'orange'); + seg._visited = interSeg._visited; + seg = interSeg; + dir = 1; + firstOverlap = true; + } + } else 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, @@ -430,19 +520,24 @@ PathItem.inject(new function() { // contour to traverse, stay on the same contour. if (nextSeg._visited && seg._path !== nextSeg._path || !operator(nextSeg._winding)) { - dir = 1; + drawSegment(nextSeg, 'not suitable ' + signature + ', old dir: ' + oldDir, i, 'orange'); + dir = 1; // TODO: oldDir? } else { // Switch to the intersection segment. seg._visited = interSeg._visited; seg = interSeg; + drawSegment(seg, 'switch ' + signature, i, 'green'); if (nextSeg._visited) dir = 1; } } else { + drawSegment(seg, 'no cross ' + signature, i, 'blue'); dir = 1; } } handleOut = dir > 0 ? seg._handleOut : seg._handleIn; + } else { + drawSegment(seg, 'keep', i, 'black'); } // Add the current segment to the path, and mark the added // segment as visited. @@ -459,18 +554,17 @@ PathItem.inject(new function() { if (seg && (seg === startSeg || seg === startInterSeg)) { path.firstSegment.setHandleIn((seg === startInterSeg ? startInterSeg : seg)._handleIn); - path.setClosed(true); } else { path.lastSegment._handleOut.set(0, 0); + console.error('Boolean operation results in open path!'); } + path.setClosed(true); // 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 - // - Open paths need at least two segments - if (path._segments.length > - (path._closed ? path.isLinear() ? 2 : 0 : 1)) + if (path._segments.length > path.isLinear() ? 2 : 0) paths.push(path); } return paths;