mirror of
https://github.com/scratchfoundation/paper.js.git
synced 2025-03-15 17:29:52 -04:00
Refactor and improve handling of boolean operators.
Also detect a case where all encountered segments are part of overlaps, and add parameter startInOverlaps that handles this situation. Closes #870
This commit is contained in:
parent
7bb102e218
commit
4a10fe33d3
1 changed files with 55 additions and 62 deletions
|
@ -27,21 +27,10 @@
|
||||||
*/
|
*/
|
||||||
PathItem.inject(new function() {
|
PathItem.inject(new function() {
|
||||||
var operators = {
|
var operators = {
|
||||||
unite: function(w) {
|
unite: { 0: true, 1: true },
|
||||||
return w === 1 || w === 0;
|
intersect: { 2: true },
|
||||||
},
|
subtract: { 1: true },
|
||||||
|
exclude: { 1 : true }
|
||||||
intersect: function(w) {
|
|
||||||
return w === 2;
|
|
||||||
},
|
|
||||||
|
|
||||||
subtract: function(w) {
|
|
||||||
return w === 1;
|
|
||||||
},
|
|
||||||
|
|
||||||
exclude: function(w) {
|
|
||||||
return w === 1;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -84,10 +73,17 @@ PathItem.inject(new function() {
|
||||||
// Note that the result paths might not belong to the same type
|
// Note that the result paths might not belong to the same type
|
||||||
// i.e. subtraction(A:Path, B:Path):CompoundPath etc.
|
// i.e. subtraction(A:Path, B:Path):CompoundPath etc.
|
||||||
var _path1 = preparePath(path1, true),
|
var _path1 = preparePath(path1, true),
|
||||||
_path2 = path2 && path1 !== path2 && preparePath(path2, true);
|
_path2 = path2 && path1 !== path2 && preparePath(path2, true),
|
||||||
|
// Retrieve the operator lookup table to decide if a given winding
|
||||||
|
// number is to be considered part of the solution.
|
||||||
|
lookup = operators[operation],
|
||||||
|
// Create the operator structure, holding the lookup table and a
|
||||||
|
// simple boolean check for an operation, e.g. `if (operator.unite)`
|
||||||
|
operator = { lookup: lookup };
|
||||||
|
operator[operation] = true;
|
||||||
// Give both paths the same orientation except for subtraction
|
// Give both paths the same orientation except for subtraction
|
||||||
// and exclusion, where we need them at opposite orientation.
|
// and exclusion, where we need them at opposite orientation.
|
||||||
if (_path2 && /^(subtract|exclude)$/.test(operation)
|
if (_path2 && (operator.subtract || operator.exclude)
|
||||||
^ (_path2.isClockwise() !== _path1.isClockwise()))
|
^ (_path2.isClockwise() !== _path1.isClockwise()))
|
||||||
_path2.reverse();
|
_path2.reverse();
|
||||||
// Split curves at crossings on both paths. Note that for self-
|
// Split curves at crossings on both paths. Note that for self-
|
||||||
|
@ -99,7 +95,8 @@ PathItem.inject(new function() {
|
||||||
|
|
||||||
var segments = [],
|
var segments = [],
|
||||||
// Aggregate of all curves in both operands, monotonic in y
|
// Aggregate of all curves in both operands, monotonic in y
|
||||||
monoCurves = [];
|
monoCurves = [],
|
||||||
|
startInOverlaps = true;
|
||||||
|
|
||||||
function collect(paths) {
|
function collect(paths) {
|
||||||
for (var i = 0, l = paths.length; i < l; i++) {
|
for (var i = 0, l = paths.length; i < l; i++) {
|
||||||
|
@ -119,36 +116,42 @@ PathItem.inject(new function() {
|
||||||
// all crossings:
|
// all crossings:
|
||||||
for (var i = 0, l = crossings.length; i < l; i++) {
|
for (var i = 0, l = crossings.length; i < l; i++) {
|
||||||
propagateWinding(crossings[i]._segment, _path1, _path2, monoCurves,
|
propagateWinding(crossings[i]._segment, _path1, _path2, monoCurves,
|
||||||
operation);
|
operator);
|
||||||
}
|
}
|
||||||
// Now process the segments that are not part of any intersecting chains
|
// Now process the segments that are not part of any intersecting chains
|
||||||
for (var i = 0, l = segments.length; i < l; i++) {
|
for (var i = 0, l = segments.length; i < l; i++) {
|
||||||
var segment = segments[i];
|
var segment = segments[i],
|
||||||
|
inter = segment._intersection;
|
||||||
if (segment._winding == null) {
|
if (segment._winding == null) {
|
||||||
propagateWinding(segment, _path1, _path2, monoCurves,
|
propagateWinding(segment, _path1, _path2, monoCurves, operator);
|
||||||
operation);
|
|
||||||
}
|
}
|
||||||
|
// If there are any valid segments that are not part of overlaps,
|
||||||
|
// prefer these to start tracing boolean paths from. But if all
|
||||||
|
// segments are part of overlaps, we need to start there.
|
||||||
|
if (!(inter && inter.isOverlap()) && lookup[segment._winding])
|
||||||
|
startInOverlaps = false;
|
||||||
}
|
}
|
||||||
return finishBoolean(CompoundPath, tracePaths(segments, operation),
|
return finishBoolean(CompoundPath,
|
||||||
|
tracePaths(segments, operator, startInOverlaps),
|
||||||
path1, path2, true);
|
path1, path2, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function computeOpenBoolean(path1, path2, operation) {
|
function computeOpenBoolean(path1, path2, operator) {
|
||||||
// Only support subtract and intersect operations between an open
|
// Only support subtract and intersect operations between an open
|
||||||
// and a closed path. Assume that compound-paths are closed.
|
// and a closed path. Assume that compound-paths are closed.
|
||||||
// TODO: Should we complain about not supported operations?
|
// TODO: Should we complain about not supported operations?
|
||||||
if (!path2 || !path2._children && !path2._closed
|
if (!path2 || !path2._children && !path2._closed
|
||||||
|| !/^(subtract|intersect)$/.test(operation))
|
|| !operator.subtract && !operator.intersect)
|
||||||
return null;
|
return null;
|
||||||
var _path1 = preparePath(path1, false),
|
var _path1 = preparePath(path1, false),
|
||||||
_path2 = preparePath(path2, false),
|
_path2 = preparePath(path2, false),
|
||||||
crossings = _path1.getCrossings(_path2, true),
|
crossings = _path1.getCrossings(_path2, true),
|
||||||
sub = operation === 'subtract',
|
sub = operator.subtract,
|
||||||
paths = [];
|
paths = [];
|
||||||
|
|
||||||
function addPath(path) {
|
function addPath(path) {
|
||||||
// Simple see if the point halfway across the open path is inside
|
// Simple see if the point halfway across the open path is inside
|
||||||
// path2, and include / exclude the path based on the operation.
|
// 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)) ^ sub) {
|
||||||
paths.unshift(path);
|
paths.unshift(path);
|
||||||
return true;
|
return true;
|
||||||
|
@ -449,7 +452,7 @@ PathItem.inject(new function() {
|
||||||
return Math.max(abs(windLeft), abs(windRight));
|
return Math.max(abs(windLeft), abs(windRight));
|
||||||
}
|
}
|
||||||
|
|
||||||
function propagateWinding(segment, path1, path2, monoCurves, operation) {
|
function propagateWinding(segment, path1, path2, monoCurves, operator) {
|
||||||
// Here we try to determine the most probable winding number
|
// Here we try to determine the most probable winding number
|
||||||
// contribution for the curve-chain starting with this segment. Once we
|
// contribution for the curve-chain starting with this segment. Once we
|
||||||
// have enough confidence in the winding contribution, we can propagate
|
// have enough confidence in the winding contribution, we can propagate
|
||||||
|
@ -494,7 +497,7 @@ PathItem.inject(new function() {
|
||||||
// While subtracting, we need to omit this curve if it is
|
// While subtracting, we need to omit this curve if it is
|
||||||
// contributing to the second operand and is outside the
|
// contributing to the second operand and is outside the
|
||||||
// first operand.
|
// first operand.
|
||||||
windingSum += operation === 'subtract' && path2
|
windingSum += operator.subtract && path2
|
||||||
&& (path === path1 && path2._getWinding(pt, hor)
|
&& (path === path1 && path2._getWinding(pt, hor)
|
||||||
|| path === path2 && !path1._getWinding(pt, hor))
|
|| path === path2 && !path1._getWinding(pt, hor))
|
||||||
? 0
|
? 0
|
||||||
|
@ -522,24 +525,14 @@ PathItem.inject(new function() {
|
||||||
* not
|
* not
|
||||||
* @return {Path[]} the contours traced
|
* @return {Path[]} the contours traced
|
||||||
*/
|
*/
|
||||||
function tracePaths(segments, operation) {
|
function tracePaths(segments, operator, startInOverlaps) {
|
||||||
var paths = [],
|
var paths = [],
|
||||||
start,
|
start,
|
||||||
otherStart,
|
otherStart;
|
||||||
operator = operators[operation],
|
|
||||||
// Adjust winding contributions for unite operation on overlaps:
|
|
||||||
overlapWinding = operation === 'unite' && { 1: 2 };
|
|
||||||
|
|
||||||
function isValid(seg, adjusted) {
|
function isValid(seg) {
|
||||||
if (seg._visited)
|
return !seg._visited
|
||||||
return false;
|
&& (!operator || operator.lookup[seg._winding]);
|
||||||
if (!operator) // For self-intersection, we're always valid!
|
|
||||||
return true;
|
|
||||||
var winding = seg._winding,
|
|
||||||
inter = seg._intersection;
|
|
||||||
if (inter && adjusted && overlapWinding && inter.isOverlap())
|
|
||||||
winding = overlapWinding[winding] || winding;
|
|
||||||
return operator(winding);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isStart(seg) {
|
function isStart(seg) {
|
||||||
|
@ -563,18 +556,14 @@ PathItem.inject(new function() {
|
||||||
// passes, first with strict = true, then false:
|
// passes, first with strict = true, then false:
|
||||||
// In strict mode, the current and the next segment are both
|
// In strict mode, the current and the next segment are both
|
||||||
// checked for validity, and only the current one is allowed to
|
// checked for validity, and only the current one is allowed to
|
||||||
// be an overlap (passing true for unadjusted in isValid()).
|
// be an overlap.
|
||||||
// If this pass does not yield a result, the non-strict mode is
|
// If this pass does not yield a result, the non-strict mode is
|
||||||
// used, in which invalid current segments are tolerated, and
|
// used, in which invalid current segments are tolerated, and
|
||||||
// overlaps for the next segment are allowed as long as they are
|
// overlaps for the next segment are allowed.
|
||||||
// valid when not adjusted.
|
|
||||||
if (isStart(seg) || isStart(nextSeg)
|
if (isStart(seg) || isStart(nextSeg)
|
||||||
|| !seg._visited && !nextSeg._visited
|
|| !seg._visited && !nextSeg._visited
|
||||||
// Self-intersections (!operator) don't need isValid() calls
|
// Self-intersections (!operator) don't need isValid() calls
|
||||||
&& (!operator
|
&& (!operator
|
||||||
// Do not use the overlap-adjusted winding here since an
|
|
||||||
// overlap crossing might have brought us here, in which
|
|
||||||
// case isValid(seg) might be false.
|
|
||||||
|| (!strict || isValid(seg))
|
|| (!strict || isValid(seg))
|
||||||
// Do not consider nextSeg in strict mode if it is part
|
// Do not consider nextSeg in strict mode if it is part
|
||||||
// of an overlap, in order to give non-overlapping
|
// of an overlap, in order to give non-overlapping
|
||||||
|
@ -594,23 +583,26 @@ PathItem.inject(new function() {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var i = 0, l = segments.length; i < l; i++) {
|
for (var i = 0, l = segments.length; i < l; i++) {
|
||||||
var seg = segments[i],
|
var path = null,
|
||||||
path = null,
|
|
||||||
finished = false,
|
finished = false,
|
||||||
|
seg = segments[i],
|
||||||
|
inter = seg._intersection,
|
||||||
handleIn;
|
handleIn;
|
||||||
// Do not start a chain with already visited segments, and segments
|
// Do not start paths with invalid segments (segments that were
|
||||||
// that are not going to be part of the resulting operation.
|
// already visited, or that are not going to be part of the result).
|
||||||
if (!isValid(seg, true))
|
// Also don't start in overlaps, unless all segments are part of
|
||||||
|
// overlaps, in which case we have no other choice.
|
||||||
|
if (!isValid(seg) || !startInOverlaps
|
||||||
|
&& inter && seg._winding && inter.isOverlap())
|
||||||
continue;
|
continue;
|
||||||
start = otherStart = null;
|
start = otherStart = null;
|
||||||
while (!finished) {
|
while (true) {
|
||||||
var inter = seg._intersection;
|
|
||||||
handleIn = path && seg._handleIn;
|
handleIn = path && seg._handleIn;
|
||||||
// Once we started a chain, see if there are multiple
|
// For each segment we encounter, see if there are multiple
|
||||||
// intersections, and if so, pick the best one:
|
// intersections, and if so, pick the best one:
|
||||||
inter = inter && (findBestIntersection(inter, true)
|
inter = inter && (findBestIntersection(inter, true)
|
||||||
|| findBestIntersection(inter, false)) || inter;
|
|| findBestIntersection(inter, false)) || inter;
|
||||||
// Get a reference to the other segment on the intersection.
|
// Get the reference to the other segment on the intersection.
|
||||||
var other = inter && inter._segment;
|
var other = inter && inter._segment;
|
||||||
if (isStart(seg)) {
|
if (isStart(seg)) {
|
||||||
finished = true;
|
finished = true;
|
||||||
|
@ -624,7 +616,7 @@ PathItem.inject(new function() {
|
||||||
// the boolean result, switch over.
|
// the boolean result, switch over.
|
||||||
// We need to mark overlap segments as visited when
|
// We need to mark overlap segments as visited when
|
||||||
// processing intersection.
|
// processing intersection.
|
||||||
if (inter.isOverlap() && operation === 'intersect')
|
if (operator && operator.intersect && inter.isOverlap())
|
||||||
seg._visited = true;
|
seg._visited = true;
|
||||||
seg = other;
|
seg = other;
|
||||||
}
|
}
|
||||||
|
@ -645,10 +637,11 @@ PathItem.inject(new function() {
|
||||||
path.add(new Segment(seg._point, handleIn, seg._handleOut));
|
path.add(new Segment(seg._point, handleIn, seg._handleOut));
|
||||||
seg._visited = true;
|
seg._visited = true;
|
||||||
seg = seg.getNext();
|
seg = seg.getNext();
|
||||||
|
inter = seg._intersection;
|
||||||
}
|
}
|
||||||
// Finish with closing the paths if necessary, correctly linking up
|
|
||||||
// curves etc.
|
|
||||||
if (finished) {
|
if (finished) {
|
||||||
|
// Finish with closing the paths, and carrying over the last
|
||||||
|
// handleIn to the first segment.
|
||||||
path.firstSegment.setHandleIn(handleIn);
|
path.firstSegment.setHandleIn(handleIn);
|
||||||
path.setClosed(true);
|
path.setClosed(true);
|
||||||
} else if (path) {
|
} else if (path) {
|
||||||
|
|
Loading…
Reference in a new issue