Boolean Subtraction added

This commit is contained in:
hkrish 2013-04-19 19:49:44 +02:00
parent 27eeb24c4f
commit aabec49446
3 changed files with 104 additions and 50 deletions

View file

@ -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 path1path2
* 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;

View file

@ -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; }

View file

@ -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();
}
}