mirror of
https://github.com/scratchfoundation/paper.js.git
synced 2025-01-06 04:42:15 -05:00
Boolean Union and Intersection working
This commit is contained in:
parent
b96136fc3e
commit
7b6b94fc21
8 changed files with 1859 additions and 445 deletions
3
.exrc
Normal file
3
.exrc
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
|
||||||
|
set tags+=./tags
|
||||||
|
|
488
Boolean.js
Normal file
488
Boolean.js
Normal file
|
@ -0,0 +1,488 @@
|
||||||
|
|
||||||
|
|
||||||
|
/*!
|
||||||
|
*
|
||||||
|
* Vector boolean operations on paperjs objects
|
||||||
|
* This is mostly written for clarity (I hope it is clear) and compatibility,
|
||||||
|
* not optimised for performance, and has to be tested heavily for stability.
|
||||||
|
* (Looking up to Java's Area path boolean algorithms for stability,
|
||||||
|
* but the code is too complex —mainly because the operations are stored and
|
||||||
|
* enumerable, such as quadraticCurveTo, cubicCurveTo etc.; and is largely
|
||||||
|
* undocumented to directly adapt from)
|
||||||
|
*
|
||||||
|
* Supported
|
||||||
|
* - paperjs Path objects
|
||||||
|
* - Boolean Union operations
|
||||||
|
* - Boolean Intersection operations
|
||||||
|
* - handles path complexity quite nicely
|
||||||
|
*
|
||||||
|
* Not supported yet ( which I would like to see supported )
|
||||||
|
* - Compound Paths as input ( however compound paths are correctly handled in the output )
|
||||||
|
* - Self-intersecting Paths
|
||||||
|
* - Boolean Subtraction operation ( depends on compound paths as input )
|
||||||
|
* - Paths are clones of each other that ovelap exactly on top of each other!
|
||||||
|
*
|
||||||
|
* In the Not-supported-yet list, the first three can be easily implemented,
|
||||||
|
* as for the last point, I need help! Thanks! :)
|
||||||
|
*
|
||||||
|
* ------
|
||||||
|
* Harikrishnan Gopalakrishnan
|
||||||
|
* http://hkrish.com/playground/paperbool.html
|
||||||
|
*
|
||||||
|
* ------
|
||||||
|
* Paperjs
|
||||||
|
* Copyright (c) 2011, Juerg Lehni & Jonathan Puckey
|
||||||
|
* http://paperjs.org/license/
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BooleanOps defines the boolean operator functions to use.
|
||||||
|
* A boolean operator is a function f( link:Link, isInsidePath1:Boolean, isInsidePath2:Boolean ) :
|
||||||
|
* should return a Boolean value indicating whether to keep the link or not.
|
||||||
|
* return true - keep the path
|
||||||
|
* return false - discard the path
|
||||||
|
*/
|
||||||
|
var BooleanOps = {
|
||||||
|
Union: function( lnk, isInsidePath1, isInsidePath2 ){
|
||||||
|
if( isInsidePath1 || isInsidePath2 ){
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
Intersection: function( lnk, isInsidePath1, isInsidePath2 ){
|
||||||
|
if( !isInsidePath1 && !isInsidePath2 ){
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The datastructure for boolean computation:
|
||||||
|
* Graph - List of Links
|
||||||
|
* Link - Connects 2 Nodes, represents a Curve
|
||||||
|
* Node - Connects 2 Links, represents a Segment
|
||||||
|
*/
|
||||||
|
|
||||||
|
var NORMAL_NODE = 1;
|
||||||
|
var INTERSECTION_NODE = 2;
|
||||||
|
var IntersectionID = 1;
|
||||||
|
var UNIQUE_ID = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nodes in the graph are analogous to Segment objects
|
||||||
|
* with additional linkage information to track intersections etc.
|
||||||
|
* (enough to do a complete graph traversal)
|
||||||
|
* @param {Point} _point
|
||||||
|
* @param {Point} _handleIn
|
||||||
|
* @param {Point} _handleOut
|
||||||
|
* @param {Any} _id
|
||||||
|
*/
|
||||||
|
function Node( _point, _handleIn, _handleOut, _id ){
|
||||||
|
this.id = _id;
|
||||||
|
this.type = NORMAL_NODE;
|
||||||
|
this.point = _point;
|
||||||
|
this.handleIn = _handleIn; // handleIn
|
||||||
|
this.handleOut = _handleOut; // handleOut
|
||||||
|
this.linkIn = null; // aka linkIn
|
||||||
|
this.linkOut = null; // linkOut
|
||||||
|
this.uniqueID = ++UNIQUE_ID;
|
||||||
|
|
||||||
|
// 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.pointB = this.point; // point should be the same
|
||||||
|
this.handleBIn = null;
|
||||||
|
this.handleBOut = null;
|
||||||
|
this.linkBIn = null;
|
||||||
|
this.linkBOut = null;
|
||||||
|
|
||||||
|
this._segment = null;
|
||||||
|
|
||||||
|
this.getSegment = function( recalculate ){
|
||||||
|
if( this.type === INTERSECTION_NODE && recalculate ){
|
||||||
|
// point this.linkIn and this.linkOut to those active ones
|
||||||
|
// also point this.handleIn and this.handleOut to correct in and out handles
|
||||||
|
// If a link is null, make sure the corresponding handle is also null
|
||||||
|
this.handleIn = (this.linkIn)? this.handleIn : null;
|
||||||
|
this.handleOut = (this.linkOut)? this.handleOut : null;
|
||||||
|
this.handleBIn = (this.linkBIn)? this.handleBIn : null;
|
||||||
|
this.handleBOut = (this.linkBOut)? this.handleBOut : null;
|
||||||
|
// Select the valid links
|
||||||
|
this.linkIn = this.linkIn || this.linkBIn; // linkIn
|
||||||
|
this.linkOut = this.linkOut || this.linkBOut; // linkOut
|
||||||
|
// Also update the references in links to point to "this" Node
|
||||||
|
this.linkIn.nodeOut = this; // linkIn.nodeEnd
|
||||||
|
this.linkOut.nodeIn = this; // linkOut.nodeStart
|
||||||
|
this.handleIn = this.handleIn || this.handleBIn;
|
||||||
|
this.handleOut = this.handleOut || this.handleBOut;
|
||||||
|
}
|
||||||
|
this._segment = this._segment || new Segment( this.point, this.handleIn, this.handleOut );
|
||||||
|
return this._segment;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Links in the graph are analogous to CUrve objects
|
||||||
|
* @param {Node} _nodeIn
|
||||||
|
* @param {Node} _nodeOut
|
||||||
|
* @param {Any} _id
|
||||||
|
*/
|
||||||
|
function Link( _nodeIn, _nodeOut, _id ) {
|
||||||
|
this.id = _id;
|
||||||
|
this.nodeIn = _nodeIn; // nodeStart
|
||||||
|
this.nodeOut = _nodeOut; // nodeEnd
|
||||||
|
this.nodeIn.linkOut = this; // nodeStart.linkOut
|
||||||
|
this.nodeOut.linkIn = this; // nodeEnd.linkIn
|
||||||
|
this._curve = null;
|
||||||
|
this.intersections = [];
|
||||||
|
|
||||||
|
// for reusing the paperjs function we need to (temperorily) build a Curve object from this Link
|
||||||
|
// for performance reasons we cache it.
|
||||||
|
this.getCurve = function() {
|
||||||
|
this._curve = this._curve || new Curve( this.nodeIn.getSegment(), this.nodeOut.getSegment() );
|
||||||
|
return this._curve;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* makes a graph. Only works on paths, for compound paths we need to
|
||||||
|
* make graphs for each of the child paths and merge them.
|
||||||
|
* @param {Path} path
|
||||||
|
* @param {Integer} id
|
||||||
|
* @return {Array} Links
|
||||||
|
*/
|
||||||
|
function makeGraph( path, id ){
|
||||||
|
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 );
|
||||||
|
if( prevNode ) {
|
||||||
|
nuLink = new Link( prevNode, nuNode, id );
|
||||||
|
graph.push( nuLink );
|
||||||
|
}
|
||||||
|
prevNode = nuNode;
|
||||||
|
if( !firstNode ){
|
||||||
|
firstNode = nuNode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// the path is closed
|
||||||
|
nuLink = new Link( prevNode, firstNode, id );
|
||||||
|
graph.push( nuLink );
|
||||||
|
return graph;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the Union of two paths
|
||||||
|
* Boolean API.
|
||||||
|
* @param {Path} path1
|
||||||
|
* @param {Path} path2
|
||||||
|
* @return {CompoundPath} union of path1 & path2
|
||||||
|
*/
|
||||||
|
function boolUnion( path1, path2 ){
|
||||||
|
return computeBoolean( path1, path2, BooleanOps.Union );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the Intersection between two paths
|
||||||
|
* Boolean API.
|
||||||
|
* @param {Path} path1
|
||||||
|
* @param {Path} path2
|
||||||
|
* @return {CompoundPath} Intersection of path1 & path2
|
||||||
|
*/
|
||||||
|
function boolIntersection( path1, path2 ){
|
||||||
|
return computeBoolean( path1, path2, BooleanOps.Intersection );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actual function that computes the boolean
|
||||||
|
* @param {Path} _path1 (cannot be self-intersecting at the moment)
|
||||||
|
* @param {Path} _path2 (cannot be self-intersecting at the moment)
|
||||||
|
* @param {BooleanOps type} operator
|
||||||
|
* @return {CompoundPath} boolean result
|
||||||
|
*/
|
||||||
|
function computeBoolean( _path1, _path2, operator ){
|
||||||
|
IntersectionID = 1;
|
||||||
|
UNIQUE_ID = 1;
|
||||||
|
|
||||||
|
// The boolean operation may modify the original paths
|
||||||
|
var path1 = _path1.clone();
|
||||||
|
var path2 = _path2.clone();
|
||||||
|
if( !path1.clockwise ){ path1.reverse(); }
|
||||||
|
if( !path2.clockwise ){ path2.reverse(); }
|
||||||
|
|
||||||
|
// Prepare the graphs. Graphs are list of Links that retains
|
||||||
|
// 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 = makeGraph( path1, 1 );
|
||||||
|
var graph2 = makeGraph( path2, 2 );
|
||||||
|
// Merge the two graphs. Since we have unique id's for each Link and Node,
|
||||||
|
// retrieveing the original graphs is rather simple.
|
||||||
|
graph = graph.concat( graph2 );
|
||||||
|
|
||||||
|
// Sort function to sort intersections according to the 'parameter'(t) in a link (curve)
|
||||||
|
function ixSort( a, b ){ return a._parameter - b._parameter; }
|
||||||
|
|
||||||
|
var i, j, k, l, lnk, crv, node, nuNode, leftLink, rightLink;
|
||||||
|
/*
|
||||||
|
* Pass 1:
|
||||||
|
* Calculate the intersections for all graphs
|
||||||
|
* TODO: test if this takes are of self intersecting paths - NO
|
||||||
|
* And since it doesn't take self-intersecting curves, we need to only calculate
|
||||||
|
* intersections if the "id" of the links differ.
|
||||||
|
* The rest of the algorithm can easily be modified to resolve self-intersections
|
||||||
|
*/
|
||||||
|
for ( i = graph.length - 1; i >= 0; i--) {
|
||||||
|
var c1 = graph[i].getCurve();
|
||||||
|
var v1 = c1.getValues();
|
||||||
|
for ( j = i -1; j >= 0; j-- ) {
|
||||||
|
if( graph[j].id === graph[i].id ){ continue; }
|
||||||
|
var c2 = graph[j].getCurve();
|
||||||
|
var v2 = c2.getValues();
|
||||||
|
var loc = [];
|
||||||
|
Curve._addIntersections( v1, v2, loc );
|
||||||
|
if( loc.length ){
|
||||||
|
for (k = 0, l=loc.length; k<l; k++) {
|
||||||
|
var loc1 = loc[k].clone();
|
||||||
|
loc1._intersectionID = loc[k]._intersectionID;
|
||||||
|
loc1._parameter = c1.getNearestLocation( loc[k] ).parameter; // For sorting on curve1
|
||||||
|
graph[i].intersections.push( loc1 );
|
||||||
|
var loc2 = loc[k].clone();
|
||||||
|
loc2._intersectionID = loc[k]._intersectionID;
|
||||||
|
loc2._parameter = c2.getNearestLocation( loc[k] ).parameter; // For sorting on curve2
|
||||||
|
graph[j].intersections.push( loc2 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Pass 2:
|
||||||
|
* Walk the graph, sort the intersections on each individual link.
|
||||||
|
* for each link that intersects with another one, replace it with new split links.
|
||||||
|
*/
|
||||||
|
for ( i = graph.length - 1; i >= 0; i--) {
|
||||||
|
if( graph[i].intersections.length ){
|
||||||
|
var ix = graph[i].intersections;
|
||||||
|
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++) {
|
||||||
|
var splitLinks = [];
|
||||||
|
crv = lnk.getCurve();
|
||||||
|
// We need to recalculate parameter after each curve split
|
||||||
|
// This operation (except for recalculating the curve parameter),
|
||||||
|
// is fairly similar to Curve.split method, except that it operates on Node and Link objects.
|
||||||
|
var param = crv.getNearestLocation( ix[j] ).parameter;
|
||||||
|
if( param === 0.0 || param === 1.0) {
|
||||||
|
// Intersection falls on an existing node
|
||||||
|
// there is no need to split the link
|
||||||
|
nuNode = ( param === 0.0 )? lnk.nodeIn : lnk.nodeOut;
|
||||||
|
nuNode.type = INTERSECTION_NODE;
|
||||||
|
nuNode._intersectionID = ix[j]._intersectionID;
|
||||||
|
if( param === 1.0 ){
|
||||||
|
leftLink = null;
|
||||||
|
rightLink = lnk;
|
||||||
|
} else {
|
||||||
|
leftLink = lnk;
|
||||||
|
rightLink = null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var parts = Curve.subdivide(crv.getValues(), param);
|
||||||
|
var left = parts[0];
|
||||||
|
var right = parts[1];
|
||||||
|
// Make new link and convert handles from absolute to relative
|
||||||
|
// 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 );
|
||||||
|
nuNode.type = INTERSECTION_NODE;
|
||||||
|
nuNode._intersectionID = ix[j]._intersectionID;
|
||||||
|
// clear the cached Segment on original end nodes and Update their handles
|
||||||
|
lnk.nodeIn._segment = null;
|
||||||
|
var tmppnt = lnk.nodeIn.point;
|
||||||
|
lnk.nodeIn.handleOut = new Point( left[2] - tmppnt.x, left[3] - tmppnt.y );
|
||||||
|
lnk.nodeOut._segment = null;
|
||||||
|
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 );
|
||||||
|
rightLink = new Link( nuNode, lnk.nodeOut, lnk.id );
|
||||||
|
}
|
||||||
|
// 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.
|
||||||
|
if( leftLink ){
|
||||||
|
graph.splice( i, 0, leftLink );
|
||||||
|
}
|
||||||
|
// continue with the second split link, to see if
|
||||||
|
// there are more intersections to deal with
|
||||||
|
lnk = rightLink;
|
||||||
|
}
|
||||||
|
// Add the last split link back to the graph
|
||||||
|
if( lnk ){
|
||||||
|
graph.splice( i, 0, lnk );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pass 3:
|
||||||
|
* Merge matching intersection Node Pairs (type is INTERSECTION_NODE &&
|
||||||
|
* a._intersectionID == b._intersectionID )
|
||||||
|
*
|
||||||
|
* Mark each Link(Curve) according to whether it is
|
||||||
|
* case 1. inside Path1 ( and only Path1 )
|
||||||
|
* 2. inside Path2 ( and only Path2 )
|
||||||
|
* 3. inside both ( fully contained holes that completely overlap )
|
||||||
|
* 4. outside (normal case)
|
||||||
|
*
|
||||||
|
* Take a test function "operator" which will discard links
|
||||||
|
* according to the above
|
||||||
|
* * Union -> discard cases 1, 2 and 3
|
||||||
|
* * Intersection -> discard case 4
|
||||||
|
* * Path1-Path2 -> discard cases 2, 3[Path1] and 4[Path2]‡
|
||||||
|
* * Path2-Path1 -> discard cases 1, 3[Path2] and 4[Path1]
|
||||||
|
* ‡ - 4[Path2] means curves of case 4 that belongs to Path2
|
||||||
|
*/
|
||||||
|
|
||||||
|
// step 1: discard invalid links according to the boolean operator
|
||||||
|
for ( i = graph.length - 1; i >= 0; i--) {
|
||||||
|
lnk = graph[i];
|
||||||
|
crv = lnk.getCurve();
|
||||||
|
// var midPoint = new Point(lnk.nodeIn.point);
|
||||||
|
var midPoint = crv.getPoint( 0.5 );
|
||||||
|
var insidePath1 = (lnk.id === 1 )? false : path1.contains( midPoint );
|
||||||
|
var insidePath2 = (lnk.id === 2 )? false : path2.contains( midPoint );
|
||||||
|
if( !operator( lnk, insidePath1, insidePath2 ) ){
|
||||||
|
// lnk = graph.splice( i, 1 )[0];
|
||||||
|
lnk.INVALID = true;
|
||||||
|
lnk.nodeIn.linkOut = null;
|
||||||
|
lnk.nodeOut.linkIn = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// step 2: Match nodes according to their _intersectionID and merge them together
|
||||||
|
var len = graph.length;
|
||||||
|
while( len-- ){
|
||||||
|
node = graph[len].nodeIn;
|
||||||
|
if( node.type === INTERSECTION_NODE ){
|
||||||
|
var otherNode = null;
|
||||||
|
for (i = len - 1; i >= 0; i--) {
|
||||||
|
var tmpnode = graph[i].nodeIn;
|
||||||
|
if( tmpnode._intersectionID === node._intersectionID &&
|
||||||
|
tmpnode.uniqueID !== node.uniqueID ) {
|
||||||
|
otherNode = tmpnode;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if( otherNode ) {
|
||||||
|
//Check if it is a self-intersecting Node
|
||||||
|
if( node.id === otherNode.id ){
|
||||||
|
// Swap the outgoing links, this will resolve a knot and create two paths,
|
||||||
|
// the portion of the original path on one side of a self crossing is counter-clockwise,
|
||||||
|
// so one of the resulting paths will also be counter-clockwise
|
||||||
|
var tmp = otherNode.linkOut;
|
||||||
|
otherNode.linkOut = node.linkOut;
|
||||||
|
node.linkOut = tmp;
|
||||||
|
tmp = otherNode.handleOut;
|
||||||
|
otherNode.handleOut = node.handleOut;
|
||||||
|
node.handleOut = tmp;
|
||||||
|
node.type = otherNode.type = NORMAL_NODE;
|
||||||
|
node._intersectionID = null;
|
||||||
|
node._segment = otherNode._segment = null;
|
||||||
|
} else {
|
||||||
|
// Merge the nodes together, by adding this node's information to the other node
|
||||||
|
otherNode.idB = node.id;
|
||||||
|
otherNode.handleBIn = node.handleIn;
|
||||||
|
otherNode.handleBOut = node.handleOut;
|
||||||
|
otherNode.linkBIn = node.linkIn;
|
||||||
|
otherNode.linkBOut = node.linkOut;
|
||||||
|
otherNode._segment = null;
|
||||||
|
if( node.linkIn ){ node.linkIn.nodeOut = otherNode; }
|
||||||
|
if( node.linkOut ){ node.linkOut.nodeIn = otherNode; }
|
||||||
|
// Clear this node's intersectionID, so that we won't iterate over it again
|
||||||
|
node._intersectionID = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final step: Retrieve the resulting paths from the graph
|
||||||
|
var boolResult = new CompoundPath();
|
||||||
|
var firstNode = true, nextNode;
|
||||||
|
while( firstNode ){
|
||||||
|
firstNode = nextNode = null;
|
||||||
|
len = graph.length;
|
||||||
|
while( len-- ){
|
||||||
|
if( !graph[len].INVALID && !graph[len].nodeIn.visited && !firstNode ){
|
||||||
|
firstNode = graph[len].nodeIn;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if( firstNode ){
|
||||||
|
var path = new Path();
|
||||||
|
path.add( firstNode.getSegment( true ) );
|
||||||
|
firstNode.visited = true;
|
||||||
|
nextNode = firstNode.linkOut.nodeOut;
|
||||||
|
while( firstNode.uniqueID !== nextNode.uniqueID ){
|
||||||
|
path.add( nextNode.getSegment( true ) );
|
||||||
|
nextNode.visited = true;
|
||||||
|
nextNode = nextNode.linkOut.nodeOut;
|
||||||
|
}
|
||||||
|
path.closed = true;
|
||||||
|
// path.clockwise = true;
|
||||||
|
boolResult.addChild( path );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
boolResult = boolResult.reduce();
|
||||||
|
|
||||||
|
return boolResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Same as the paperjs' Numerical class,
|
||||||
|
// added here because I can't access the original from this scope
|
||||||
|
var Numerical = {
|
||||||
|
TOLERANCE : 10e-6
|
||||||
|
};
|
||||||
|
|
||||||
|
// paperjs' Curve._addIntersections modified to return just intersection Point with a
|
||||||
|
// unique id.
|
||||||
|
paper.Curve._addIntersections = function(v1, v2, locations) {
|
||||||
|
var bounds1 = Curve.getBounds(v1),
|
||||||
|
bounds2 = Curve.getBounds(v2);
|
||||||
|
if (bounds1.touches(bounds2)) {
|
||||||
|
// See if both curves are flat enough to be treated as lines.
|
||||||
|
if (Curve.isFlatEnough(v1, /*#=*/ Numerical.TOLERANCE) &&
|
||||||
|
Curve.isFlatEnough(v2, /*#=*/ Numerical.TOLERANCE)) {
|
||||||
|
// See if the parametric equations of the lines interesct.
|
||||||
|
var point = new Line(v1[0], v1[1], v1[6], v1[7], false)
|
||||||
|
.intersect(new Line(v2[0], v2[1], v2[6], v2[7], false),
|
||||||
|
// Filter out beginnings of the curves, to avoid
|
||||||
|
// duplicate solutions where curves join.
|
||||||
|
true, false);
|
||||||
|
if (point){
|
||||||
|
point._intersectionID = IntersectionID++;
|
||||||
|
locations.push( point );
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Subdivide both curves, and see if they intersect.
|
||||||
|
var v1s = Curve.subdivide(v1),
|
||||||
|
v2s = Curve.subdivide(v2);
|
||||||
|
for (var i = 0; i < 2; i++)
|
||||||
|
for (var j = 0; j < 2; j++)
|
||||||
|
this._addIntersections(v1s[i], v2s[j], locations);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return locations;
|
||||||
|
};
|
89
boolean1.js
89
boolean1.js
|
@ -1,89 +0,0 @@
|
||||||
|
|
||||||
project.currentStyle.fillColor = 'black';
|
|
||||||
|
|
||||||
var path11 = new Path.Rectangle([100, 100], [100, 100]);
|
|
||||||
var path21 = new Path.Rectangle([50, 30], [100, 100]);
|
|
||||||
var newPath = new Path();
|
|
||||||
path11.style.fillColor = 'rgb( 71, 91, 98 )'
|
|
||||||
path21.style.fillColor = 'rgb( 129, 144, 144 )'
|
|
||||||
|
|
||||||
|
|
||||||
// onFrame = function( event ) {
|
|
||||||
path21.rotate( 148 );
|
|
||||||
|
|
||||||
var path1 = path11.clone();
|
|
||||||
var path2 = path21.clone();
|
|
||||||
|
|
||||||
newPath.removeSegments();
|
|
||||||
|
|
||||||
// Intersections of path1 with path2
|
|
||||||
var ixs = path1.getIntersections( path2 );
|
|
||||||
|
|
||||||
// TODO for both paths, first sort ixs according to curveOffset,
|
|
||||||
// so that, insert order is correct
|
|
||||||
|
|
||||||
ixs.forEach( function( item, index ){
|
|
||||||
// T
|
|
||||||
console.log( item.curveOffset )
|
|
||||||
var newSeg1 = new Segment( item.point );
|
|
||||||
var newSeg2 = new Segment( item.point );
|
|
||||||
newSeg1._ixOtherSeg = newSeg2;
|
|
||||||
newSeg2._ixOtherSeg = newSeg1;
|
|
||||||
path1.insertSegment( item.curve.segment1.index + 1, newSeg1 );
|
|
||||||
path2.insertSegment( item._ixCurve.segment1.index + 1, newSeg2 );
|
|
||||||
});
|
|
||||||
|
|
||||||
// console.log( path2.segments )
|
|
||||||
|
|
||||||
var startSeg = path1.firstSegment;
|
|
||||||
// TODO Make sure, if path1 is not completely inside path2
|
|
||||||
while( path2.contains( startSeg.point ) ) {
|
|
||||||
startSeg = startSeg.next;
|
|
||||||
}
|
|
||||||
|
|
||||||
// path2.firstSegment.selected = path2.firstSegment.next.selected = true;
|
|
||||||
// path1.firstSegment.selected = path1.firstSegment.next.selected = true;
|
|
||||||
|
|
||||||
console.log( path1.isClockwise() )
|
|
||||||
console.log( path2.isClockwise() )
|
|
||||||
|
|
||||||
// path2.reverse()
|
|
||||||
// startSeg.selected = true;
|
|
||||||
|
|
||||||
var curSeg;
|
|
||||||
var count = 1;
|
|
||||||
var ixswitch = true;
|
|
||||||
while( curSeg !== startSeg ) {
|
|
||||||
if( !curSeg ) {
|
|
||||||
curSeg = startSeg;
|
|
||||||
}
|
|
||||||
if( curSeg._ixOtherSeg ){
|
|
||||||
curSeg = curSeg._ixOtherSeg;
|
|
||||||
ixswitch = !ixswitch
|
|
||||||
}
|
|
||||||
newPath.addSegment( new Segment( curSeg ) );
|
|
||||||
|
|
||||||
var text = new PointText( curSeg.point - [ 5, 5 ] );
|
|
||||||
text.justification = 'center';
|
|
||||||
if( ixswitch ) {
|
|
||||||
text.fillColor = 'black';
|
|
||||||
}else{
|
|
||||||
text.fillColor = 'blue';
|
|
||||||
}
|
|
||||||
text.content = count.toString();
|
|
||||||
count++;
|
|
||||||
|
|
||||||
curSeg = curSeg.next;
|
|
||||||
}
|
|
||||||
|
|
||||||
newPath.translate( [200, 0] );
|
|
||||||
newPath.style.fillColor = 'rgb( 209, 28, 36 )';
|
|
||||||
newPath.selected = true;
|
|
||||||
|
|
||||||
// path1.remove();
|
|
||||||
// path2.remove();
|
|
||||||
// }
|
|
||||||
|
|
||||||
// path1.selected = true;
|
|
||||||
// path1.selected = true;
|
|
||||||
|
|
186
boolean2.js
186
boolean2.js
|
@ -1,186 +0,0 @@
|
||||||
|
|
||||||
project.currentStyle.fillColor = 'black';
|
|
||||||
|
|
||||||
var path11 = new Path.Circle([80, 80], 50);
|
|
||||||
// var path11 = new Path.Rectangle([100, 100], [100, 100]);
|
|
||||||
var path21 = new Path.Rectangle([100, 100], [100, 100]);
|
|
||||||
// var path21 = new Path.Polygon
|
|
||||||
var newPath = new Path();
|
|
||||||
path11.style.fillColor = 'rgb( 71, 91, 98 )'
|
|
||||||
path21.style.fillColor = 'rgb( 129, 144, 144 )'
|
|
||||||
|
|
||||||
|
|
||||||
// onFrame = function( event ) {
|
|
||||||
path21.rotate( 1 );
|
|
||||||
|
|
||||||
newPath.removeSegments();
|
|
||||||
|
|
||||||
var path1 = path11.clone();
|
|
||||||
var path2 = path21.clone();
|
|
||||||
|
|
||||||
// console.log(path1.isClockwise())
|
|
||||||
// console.log(path2.isClockwise())
|
|
||||||
|
|
||||||
// Intersections of path1 with path2
|
|
||||||
var ixs = path1.getIntersections( path2 );
|
|
||||||
|
|
||||||
// TODO for both paths, first sort ixs according to curveOffset,
|
|
||||||
// so that, insert order is correct
|
|
||||||
|
|
||||||
if( ixs.length > 0 ) {
|
|
||||||
ixs.forEach( function( item, index ){
|
|
||||||
|
|
||||||
var newSeg1 = new Segment( item.point );
|
|
||||||
var newSeg2 = new Segment( item.point );
|
|
||||||
newSeg1._ixCurveOffset = item.curveOffset;
|
|
||||||
newSeg2._ixCurveOffset = item._ixLocation.curveOffset;
|
|
||||||
newSeg1._ixOtherSeg = newSeg2;
|
|
||||||
newSeg2._ixOtherSeg = newSeg1;
|
|
||||||
|
|
||||||
if( item.curve.segment1._ixPoints === undefined ){
|
|
||||||
item.curve.segment1._ixPoints = [ newSeg1 ];
|
|
||||||
} else {
|
|
||||||
item.curve.segment1._ixPoints.push( newSeg1 );
|
|
||||||
}
|
|
||||||
if( item._ixLocation.curve.segment1._ixPoints === undefined ){
|
|
||||||
item._ixLocation.curve.segment1._ixPoints = [ newSeg2 ];
|
|
||||||
} else {
|
|
||||||
item._ixLocation.curve.segment1._ixPoints.push( newSeg2 );
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// path1.segments.forEach( function( item, index ){
|
|
||||||
// if( item._ixPoints ) {
|
|
||||||
// if( item._ixPoints.length > 1 ) {
|
|
||||||
// item._ixPoints.sort( compare_ixPoints );
|
|
||||||
// }
|
|
||||||
// path1.insertSegments( item.index + 1, item._ixPoints );
|
|
||||||
// item._ixPoints = undefined;
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
// path2.segments.forEach( function( item, index ){
|
|
||||||
// console.log(item._ixPoints)
|
|
||||||
// if( item._ixPoints ) {
|
|
||||||
// if( item._ixPoints.length > 1 ) {
|
|
||||||
// item._ixPoints.sort( compare_ixPoints );
|
|
||||||
// }
|
|
||||||
// path2.insertSegments( item.index + 1, item._ixPoints );
|
|
||||||
// item._ixPoints = undefined;
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
|
|
||||||
// TODO make sure path is closed
|
|
||||||
|
|
||||||
// Walk the segments in path1, backwards (counter-clockwise)
|
|
||||||
var pathSeg = path1.lastSegment;
|
|
||||||
do{
|
|
||||||
if( pathSeg._ixPoints ) {
|
|
||||||
if( pathSeg._ixPoints.length > 1 ) {
|
|
||||||
pathSeg._ixPoints.sort( compare_ixPoints );
|
|
||||||
}
|
|
||||||
path1.insertSegments( pathSeg.index + 1, pathSeg._ixPoints );
|
|
||||||
pathSeg._ixPoints = undefined;
|
|
||||||
}
|
|
||||||
pathSeg = pathSeg.previous;
|
|
||||||
} while ( pathSeg !== path1.lastSegment );
|
|
||||||
|
|
||||||
// Walk the segments in path2, backwards (counter-clockwise)
|
|
||||||
var pathSeg = path2.lastSegment;
|
|
||||||
do {
|
|
||||||
if( pathSeg._ixPoints ) {
|
|
||||||
if( pathSeg._ixPoints.length > 1 ) {
|
|
||||||
pathSeg._ixPoints.sort( compare_ixPoints );
|
|
||||||
}
|
|
||||||
path2.insertSegments( pathSeg.index + 1, pathSeg._ixPoints );
|
|
||||||
pathSeg._ixPoints = undefined;
|
|
||||||
}
|
|
||||||
pathSeg = pathSeg.previous;
|
|
||||||
} while ( pathSeg !== path2.lastSegment );
|
|
||||||
|
|
||||||
|
|
||||||
// TODO Make sure, if path1 is not completely inside path2.
|
|
||||||
// TODO. This part will differ for different boolean ops.
|
|
||||||
// For Union
|
|
||||||
var startSeg = path1.firstSegment;
|
|
||||||
while( path2.contains( startSeg.point ) || startSeg._ixOtherSeg ) {
|
|
||||||
startSeg = startSeg.next;
|
|
||||||
}
|
|
||||||
|
|
||||||
// path11.segments[startSeg.index].selected = true;
|
|
||||||
|
|
||||||
// startSeg.selected = true;
|
|
||||||
|
|
||||||
var curSeg;
|
|
||||||
var count = 1;
|
|
||||||
var ixswitch = true;
|
|
||||||
do {
|
|
||||||
if( !curSeg ) {
|
|
||||||
curSeg = startSeg;
|
|
||||||
}
|
|
||||||
if( curSeg._ixOtherSeg ){
|
|
||||||
curSeg = curSeg._ixOtherSeg;
|
|
||||||
ixswitch = !ixswitch
|
|
||||||
}
|
|
||||||
newPath.addSegment( new Segment( curSeg ) );
|
|
||||||
|
|
||||||
// var text = new PointText( curSeg.point - [ 5, 5 ] );
|
|
||||||
// text.justification = 'center';
|
|
||||||
// if( ixswitch ) {
|
|
||||||
// text.fillColor = 'black';
|
|
||||||
// }else{
|
|
||||||
// text.fillColor = 'blue';
|
|
||||||
// }
|
|
||||||
// text.content = count.toString();
|
|
||||||
|
|
||||||
count++;
|
|
||||||
|
|
||||||
curSeg = curSeg.next;
|
|
||||||
} while( curSeg !== startSeg && count < 50);
|
|
||||||
|
|
||||||
// console.log(count);
|
|
||||||
|
|
||||||
|
|
||||||
// annotateSegments( path1, 5, '#f00' )
|
|
||||||
// annotateSegments( path2, -5, '#000' )
|
|
||||||
|
|
||||||
newPath.closePath()
|
|
||||||
newPath.translate( [200, 0] );
|
|
||||||
newPath.style.fillColor = 'rgb( 209, 28, 36 )';
|
|
||||||
newPath.fullySelected = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// path1.remove();
|
|
||||||
// path2.remove();
|
|
||||||
// }
|
|
||||||
|
|
||||||
// path1.selected = true;
|
|
||||||
// path2.selected = true;
|
|
||||||
|
|
||||||
|
|
||||||
// Sort new intersection points according to their
|
|
||||||
// distance along the curve, so that when we
|
|
||||||
// insert them in to the respective paths, the orientation
|
|
||||||
// of the path is maintained ( in out case clockwise )
|
|
||||||
function compare_ixPoints( a, b ) {
|
|
||||||
if( a._ixCurveOffset < b._ixCurveOffset ) {
|
|
||||||
return -1
|
|
||||||
} else if( a._ixCurveOffset > b._ixCurveOffset ){
|
|
||||||
return 1;
|
|
||||||
} else {
|
|
||||||
// This shouldn't happen?!
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// For debugging: Show a number next to each segment ina path
|
|
||||||
function annotateSegments( p, d, c ) {
|
|
||||||
var count = 0;
|
|
||||||
p.segments.forEach( function( item, index ){
|
|
||||||
var text = new PointText( item.point - [ d, d ] );
|
|
||||||
text.style.fillColor = c;
|
|
||||||
text.justification = 'center';
|
|
||||||
text.content = count.toString();
|
|
||||||
count++;
|
|
||||||
});
|
|
||||||
}
|
|
166
boolean3.js
166
boolean3.js
|
@ -1,166 +0,0 @@
|
||||||
project.currentStyle.fillColor = 'black';
|
|
||||||
|
|
||||||
// var path11 = new Path.Circle([95, 95], 50);
|
|
||||||
// var path21 = new Path.Rectangle([100, 100], [100, 100]);
|
|
||||||
|
|
||||||
// var path11 = new Path.Rectangle([100, 100], [100, 100]);
|
|
||||||
// var path21 = new Path.Polygon
|
|
||||||
|
|
||||||
var path11 = new Path.Star(new Point(260, 250), 10, 50, 150);
|
|
||||||
var path21 = new Path.Star(new Point(350, 250), 10, 70, 250);
|
|
||||||
// path11.smooth();
|
|
||||||
// path21.smooth();
|
|
||||||
|
|
||||||
path11.style.fillColor = 'rgb( 71, 91, 98 )'
|
|
||||||
path21.style.fillColor = 'rgb( 129, 144, 144 )'
|
|
||||||
|
|
||||||
|
|
||||||
function compare_ixPoints( a, b ) {
|
|
||||||
if( a.curveOffset < b.curveOffset ) {
|
|
||||||
return -1
|
|
||||||
} else if( a.curveOffset > b.curveOffset ){
|
|
||||||
return 1;
|
|
||||||
} else {
|
|
||||||
// This shouldn't happen?!
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Path.prototype.getUnion = function( other ) {
|
|
||||||
var path1 = this.clone();
|
|
||||||
var path2 = other.clone();
|
|
||||||
// TODO do the necessary checks here.
|
|
||||||
if( !path1.isClockwise() ) path1.reverse();
|
|
||||||
if( !path2.isClockwise() ) path2.reverse();
|
|
||||||
|
|
||||||
console.time("Lines");
|
|
||||||
var ixs = path1.getIntersections( path2 );
|
|
||||||
console.timeEnd("Lines");
|
|
||||||
|
|
||||||
console.time("sort");
|
|
||||||
ixs.sort( compare_ixPoints );
|
|
||||||
console.timeEnd("sort");
|
|
||||||
|
|
||||||
// console.log( ixs.length )
|
|
||||||
|
|
||||||
for (var i = 0, l = ixs.length; i < l; i++) {
|
|
||||||
for (var j = i + 1, l = ixs.length; j < l; j++) {
|
|
||||||
if(ixs[i].point == ixs[j].point &&
|
|
||||||
ixs[i].curve.index === ixs[j].curve.index &&
|
|
||||||
ixs[i]._ixLocation.curve.index === ixs[j]._ixLocation.curve.index ){
|
|
||||||
ixs[i]._ixdup = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var counter = 0;
|
|
||||||
|
|
||||||
if( ixs.length > 1 ){
|
|
||||||
ixs.forEach( function( item, index ){
|
|
||||||
if(item._ixdup) return;
|
|
||||||
var crv1 = item.divide();
|
|
||||||
var crv2 = item._ixLocation.divide();
|
|
||||||
|
|
||||||
if( !crv1 ) {
|
|
||||||
if( item.parameter === 1 ){
|
|
||||||
crv1 = item.curve.next;
|
|
||||||
} else {
|
|
||||||
crv1 = item.curve;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if( !crv2 ) {
|
|
||||||
console.log( item._ixLocation )
|
|
||||||
|
|
||||||
// TODO if _ixLocation.parameter is null
|
|
||||||
// patch the _addIntersections method,
|
|
||||||
// to intersect the curve back again at that point
|
|
||||||
|
|
||||||
if( item._ixLocation.parameter === 1 ){
|
|
||||||
crv2 = item._ixLocation.curve.next;
|
|
||||||
} else {
|
|
||||||
crv2 = item._ixLocation.curve;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
annotateSegment(crv2.segment1, -10, "#000", false, counter++)
|
|
||||||
crv1.segment1._ixLink = crv2.segment1;
|
|
||||||
crv2.segment1._ixLink = crv1.segment1;
|
|
||||||
});
|
|
||||||
|
|
||||||
// annotateSegments( path1, 5, '#000', true )
|
|
||||||
// annotateSegments( path2, -5, '#00f' )
|
|
||||||
// path1.fullySelected = true;
|
|
||||||
// path2.fullySelected = true;
|
|
||||||
|
|
||||||
var newPath = new Path(),
|
|
||||||
startSeg = path1.firstSegment,
|
|
||||||
curSeg, loopCut = 0;
|
|
||||||
|
|
||||||
while( path2.contains( startSeg.point ) || startSeg._ixLink ) {
|
|
||||||
startSeg = startSeg.next;
|
|
||||||
}
|
|
||||||
|
|
||||||
annotateSegment( startSeg, -5, "#0ff" )
|
|
||||||
|
|
||||||
do {
|
|
||||||
if( !curSeg ) {
|
|
||||||
curSeg = startSeg;
|
|
||||||
}
|
|
||||||
if( curSeg._ixLink ){
|
|
||||||
newPath.addSegment( new Segment( curSeg.point, curSeg.handleIn, curSeg._ixLink.handleOut) );
|
|
||||||
console.log( "S - " + curSeg.index + " -> " + curSeg._ixLink.index )
|
|
||||||
curSeg = curSeg._ixLink;
|
|
||||||
} else {
|
|
||||||
newPath.addSegment( new Segment( curSeg ) );
|
|
||||||
}
|
|
||||||
loopCut++;
|
|
||||||
curSeg = curSeg.next;
|
|
||||||
} while( curSeg !== startSeg && loopCut < 50);
|
|
||||||
|
|
||||||
newPath.closePath();
|
|
||||||
// newPath.style.fillColor = null
|
|
||||||
newPath.translate( [500, 0] )
|
|
||||||
annotateSegments( newPath , -5, '#0f0' );
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// TODO one path is either completely inside
|
|
||||||
// or outside the other one.
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debug code
|
|
||||||
// console.log( newPath.segments.length );
|
|
||||||
// annotateSegments( path1, 5, '#000', true )
|
|
||||||
// path1.fullySelected = true;
|
|
||||||
// annotateSegments( path2, -5, '#00f' )
|
|
||||||
// path2.fullySelected = true;
|
|
||||||
// path1.selected = true;
|
|
||||||
path2.selected = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
path11.getUnion( path21 )
|
|
||||||
|
|
||||||
// For debugging: Show a number next to each segment ina path
|
|
||||||
function annotateSegments( p, d, c, hiLink ) {
|
|
||||||
hiLink = hiLink || false;
|
|
||||||
p.segments.forEach( function( item, index ){
|
|
||||||
annotateSegment( item, d, c, hiLink )
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function annotateSegment( s, d, c, hiLink , txt) {
|
|
||||||
hiLink = hiLink || false;
|
|
||||||
var text = new PointText( s.point - [ d, d ] );
|
|
||||||
text.style.fillColor = c;
|
|
||||||
text.justification = 'center';
|
|
||||||
if( txt === undefined )
|
|
||||||
text.content = s.index.toString();
|
|
||||||
else
|
|
||||||
text.content = txt;
|
|
||||||
if( hiLink && s._ixLink) {
|
|
||||||
annotateSegment( s._ixLink, d - 5, '#f00', false )
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function markpoint( p ) {
|
|
||||||
new Path.Circle(p, 2).style = { strokeColor: '#f0f', fillColor: '#000'}
|
|
||||||
}
|
|
||||||
|
|
||||||
// markpoint( [236.34995495235776, 152.72369685178597 ])
|
|
|
@ -1,13 +1,44 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
<meta charset="utf-8">
|
||||||
<title>Boolean Study</title>
|
<title>Boolean Study</title>
|
||||||
<link rel="stylesheet" href="../dist/style.css">
|
|
||||||
<script type="text/javascript" src="../dist/paper.js"></script>
|
<script type="text/javascript" src="../dist/paper.js"></script>
|
||||||
<script type="text/paperscript" canvas="canvas" src="boolean3.js"></script>
|
<script type="text/javascript" src="Boolean.js"></script>
|
||||||
|
<script type="text/javascript" src="booleanTests.js"></script>
|
||||||
|
<style>
|
||||||
|
body { height: 100%; overflow: auto; }
|
||||||
|
#container { display: block; width: 800px; 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 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; }
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<canvas id="canvas" resize></canvas>
|
<div id="container">
|
||||||
|
<h1>paperjs - Boolean Tests</h1>
|
||||||
|
<button id="testStart" value="Start tests" onClick="runTests();">Start tests</button>
|
||||||
|
</div>
|
||||||
|
<footer>
|
||||||
|
<p>Vector boolean operations on paperjs objects. <br />
|
||||||
|
Still under development, mostly written for clarity and compatibility,
|
||||||
|
not optimised for performance, and has to be tested heavily.</p>
|
||||||
|
<p>--<br />
|
||||||
|
hari</p>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<svg class="hide" version="1.1" id="glyphsys" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
width="200px" height="200px" viewBox="0 0 200 200" enable-background="new 0 0 200 200" xml:space="preserve">
|
||||||
|
<path fill="none" d="M68.836,146.216c8.889,5.698,21.647,10.021,35.324,10.021c20.283,0,32.142-10.689,32.142-26.203
|
||||||
|
c0-14.122-8.203-22.557-28.949-30.305c-25.072-9.121-40.577-22.343-40.577-43.759c0-23.93,19.829-41.708,49.688-41.708
|
||||||
|
c15.495,0,27.112,3.646,33.719,7.516l-5.456,16.182c-4.787-2.959-15.031-7.293-28.949-7.293c-20.961,0-28.94,12.536-28.94,23.021
|
||||||
|
c0,14.354,9.344,21.425,30.536,29.627c25.981,10.03,38.971,22.565,38.971,45.123c0,23.717-17.313,44.444-53.557,44.444
|
||||||
|
c-14.809,0-30.991-4.546-39.194-10.039L68.836,146.216z"/>
|
||||||
|
<path fill="none" d="M82.734,183.337v-66.265L33.15,27.158h23.17l22.009,43.104c5.792,11.811,10.66,21.321,15.528,32.207h0.462
|
||||||
|
c4.17-10.207,9.736-20.396,15.764-32.207l22.473-43.104h22.707l-52.131,89.669v66.51H82.734z"/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
225
booleanTests.js
Normal file
225
booleanTests.js
Normal file
|
@ -0,0 +1,225 @@
|
||||||
|
|
||||||
|
paper.install(window);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function runTests() {
|
||||||
|
var caption, pathA, pathB;
|
||||||
|
|
||||||
|
var container = document.getElementById( 'container' );
|
||||||
|
|
||||||
|
caption = prepareTest( 'Overlapping circles', container );
|
||||||
|
pathA = new Path.Circle(new Point(80, 110), 50);
|
||||||
|
pathB = new Path.Circle(new Point(150, 110), 70);
|
||||||
|
testBooleanStatic( pathA, pathB, caption );
|
||||||
|
|
||||||
|
caption = prepareTest( 'Disjoint circles', container );
|
||||||
|
pathA = new Path.Circle(new Point(60, 110), 50);
|
||||||
|
pathB = new Path.Circle(new Point(170, 110), 50);
|
||||||
|
testBooleanStatic( pathA, pathB, caption );
|
||||||
|
|
||||||
|
caption = prepareTest( 'Overlapping circles - enveloping', container );
|
||||||
|
pathA = new Path.Circle(new Point(110, 110), 100);
|
||||||
|
pathB = new Path.Circle(new Point(120, 110), 60);
|
||||||
|
testBooleanStatic( pathA, pathB, caption );
|
||||||
|
|
||||||
|
caption = prepareTest( 'Polygon and square', container );
|
||||||
|
pathA = new Path.RegularPolygon(new Point(80, 110), 12, 80);
|
||||||
|
pathB = new Path.Rectangle(new Point(100, 80), [80, 80] );
|
||||||
|
testBooleanStatic( pathA, pathB, caption );
|
||||||
|
|
||||||
|
caption = prepareTest( 'Circle and square (overlaps exactly on existing segments)', container );
|
||||||
|
pathA = new Path.Circle(new Point(110, 110), 80);
|
||||||
|
pathB = new Path.Rectangle(new Point(110, 110), [80, 80] );
|
||||||
|
testBooleanStatic( pathA, pathB, caption );
|
||||||
|
|
||||||
|
caption = prepareTest( 'Circle and banana (multiple intersections within same curve segment)', container );
|
||||||
|
pathA = new Path.Circle(new Point(80, 110), 80);
|
||||||
|
pathB = new Path.Circle(new Point(130, 110), 80 );
|
||||||
|
pathB.segments[3].point = pathB.segments[3].point.add( [ 0, -120 ] );
|
||||||
|
testBooleanStatic( pathA, pathB, caption );
|
||||||
|
|
||||||
|
caption = prepareTest( 'Overlapping stars 1', container );
|
||||||
|
pathA = new Path.Star(new Point(80, 110), 10, 20, 80);
|
||||||
|
pathB = new Path.Star(new Point(120, 110), 10, 30, 100);
|
||||||
|
testBooleanStatic( pathA, pathB, caption );
|
||||||
|
|
||||||
|
caption = prepareTest( 'Overlapping stars 2', container );
|
||||||
|
pathA = new Path.Star(new Point(110, 110), 20, 20, 80);
|
||||||
|
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( 'Maximum possible intersections between 2 cubic bezier curve segments - 9', container );
|
||||||
|
pathA = new Path();
|
||||||
|
pathA.add( new Segment( [173, 44], [-281, 268], [-86, 152] ) );
|
||||||
|
pathA.add( new Segment( [47, 93], [-89, 100], [240, -239] ) );
|
||||||
|
pathA.closed = true;
|
||||||
|
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] );
|
||||||
|
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' ) );
|
||||||
|
pathA = group.children[0];
|
||||||
|
pathB = group.children[1];
|
||||||
|
testBooleanStatic( pathA, pathB, caption );
|
||||||
|
|
||||||
|
|
||||||
|
function prepareTest( testName, parentNode ){
|
||||||
|
console.log( '\n' + testName );
|
||||||
|
var caption = document.createElement('h3');
|
||||||
|
caption.appendChild( document.createTextNode( testName ) );
|
||||||
|
var canvas = document.createElement('CANVAS');
|
||||||
|
parentNode.appendChild( caption );
|
||||||
|
parentNode.appendChild( canvas );
|
||||||
|
paper.setup( canvas );
|
||||||
|
return caption;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var booleanStyle = {
|
||||||
|
fillColor: new Color( 1, 0, 0, 0.5 ),
|
||||||
|
strokeColor: new Color( 0, 0, 0 ),
|
||||||
|
strokeWidth: 2
|
||||||
|
};
|
||||||
|
|
||||||
|
var pathStyleNormal = {
|
||||||
|
strokeColor: new Color( 0, 0, 0 ),
|
||||||
|
fillColor: new Color( 0, 0, 0, 0.0 ),
|
||||||
|
strokeWidth: 1
|
||||||
|
};
|
||||||
|
|
||||||
|
var pathStyleBoolean = {
|
||||||
|
strokeColor: new Color( 0.8 ),
|
||||||
|
fillColor: new Color( 0, 0, 0, 0.0 ),
|
||||||
|
strokeWidth: 1
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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] );
|
||||||
|
console.time( 'Union' );
|
||||||
|
var boolPathU = boolUnion( _p1U, _p2U );
|
||||||
|
console.timeEnd( 'Union' );
|
||||||
|
|
||||||
|
var _p1I = path1.clone().translate( [560, 0] );
|
||||||
|
var _p2I = path2.clone().translate( [560, 0] );
|
||||||
|
console.time( 'Intersection' );
|
||||||
|
var boolPathI = boolIntersection( _p1I, _p2I );
|
||||||
|
console.timeEnd( 'Intersection' );
|
||||||
|
|
||||||
|
path1.style = path2.style = pathStyleNormal;
|
||||||
|
_p1U.style = _p2U.style = _p1I.style = _p2I.style = pathStyleBoolean;
|
||||||
|
boolPathU.style = boolPathI.style = booleanStyle;
|
||||||
|
} catch( e ){
|
||||||
|
console.error( e.message );
|
||||||
|
if( caption ) { caption.className += ' error'; }
|
||||||
|
paper.project.view.element.className += ' hide';
|
||||||
|
} finally {
|
||||||
|
console.timeEnd( 'Union' );
|
||||||
|
console.timeEnd( 'Intersection' );
|
||||||
|
view.draw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// ==============================================================
|
||||||
|
// On screen debug helpers
|
||||||
|
function markPoint( pnt, t, c, tc, remove ) {
|
||||||
|
if( !pnt ) return;
|
||||||
|
c = c || '#000';
|
||||||
|
if( remove === undefined ){ remove = true; }
|
||||||
|
var cir = new Path.Circle( pnt, 2 );
|
||||||
|
cir.style.fillColor = c;
|
||||||
|
cir.style.strokeColor = tc;
|
||||||
|
if( t !== undefined || t !== null ){
|
||||||
|
var text = new PointText( pnt.add([0, -3]) );
|
||||||
|
text.justification = 'center';
|
||||||
|
text.fillColor = c;
|
||||||
|
text.content = t;
|
||||||
|
if( remove ){
|
||||||
|
text.removeOnMove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if( remove ) {
|
||||||
|
cir.removeOnMove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function annotatePath( path, t, c, tc, remove ) {
|
||||||
|
if( !path ) return;
|
||||||
|
var crvs = path.curves;
|
||||||
|
for (i = crvs.length - 1; i >= 0; i--) {
|
||||||
|
annotateCurve( crvs[i], t, c, tc, remove );
|
||||||
|
}
|
||||||
|
var segs = path.segments;
|
||||||
|
for (i = segs.length - 1; i >= 0; i--) {
|
||||||
|
annotateSegment( segs[i], t, c, tc, remove, true );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function annotateSegment( s, t, c, tc, remove, skipCurves ) {
|
||||||
|
if( !s ) return;
|
||||||
|
c = c || '#000';
|
||||||
|
tc = tc || '#ccc';
|
||||||
|
t = t || s.index;
|
||||||
|
if( remove === undefined ){ remove = true; }
|
||||||
|
var crv = s.curve;
|
||||||
|
var t1 = crv.getNormal( 0 ).normalize( 10 );
|
||||||
|
var p = s.point.clone().add( t1 );
|
||||||
|
var cir = new Path.Circle( s.point, 2 );
|
||||||
|
cir.style.fillColor = c;
|
||||||
|
cir.style.strokeColor = tc;
|
||||||
|
var text = new PointText( p );
|
||||||
|
text.justification = 'center';
|
||||||
|
text.fillColor = c;
|
||||||
|
text.content = t;
|
||||||
|
if( remove ) {
|
||||||
|
cir.removeOnMove();
|
||||||
|
text.removeOnMove();
|
||||||
|
}
|
||||||
|
if( !skipCurves ) {
|
||||||
|
annotateCurve( s.curveIn, null, c, tc, remove );
|
||||||
|
annotateCurve( s.curveOut, null, c, tc, remove );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function annotateCurve( crv, t, c, tc, remove ) {
|
||||||
|
if( !crv ) return;
|
||||||
|
c = c || '#000';
|
||||||
|
tc = tc || '#ccc';
|
||||||
|
t = t || crv.index;
|
||||||
|
if( remove === undefined ){ remove = true; }
|
||||||
|
var p = crv.getPoint( 0.57 );
|
||||||
|
var t1 = crv.getTangent( 0.57 ).normalize( -10 );
|
||||||
|
var p2 = p.clone().add( t1 );
|
||||||
|
var l = new Path.Line( p, p2 ).rotate( 30, p );
|
||||||
|
var l2 = new Path.Line( p, p2 ).rotate( -30, p );
|
||||||
|
p = crv.getPoint( 0.43 );
|
||||||
|
var cir = new Path.Circle( p, 8 );
|
||||||
|
var text = new PointText( p.subtract( [0, -4] ) );
|
||||||
|
text.justification = 'center';
|
||||||
|
text.fillColor = tc;
|
||||||
|
text.content = t;
|
||||||
|
l.style.strokeColor = l2.style.strokeColor = cir.style.fillColor = c;
|
||||||
|
if( remove ) {
|
||||||
|
l.removeOnMove();
|
||||||
|
l2.removeOnMove();
|
||||||
|
cir.removeOnMove();
|
||||||
|
text.removeOnMove();
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue