Merge boolean debug code into latest state from develop branch.

This commit is contained in:
Jürg Lehni 2017-03-18 13:46:25 +01:00
parent 993afe3c83
commit 84f3e96783
3 changed files with 244 additions and 1 deletions

View file

@ -1747,6 +1747,8 @@ new function() { // Scope for bezier intersection using fat-line clipping
// Link the two locations to each other.
loc1._intersection = loc2;
loc2._intersection = loc1;
// NOTE: Only required for boolean-debug branch.
loc2._other = true;
if (!include || include(loc1)) {
CurveLocation.insert(locations, loc1, true);
}

View file

@ -39,6 +39,8 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{
* @param {Point} [point]
*/
initialize: function CurveLocation(curve, time, point, _overlap, _distance) {
// NOTE: Only required for boolean-debug branch.
this._id = UID.get(CurveLocation);
// Merge intersections very close to the end of a curve with the
// beginning of the next curve.
if (time >= /*#=*/(1 - Numerical.CURVETIME_EPSILON)) {

View file

@ -78,7 +78,27 @@ PathItem.inject(new function() {
return result;
}
var scaleFactor = 1;
var textAngle = 0;
var fontSize = 5;
var segmentOffset;
var pathCount;
function initializeReporting() {
scaleFactor = Base.pick(window.scaleFactor, scaleFactor);
textAngle = Base.pick(window.textAngle, 0);
segmentOffset = {};
}
function computeBoolean(path1, path2, operation, options) {
initializeReporting();
var reportSegments = window.reportSegments;
var reportWindings = window.reportWindings;
var reportIntersections = window.reportIntersections;
window.reportSegments = false;
window.reportWindings = false;
window.reportIntersections = false;
// Only support subtract and intersect operations when computing stroke
// based boolean operations.
if (options && options.stroke &&
@ -95,6 +115,9 @@ PathItem.inject(new function() {
// Add a simple boolean property to check for a given operation,
// e.g. `if (operator.unite)`
operator[operation] = true;
window.reportSegments = reportSegments;
window.reportWindings = reportWindings;
window.reportIntersections = reportIntersections;
// Give both paths the same orientation except for subtraction
// and exclusion, where we need them at opposite orientation.
if (_path2 && (operator.subtract || operator.exclude)
@ -195,6 +218,19 @@ PathItem.inject(new function() {
return createResult(Group, paths, false, path1, path2);
}
function logIntersection(inter) {
var other = inter._intersection;
var log = ['Intersection', inter._id, 'id', inter.getPath()._id,
'i', inter.getIndex(), 't', inter.getParameter(),
'o', inter.hasOverlap(), 'p', inter.getPoint(),
'Other', other._id, 'id', other.getPath()._id,
'i', other.getIndex(), 't', other.getParameter(),
'o', other.hasOverlap(), 'p', other.getPoint()];
console.log(log.map(function(v) {
return v == null ? '-' : v;
}).join(' '));
}
/*
* Creates linked lists between intersections through their _next and _prev
* properties.
@ -323,7 +359,23 @@ PathItem.inject(new function() {
* were divided
* @private
*/
function divideLocations(locations, include, clearLater) {
if (window.reportIntersections) {
console.log('Crossings', locations.length / 2);
locations.forEach(function(inter) {
if (inter._other)
return;
logIntersection(inter);
new Path.Circle({
center: inter.point,
radius: 2 * scaleFactor,
strokeColor: 'red',
strokeScaling: false
});
});
}
var results = include && [],
tMin = /*#=*/Numerical.CURVETIME_EPSILON,
tMax = 1 - tMin,
@ -433,6 +485,16 @@ PathItem.inject(new function() {
segment._intersection = dest;
}
}
if (window.reportIntersections) {
console.log('Split Crossings');
locations.forEach(function(inter) {
if (!inter._other) {
logIntersection(inter);
}
});
}
// Clear curve handles right away if we're not storing them for later.
if (!clearLater)
clearCurveHandles(clearCurves);
@ -759,6 +821,89 @@ PathItem.inject(new function() {
* @return {Path[]} the traced closed paths
*/
function tracePaths(segments, operator) {
pathCount = 1;
function labelSegment(seg, label, windingOnly) {
var path = seg._path,
inter = seg._intersection,
other = inter && inter._segment,
nx1 = inter && inter._next,
nx2 = nx1 && nx1._next,
nx3 = nx2 && nx2._next,
intersections = {
'ix': inter,
'nx¹': nx1,
'nx²': nx2,
'nx³': nx3
};
if (windingOnly) {
label += (seg._winding && seg._winding.winding);
} else {
label += ' id: ' + path._id + '.' + seg._index
+ (other ? ' -> ' + other._path._id + '.' + other._index
: '')
+ ' vi: ' + (seg._visited ? 1 : 0)
+ ' pt: ' + seg._point
+ ' vd: ' + (isValid(seg) || starts && isStart(seg))
+ ' ov: ' + !!(inter && inter.hasOverlap())
+ ' wi: ' + (seg._winding && seg._winding.winding);
for (var key in intersections) {
var ix = intersections[key],
s = ix && ix._segment;
if (s) {
label += ' ' + key + ': ' + s._path._id + '.' + s._index
+ '(' + ix._id + ')';
}
}
label += ' | ' + path._validOverlapsOnly + ', ' + path._overlapsOnly;
}
var item = path._parent instanceof CompoundPath
? path._parent : path,
color = item.strokeColor || item.fillColor || 'black',
point = seg.point,
key = Math.round(point.x / scaleFactor)
+ ',' + Math.round(point.y / scaleFactor),
offset = segmentOffset[key] || 0,
size = fontSize * scaleFactor,
text = new PointText({
point: point.add(new Point(size, size / 2)
.add(0, offset * size * 1.2)
.rotate(textAngle)),
content: label,
justification: 'left',
fillColor: color,
fontSize: fontSize
});
segmentOffset[key] = offset + 1;
// TODO! PointText should have pivot in #point by default!
text.pivot = text.globalToLocal(text.point);
text.scale(scaleFactor);
text.rotate(textAngle);
new Path.Line({
from: text.point,
to: seg.point,
strokeColor: color,
strokeScaling: false
});
return text;
}
function drawSegment(seg, path, text, index) {
if (!window.reportSegments || window.reportFilter != null
&& pathCount != window.reportFilter)
return;
labelSegment(seg, '#' + pathCount + '.'
+ (path ? path._segments.length + 1 : 1)
+ ' (' + (index + 1) + '): ' + text);
}
if (window.reportWindings) {
for (var i = 0; i < segments.length; i++) {
labelSegment(segments[i], '', true);
}
}
var paths = [],
starts;
@ -808,6 +953,26 @@ PathItem.inject(new function() {
path = other._path,
next = other.getNext() || path && path.getFirstSegment(),
nextInter = next && next._intersection;
if (window.reportSegments && other !== segment) {
console.log('getIntersection()'
+ ', other: ' + other._path._id + '.' + other._index
+ ', next: ' + next._path._id + '.'
+ next._index
+ ', seg vis:' + !!other._visited
+ ', next vis:' + !!next._visited
+ ', next start:' + isStart(next)
+ ', seg wi:' + (other._winding && other._winding.winding)
+ ', next wi:' + (next._winding && next._winding.winding)
+ ', other vd:' + (isValid(other) || isStart(other))
+ ', next vd:' + (
(isValid(next) || isStart(next))
|| nextInter && isValid(nextInter._segment))
+ ', other ov: ' + !!(other._intersection
&& other._intersection.hasOverlap())
+ ', next ov: ' + !!(next._intersection
&& next._intersection.hasOverlap())
+ ', more: ' + (!!inter._next));
}
// See if this segment and the next are both not visited
// yet, or are bringing us back to the beginning, and are
// both valid, meaning they are part of the boolean result.
@ -893,18 +1058,35 @@ PathItem.inject(new function() {
while (valid) {
// For each segment we encounter, see if there are multiple
// crossings, and if so, pick the best one:
if (window.reportSegments && seg._intersection) {
var inter = seg._intersection;
console.log('-----\n'
+ '#' + pathCount + '.'
+ (path ? path._segments.length + 1 : 1)
+ ': Before getIntersections()'
+ ', seg: ' + seg._path._id + '.' + seg._index
+ ', other: ' + inter._segment._path._id + '.'
+ inter._segment._index);
}
var first = !path,
crossings = getCrossingSegments(seg, first),
// Get the other segment of the first found crossing.
other = crossings.shift(),
finished = !first && (isStart(seg) || isStart(other)),
cross = !finished && other;
if (window.reportSegments && inter) {
console.log('After findBestIntersection()'
+ ', seg: ' + seg._path._id + '.' + seg._index
+ ', other: ' + inter._segment._path._id + '.'
+ inter._segment._index);
}
if (first) {
path = new Path(Item.NO_INSERT);
// Clear branch to start a new one with each new path.
branch = null;
}
if (finished) {
drawSegment(seg, path, 'done', i);
// If we end up on the first or last segment of an operand,
// copy over its closed state, to support mixed open/closed
// scenarios as described in #1036
@ -931,16 +1113,27 @@ PathItem.inject(new function() {
handleIn: handleIn
};
}
if (cross)
if (cross) {
drawSegment(seg, path, 'cross', i);
seg = other;
}
// If an invalid segment is encountered, go back to the last
// crossing and try other possible crossings, as well as not
// crossing at the branch's root.
if (!isValid(seg)) {
// We didn't manage to switch, so stop right here.
console.info('Invalid segment encountered #'
+ pathCount + '.'
+ (path ? path._segments.length + 1 : 1)
+ ', id: ' + seg._path._id + '.' + seg._index
+ ', multiple: ' + !!(inter && inter._next));
drawSegment(seg, path, 'invalid', i);
// Remove the already added segments, and mark them as not
// visited so they become available again as options.
path.removeSegments(branch.start);
for (var j = 0, k = visited.length; j < k; j++) {
var s = visited[j];
console.log('Unvisit ' + s._path._id + '.' + s._index);
visited[j]._visited = false;
}
visited.length = 0;
@ -957,12 +1150,18 @@ PathItem.inject(new function() {
if (branch) {
visited = branch.visited;
handleIn = branch.handleIn;
console.info('Trying new branch', branches.length);
} else {
console.info('Boolean Operations run out of branches.');
}
}
} while (branch && !isValid(seg));
if (!seg)
break;
}
if (!cross) {
drawSegment(seg, path, 'add', i);
}
// Add the segment to the path, and mark it as visited.
// But first we need to look ahead. If we encounter the end of
// an open path, we need to treat it the same way as the fill of
@ -971,6 +1170,11 @@ PathItem.inject(new function() {
var next = seg.getNext();
path.add(new Segment(seg._point, handleIn,
next && seg._handleOut));
if (window.reportSegments) {
console.log('#' + pathCount + '.' + path._segments.length
+ ': Added', seg._path._id + '.' + seg._index
+ ': ' + path.lastSegment);
}
seg._visited = true;
visited.push(seg);
// If this is the end of an open path, go back to its first
@ -987,6 +1191,40 @@ PathItem.inject(new function() {
// Only add finished paths that cover an area to the result.
if (path.getArea() !== 0) {
paths.push(path);
if (window.reportSegments) {
console.log('#' + pathCount + '.'
+ (path._segments.length + 1)
+ ': Boolean operation completed');
}
}
} else if (path) {
// Only complain about open paths if they would actually contain
// an area when closed. Open paths that can silently discarded
// can occur due to epsilons, e.g. when two segments are so
// 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.
var colors = ['cyan', 'green', 'orange', 'yellow'];
var color = new Color(
colors[pathCount % (colors.length - 1)]);
console.error('%cBoolean operation results in open path',
'background: ' + color.toCSS() + '; color: #fff;',
'segments =', path._segments.length,
'length =', path.getLength(),
'area=', area,
'#' + pathCount + '.' +
(path ? path._segments.length + 1 : 1));
if (window.reportTraces) {
paper.project.activeLayer.addChild(path);
color.alpha = 0.5;
path.strokeColor = color;
path.strokeWidth = 3;
path.strokeScaling = false;
}
}
}
}
@ -1111,6 +1349,7 @@ PathItem.inject(new function() {
* @return {PahtItem} the resulting path item
*/
resolveCrossings: function() {
initializeReporting();
var children = this._children,
// Support both path and compound-path items
paths = children || [this];