/* * Paper.js - The Swiss Army Knife of Vector Graphics Scripting. * http://paperjs.org/ * * Copyright (c) 2011 - 2014, Juerg Lehni & Jonathan Puckey * http://scratchdisk.com/ & http://jonathanpuckey.com/ * * Distributed under the MIT license. See LICENSE file for details. * * All rights reserved. */ /* * 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 * - Paths are clones of each other that ovelap exactly on top of each other! * * @author Harikrishnan Gopalakrishnan * http://hkrish.com/playground/paperjs/booleanStudy.html */ PathItem.inject(/** @lends PathItem# */{ // 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. _computeBoolean: function(other, operator, subtract) { // To deal with a HTML5 canvas requirement where CompoundPaths' child // contours has to be of different winding direction for correctly // filling holes. But if some individual countours are disjoint, i.e. // islands, we have to reorient them so that: // - The holes have opposite winding direction, already handled by paper // - Islands have to have the same winding direction as the first child // NOTE: Does NOT handle self-intersecting CompoundPaths. function reorientPath(path) { if (path instanceof CompoundPath) { var children = path.removeChildren(), length = children.length, bounds = new Array(length), counters = new Array(length), clockwise; children.sort(function(a, b) { return b.getBounds().getArea() - a.getBounds().getArea(); }); path.addChildren(children); clockwise = children[0].isClockwise(); for (var i = 0; i < length; i++) { bounds[i] = children[i].getBounds(); counters[i] = 0; } for (var i = 0; i < length; i++) { for (var j = 1; j < length; j++) { if (i !== j && bounds[i].intersects(bounds[j])) counters[j]++; } // Omit the first child if (i > 0 && counters[i] % 2 === 0) children[i].setClockwise(clockwise); } } return path; } // We do not modify the operands themselves // The result might not belong to the same type // i.e. subtraction(A:Path, B:Path):CompoundPath etc. // We call reduce() on both cloned paths to simplify compound paths and // remove empty curves. We also apply matrices to both paths in case // they were transformed. var path1 = reorientPath(this.clone(false).reduce().applyMatrix()); path2 = this !== other && reorientPath(other.clone(false).reduce().applyMatrix()); // Do operator specific calculations before we begin // Make both paths at clockwise orientation, except when subtract = true // We need both paths at opposite orientation for subtraction. if (!path1.isClockwise()) path1.reverse(); if (path2 && !(subtract ^ path2.isClockwise())) path2.reverse(); // Split curves at intersections on both paths. PathItem._splitPath(path1.getIntersections(path2 || path1, true)); var chain = [], windings = [], lengths = [], segments = [], // Aggregate of all curves in both operands, monotonic in y monoCurves = []; function collect(paths) { for (var i = 0, l = paths.length; i < l; i++) { var path = paths[i]; segments.push.apply(segments, path._segments); monoCurves.push.apply(monoCurves, path._getMonoCurves()); } } // Collect all segments and monotonic curves collect(path1._children || [path1]); if (path2) 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 begining. segments.sort(function(a, b) { var _a = a._intersection, _b = b._intersection; return !_a && !_b || _a && _b ? 0 : _a ? -1 : 1; }); 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 = windings.length = lengths.length = 0; var totalLength = 0, startSeg = segment; do { chain.push(segment); lengths.push(totalLength += segment.getCurve().getLength()); segment = segment.getNext(); } while (segment && !segment._intersection && segment !== startSeg); // Select the median winding of three random points along this curve // chain, as a representative winding number. The random 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++) { var length = totalLength * Math.random(), amount = lengths.length; k = 0; do { if (lengths[k] >= length) { if (k > 0) length -= lengths[k - 1]; break; } } while (++k < amount); var curve = chain[k].getCurve(), point = curve.getPointAt(length), hor = curve.isHorizontal(), path = curve._path; if (path._parent instanceof CompoundPath) path = 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. windings[j] = subtract && path2 && (path === path1 && path2._getWinding(point, hor) || path === path2 && !path1._getWinding(point, hor)) ? 0 : PathItem._getWinding(point, monoCurves, hor); } windings.sort(); // Assign the median winding to the entire curve chain. var winding = windings[1]; for (var j = chain.length - 1; j >= 0; j--) chain[j]._winding = winding; } // Trace closed contours and insert them into the result. var result = new CompoundPath(); result.addChildren(PathItem._tracePaths(segments, operator), true); // Delete the proxies path1.remove(); if (path2) path2.remove(); // And then, we are done. return result.reduce(); }, /** * {@grouptitle Boolean Path Operations} * * Merges the geometry of the specified path from this path's * geometry and returns the result as a new path item. * * @param {PathItem} path the path to unite with * @return {PathItem} the resulting path item */ unite: function(path) { return this._computeBoolean(path, function(w) { return w === 1 || w === 0; }, false); }, /** * Intersects the geometry of the specified path with this path's * geometry and returns the result as a new path item. * * @param {PathItem} path the path to intersect with * @return {PathItem} the resulting path item */ intersect: function(path) { return this._computeBoolean(path, function(w) { return w === 2; }, false); }, /** * Subtracts the geometry of the specified path from this path's * geometry and returns the result as a new path item. * * @param {PathItem} path the path to subtract * @return {PathItem} the resulting path item */ subtract: function(path) { return this._computeBoolean(path, function(w) { return w === 1; }, true); }, // Compound boolean operators combine the basic boolean operations such // as union, intersection, subtract etc. /** * Excludes the intersection of the geometry of the specified path with * this path's geometry and returns the result as a new group item. * * @param {PathItem} path the path to exclude the intersection of * @return {Group} the resulting group item */ exclude: function(path) { return new Group([this.subtract(path), path.subtract(this)]); }, /** * Splits the geometry of this path along the geometry of the specified * path returns the result as a new group item. * * @param {PathItem} path the path to divide by * @return {Group} the resulting group item */ divide: function(path) { return new Group([this.subtract(path), this.intersect(path)]); } });