/* * 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(new function() { /** * 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 opposit winding direction (already handled by paper.js) * - 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; } function computeBoolean(path1, path2, operator, subtract) { // 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 selfOp = path1 === path2; path1 = reorientPath(path1.clone(false).reduce().applyMatrix()); path2 = selfOp ? path1 : reorientPath(path2.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 opposit orientation for subtraction if (!path1.isClockwise()) path1.reverse(); if (!selfOp && !(subtract ^ path2.isClockwise())) path2.reverse(); var chain = [], windings = [], lengths = [], paths = [], segments = [], // Aggregate of all curves in both operands, monotonic in y monoCurves = [], TOLERANCE = /*#=*/ Numerical.TOLERANCE, intersections = path1.getIntersections(path2, true); // Split curves at intersections on both paths. PathItem._splitPath(intersections); // Collect all sub paths and segments paths.push.apply(paths, path1._children || [path1]); if (!selfOp) paths.push.apply(paths, path2._children || [path2]); for (var i = 0, l = paths.length; i < l; i++) { segments.push.apply(segments, paths[i].getSegments()); monoCurves.push.apply(monoCurves, paths[i]._getMonoCurves()); } // 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 && (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 (!selfOp) path2.remove(); // And then, we are done. return result.reduce(); } // 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. return /** @lends PathItem# */{ /** * {@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 computeBoolean(this, 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 computeBoolean(this, 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 computeBoolean(this, 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)]); } }; });