mirror of
https://github.com/scratchfoundation/paper.js.git
synced 2025-01-03 19:45:44 -05:00
Boolean: More work on new branching code.
It now checks all intersections when there are multiple options first.
This commit is contained in:
parent
f4f4b3472f
commit
78c5c27bb3
1 changed files with 90 additions and 105 deletions
|
@ -647,23 +647,26 @@ PathItem.inject(new function() {
|
||||||
*/
|
*/
|
||||||
function tracePaths(segments, operator) {
|
function tracePaths(segments, operator) {
|
||||||
var paths = [],
|
var paths = [],
|
||||||
start,
|
starts;
|
||||||
otherStart;
|
|
||||||
|
|
||||||
function isValid(seg, excludeContour) {
|
function isValid(seg) {
|
||||||
// Unite operations need special handling of segments with a winding
|
|
||||||
// contribution of two (part of both involved areas) but which are
|
|
||||||
// also part of the contour of the result. Such segments are not
|
|
||||||
// chosen as the start of new paths and are not always counted as a
|
|
||||||
// valid next step, as controlled by the excludeContour parameter.
|
|
||||||
var winding;
|
var winding;
|
||||||
return !!(seg && !seg._visited && (!operator
|
return !!(seg && !seg._visited && (!operator
|
||||||
|| operator[(winding = seg._winding).winding]
|
|| operator[(winding = seg._winding || {}).winding]
|
||||||
|| !excludeContour && operator.unite && winding.onContour));
|
// Unite operations need special handling of segments with a
|
||||||
|
// winding contribution of two (part of both involved areas)
|
||||||
|
// but which are also part of the contour of the result.
|
||||||
|
|| operator.unite && winding.onContour));
|
||||||
}
|
}
|
||||||
|
|
||||||
function isStart(seg) {
|
function isStart(seg) {
|
||||||
return seg === start || seg === otherStart;
|
if (seg) {
|
||||||
|
for (var i = 0, l = starts.length; i < l; i++) {
|
||||||
|
if (seg === starts[i])
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function visitPath(path) {
|
function visitPath(path) {
|
||||||
|
@ -673,32 +676,47 @@ PathItem.inject(new function() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there are multiple possible intersections, find the one that's
|
// If there are multiple possible intersections, find the ones that's
|
||||||
// either connecting back to start or is not visited yet, and will be
|
// either connecting back to start or are not visited yet, and will be
|
||||||
// part of the boolean result:
|
// part of the boolean result:
|
||||||
function findBestIntersection(segment) {
|
function getIntersections(segment, collectStarts) {
|
||||||
var inter = segment._intersection,
|
var inter = segment._intersection,
|
||||||
start = inter;
|
start = inter,
|
||||||
while (inter) {
|
inters = [];
|
||||||
var other = inter._segment,
|
if (collectStarts)
|
||||||
next = other.getNext(),
|
starts = [segment];
|
||||||
nextInter = next && next._intersection;
|
|
||||||
// See if this segment and the next are both not visited yet, or
|
function collect(inter, end) {
|
||||||
// are bringing us back to the beginning, and are both valid,
|
while (inter && inter !== end) {
|
||||||
// meaning they are part of the boolean result.
|
var other = inter._segment,
|
||||||
if (other !== segment && (isStart(other) || isStart(next)
|
path = other._path,
|
||||||
|| next && !other._visited && !next._visited
|
next = other.getNext() || path && path.getFirstSegment(),
|
||||||
// Self-intersections (!operator) don't need isValid() calls
|
nextInter = next && next._intersection;
|
||||||
&& (!operator || isValid(other) && (isValid(next)
|
// See if this segment and the next are both not visited
|
||||||
// If the next segment isn't valid, its intersection
|
// yet, or are bringing us back to the beginning, and are
|
||||||
// to which we may switch might be, so check that.
|
// both valid, meaning they are part of the boolean result.
|
||||||
|| nextInter && isValid(nextInter._segment)))
|
if (other !== segment && (isStart(other) || isStart(next)
|
||||||
))
|
|| next && (isValid(other) && (isValid(next)
|
||||||
break;
|
// If the next segment isn't valid, its intersection
|
||||||
// If it's no match, continue with the next linked intersection.
|
// to which we may switch might be, so check that.
|
||||||
inter = inter._next;
|
|| nextInter && isValid(nextInter._segment))))) {
|
||||||
|
inters.push(inter);
|
||||||
|
}
|
||||||
|
if (collectStarts)
|
||||||
|
starts.push(other);
|
||||||
|
inter = inter._next;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return inter || start;
|
|
||||||
|
if (inter) {
|
||||||
|
collect(inter);
|
||||||
|
// Find the beginning of the linked intersections and loop all
|
||||||
|
// the way back to start, to collect all valid intersections.
|
||||||
|
while (inter && inter._prev)
|
||||||
|
inter = inter._prev;
|
||||||
|
collect(inter, start);
|
||||||
|
}
|
||||||
|
return inters;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort segments to give non-ambiguous segments the preference as
|
// Sort segments to give non-ambiguous segments the preference as
|
||||||
|
@ -728,6 +746,7 @@ 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 seg = segments[i],
|
||||||
|
valid = isValid(seg),
|
||||||
path = null,
|
path = null,
|
||||||
finished = false,
|
finished = false,
|
||||||
closed = true,
|
closed = true,
|
||||||
|
@ -737,7 +756,7 @@ PathItem.inject(new function() {
|
||||||
handleIn;
|
handleIn;
|
||||||
// If all encountered segments in a path are overlaps, we may have
|
// If all encountered segments in a path are overlaps, we may have
|
||||||
// two fully overlapping paths that need special handling.
|
// two fully overlapping paths that need special handling.
|
||||||
if (!seg._visited && seg._path._overlapsOnly) {
|
if (valid && seg._path._overlapsOnly) {
|
||||||
// TODO: Don't we also need to check for multiple overlaps?
|
// TODO: Don't we also need to check for multiple overlaps?
|
||||||
var path1 = seg._path,
|
var path1 = seg._path,
|
||||||
path2 = seg._intersection._segment._path;
|
path2 = seg._intersection._segment._path;
|
||||||
|
@ -750,48 +769,30 @@ PathItem.inject(new function() {
|
||||||
// Now mark all involved segments as visited.
|
// Now mark all involved segments as visited.
|
||||||
visitPath(path1);
|
visitPath(path1);
|
||||||
visitPath(path2);
|
visitPath(path2);
|
||||||
|
valid = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Exclude three cases of invalid starting segments:
|
// Do not start with invalid segments (segments that were already
|
||||||
// - Do not start with invalid segments (segments that were already
|
// visited, or that are not going to be part of the result).
|
||||||
// visited, or that are not going to be part of the result).
|
while (valid) {
|
||||||
// - Do not start in segments that have an invalid winding
|
|
||||||
// contribution but are part of the contour (excludeContour=true).
|
|
||||||
// - Do not start in overlaps, unless all segments are part of
|
|
||||||
// overlaps, in which case we have no other choice.
|
|
||||||
if (!isValid(seg, true))
|
|
||||||
continue;
|
|
||||||
start = otherStart = null;
|
|
||||||
while (true) {
|
|
||||||
// For each segment we encounter, 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:
|
||||||
var inter = findBestIntersection(seg),
|
var first = !path,
|
||||||
|
intersections = getIntersections(seg, first),
|
||||||
|
inter = intersections.shift(),
|
||||||
// Get the other segment on the intersection.
|
// Get the other segment on the intersection.
|
||||||
other = inter && inter._segment,
|
other = inter && inter._segment,
|
||||||
first = !path,
|
finished = !first && (isStart(seg) || isStart(other)),
|
||||||
cross = false;
|
cross = !finished && other;
|
||||||
if (first) {
|
if (first)
|
||||||
path = new Path(Item.NO_INSERT);
|
path = new Path(Item.NO_INSERT);
|
||||||
start = seg;
|
|
||||||
otherStart = other;
|
|
||||||
}
|
|
||||||
finished = !first && isStart(seg);
|
|
||||||
if (!finished && other) {
|
|
||||||
finished = !first && isStart(other);
|
|
||||||
// Are we at the end or at a crossing and the other segment
|
|
||||||
// is part of the boolean result? If so, switch over.
|
|
||||||
cross = finished || isValid(other, isValid(seg, true));
|
|
||||||
// NOTE: We pass `true` for excludeContour here if the
|
|
||||||
// current segment is valid and not a contour segment.
|
|
||||||
// See isValid()/getWinding() for explanations.
|
|
||||||
}
|
|
||||||
if (finished) {
|
if (finished) {
|
||||||
seg._visited = true;
|
|
||||||
// If we end up on the first or last segment of an operand,
|
// If we end up on the first or last segment of an operand,
|
||||||
// copy over its closed state, to support mixed open/closed
|
// copy over its closed state, to support mixed open/closed
|
||||||
// scenarios as described in #1036
|
// scenarios as described in #1036
|
||||||
if (seg.isFirst() || seg.isLast())
|
if (seg.isFirst() || seg.isLast())
|
||||||
closed = seg._path._closed;
|
closed = seg._path._closed;
|
||||||
|
seg._visited = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (cross && branch) {
|
if (cross && branch) {
|
||||||
|
@ -804,8 +805,9 @@ PathItem.inject(new function() {
|
||||||
branch = {
|
branch = {
|
||||||
start: path._segments.length,
|
start: path._segments.length,
|
||||||
segment: seg,
|
segment: seg,
|
||||||
handleIn: handleIn,
|
intersections: intersections,
|
||||||
visited: visited = []
|
visited: visited = [],
|
||||||
|
handleIn: handleIn
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (cross)
|
if (cross)
|
||||||
|
@ -814,23 +816,27 @@ PathItem.inject(new function() {
|
||||||
// crossing and try the other direction by not crossing at the
|
// crossing and try the other direction by not crossing at the
|
||||||
// intersection.
|
// intersection.
|
||||||
if (!isValid(seg)) {
|
if (!isValid(seg)) {
|
||||||
// Remove the already added segments, and mark them as no
|
// Remove the already added segments, and mark them as not
|
||||||
// visited so they become available again as options.
|
// visited so they become available again as options.
|
||||||
path.removeSegments(branch.start);
|
path.removeSegments(branch.start);
|
||||||
for (var j = 0, k = visited.length; j < k; j++) {
|
for (var j = 0, k = visited.length; j < k; j++) {
|
||||||
visited[j]._visited = false;
|
visited[j]._visited = false;
|
||||||
}
|
}
|
||||||
// Go back to the segment at which the crossing happened,
|
// Go back to the segment at which the crossing happened,
|
||||||
// but don't cross this time.
|
// and try other crossings first.
|
||||||
seg = branch.segment;
|
if (inter = branch.intersections.shift()) {
|
||||||
|
seg = inter._segment;
|
||||||
|
visited.length = 0;
|
||||||
|
} else {
|
||||||
|
// If there are no crossings left, try not crossing:
|
||||||
|
// Restore the previous branch and keep adding to it,
|
||||||
|
// but stop once we run out of branches to try.
|
||||||
|
if (!(branch = branches.pop()) ||
|
||||||
|
!isValid(seg = branch.segment))
|
||||||
|
break;
|
||||||
|
visited = branch.visited;
|
||||||
|
}
|
||||||
handleIn = branch.handleIn;
|
handleIn = branch.handleIn;
|
||||||
visited = branch.visited;
|
|
||||||
// Now restore the previous branch and keep adding to it,
|
|
||||||
// since we don't cross here anymore.
|
|
||||||
branch = branches.pop();
|
|
||||||
// Stop once we run out of branches to try.
|
|
||||||
if (!branch)
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
// Add the segment to the path, and mark it as visited.
|
// Add the segment to the path, and mark it as visited.
|
||||||
// But first we need to look ahead. If we encounter the end of
|
// But first we need to look ahead. If we encounter the end of
|
||||||
|
@ -848,36 +854,15 @@ PathItem.inject(new function() {
|
||||||
handleIn = next && next._handleIn;
|
handleIn = next && next._handleIn;
|
||||||
}
|
}
|
||||||
if (finished) {
|
if (finished) {
|
||||||
// Finish with closing the paths, and carrying over the last
|
if (closed) {
|
||||||
// handleIn to the first segment.
|
// Carry over the last handleIn to the first segment.
|
||||||
path.firstSegment.setHandleIn(handleIn);
|
path.firstSegment.setHandleIn(handleIn);
|
||||||
path.setClosed(closed);
|
path.setClosed(closed);
|
||||||
} else if (path) {
|
}
|
||||||
// Only complain about open paths if they would actually contain
|
// Only add finished paths that cover an area to the result.
|
||||||
// an area when closed. Open paths that can silently discarded
|
if (path.getArea() !== 0) {
|
||||||
// can occur due to epsilons, e.g. when two segments are so
|
paths.push(path);
|
||||||
// close to each other that they are considered the same
|
|
||||||
// location, but the winding calculation still produces a valid
|
|
||||||
// number due to their slight differences producing a tiny area.
|
|
||||||
var area = path.getArea();
|
|
||||||
if (abs(area) >= /*#=*/Numerical.GEOMETRIC_EPSILON) {
|
|
||||||
// This path wasn't finished and is hence invalid.
|
|
||||||
// Report the error to the console for the time being.
|
|
||||||
console.error('Boolean operation resulted in open path',
|
|
||||||
'segments =', path._segments.length,
|
|
||||||
'length =', path.getLength(),
|
|
||||||
'area=', area);
|
|
||||||
}
|
}
|
||||||
path = null;
|
|
||||||
}
|
|
||||||
// Add the path to the result, while avoiding stray segments and
|
|
||||||
// paths that are incomplete or cover no area.
|
|
||||||
// As an optimization, only check paths with 8 or less segments
|
|
||||||
// for their area, and assume that they cover an area when more.
|
|
||||||
if (path && (path._segments.length > 8
|
|
||||||
|| !Numerical.isZero(path.getArea()))) {
|
|
||||||
paths.push(path);
|
|
||||||
path = null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return paths;
|
return paths;
|
||||||
|
|
Loading…
Reference in a new issue