mirror of
https://github.com/scratchfoundation/paper.js.git
synced 2025-01-04 03:45:58 -05:00
Boolean Subtraction added
This commit is contained in:
parent
27eeb24c4f
commit
aabec49446
3 changed files with 104 additions and 50 deletions
103
Boolean.js
103
Boolean.js
|
@ -57,6 +57,17 @@
|
|||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
// path1 - path2
|
||||
Subtraction: function( lnk, isInsidePath1, isInsidePath2 ){
|
||||
var lnkid = lnk.id;
|
||||
if( lnkid === 1 && isInsidePath2 ){
|
||||
return false;
|
||||
} else if( lnkid === 2 && !isInsidePath1 ){
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -81,9 +92,9 @@
|
|||
* @param {Point} _handleOut
|
||||
* @param {Any} _id
|
||||
*/
|
||||
function Node( _point, _handleIn, _handleOut, _id, _childId ){
|
||||
function Node( _point, _handleIn, _handleOut, _id, isBaseContour ){
|
||||
this.id = _id;
|
||||
this.childId = _childId;
|
||||
this.isBaseContour = isBaseContour;
|
||||
this.type = NORMAL_NODE;
|
||||
this.point = _point;
|
||||
this.handleIn = _handleIn; // handleIn
|
||||
|
@ -95,6 +106,7 @@
|
|||
// In case of an intersection this will be a merged node.
|
||||
// And we need space to save the "other Node's" parameters before merging.
|
||||
this.idB = null;
|
||||
this.isBaseContourB = false;
|
||||
// this.pointB = this.point; // point should be the same
|
||||
this.handleBIn = null;
|
||||
this.handleBOut = null;
|
||||
|
@ -120,6 +132,7 @@
|
|||
this.linkOut.nodeIn = this; // linkOut.nodeStart
|
||||
this.handleIn = this.handleIn || this.handleBIn;
|
||||
this.handleOut = this.handleOut || this.handleBOut;
|
||||
this.isBaseContour = this.isBaseContour | this.isBaseContourB;
|
||||
}
|
||||
this._segment = this._segment || new Segment( this.point, this.handleIn, this.handleOut );
|
||||
return this._segment;
|
||||
|
@ -132,9 +145,9 @@
|
|||
* @param {Node} _nodeOut
|
||||
* @param {Any} _id
|
||||
*/
|
||||
function Link( _nodeIn, _nodeOut, _id, _childId ) {
|
||||
function Link( _nodeIn, _nodeOut, _id, isBaseContour ) {
|
||||
this.id = _id;
|
||||
this.childId = _childId;
|
||||
this.isBaseContour = isBaseContour;
|
||||
this.nodeIn = _nodeIn; // nodeStart
|
||||
this.nodeOut = _nodeOut; // nodeEnd
|
||||
this.nodeIn.linkOut = this; // nodeStart.linkOut
|
||||
|
@ -157,14 +170,14 @@
|
|||
* @param {Integer} id
|
||||
* @return {Array} Links
|
||||
*/
|
||||
function makeGraph( path, id, childId ){
|
||||
function makeGraph( path, id, isBaseContour ){
|
||||
var graph = [];
|
||||
var segs = path.segments, prevNode = null, firstNode = null, nuLink, nuNode;
|
||||
for( i = 0, l = segs.length; i < l; i++ ){
|
||||
var nuSeg = segs[i].clone();
|
||||
nuNode = new Node( nuSeg.point, nuSeg.handleIn, nuSeg.handleOut, id, childId );
|
||||
nuNode = new Node( nuSeg.point, nuSeg.handleIn, nuSeg.handleOut, id, isBaseContour );
|
||||
if( prevNode ) {
|
||||
nuLink = new Link( prevNode, nuNode, id, childId );
|
||||
nuLink = new Link( prevNode, nuNode, id, isBaseContour );
|
||||
graph.push( nuLink );
|
||||
}
|
||||
prevNode = nuNode;
|
||||
|
@ -173,12 +186,28 @@
|
|||
}
|
||||
}
|
||||
// the path is closed
|
||||
nuLink = new Link( prevNode, firstNode, id, childId );
|
||||
nuLink = new Link( prevNode, firstNode, id, isBaseContour );
|
||||
graph.push( nuLink );
|
||||
return graph;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* makes a graph for a pathItem
|
||||
* @param {Path} path
|
||||
* @param {Integer} id
|
||||
* @return {Array} Links
|
||||
*/
|
||||
function makeGraph2( path, id ){
|
||||
var graph = [];
|
||||
var curves = path.getCurves(), firstChildCount , counter, isBaseContour = true, i, len;
|
||||
firstChildCount = ( path instanceof CompoundPath )? path.children[0].curves.length : path.curves.length;
|
||||
// Segments need an ID, so that we can compare them
|
||||
for (i = 0, len = curves.length; i < len; i++, firstChildCount--) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Calculates the Union of two paths
|
||||
* Boolean API.
|
||||
|
@ -186,7 +215,7 @@
|
|||
* @param {Path} path2
|
||||
* @return {CompoundPath} union of path1 & path2
|
||||
*/
|
||||
function boolUnion( path1, path2 ){
|
||||
function boolUnion( path1, path2 ){
|
||||
return computeBoolean( path1, path2, BooleanOps.Union );
|
||||
}
|
||||
|
||||
|
@ -198,11 +227,23 @@
|
|||
* @param {Path} path2
|
||||
* @return {CompoundPath} Intersection of path1 & path2
|
||||
*/
|
||||
function boolIntersection( path1, path2 ){
|
||||
function boolIntersection( path1, path2 ){
|
||||
return computeBoolean( path1, path2, BooleanOps.Intersection );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Calculates path1—path2
|
||||
* Boolean API.
|
||||
* @param {Path} path1
|
||||
* @param {Path} path2
|
||||
* @return {CompoundPath} path1 <minus> path2
|
||||
*/
|
||||
function boolSubtract( path1, path2 ){
|
||||
return computeBoolean( path1, path2, BooleanOps.Subtraction );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Actual function that computes the boolean
|
||||
* @param {Path} _path1 (cannot be self-intersecting at the moment)
|
||||
|
@ -210,7 +251,7 @@
|
|||
* @param {BooleanOps type} operator
|
||||
* @return {CompoundPath} boolean result
|
||||
*/
|
||||
function computeBoolean( _path1, _path2, operator ){
|
||||
function computeBoolean( _path1, _path2, operator ){
|
||||
IntersectionID = 1;
|
||||
UNIQUE_ID = 1;
|
||||
|
||||
|
@ -226,34 +267,36 @@
|
|||
// full connectivity information. The order of links in a graph is not important
|
||||
// That allows us to sort and merge graphs and 'splice' links with their splits easily.
|
||||
// Also, this is the place to resolve self-intersecting paths
|
||||
var graph = [], path1Children, path2Children;
|
||||
var graph = [], path1Children, path2Children, base;
|
||||
if( path1 instanceof CompoundPath ){
|
||||
path1Children = path1.children;
|
||||
for (i = 0, l = path1Children.length; i < l; i++) {
|
||||
for (i = 0, base = true, l = path1Children.length; i < l; i++, base = false) {
|
||||
path1Children[i].closed = true;
|
||||
graph = graph.concat( makeGraph( path1Children[i], 1, i + 1 ) );
|
||||
graph = graph.concat( makeGraph( path1Children[i], 1, base ) );
|
||||
}
|
||||
} else {
|
||||
path1.closed = true;
|
||||
path1.clockwise = true;
|
||||
graph = graph.concat( makeGraph( path1, 1, 1 ) );
|
||||
// path1.clockwise = true;
|
||||
graph = graph.concat( makeGraph( path1, 1, 1, true ) );
|
||||
}
|
||||
|
||||
// TODO: if operator === BooleanOps.subtract, then for path2, clockwise must be false
|
||||
// if operator === BooleanOps.Subtraction, then reverse path2
|
||||
// so that the nodes and links will link correctly
|
||||
var reverse = ( operator === BooleanOps.Subtraction )? true: false;
|
||||
if( path2 instanceof CompoundPath ){
|
||||
path2Children = path2.children;
|
||||
for (i = 0, l = path2Children.length; i < l; i++) {
|
||||
for (i = 0, base = true, l = path2Children.length; i < l; i++, base = false) {
|
||||
path2Children[i].closed = true;
|
||||
graph = graph.concat( makeGraph( path2Children[i], 2, i + 1 ) );
|
||||
if( reverse ){ path2Children[i].reverse(); }
|
||||
graph = graph.concat( makeGraph( path2Children[i], 2, i + 1, base ) );
|
||||
}
|
||||
} else {
|
||||
path2.closed = true;
|
||||
path2.clockwise = true;
|
||||
graph = graph.concat( makeGraph( path2, 2, 1 ) );
|
||||
// path2.clockwise = true;
|
||||
if( reverse ){ path2.reverse(); }
|
||||
graph = graph.concat( makeGraph( path2, 2, 1, true ) );
|
||||
}
|
||||
|
||||
window.g = graph
|
||||
|
||||
// Sort function to sort intersections according to the 'parameter'(t) in a link (curve)
|
||||
function ixSort( a, b ){ return a._parameter - b._parameter; }
|
||||
|
||||
|
@ -289,6 +332,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Pass 2:
|
||||
* Walk the graph, sort the intersections on each individual link.
|
||||
|
@ -297,7 +341,8 @@
|
|||
for ( i = graph.length - 1; i >= 0; i--) {
|
||||
if( graph[i].intersections.length ){
|
||||
var ix = graph[i].intersections;
|
||||
ix.sort( ixSort );
|
||||
// Sort the intersections if there is more than one
|
||||
if( graph[i].intersections.length > 1 ){ ix.sort( ixSort ); }
|
||||
// Remove the graph link, this link has to be split and replaced with the splits
|
||||
lnk = graph.splice( i, 1 )[0];
|
||||
for (j =0, l=ix.length; j<l && lnk; j++) {
|
||||
|
@ -328,7 +373,7 @@
|
|||
// TODO: check if link is linear and set handles to null
|
||||
var ixPoint = new Point( left[6], left[7] );
|
||||
nuNode = new Node( ixPoint, new Point(left[4] - ixPoint.x, left[5] - ixPoint.y),
|
||||
new Point(right[2] - ixPoint.x, right[3] - ixPoint.y), lnk.id, lnk.childId );
|
||||
new Point(right[2] - ixPoint.x, right[3] - ixPoint.y), lnk.id, lnk.isBaseContour );
|
||||
nuNode.type = INTERSECTION_NODE;
|
||||
nuNode._intersectionID = ix[j]._intersectionID;
|
||||
// clear the cached Segment on original end nodes and Update their handles
|
||||
|
@ -339,8 +384,8 @@
|
|||
tmppnt = lnk.nodeOut.point;
|
||||
lnk.nodeOut.handleIn = new Point( right[4] - tmppnt.x, right[5] - tmppnt.y );
|
||||
// Make new links after the split
|
||||
leftLink = new Link( lnk.nodeIn, nuNode, lnk.id, lnk.childId);
|
||||
rightLink = new Link( nuNode, lnk.nodeOut, lnk.id, lnk.childId );
|
||||
leftLink = new Link( lnk.nodeIn, nuNode, lnk.id, lnk.isBaseContour );
|
||||
rightLink = new Link( nuNode, lnk.nodeOut, lnk.id, lnk.isBaseContour );
|
||||
}
|
||||
// Add the first split link back to the graph, since we sorted the intersections
|
||||
// already, this link should contain no more intersections to the left.
|
||||
|
@ -427,6 +472,7 @@
|
|||
} else {
|
||||
// Merge the nodes together, by adding this node's information to the other node
|
||||
otherNode.idB = node.id;
|
||||
otherNode.isBaseContourB = node.isBaseContour;
|
||||
otherNode.handleBIn = node.handleIn;
|
||||
otherNode.handleBOut = node.handleOut;
|
||||
otherNode.linkBIn = node.linkIn;
|
||||
|
@ -441,6 +487,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
// Final step: Retrieve the resulting paths from the graph
|
||||
// TODO: start from a path where childId === 1
|
||||
var boolResult = new CompoundPath();
|
||||
|
@ -450,7 +497,7 @@
|
|||
len = graph.length;
|
||||
while( len-- ){
|
||||
if( !graph[len].INVALID && !graph[len].nodeIn.visited && !firstNode ){
|
||||
if( !foundBasePath && graph[len].childId === 1 ){
|
||||
if( !foundBasePath && graph[len].isBaseContour === 1 ){
|
||||
firstNode = graph[len].nodeIn;
|
||||
foundBasePath = true;
|
||||
break;
|
||||
|
|
|
@ -8,9 +8,9 @@
|
|||
<script type="text/javascript" src="booleanTests.js"></script>
|
||||
<style>
|
||||
body { height: 100%; overflow: auto; }
|
||||
#container { display: block; width: 800px; margin: 0 auto 50px; }
|
||||
#container { display: block; width: 1000px; margin: 0 auto 50px; }
|
||||
h1, h3 { font-family: 'Helvetica Neue'; font-weight: 300; margin: 50px 0 20px; }
|
||||
footer{display: block; width: 800px; height: 100px; margin: 30px auto; color: #999; }
|
||||
footer{display: block; width: 1000px; height: 100px; margin: 30px auto; color: #999; }
|
||||
footer p { font-family: 'Helvetica Neue'; font-style: italic; font-weight: 300; }
|
||||
canvas { cursor: crosshair; width: 100%; height: 220px; margin: 5px 0;}
|
||||
.error { color: #a00; } .hide{ display: none; }
|
||||
|
|
|
@ -4,7 +4,7 @@ paper.install(window);
|
|||
|
||||
|
||||
function runTests() {
|
||||
var caption, pathA, pathB;
|
||||
var caption, pathA, pathB, group;
|
||||
|
||||
var container = document.getElementById( 'container' );
|
||||
|
||||
|
@ -49,10 +49,10 @@ function runTests() {
|
|||
pathB = new Path.Star(new Point(110, 110), 6, 30, 100);
|
||||
testBooleanStatic( pathA, pathB, caption );
|
||||
|
||||
caption = prepareTest( 'Circles overlap exactly over each other', container );
|
||||
pathA = new Path.Circle(new Point(110, 110), 100);
|
||||
pathB = new Path.Circle(new Point(110, 110), 100 );
|
||||
testBooleanStatic( pathA, pathB, caption );
|
||||
// caption = prepareTest( 'Circles overlap exactly over each other', container );
|
||||
// pathA = new Path.Circle(new Point(110, 110), 100);
|
||||
// pathB = new Path.Circle(new Point(110, 110), 100 );
|
||||
// testBooleanStatic( pathA, pathB, caption );
|
||||
|
||||
caption = prepareTest( 'Maximum possible intersections between 2 cubic bezier curve segments - 9', container );
|
||||
pathA = new Path();
|
||||
|
@ -62,34 +62,33 @@ function runTests() {
|
|||
pathB = pathA.clone();
|
||||
pathB.rotate( -90 );
|
||||
// FIXME: hangs when I move pathA, pathB apart by [-10,0] & [10,0] or [9...] etc.
|
||||
pathA.translate( [-11,0] );
|
||||
pathB.translate( [11,0] );
|
||||
pathA.translate( [-10,0] );
|
||||
pathB.translate( [10,0] );
|
||||
testBooleanStatic( pathA, pathB, caption );
|
||||
annotatePath( pathA, null, '#008' );
|
||||
annotatePath( pathB, null, '#800' );
|
||||
view.draw();
|
||||
|
||||
caption = prepareTest( 'Glyphs imported from SVG', container );
|
||||
var group = paper.project.importSvg( document.getElementById( 'glyphsys' ) );
|
||||
group = paper.project.importSvg( document.getElementById( 'glyphsys' ) );
|
||||
pathA = group.children[0];
|
||||
pathB = group.children[1];
|
||||
testBooleanStatic( pathA, pathB, caption );
|
||||
|
||||
caption = prepareTest( 'CompoundPaths 1', container );
|
||||
var group = paper.project.importSvg( document.getElementById( 'glyphsacirc' ) );
|
||||
group = paper.project.importSvg( document.getElementById( 'glyphsacirc' ) );
|
||||
pathA = group.children[0];
|
||||
pathB = group.children[1];
|
||||
testBooleanStatic( pathA, pathB, caption );
|
||||
|
||||
caption = prepareTest( 'CompoundPaths 2', container );
|
||||
var group = paper.project.importSvg( document.getElementById( 'glyphsacirc' ) );
|
||||
group = paper.project.importSvg( document.getElementById( 'glyphsacirc' ) );
|
||||
pathA = group.children[0];
|
||||
pathB = new CompoundPath();
|
||||
group.children[1].clockwise = true;
|
||||
pathB.addChild(group.children[1]);
|
||||
var npath = new Path.Circle([110, 110], 30);
|
||||
console.log(npath.clockwise)
|
||||
pathB.addChild( npath );
|
||||
console.log(npath.clockwise)
|
||||
testBooleanStatic( pathA, pathB, caption );
|
||||
|
||||
window.p = pathB;
|
||||
|
@ -129,23 +128,30 @@ var pathStyleBoolean = {
|
|||
// Better if path1 and path2 fit nicely inside a 200x200 pixels rect
|
||||
function testBooleanStatic( path1, path2, caption ) {
|
||||
try{
|
||||
var _p1U = path1.clone().translate( [280, 0] );
|
||||
var _p2U = path2.clone().translate( [280, 0] );
|
||||
var _p1U = path1.clone().translate( [250, 0] );
|
||||
var _p2U = path2.clone().translate( [250, 0] );
|
||||
_p1U.style = _p2U.style = pathStyleBoolean;
|
||||
console.time( 'Union' );
|
||||
var boolPathU = boolUnion( _p1U, _p2U );
|
||||
console.timeEnd( 'Union' );
|
||||
|
||||
window.b = boolPathU
|
||||
|
||||
var _p1I = path1.clone().translate( [560, 0] );
|
||||
var _p2I = path2.clone().translate( [560, 0] );
|
||||
var _p1I = path1.clone().translate( [500, 0] );
|
||||
var _p2I = path2.clone().translate( [500, 0] );
|
||||
_p1I.style = _p2I.style = pathStyleBoolean;
|
||||
console.time( 'Intersection' );
|
||||
var boolPathI = boolIntersection( _p1I, _p2I );
|
||||
console.timeEnd( 'Intersection' );
|
||||
|
||||
|
||||
var _p1S = path1.clone().translate( [750, 0] );
|
||||
var _p2S = path2.clone().translate( [750, 0] );
|
||||
_p1S.style = _p2S.style = pathStyleBoolean;
|
||||
console.time( 'Subtraction' );
|
||||
var boolPathS = boolSubtract( _p1S, _p2S );
|
||||
console.timeEnd( 'Subtraction' );
|
||||
|
||||
path1.style = path2.style = pathStyleNormal;
|
||||
_p1U.style = _p2U.style = _p1I.style = _p2I.style = pathStyleBoolean;
|
||||
boolPathU.style = boolPathI.style = booleanStyle;
|
||||
boolPathS.style = booleanStyle;
|
||||
} catch( e ){
|
||||
console.error( e.message );
|
||||
if( caption ) { caption.className += ' error'; }
|
||||
|
@ -153,6 +159,7 @@ function testBooleanStatic( path1, path2, caption ) {
|
|||
} finally {
|
||||
console.timeEnd( 'Union' );
|
||||
console.timeEnd( 'Intersection' );
|
||||
console.timeEnd( 'Subtraction' );
|
||||
view.draw();
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue