mirror of
https://github.com/scratchfoundation/paper.js.git
synced 2025-01-07 13:22:07 -05:00
Start implementing support for touching and overlapping shapes in boolean operations.
Relates to #449, #450, #648, #719
This commit is contained in:
parent
edfabcbbd8
commit
85d21c84b8
3 changed files with 202 additions and 18 deletions
|
@ -1203,8 +1203,12 @@ new function() { // Scope for methods that require private functions
|
||||||
function addLocation(locations, include, curve1, t1, point1, curve2, t2,
|
function addLocation(locations, include, curve1, t1, point1, curve2, t2,
|
||||||
point2) {
|
point2) {
|
||||||
var loc = new CurveLocation(curve1, t1, point1, curve2, t2, point2);
|
var loc = new CurveLocation(curve1, t1, point1, curve2, t2, point2);
|
||||||
if (!include || include(loc))
|
if (!include || include(loc)) {
|
||||||
locations.push(loc);
|
locations.push(loc);
|
||||||
|
} else {
|
||||||
|
loc = null;
|
||||||
|
}
|
||||||
|
return loc;
|
||||||
}
|
}
|
||||||
|
|
||||||
function addCurveIntersections(v1, v2, curve1, curve2, locations, include,
|
function addCurveIntersections(v1, v2, curve1, curve2, locations, include,
|
||||||
|
@ -1476,11 +1480,86 @@ new function() { // Scope for methods that require private functions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Code to detect overlaps of intersecting curves by @iconexperience:
|
||||||
|
* https://github.com/paperjs/paper.js/issues/648
|
||||||
|
*/
|
||||||
|
function addOverlap(v1, v2, curve1, curve2, locations, include) {
|
||||||
|
var abs = Math.abs,
|
||||||
|
tolerance = Numerical.TOLERANCE,
|
||||||
|
isLinear = Curve.isLinear(v1) || Curve.isLinear(v2);
|
||||||
|
if (isLinear) {
|
||||||
|
// If one curve is linear, the other curve must be linear, too. Otherwise they cannot overlap.
|
||||||
|
// Linear curves can only overlap if they are collinear, which means they must be are parallel and
|
||||||
|
// any point of curve 1 must be on curve 2
|
||||||
|
if (!Curve.isLinear(v1) || !Curve.isLinear(v2) ||
|
||||||
|
abs((v1[0] - v1[6]) * (v2[1] - v2[7]) - (v1[1] - v1[7]) * (v2[0] - v2[6])) > tolerance ||
|
||||||
|
abs(Line.getSignedDistance(v2[0], v2[1], v2[6], v2[7], v1[0], v1[1], false)) > tolerance) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var v = [v1, v2],
|
||||||
|
matches = [];
|
||||||
|
// Iterate through all end points. First p1 and p2 of curve 1, then p1 and p2 of curve 2
|
||||||
|
for (var vIdx = 0, t1 = 0; vIdx < 2 && matches.length < 2; vIdx += t1 === 0 ? 0 : 1, t1 = t1 ^ 1) {
|
||||||
|
var t2 = Curve.getParameterOf(v[vIdx^1], v[vIdx][t1 === 0 ? 0 : 6], v[vIdx][t1 === 0 ? 1 : 7]);
|
||||||
|
if (t2 != null) { // if point is on curve
|
||||||
|
var match = vIdx === 0 ? [t1, t2] : [t2, t1];
|
||||||
|
if (matches.length === 1 && match[0] < matches[0][0]) {
|
||||||
|
matches.unshift(match);
|
||||||
|
} else if (matches.length === 0 || match[0] != matches[0][0] || match[1] != matches[0][1]) {
|
||||||
|
matches.push(match);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (vIdx === 1 && matches.length == 0) {
|
||||||
|
return false; // if we checked three points but found no match then curves cannot overlap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if we found two matches, the end points of v1 and v2 should be the same. We only have to check if the
|
||||||
|
// handles are the same, too.
|
||||||
|
var overlap = 1;
|
||||||
|
if (matches.length == 2) {
|
||||||
|
// create values for overlapping part of each curve
|
||||||
|
v[0] = Curve.getPart(v[0], matches[0][0], matches[1][0]);
|
||||||
|
v[1] = Curve.getPart(v[1], Math.min(matches[0][1], matches[1][1]), Math.max(matches[0][1], matches[1][1]));
|
||||||
|
// reverse values of second curve if necessary
|
||||||
|
if (abs(v[0][0] - v[1][6]) < tolerance && abs(v[0][1] - v[1][7]) < tolerance) {
|
||||||
|
overlap = -1;
|
||||||
|
v[1] = [v[1][6], v[1][7], v[1][4], v[1][5], v[1][2], v[1][3], v[1][0], v[1][1]];
|
||||||
|
}
|
||||||
|
// check if handles of overlapping paths are similar enough. We could do another check for curve identity
|
||||||
|
// here if we find a better criteria
|
||||||
|
if (isLinear ||
|
||||||
|
abs(v[0][2] - v[1][2]) < tolerance && abs(v[0][3] - v[1][3]) < tolerance &&
|
||||||
|
abs(v[0][4] - v[1][4]) < tolerance && abs(v[0][5] - v[1][5]) < tolerance) {
|
||||||
|
// overlapping parts are identical
|
||||||
|
var t1 = matches[0][0],
|
||||||
|
t2 = matches[0][1],
|
||||||
|
loc = addLocation(locations, include,
|
||||||
|
curve1, t1, Curve.getPoint(v1, t1),
|
||||||
|
curve2, t2, Curve.getPoint(v2, t2), true);
|
||||||
|
if (loc)
|
||||||
|
loc._overlap = overlap;
|
||||||
|
var t1 = matches[1][0],
|
||||||
|
t2 = matches[1][1];
|
||||||
|
loc = addLocation(locations, include,
|
||||||
|
curve1, t1, Curve.getPoint(v1, t1),
|
||||||
|
curve2, t2, Curve.getPoint(v2, t2), true);
|
||||||
|
if (loc)
|
||||||
|
loc._overlap = overlap;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return { statics: /** @lends Curve */{
|
return { statics: /** @lends Curve */{
|
||||||
// We need to provide the original left curve reference to the
|
// We need to provide the original left curve reference to the
|
||||||
// #getIntersections() calls as it is required to create the resulting
|
// #getIntersections() calls as it is required to create the resulting
|
||||||
// CurveLocation objects.
|
// CurveLocation objects.
|
||||||
getIntersections: function(v1, v2, c1, c2, locations, include) {
|
getIntersections: function(v1, v2, c1, c2, locations, include) {
|
||||||
|
if (addOverlap(v1, v2, c1, c2, locations, include))
|
||||||
|
return locations;
|
||||||
var linear1 = Curve.isLinear(v1),
|
var linear1 = Curve.isLinear(v1),
|
||||||
linear2 = Curve.isLinear(v2),
|
linear2 = Curve.isLinear(v2),
|
||||||
c1p1 = c1.getPoint1(),
|
c1p1 = c1.getPoint1(),
|
||||||
|
@ -1550,12 +1629,20 @@ new function() { // Scope for methods that require private functions
|
||||||
|
|
||||||
if (last > 0) {
|
if (last > 0) {
|
||||||
locations.sort(compare);
|
locations.sort(compare);
|
||||||
// Filter out duplicate locations.
|
// Filter out duplicate locations, but preserve _overlap setting
|
||||||
for (var i = last; i > 0; i--) {
|
// among all duplicated (only one of them will have it defined)
|
||||||
if (locations[i].equals(locations[i - 1])) {
|
var i = last,
|
||||||
locations.splice(i, 1);
|
loc = locations[i];
|
||||||
|
while(--i >= 0) {
|
||||||
|
var prev = locations[i];
|
||||||
|
if (prev.equals(loc)) {
|
||||||
|
var overlap = loc._overlap;
|
||||||
|
if (overlap)
|
||||||
|
prev._overlap = overlap;
|
||||||
|
locations.splice(i + 1, 1); // Remove loc
|
||||||
last--;
|
last--;
|
||||||
}
|
}
|
||||||
|
loc = prev;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (_expand) {
|
if (_expand) {
|
||||||
|
|
|
@ -214,7 +214,8 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{
|
||||||
// If we have the parameter on the other curve use that for
|
// If we have the parameter on the other curve use that for
|
||||||
// intersection rather than the point.
|
// intersection rather than the point.
|
||||||
this._intersection = intersection = new CurveLocation(this._curve2,
|
this._intersection = intersection = new CurveLocation(this._curve2,
|
||||||
this._parameter2, this._point2 || this._point, this);
|
this._parameter2, this._point2 || this._point);
|
||||||
|
intersection._overlap = this._overlap;
|
||||||
intersection._intersection = this;
|
intersection._intersection = this;
|
||||||
}
|
}
|
||||||
return intersection;
|
return intersection;
|
||||||
|
@ -246,6 +247,8 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{
|
||||||
*
|
*
|
||||||
* @type Number
|
* @type Number
|
||||||
* @bean
|
* @bean
|
||||||
|
* @see Curve#getNearestLocation(point)
|
||||||
|
* @see Path#getNearestLocation(point)
|
||||||
*/
|
*/
|
||||||
getDistance: function() {
|
getDistance: function() {
|
||||||
return this._distance;
|
return this._distance;
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
*
|
*
|
||||||
* Not supported yet
|
* Not supported yet
|
||||||
* - Boolean operations on self-intersecting Paths
|
* - Boolean operations on self-intersecting Paths
|
||||||
* - Paths are clones of each other that ovelap exactly on top of each other!
|
* - Paths are clones of each other that overlap exactly on top of each other!
|
||||||
*
|
*
|
||||||
* @author Harikrishnan Gopalakrishnan
|
* @author Harikrishnan Gopalakrishnan
|
||||||
* http://hkrish.com/playground/paperjs/booleanStudy.html
|
* http://hkrish.com/playground/paperjs/booleanStudy.html
|
||||||
|
@ -158,6 +158,18 @@ PathItem.inject(new function() {
|
||||||
|| path === _path2 && !_path1._getWinding(pt, hor))
|
|| path === _path2 && !_path1._getWinding(pt, hor))
|
||||||
? 0
|
? 0
|
||||||
: getWinding(pt, monoCurves, hor);
|
: getWinding(pt, monoCurves, hor);
|
||||||
|
/*
|
||||||
|
new Path.Circle({
|
||||||
|
center: pt,
|
||||||
|
radius: 3,
|
||||||
|
strokeColor: 'red'
|
||||||
|
});
|
||||||
|
new PointText({
|
||||||
|
point: pt,
|
||||||
|
content: getWinding(pt, monoCurves, hor),
|
||||||
|
fillColor: 'red'
|
||||||
|
});
|
||||||
|
*/
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
length -= curveLength;
|
length -= curveLength;
|
||||||
|
@ -165,15 +177,27 @@ PathItem.inject(new function() {
|
||||||
}
|
}
|
||||||
// Assign the average winding to the entire curve chain.
|
// Assign the average winding to the entire curve chain.
|
||||||
var winding = Math.round(windingSum / 3);
|
var winding = Math.round(windingSum / 3);
|
||||||
for (var j = chain.length - 1; j >= 0; j--)
|
for (var j = chain.length - 1; j >= 0; j--) {
|
||||||
chain[j].segment._winding = winding;
|
var seg = chain[j].segment,
|
||||||
|
inter = seg._intersection;
|
||||||
|
seg._winding = winding;
|
||||||
|
if (inter && inter._overlap && winding === 1)
|
||||||
|
seg._winding = 2;
|
||||||
|
/*
|
||||||
|
new PointText({
|
||||||
|
point: seg.point,
|
||||||
|
content: seg._winding,
|
||||||
|
fillColor: 'green'
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Trace closed contours and insert them into the result.
|
// Trace closed contours and insert them into the result.
|
||||||
var result = new CompoundPath(Item.NO_INSERT);
|
var result = new CompoundPath(Item.NO_INSERT);
|
||||||
result.insertAbove(path1);
|
|
||||||
result.addChildren(tracePaths(segments, operator), true);
|
result.addChildren(tracePaths(segments, operator), true);
|
||||||
// See if the CompoundPath can be reduced to just a simple Path.
|
// See if the CompoundPath can be reduced to just a simple Path.
|
||||||
result = result.reduce();
|
result = result.reduce();
|
||||||
|
result.insertAbove(path1);
|
||||||
// Copy over the left-hand item's style and we're done.
|
// Copy over the left-hand item's style and we're done.
|
||||||
// TODO: Consider using Item#_clone() for this, but find a way to not
|
// TODO: Consider using Item#_clone() for this, but find a way to not
|
||||||
// clone children / name (content).
|
// clone children / name (content).
|
||||||
|
@ -189,6 +213,7 @@ PathItem.inject(new function() {
|
||||||
* @param {CurveLocation[]} intersections Array of CurveLocation objects
|
* @param {CurveLocation[]} intersections Array of CurveLocation objects
|
||||||
*/
|
*/
|
||||||
function splitPath(intersections) {
|
function splitPath(intersections) {
|
||||||
|
// TODO: Make public in API, since useful!
|
||||||
var tMin = /*#=*/Numerical.TOLERANCE,
|
var tMin = /*#=*/Numerical.TOLERANCE,
|
||||||
tMax = 1 - tMin,
|
tMax = 1 - tMin,
|
||||||
isStraight = false,
|
isStraight = false,
|
||||||
|
@ -373,15 +398,66 @@ PathItem.inject(new function() {
|
||||||
* @return {Path[]} the contours traced
|
* @return {Path[]} the contours traced
|
||||||
*/
|
*/
|
||||||
function tracePaths(segments, operator, selfOp) {
|
function tracePaths(segments, operator, selfOp) {
|
||||||
|
var segmentCount = 0;
|
||||||
|
var segmentOffset = {};
|
||||||
|
|
||||||
|
function labelSegment(seg, text, color) {
|
||||||
|
var textAngle = 45;
|
||||||
|
var point = seg.point;
|
||||||
|
var key = Math.round(point.x * 1000) + ',' + Math.round(point.y * 1000);
|
||||||
|
var offset = segmentOffset[key] || 0;
|
||||||
|
segmentOffset[key] = offset + 1;
|
||||||
|
var text = new PointText({
|
||||||
|
point: point.add(new Point(8, 4).rotate(textAngle).add(0, offset * 14)),
|
||||||
|
content: text,
|
||||||
|
justification: 'left',
|
||||||
|
fillColor: color
|
||||||
|
});
|
||||||
|
text.pivot = text.globalToLocal(text.point);
|
||||||
|
text.rotation = textAngle;
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawSegment(seg, text, index, color) {
|
||||||
|
if (false)
|
||||||
|
return;
|
||||||
|
// return;
|
||||||
|
new Path.Circle({
|
||||||
|
center: seg.point,
|
||||||
|
radius: 3,
|
||||||
|
strokeColor: color
|
||||||
|
});
|
||||||
|
var inter = seg._intersection;
|
||||||
|
labelSegment(seg, (segmentCount++) + '/' + index + ': ' + text
|
||||||
|
+ ' v: ' + !!seg._visited
|
||||||
|
+ ' op: ' + operator(seg._winding)
|
||||||
|
+ ' o: ' + (inter ? inter._overlap : 0)
|
||||||
|
+ ' w: ' + seg._winding
|
||||||
|
, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < 0 && segments.length; i++) {
|
||||||
|
var seg = segments[i];
|
||||||
|
var point = seg.point;
|
||||||
|
var inter = seg._intersection;
|
||||||
|
labelSegment(seg, i
|
||||||
|
+ ' i: ' + !!inter
|
||||||
|
+ ' o: ' + (inter ? inter._overlap : 0)
|
||||||
|
+ ' w: ' + seg._winding
|
||||||
|
, 'green');
|
||||||
|
}
|
||||||
|
|
||||||
var paths = [];
|
var paths = [];
|
||||||
for (var i = 0, seg, startSeg, l = segments.length; i < l; i++) {
|
for (var i = 0, seg, startSeg, l = segments.length; i < l; i++) {
|
||||||
seg = startSeg = segments[i];
|
seg = startSeg = segments[i];
|
||||||
if (seg._visited || !operator(seg._winding))
|
if (seg._visited || !operator(seg._winding)) {
|
||||||
|
drawSegment(seg, 'ignore', i, 'red');
|
||||||
continue;
|
continue;
|
||||||
|
}
|
||||||
var path = new Path(Item.NO_INSERT),
|
var path = new Path(Item.NO_INSERT),
|
||||||
inter = seg._intersection,
|
inter = seg._intersection,
|
||||||
startInterSeg = inter && inter._segment,
|
startInterSeg = inter && inter._segment,
|
||||||
added = false, // Whether a first segment as added already
|
added = false, // Whether a first segment as added already
|
||||||
|
firstOverlap = true,
|
||||||
dir = 1;
|
dir = 1;
|
||||||
do {
|
do {
|
||||||
var handleIn = dir > 0 ? seg._handleIn : seg._handleOut,
|
var handleIn = dir > 0 ? seg._handleIn : seg._handleOut,
|
||||||
|
@ -404,7 +480,7 @@ PathItem.inject(new function() {
|
||||||
var c1 = seg.getCurve();
|
var c1 = seg.getCurve();
|
||||||
if (dir > 0)
|
if (dir > 0)
|
||||||
c1 = c1.getPrevious();
|
c1 = c1.getPrevious();
|
||||||
var t1 = c1.getTangentAt(dir < 1 ? 0 : 1, true),
|
var t1 = c1.getTangentAt(dir < 0 ? 0 : 1, true),
|
||||||
// Get both curves at the intersection (except the
|
// Get both curves at the intersection (except the
|
||||||
// entry curves).
|
// entry curves).
|
||||||
c4 = interSeg.getCurve(),
|
c4 = interSeg.getCurve(),
|
||||||
|
@ -417,7 +493,21 @@ PathItem.inject(new function() {
|
||||||
// the correct contour to traverse next.
|
// the correct contour to traverse next.
|
||||||
w3 = t1.cross(t3),
|
w3 = t1.cross(t3),
|
||||||
w4 = t1.cross(t4);
|
w4 = t1.cross(t4);
|
||||||
if (w3 * w4 !== 0) {
|
var signature = (w3 * w4).toPrecision(1) + ' (' + w3.toPrecision(1) + ' * ' + w4.toPrecision(1) + ')';
|
||||||
|
var overlap = inter._overlap;
|
||||||
|
if (overlap) {
|
||||||
|
// Switch to the overlapping intersection segment.
|
||||||
|
if (firstOverlap && overlap === 1) {
|
||||||
|
drawSegment(seg, '1st overlap ' + signature, i, 'orange');
|
||||||
|
firstOverlap = false;
|
||||||
|
} else {
|
||||||
|
drawSegment(seg, '2nd overlap ' + signature, i, 'orange');
|
||||||
|
seg._visited = interSeg._visited;
|
||||||
|
seg = interSeg;
|
||||||
|
dir = 1;
|
||||||
|
firstOverlap = true;
|
||||||
|
}
|
||||||
|
} else if (Math.abs(w3 * w4) > Numerical.EPSILON) {
|
||||||
// Do not attempt to switch contours if we aren't
|
// Do not attempt to switch contours if we aren't
|
||||||
// sure that there is a possible candidate.
|
// sure that there is a possible candidate.
|
||||||
var curve = w3 < w4 ? c3 : c4,
|
var curve = w3 < w4 ? c3 : c4,
|
||||||
|
@ -430,19 +520,24 @@ PathItem.inject(new function() {
|
||||||
// contour to traverse, stay on the same contour.
|
// contour to traverse, stay on the same contour.
|
||||||
if (nextSeg._visited && seg._path !== nextSeg._path
|
if (nextSeg._visited && seg._path !== nextSeg._path
|
||||||
|| !operator(nextSeg._winding)) {
|
|| !operator(nextSeg._winding)) {
|
||||||
dir = 1;
|
drawSegment(nextSeg, 'not suitable ' + signature + ', old dir: ' + oldDir, i, 'orange');
|
||||||
|
dir = 1; // TODO: oldDir?
|
||||||
} else {
|
} else {
|
||||||
// Switch to the intersection segment.
|
// Switch to the intersection segment.
|
||||||
seg._visited = interSeg._visited;
|
seg._visited = interSeg._visited;
|
||||||
seg = interSeg;
|
seg = interSeg;
|
||||||
|
drawSegment(seg, 'switch ' + signature, i, 'green');
|
||||||
if (nextSeg._visited)
|
if (nextSeg._visited)
|
||||||
dir = 1;
|
dir = 1;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
drawSegment(seg, 'no cross ' + signature, i, 'blue');
|
||||||
dir = 1;
|
dir = 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
handleOut = dir > 0 ? seg._handleOut : seg._handleIn;
|
handleOut = dir > 0 ? seg._handleOut : seg._handleIn;
|
||||||
|
} else {
|
||||||
|
drawSegment(seg, 'keep', i, 'black');
|
||||||
}
|
}
|
||||||
// Add the current segment to the path, and mark the added
|
// Add the current segment to the path, and mark the added
|
||||||
// segment as visited.
|
// segment as visited.
|
||||||
|
@ -459,18 +554,17 @@ PathItem.inject(new function() {
|
||||||
if (seg && (seg === startSeg || seg === startInterSeg)) {
|
if (seg && (seg === startSeg || seg === startInterSeg)) {
|
||||||
path.firstSegment.setHandleIn((seg === startInterSeg
|
path.firstSegment.setHandleIn((seg === startInterSeg
|
||||||
? startInterSeg : seg)._handleIn);
|
? startInterSeg : seg)._handleIn);
|
||||||
path.setClosed(true);
|
|
||||||
} else {
|
} else {
|
||||||
path.lastSegment._handleOut.set(0, 0);
|
path.lastSegment._handleOut.set(0, 0);
|
||||||
|
console.error('Boolean operation results in open path!');
|
||||||
}
|
}
|
||||||
|
path.setClosed(true);
|
||||||
// Add the path to the result, while avoiding stray segments and
|
// Add the path to the result, while avoiding stray segments and
|
||||||
// incomplete paths. The amount of segments for valid paths depend
|
// incomplete paths. The amount of segments for valid paths depend
|
||||||
// on their geometry:
|
// on their geometry:
|
||||||
// - Closed paths with only straight lines need more than 2 segments
|
// - Closed paths with only straight lines need more than 2 segments
|
||||||
// - Closed paths with curves can consist of only one segment
|
// - Closed paths with curves can consist of only one segment
|
||||||
// - Open paths need at least two segments
|
if (path._segments.length > path.isLinear() ? 2 : 0)
|
||||||
if (path._segments.length >
|
|
||||||
(path._closed ? path.isLinear() ? 2 : 0 : 1))
|
|
||||||
paths.push(path);
|
paths.push(path);
|
||||||
}
|
}
|
||||||
return paths;
|
return paths;
|
||||||
|
|
Loading…
Reference in a new issue