Przeglądaj źródła

SVGLoader: Implement custom createShapes() method. (#21380)

* added a scanline method to add support
for svg fillrule

* changed ShapePath

* lint pass

* lint pass

* removed edits in package.json

* fix refectoring

* removing useless imports.

* solving hole issue when scanline goes through point

* finish

* added yomboprime's function revisions and his text example

* fix package-lock

* fix unintialized variables

* re modularize

* documentation done
Tom Valkeneers 4 lat temu
rodzic
commit
053b48bd96

+ 11 - 1
docs/examples/en/loaders/SVGLoader.html

@@ -41,7 +41,7 @@
 						depthWrite: false
 					} );
 
-					const shapes = path.toShapes( true );
+					const shapes = SVGLoader.createShapes( path );
 
 					for ( let j = 0; j < shapes.length; j ++ ) {
 
@@ -104,6 +104,16 @@
 		Begin loading from url and call onLoad with the response content.
 		</p>
 
+		<h2>Static Methods</h2>
+
+		<h3>[method:Array createShapes]( [param:ShapePath shape] )</h3>
+		<p>
+		[page:ShapePath shape] — A ShapePath from the array of [page:ShapePath], given as argument in the onLoad function for the load function of [page:SVGLoader].<br />
+		</p>
+		<p>
+		Returns one or more [page:Shape] objects created from the [param:ShapePath shape] provided as an argument in this function.
+		</p>
+
 		<h2>Source</h2>
 
 		<p>

+ 11 - 1
docs/examples/zh/loaders/SVGLoader.html

@@ -41,7 +41,7 @@
 						depthWrite: false
 					} );
 
-					const shapes = path.toShapes( true );
+					const shapes = SVGLoader.createShapes( path );
 
 					for ( let j = 0; j < shapes.length; j ++ ) {
 
@@ -104,6 +104,16 @@
 		Begin loading from url and call onLoad with the response content.
 		</p>
 
+		<h2>Static Methods</h2>
+
+		<h3>[method:Array createShapes]( [param:ShapePath shape] )</h3>
+		<p>
+		[page:ShapePath shape] — A ShapePath from the array of [page:ShapePath], given as argument in the onLoad function for the load function of [page:SVGLoader].<br />
+		</p>
+		<p>
+		Returns one or more [page:Shape] objects created from the [param:ShapePath shape] provided as an argument in this function.
+		</p>
+
 		<h2>Source</h2>
 
 		<p>

+ 1 - 1
editor/js/Loader.js

@@ -492,7 +492,7 @@ function Loader( editor ) {
 							depthWrite: false
 						} );
 
-						var shapes = path.toShapes( true );
+						var shapes = SVGLoader.createShapes( path );
 
 						for ( var j = 0; j < shapes.length; j ++ ) {
 

+ 437 - 0
examples/js/loaders/SVGLoader.js

@@ -1614,6 +1614,443 @@ THREE.SVGLoader.prototype = Object.assign( Object.create( THREE.Loader.prototype
 
 } );
 
+THREE.SVGLoader.createShapes = function ( shapePath ) {
+
+	// Param shapePath: a shapepath as returned by the parse function of this class
+	// Returns Shape object
+
+	const BIGNUMBER = 999999999;
+
+	const IntersectionLocationType = {
+		ORIGIN: 0,
+		DESTINATION: 1,
+		BETWEEN: 2,
+		LEFT: 3,
+		RIGHT: 4,
+		BEHIND: 5,
+		BEYOND: 6
+	};
+
+	const classifyResult = {
+		loc: IntersectionLocationType.ORIGIN,
+		t: 0
+	}
+
+	function findEdgeIntersection( a0, a1, b0, b1 ) {
+
+		var x1 = a0.x;
+		var x2 = a1.x;
+		var x3 = b0.x;
+		var x4 = b1.x;
+		var y1 = a0.y;
+		var y2 = a1.y;
+		var y3 = b0.y;
+		var y4 = b1.y;
+		var nom1 = ( x4 - x3 ) * ( y1 - y3 ) - ( y4 - y3 ) * ( x1 - x3 );
+		var nom2 = ( x2 - x1 ) * ( y1 - y3 ) - ( y2 - y1 ) * ( x1 - x3 );
+		var denom = ( y4 - y3 ) * ( x2 - x1 ) - ( x4 - x3 ) * ( y2 - y1 );
+		var t1 = nom1 / denom;
+		var t2 = nom2 / denom;
+
+		if ( ( ( denom === 0 ) && ( nom1 !== 0 ) ) || ( t1 <= 0 ) || ( t1 >= 1 ) || ( t2 < 0 ) || ( t2 > 1 ) ) {
+
+			//1. lines are parallel or edges don't intersect
+
+			return null;
+
+		} else if ( ( nom1 === 0 ) && ( denom === 0 ) ) {
+
+			//2. lines are colinear
+
+			//check if endpoints of edge2 (b0-b1) lies on edge1 (a0-a1)
+			for ( var i = 0; i < 2; i ++ ) {
+
+				classifyPoint( i === 0 ? b0 : b1, a0, a1 );
+				//find position of this endpoints relatively to edge1
+				if ( classifyResult.loc == IntersectionLocationType.ORIGIN ) {
+
+					var point = ( i === 0 ? b0 : b1 );
+					return { x: point.x, y: point.y, t: classifyResult.t };
+
+				} else if ( classifyResult.loc == IntersectionLocationType.BETWEEN ) {
+
+					var x = + ( ( x1 + classifyResult.t * ( x2 - x1 ) ).toPrecision( 10 ) );
+					var y = + ( ( y1 + classifyResult.t * ( y2 - y1 ) ).toPrecision( 10 ) );
+					return { x: x, y: y, t: classifyResult.t, };
+
+				}
+
+			}
+
+			return null;
+
+		} else {
+
+			//3. edges intersect
+
+			for ( var i = 0; i < 2; i ++ ) {
+
+				classifyPoint( i === 0 ? b0 : b1, a0, a1 );
+
+				if ( classifyResult.loc == IntersectionLocationType.ORIGIN ) {
+
+					var point = ( i === 0 ? b0 : b1 );
+					return { x: point.x, y: point.y, t: classifyResult.t };
+
+				}
+
+			}
+
+			var x = + ( ( x1 + t1 * ( x2 - x1 ) ).toPrecision( 10 ) );
+			var y = + ( ( y1 + t1 * ( y2 - y1 ) ).toPrecision( 10 ) );
+			return { x: x, y: y, t: t1 };
+
+		}
+
+	}
+
+	function classifyPoint( p, edgeStart, edgeEnd ) {
+
+		var ax = edgeEnd.x - edgeStart.x;
+		var ay = edgeEnd.y - edgeStart.y;
+		var bx = p.x - edgeStart.x;
+		var by = p.y - edgeStart.y;
+		var sa = ax * by - bx * ay;
+
+		if ( ( p.x === edgeStart.x ) && ( p.y === edgeStart.y ) ) {
+
+			classifyResult.loc = IntersectionLocationType.ORIGIN;
+			classifyResult.t = 0;
+			return;
+
+		}
+
+		if ( ( p.x === edgeEnd.x ) && ( p.y === edgeEnd.y ) ) {
+
+			classifyResult.loc = IntersectionLocationType.DESTINATION;
+			classifyResult.t = 1;
+			return;
+
+		}
+
+		if ( sa < - Number.EPSILON ) {
+
+			classifyResult.loc = IntersectionLocationType.LEFT;
+			return;
+
+		}
+
+		if ( sa > Number.EPSILON ) {
+
+			classifyResult.loc = IntersectionLocationType.RIGHT;
+			return;
+
+
+		}
+
+		if ( ( ( ax * bx ) < 0 ) || ( ( ay * by ) < 0 ) ) {
+
+			classifyResult.loc = IntersectionLocationType.BEHIND;
+			return;
+
+		}
+
+		if ( ( Math.sqrt( ax * ax + ay * ay ) ) < ( Math.sqrt( bx * bx + by * by ) ) ) {
+
+			classifyResult.loc = IntersectionLocationType.BEYOND;
+			return;
+
+		}
+
+		var t;
+
+		if ( ax !== 0 ) {
+
+			t = bx / ax;
+
+		} else {
+
+			t = by / ay;
+
+		}
+
+		classifyResult.loc = IntersectionLocationType.BETWEEN;
+		classifyResult.t = t;
+
+	}
+
+	function getIntersections( path1, path2 ) {
+
+		const intersectionsRaw = [];
+		const intersections = [];
+
+		for ( let index = 1; index < path1.length; index ++ ) {
+
+			const path1EdgeStart = path1[ index - 1 ];
+			const path1EdgeEnd = path1[ index ];
+
+			for ( let index2 = 1; index2 < path2.length; index2 ++ ) {
+
+				const path2EdgeStart = path2[ index2 - 1 ];
+				const path2EdgeEnd = path2[ index2 ];
+
+				const intersection = findEdgeIntersection( path1EdgeStart, path1EdgeEnd, path2EdgeStart, path2EdgeEnd );
+
+				if ( intersection !== null && intersectionsRaw.find(i => i.t <= intersection.t + Number.EPSILON && i.t >= intersection.t - Number.EPSILON) === undefined ) {
+
+					intersectionsRaw.push( intersection );
+					intersections.push( new THREE.Vector2( intersection.x, intersection.y ) );
+
+				}
+
+			}
+
+		}
+
+		return intersections;
+
+	}
+
+	function getScanlineIntersections( scanline, boundingBox, paths ) {
+
+		const center = new THREE.Vector2();
+		boundingBox.getCenter( center );
+
+		const allIntersections = [];
+
+		paths.forEach( path => {
+
+			// check if the center of the bounding box is in the bounding box of the paths.
+			// this is a pruning method to limit the search of intersections in paths that can't envelop of the current path.
+			// if a path envelops another path. The center of that oter path, has to be inside the bounding box of the enveloping path.
+			if ( path.boundingBox.containsPoint( center ) ) {
+
+				const intersections = getIntersections( scanline, path.points );
+
+				intersections.forEach( p => {
+
+					allIntersections.push( { identifier: path.identifier, isCW: path.isCW, point: p } );
+
+				} );
+
+			}
+
+		} );
+
+		allIntersections.sort( ( i1, i2 ) => {
+
+			return i1.point.x - i2.point.x;
+
+		} );
+
+		return allIntersections;
+
+	}
+
+	function isHoleTo( simplePath, allPaths, scanlineMinX, scanlineMaxX, _fillRule ) {
+
+		if ( _fillRule === null || _fillRule === undefined || _fillRule === '' ) {
+
+			_fillRule = 'nonzero';
+
+		}
+
+		const centerBoundingBox = new THREE.Vector2();
+		simplePath.boundingBox.getCenter( centerBoundingBox );
+
+		const scanline = [ new THREE.Vector2( scanlineMinX, centerBoundingBox.y ), new THREE.Vector2( scanlineMaxX, centerBoundingBox.y ) ];
+
+		const scanlineIntersections = getScanlineIntersections( scanline, simplePath.boundingBox, allPaths );
+
+		scanlineIntersections.sort( ( i1, i2 ) => {
+
+			return i1.point.x - i2.point.x;
+
+		} );
+
+		const baseIntersections = [];
+		const otherIntersections = [];
+
+		scanlineIntersections.forEach( i => {
+
+			if ( i.identifier === simplePath.identifier ) {
+
+				baseIntersections.push( i );
+
+			} else {
+
+				otherIntersections.push( i );
+
+			}
+
+		} );
+
+		const firstXOfPath = baseIntersections[ 0 ].point.x;
+
+		// build up the path hierarchy
+		const stack = [];
+		let i = 0;
+
+		while ( i < otherIntersections.length && otherIntersections[ i ].point.x < firstXOfPath ) {
+
+			if ( stack.length > 0 && stack[ stack.length - 1 ] === otherIntersections[ i ].identifier ) {
+
+				stack.pop();
+
+			} else {
+
+				stack.push( otherIntersections[ i ].identifier );
+
+			}
+
+			i ++;
+
+		}
+
+		stack.push( simplePath.identifier );
+
+		if ( _fillRule === 'evenodd' ) {
+
+			const isHole = stack.length % 2 === 0 ? true : false;
+			const isHoleFor = stack[ stack.length - 2 ];
+
+			return { identifier: simplePath.identifier, isHole: isHole, for: isHoleFor };
+
+		} else if ( _fillRule === 'nonzero' ) {
+
+			// check if path is a hole by counting the amount of paths with alternating rotations it has to cross.
+			let isHole = true;
+			let isHoleFor = null;
+			let lastCWValue = null;
+
+			for ( let i = 0; i < stack.length; i ++ ) {
+
+				const identifier = stack[ i ];
+				if ( isHole ) {
+
+					lastCWValue = allPaths[ identifier ].isCW;
+					isHole = false;
+					isHoleFor = identifier;
+
+				} else if ( lastCWValue !== allPaths[ identifier ].isCW ) {
+
+					lastCWValue = allPaths[ identifier ].isCW;
+					isHole = true;
+
+				}
+
+			}
+
+			return { identifier: simplePath.identifier, isHole: isHole, for: isHoleFor };
+
+		} else {
+
+			console.warn( 'fill-rule: "' + _fillRule + '" is currently not implemented.' );
+
+		}
+
+	}
+
+	// check for self intersecting paths
+	// TODO
+
+	// check intersecting paths
+	// TODO
+
+	// prepare paths for hole detection
+	let identifier = 0;
+
+	let scanlineMinX = BIGNUMBER;
+	let scanlineMaxX = - BIGNUMBER;
+
+	let simplePaths = shapePath.subPaths.map( p => {
+
+		const points = p.getPoints();
+		let maxY = - BIGNUMBER;
+		let minY = BIGNUMBER;
+		let maxX = - BIGNUMBER;
+		let minX = BIGNUMBER;
+
+      	//points.forEach(p => p.y *= -1);
+
+		for ( let i = 0; i < points.length; i ++ ) {
+
+			const p = points[ i ];
+
+			if ( p.y > maxY ) {
+
+				maxY = p.y;
+
+			}
+
+			if ( p.y < minY ) {
+
+				minY = p.y;
+
+			}
+
+			if ( p.x > maxX ) {
+
+				maxX = p.x;
+
+			}
+
+			if ( p.x < minX ) {
+
+				minX = p.x;
+
+			}
+
+		}
+
+		//
+		if ( scanlineMaxX <= maxX ) {
+
+			scanlineMaxX = maxX + 1;
+
+		}
+
+		if ( scanlineMinX >= minX ) {
+
+			scanlineMinX = minX - 1;
+
+		}
+
+		return { points: points, isCW: THREE.ShapeUtils.isClockWise( points ), identifier: identifier ++, boundingBox: new THREE.Box2( new THREE.Vector2( minX, minY ), new THREE.Vector2( maxX, maxY ) ) };
+
+	} );
+
+	simplePaths = simplePaths.filter( sp => sp.points.length > 0 );
+
+	// check if path is solid or a hole
+	const isAHole = simplePaths.map( p => isHoleTo( p, simplePaths, scanlineMinX, scanlineMaxX, shapePath.userData.style.fillRule ) );
+
+
+	const shapesToReturn = [];
+	simplePaths.forEach( p => {
+
+		const amIAHole = isAHole[ p.identifier ];
+
+		if ( ! amIAHole.isHole ) {
+
+			const shape = new THREE.Shape( p.points );
+			const holes = isAHole.filter( h => h.isHole && h.for === p.identifier );
+			holes.forEach( h => {
+
+				const path = simplePaths[ h.identifier ];
+				shape.holes.push( new THREE.Path( path.points ) );
+
+			} );
+			shapesToReturn.push( shape );
+
+		}
+
+	} );
+
+	return shapesToReturn;
+
+};
+
 THREE.SVGLoader.getStrokeStyle = function ( width, color, lineJoin, lineCap, miterLimit ) {
 
 	// Param width: Stroke width

+ 440 - 0
examples/jsm/loaders/SVGLoader.js

@@ -1,11 +1,14 @@
 import {
+	Box2,
 	BufferGeometry,
 	FileLoader,
 	Float32BufferAttribute,
 	Loader,
 	Matrix3,
 	Path,
+	Shape,
 	ShapePath,
+	ShapeUtils,
 	Vector2,
 	Vector3
 } from '../../../build/three.module.js';
@@ -1626,6 +1629,443 @@ SVGLoader.prototype = Object.assign( Object.create( Loader.prototype ), {
 
 } );
 
+SVGLoader.createShapes = function ( shapePath ) {
+
+	// Param shapePath: a shapepath as returned by the parse function of this class
+	// Returns Shape object
+
+	const BIGNUMBER = 999999999;
+
+	const IntersectionLocationType = {
+		ORIGIN: 0,
+		DESTINATION: 1,
+		BETWEEN: 2,
+		LEFT: 3,
+		RIGHT: 4,
+		BEHIND: 5,
+		BEYOND: 6
+	};
+
+	const classifyResult = {
+		loc: IntersectionLocationType.ORIGIN,
+		t: 0
+	}
+
+	function findEdgeIntersection( a0, a1, b0, b1 ) {
+
+		var x1 = a0.x;
+		var x2 = a1.x;
+		var x3 = b0.x;
+		var x4 = b1.x;
+		var y1 = a0.y;
+		var y2 = a1.y;
+		var y3 = b0.y;
+		var y4 = b1.y;
+		var nom1 = ( x4 - x3 ) * ( y1 - y3 ) - ( y4 - y3 ) * ( x1 - x3 );
+		var nom2 = ( x2 - x1 ) * ( y1 - y3 ) - ( y2 - y1 ) * ( x1 - x3 );
+		var denom = ( y4 - y3 ) * ( x2 - x1 ) - ( x4 - x3 ) * ( y2 - y1 );
+		var t1 = nom1 / denom;
+		var t2 = nom2 / denom;
+
+		if ( ( ( denom === 0 ) && ( nom1 !== 0 ) ) || ( t1 <= 0 ) || ( t1 >= 1 ) || ( t2 < 0 ) || ( t2 > 1 ) ) {
+
+			//1. lines are parallel or edges don't intersect
+
+			return null;
+
+		} else if ( ( nom1 === 0 ) && ( denom === 0 ) ) {
+
+			//2. lines are colinear
+
+			//check if endpoints of edge2 (b0-b1) lies on edge1 (a0-a1)
+			for ( var i = 0; i < 2; i ++ ) {
+
+				classifyPoint( i === 0 ? b0 : b1, a0, a1 );
+				//find position of this endpoints relatively to edge1
+				if ( classifyResult.loc == IntersectionLocationType.ORIGIN ) {
+
+					var point = ( i === 0 ? b0 : b1 );
+					return { x: point.x, y: point.y, t: classifyResult.t };
+
+				} else if ( classifyResult.loc == IntersectionLocationType.BETWEEN ) {
+
+					var x = + ( ( x1 + classifyResult.t * ( x2 - x1 ) ).toPrecision( 10 ) );
+					var y = + ( ( y1 + classifyResult.t * ( y2 - y1 ) ).toPrecision( 10 ) );
+					return { x: x, y: y, t: classifyResult.t, };
+
+				}
+
+			}
+
+			return null;
+
+		} else {
+
+			//3. edges intersect
+
+			for ( var i = 0; i < 2; i ++ ) {
+
+				classifyPoint( i === 0 ? b0 : b1, a0, a1 );
+
+				if ( classifyResult.loc == IntersectionLocationType.ORIGIN ) {
+
+					var point = ( i === 0 ? b0 : b1 );
+					return { x: point.x, y: point.y, t: classifyResult.t };
+
+				}
+
+			}
+
+			var x = + ( ( x1 + t1 * ( x2 - x1 ) ).toPrecision( 10 ) );
+			var y = + ( ( y1 + t1 * ( y2 - y1 ) ).toPrecision( 10 ) );
+			return { x: x, y: y, t: t1 };
+
+		}
+
+	}
+
+	function classifyPoint( p, edgeStart, edgeEnd ) {
+
+		var ax = edgeEnd.x - edgeStart.x;
+		var ay = edgeEnd.y - edgeStart.y;
+		var bx = p.x - edgeStart.x;
+		var by = p.y - edgeStart.y;
+		var sa = ax * by - bx * ay;
+
+		if ( ( p.x === edgeStart.x ) && ( p.y === edgeStart.y ) ) {
+
+			classifyResult.loc = IntersectionLocationType.ORIGIN;
+			classifyResult.t = 0;
+			return;
+
+		}
+
+		if ( ( p.x === edgeEnd.x ) && ( p.y === edgeEnd.y ) ) {
+
+			classifyResult.loc = IntersectionLocationType.DESTINATION;
+			classifyResult.t = 1;
+			return;
+
+		}
+
+		if ( sa < - Number.EPSILON ) {
+
+			classifyResult.loc = IntersectionLocationType.LEFT;
+			return;
+
+		}
+
+		if ( sa > Number.EPSILON ) {
+
+			classifyResult.loc = IntersectionLocationType.RIGHT;
+			return;
+
+
+		}
+
+		if ( ( ( ax * bx ) < 0 ) || ( ( ay * by ) < 0 ) ) {
+
+			classifyResult.loc = IntersectionLocationType.BEHIND;
+			return;
+
+		}
+
+		if ( ( Math.sqrt( ax * ax + ay * ay ) ) < ( Math.sqrt( bx * bx + by * by ) ) ) {
+
+			classifyResult.loc = IntersectionLocationType.BEYOND;
+			return;
+
+		}
+
+		var t;
+
+		if ( ax !== 0 ) {
+
+			t = bx / ax;
+
+		} else {
+
+			t = by / ay;
+
+		}
+
+		classifyResult.loc = IntersectionLocationType.BETWEEN;
+		classifyResult.t = t;
+
+	}
+
+	function getIntersections( path1, path2 ) {
+
+		const intersectionsRaw = [];
+		const intersections = [];
+
+		for ( let index = 1; index < path1.length; index ++ ) {
+
+			const path1EdgeStart = path1[ index - 1 ];
+			const path1EdgeEnd = path1[ index ];
+
+			for ( let index2 = 1; index2 < path2.length; index2 ++ ) {
+
+				const path2EdgeStart = path2[ index2 - 1 ];
+				const path2EdgeEnd = path2[ index2 ];
+
+				const intersection = findEdgeIntersection( path1EdgeStart, path1EdgeEnd, path2EdgeStart, path2EdgeEnd );
+
+				if ( intersection !== null && intersectionsRaw.find(i => i.t <= intersection.t + Number.EPSILON && i.t >= intersection.t - Number.EPSILON) === undefined ) {
+
+					intersectionsRaw.push( intersection );
+					intersections.push( new Vector2( intersection.x, intersection.y ) );
+
+				}
+
+			}
+
+		}
+
+		return intersections;
+
+	}
+
+	function getScanlineIntersections( scanline, boundingBox, paths ) {
+
+		const center = new Vector2();
+		boundingBox.getCenter( center );
+
+		const allIntersections = [];
+
+		paths.forEach( path => {
+
+			// check if the center of the bounding box is in the bounding box of the paths.
+			// this is a pruning method to limit the search of intersections in paths that can't envelop of the current path.
+			// if a path envelops another path. The center of that oter path, has to be inside the bounding box of the enveloping path.
+			if ( path.boundingBox.containsPoint( center ) ) {
+
+				const intersections = getIntersections( scanline, path.points );
+
+				intersections.forEach( p => {
+
+					allIntersections.push( { identifier: path.identifier, isCW: path.isCW, point: p } );
+
+				} );
+
+			}
+
+		} );
+
+		allIntersections.sort( ( i1, i2 ) => {
+
+			return i1.point.x - i2.point.x;
+
+		} );
+
+		return allIntersections;
+
+	}
+
+	function isHoleTo( simplePath, allPaths, scanlineMinX, scanlineMaxX, _fillRule ) {
+
+		if ( _fillRule === null || _fillRule === undefined || _fillRule === '' ) {
+
+			_fillRule = 'nonzero';
+
+		}
+
+		const centerBoundingBox = new Vector2();
+		simplePath.boundingBox.getCenter( centerBoundingBox );
+
+		const scanline = [ new Vector2( scanlineMinX, centerBoundingBox.y ), new Vector2( scanlineMaxX, centerBoundingBox.y ) ];
+
+		const scanlineIntersections = getScanlineIntersections( scanline, simplePath.boundingBox, allPaths );
+
+		scanlineIntersections.sort( ( i1, i2 ) => {
+
+			return i1.point.x - i2.point.x;
+
+		} );
+
+		const baseIntersections = [];
+		const otherIntersections = [];
+
+		scanlineIntersections.forEach( i => {
+
+			if ( i.identifier === simplePath.identifier ) {
+
+				baseIntersections.push( i );
+
+			} else {
+
+				otherIntersections.push( i );
+
+			}
+
+		} );
+
+		const firstXOfPath = baseIntersections[ 0 ].point.x;
+
+		// build up the path hierarchy
+		const stack = [];
+		let i = 0;
+
+		while ( i < otherIntersections.length && otherIntersections[ i ].point.x < firstXOfPath ) {
+
+			if ( stack.length > 0 && stack[ stack.length - 1 ] === otherIntersections[ i ].identifier ) {
+
+				stack.pop();
+
+			} else {
+
+				stack.push( otherIntersections[ i ].identifier );
+
+			}
+
+			i ++;
+
+		}
+
+		stack.push( simplePath.identifier );
+
+		if ( _fillRule === 'evenodd' ) {
+
+			const isHole = stack.length % 2 === 0 ? true : false;
+			const isHoleFor = stack[ stack.length - 2 ];
+
+			return { identifier: simplePath.identifier, isHole: isHole, for: isHoleFor };
+
+		} else if ( _fillRule === 'nonzero' ) {
+
+			// check if path is a hole by counting the amount of paths with alternating rotations it has to cross.
+			let isHole = true;
+			let isHoleFor = null;
+			let lastCWValue = null;
+
+			for ( let i = 0; i < stack.length; i ++ ) {
+
+				const identifier = stack[ i ];
+				if ( isHole ) {
+
+					lastCWValue = allPaths[ identifier ].isCW;
+					isHole = false;
+					isHoleFor = identifier;
+
+				} else if ( lastCWValue !== allPaths[ identifier ].isCW ) {
+
+					lastCWValue = allPaths[ identifier ].isCW;
+					isHole = true;
+
+				}
+
+			}
+
+			return { identifier: simplePath.identifier, isHole: isHole, for: isHoleFor };
+
+		} else {
+
+			console.warn( 'fill-rule: "' + _fillRule + '" is currently not implemented.' );
+
+		}
+
+	}
+
+	// check for self intersecting paths
+	// TODO
+
+	// check intersecting paths
+	// TODO
+
+	// prepare paths for hole detection
+	let identifier = 0;
+
+	let scanlineMinX = BIGNUMBER;
+	let scanlineMaxX = - BIGNUMBER;
+
+	let simplePaths = shapePath.subPaths.map( p => {
+
+		const points = p.getPoints();
+		let maxY = - BIGNUMBER;
+		let minY = BIGNUMBER;
+		let maxX = - BIGNUMBER;
+		let minX = BIGNUMBER;
+
+      	//points.forEach(p => p.y *= -1);
+
+		for ( let i = 0; i < points.length; i ++ ) {
+
+			const p = points[ i ];
+
+			if ( p.y > maxY ) {
+
+				maxY = p.y;
+
+			}
+
+			if ( p.y < minY ) {
+
+				minY = p.y;
+
+			}
+
+			if ( p.x > maxX ) {
+
+				maxX = p.x;
+
+			}
+
+			if ( p.x < minX ) {
+
+				minX = p.x;
+
+			}
+
+		}
+
+		//
+		if ( scanlineMaxX <= maxX ) {
+
+			scanlineMaxX = maxX + 1;
+
+		}
+
+		if ( scanlineMinX >= minX ) {
+
+			scanlineMinX = minX - 1;
+
+		}
+
+		return { points: points, isCW: ShapeUtils.isClockWise( points ), identifier: identifier ++, boundingBox: new Box2( new Vector2( minX, minY ), new Vector2( maxX, maxY ) ) };
+
+	} );
+
+	simplePaths = simplePaths.filter( sp => sp.points.length > 0 );
+
+	// check if path is solid or a hole
+	const isAHole = simplePaths.map( p => isHoleTo( p, simplePaths, scanlineMinX, scanlineMaxX, shapePath.userData.style.fillRule ) );
+
+
+	const shapesToReturn = [];
+	simplePaths.forEach( p => {
+
+		const amIAHole = isAHole[ p.identifier ];
+
+		if ( ! amIAHole.isHole ) {
+
+			const shape = new Shape( p.points );
+			const holes = isAHole.filter( h => h.isHole && h.for === p.identifier );
+			holes.forEach( h => {
+
+				const path = simplePaths[ h.identifier ];
+				shape.holes.push( new Path( path.points ) );
+
+			} );
+			shapesToReturn.push( shape );
+
+		}
+
+	} );
+
+	return shapesToReturn;
+
+};
+
 SVGLoader.getStrokeStyle = function ( width, color, lineJoin, lineCap, miterLimit ) {
 
 	// Param width: Stroke width

+ 34 - 0
examples/models/svg/tests/testDefs/defs5.svg

@@ -0,0 +1,34 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<!-- This file was generated by dvisvgm 2.6.3 -->
+<svg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' width='83.275349pt' height='9.005688pt' viewBox='130.227832 -6.861476 83.275349 9.005688'>
+<defs>
+<path id='g0-65' d='M0.313823 -0.258032V-0.006974H1.862017V-0.258032C1.673724 -0.258032 1.366874 -0.334745 1.366874 -0.585803C1.366874 -0.760149 1.499377 -0.969365 1.562142 -1.150685C1.652802 -1.366874 1.687671 -1.590037 1.827148 -1.590037H3.724035C3.744956 -1.499377 3.793773 -1.408717 3.828643 -1.318057C3.898381 -1.12279 3.982067 -0.927522 4.05878 -0.732254L4.128518 -0.54396C4.142466 -0.509091 4.14944 -0.481196 4.14944 -0.446326C4.14944 -0.265006 3.835616 -0.258032 3.633375 -0.258032H3.591532V-0.006974H4.442341L4.581818 -0.034869H5.300125L5.558157 -0.006974V-0.258032H5.348941C5.188543 -0.258032 5.028144 -0.265006 4.937484 -0.348692C4.839851 -0.439352 4.811955 -0.564882 4.763138 -0.683437C4.2401 -2.015442 3.717061 -3.354421 3.187049 -4.686426C3.138232 -4.811955 3.11731 -4.979328 2.93599 -4.979328S2.733748 -4.811955 2.684932 -4.6934C2.196762 -3.417186 1.638854 -2.147945 1.17858 -0.857783C0.99726 -0.355666 0.697385 -0.265006 0.313823 -0.258032ZM1.834122 -1.841096L2.726775 -4.100623L3.619427 -1.841096H1.834122Z'/>
+<path id='g1-69' d='M0.328767 -6.784558V-6.475716C0.458281 -6.475716 0.587796 -6.485679 0.707347 -6.485679C1.066002 -6.485679 1.354919 -6.435866 1.354919 -6.03736V-0.747198C1.354919 -0.368618 1.026152 -0.308842 0.687422 -0.308842C0.557908 -0.308842 0.438356 -0.318804 0.328767 -0.318804V-0.009963H6.067248L6.485679 -2.580324H6.236613C6.087173 -1.673724 5.967621 -0.67746 4.881694 -0.398506C4.572852 -0.328767 4.254047 -0.318804 3.935243 -0.318804H2.660025C2.460772 -0.318804 2.241594 -0.33873 2.241594 -0.637609V-3.377335H3.158157C3.506849 -3.377335 3.895392 -3.347447 4.084682 -3.038605C4.204234 -2.82939 4.214197 -2.560399 4.214197 -2.311333V-2.211706H4.463263V-4.851806H4.214197V-4.752179C4.214197 -4.214197 4.094645 -3.785803 3.506849 -3.706102C3.317559 -3.686177 3.128269 -3.686177 2.938979 -3.686177H2.241594V-6.127024C2.241594 -6.465753 2.450809 -6.475716 2.660025 -6.475716H3.855542C4.443337 -6.475716 5.17061 -6.475716 5.559153 -5.947696C5.858032 -5.549191 5.907846 -5.021171 5.967621 -4.542964H6.216687L5.937733 -6.784558H0.328767Z'/>
+<path id='g1-76' d='M0.328767 -6.814446V-6.505604C0.458281 -6.505604 0.587796 -6.515567 0.707347 -6.515567C1.066002 -6.515567 1.354919 -6.465753 1.354919 -6.067248V-0.747198C1.354919 -0.368618 1.026152 -0.308842 0.687422 -0.308842C0.557908 -0.308842 0.438356 -0.318804 0.328767 -0.318804V-0.009963H5.519303L5.798257 -2.580324H5.549191C5.469489 -1.77335 5.339975 -0.617684 4.194271 -0.398506C3.905355 -0.318804 3.616438 -0.318804 3.327522 -0.318804H2.590286C2.391034 -0.33873 2.241594 -0.388543 2.241594 -0.657534V-6.067248C2.241594 -6.465753 2.600249 -6.515567 2.929016 -6.515567C3.038605 -6.515567 3.138232 -6.505604 3.227895 -6.505604H3.516812V-6.814446H0.328767Z'/>
+<path id='g1-84' d='M0.547945 -6.75467L0.358655 -4.513076H0.607721C0.647572 -5.031133 0.67746 -5.738481 0.986301 -6.07721C1.325031 -6.425903 1.892902 -6.455791 2.351183 -6.455791H2.600249C2.86924 -6.455791 3.148194 -6.455791 3.148194 -6.117061V-0.767123C3.148194 -0.368618 2.749689 -0.308842 2.391034 -0.308842C2.261519 -0.308842 2.132005 -0.318804 2.032379 -0.318804H1.703611V-0.009963H5.479452V-0.318804H4.801993C4.41345 -0.318804 4.034869 -0.358655 4.034869 -0.767123V-6.117061C4.034869 -6.405978 4.204234 -6.455791 4.582814 -6.455791H4.83188C5.290162 -6.455791 5.858032 -6.425903 6.196762 -6.07721C6.505604 -5.738481 6.535492 -5.031133 6.575342 -4.513076H6.824408L6.635118 -6.75467H0.547945Z'/>
+<path id='g1-88' d='M0.368618 -6.814446V-6.505604H0.607721C1.046077 -6.505604 1.275218 -6.445828 1.514321 -6.097136L3.247821 -3.486924C3.267746 -3.457036 3.297634 -3.427148 3.297634 -3.387298C3.297634 -3.317559 3.178082 -3.20797 3.138232 -3.118306C2.709838 -2.500623 2.30137 -1.882939 1.872976 -1.255293C1.703611 -1.016189 1.564134 -0.767123 1.315068 -0.597758C1.006227 -0.388543 0.607721 -0.318804 0.239103 -0.318804V-0.009963H2.610212V-0.318804C2.34122 -0.318804 2.002491 -0.458281 2.002491 -0.777086C2.002491 -0.926526 2.102117 -1.026152 2.181818 -1.145704C2.6401 -1.793275 3.068493 -2.450809 3.506849 -3.108344C3.636364 -2.879203 3.795766 -2.660025 3.945205 -2.440847C4.194271 -2.052304 4.4533 -1.673724 4.712329 -1.285181L5.021171 -0.816936C5.061021 -0.757161 5.13076 -0.67746 5.13076 -0.607721C5.100872 -0.408468 4.702366 -0.318804 4.483188 -0.318804V-0.009963H7.232877V-0.318804H6.963885C6.505604 -0.318804 6.286426 -0.408468 6.047323 -0.787049C5.449564 -1.713574 4.782067 -2.620174 4.204234 -3.566625C4.134496 -3.656289 4.084682 -3.765878 4.004981 -3.845579V-3.855542C4.064757 -3.92528 4.11457 -4.004981 4.164384 -4.07472L4.403487 -4.433375C4.652553 -4.801993 4.901619 -5.160648 5.150685 -5.529265C5.32005 -5.788294 5.479452 -6.057285 5.748443 -6.236613C6.057285 -6.445828 6.445828 -6.505604 6.814446 -6.505604V-6.814446H4.433375V-6.505604C4.692403 -6.505604 5.051059 -6.356164 5.051059 -6.047323C5.051059 -5.867995 4.811955 -5.579078 4.662516 -5.379826C4.483188 -5.100872 4.293898 -4.83188 4.104608 -4.552927C4.004981 -4.41345 3.92528 -4.26401 3.815691 -4.134496C3.536737 -4.652553 3.138232 -5.160648 2.809465 -5.668742C2.699875 -5.828144 2.470735 -6.097136 2.470735 -6.216687C2.500623 -6.41594 2.899128 -6.505604 3.118306 -6.505604V-6.814446H0.368618Z'/>
+<path id='g1-101' d='M1.115816 -2.311333H3.985056C4.094645 -2.311333 4.144458 -2.381071 4.144458 -2.49066C4.144458 -3.5467 3.496887 -4.463263 2.381071 -4.463263C1.155666 -4.463263 0.278954 -3.377335 0.278954 -2.191781C0.278954 -1.275218 0.787049 -0.458281 1.663761 -0.059776C1.892902 0.039851 2.161893 0.099626 2.410959 0.099626H2.440847C3.20797 0.099626 3.845579 -0.328767 4.11457 -1.09589C4.124533 -1.125778 4.124533 -1.165629 4.124533 -1.195517C4.124533 -1.265255 4.084682 -1.315068 4.014944 -1.315068C3.865504 -1.315068 3.805729 -0.986301 3.745953 -0.876712C3.496887 -0.438356 3.01868 -0.14944 2.500623 -0.14944C2.132005 -0.14944 1.8132 -0.358655 1.544209 -0.627646C1.145704 -1.085928 1.115816 -1.733499 1.115816 -2.311333ZM1.125778 -2.520548C1.125778 -3.287671 1.534247 -4.244085 2.351183 -4.244085H2.400996C3.377335 -4.154421 3.367372 -3.118306 3.466999 -2.520548H1.125778Z'/>
+<path id='g1-104' d='M0.318804 -6.814446V-6.505604H0.468244C0.836862 -6.505604 1.09589 -6.465753 1.09589 -5.987547V-0.956413C1.09589 -0.886675 1.105853 -0.816936 1.105853 -0.737235C1.105853 -0.358655 0.836862 -0.318804 0.557908 -0.318804H0.318804V-0.009963H2.570361V-0.318804H2.311333C2.032379 -0.318804 1.793275 -0.358655 1.793275 -0.727273V-2.550436C1.793275 -3.267746 2.161893 -4.184309 3.148194 -4.184309C3.785803 -4.184309 3.845579 -3.496887 3.845579 -3.068493V-0.687422C3.845579 -0.348692 3.556663 -0.318804 3.247821 -0.318804H3.068493V-0.009963H5.32005V-0.318804H5.070984C4.801993 -0.318804 4.542964 -0.358655 4.542964 -0.697385V-2.879203C4.542964 -3.20797 4.533001 -3.536737 4.383562 -3.835616C4.144458 -4.273973 3.646326 -4.403487 3.188045 -4.403487C2.610212 -4.403487 1.96264 -4.014944 1.77335 -3.457036L1.763387 -6.924035L0.318804 -6.814446Z'/>
+<path id='g1-105' d='M0.368618 -4.293898V-3.985056H0.557908C0.846824 -3.985056 1.105853 -3.945205 1.105853 -3.486924V-0.727273C1.105853 -0.37858 0.926526 -0.318804 0.328767 -0.318804V-0.009963H2.470735V-0.318804H2.271482C2.012453 -0.318804 1.77335 -0.348692 1.77335 -0.667497V-4.403487L0.368618 -4.293898ZM1.205479 -6.665006C0.956413 -6.635118 0.757161 -6.41594 0.757161 -6.146949C0.757161 -5.858032 1.006227 -5.618929 1.285181 -5.618929C1.554172 -5.618929 1.8132 -5.838107 1.8132 -6.146949C1.8132 -6.435866 1.564134 -6.674969 1.285181 -6.674969C1.255293 -6.674969 1.235367 -6.665006 1.205479 -6.665006Z'/>
+<path id='g1-109' d='M0.318804 -4.293898V-3.985056H0.468244C0.797011 -3.985056 1.09589 -3.955168 1.09589 -3.486924V-0.737235C1.09589 -0.328767 0.816936 -0.318804 0.37858 -0.318804H0.318804V-0.009963H2.570361V-0.318804H2.311333C2.032379 -0.318804 1.793275 -0.358655 1.793275 -0.727273V-2.550436C1.793275 -3.277709 2.191781 -4.184309 3.148194 -4.184309C3.785803 -4.184309 3.855542 -3.526775 3.855542 -3.068493V-0.697385C3.855542 -0.33873 3.556663 -0.318804 3.227895 -0.318804H3.078456V-0.009963H5.330012V-0.318804H5.070984C4.79203 -0.318804 4.552927 -0.358655 4.552927 -0.727273V-2.550436C4.552927 -3.277709 4.951432 -4.184309 5.907846 -4.184309C6.545455 -4.184309 6.615193 -3.526775 6.615193 -3.068493V-0.697385C6.615193 -0.33873 6.316314 -0.318804 5.987547 -0.318804H5.838107V-0.009963H8.089664V-0.318804H7.880448C7.581569 -0.318804 7.312578 -0.348692 7.312578 -0.697385V-2.948941C7.312578 -3.317559 7.292653 -3.646326 7.053549 -3.985056C6.794521 -4.313823 6.366127 -4.403487 5.967621 -4.403487C5.32005 -4.403487 4.79203 -4.024907 4.523039 -3.447073C4.333748 -4.154421 3.88543 -4.403487 3.198007 -4.403487C2.560399 -4.403487 1.952677 -4.004981 1.743462 -3.387298L1.733499 -4.403487L0.318804 -4.293898Z'/>
+<path id='g1-111' d='M2.34122 -4.463263C1.085928 -4.333748 0.278954 -3.277709 0.278954 -2.122042C0.278954 -0.996264 1.165629 0.099626 2.49066 0.099626C3.686177 0.099626 4.692403 -0.876712 4.692403 -2.132005C4.692403 -3.317559 3.795766 -4.473225 2.470735 -4.473225C2.430884 -4.473225 2.381071 -4.463263 2.34122 -4.463263ZM1.115816 -1.892902V-2.331258C1.115816 -3.088418 1.354919 -4.244085 2.480697 -4.244085C3.287671 -4.244085 3.745953 -3.566625 3.835616 -2.809465C3.855542 -2.590286 3.855542 -2.381071 3.855542 -2.161893C3.855542 -1.514321 3.785803 -0.707347 3.148194 -0.33873C2.948941 -0.209215 2.729763 -0.14944 2.500623 -0.14944C1.77335 -0.14944 1.265255 -0.71731 1.155666 -1.494396C1.135741 -1.62391 1.135741 -1.763387 1.115816 -1.892902Z'/>
+<path id='g1-115' d='M1.753425 -4.463263C1.384807 -4.423412 0.996264 -4.353674 0.707347 -4.104608C0.468244 -3.895392 0.328767 -3.556663 0.328767 -3.237858C0.328767 -1.534247 3.098381 -2.500623 3.098381 -1.006227C3.098381 -0.398506 2.550436 -0.119552 2.002491 -0.119552C1.24533 -0.119552 0.737235 -0.657534 0.597758 -1.514321C0.577833 -1.603985 0.56787 -1.693649 0.448319 -1.693649C0.368618 -1.693649 0.328767 -1.633873 0.328767 -1.564134V0.009963C0.33873 0.059776 0.368618 0.089664 0.418431 0.099626H0.438356C0.597758 0.099626 0.757161 -0.268991 0.876712 -0.298879H0.886675C0.966376 -0.298879 1.235367 -0.049813 1.444583 0.019925C1.613948 0.079701 1.793275 0.099626 1.972603 0.099626C2.799502 0.099626 3.58655 -0.33873 3.58655 -1.255293C3.58655 -1.912827 3.108344 -2.450809 2.450809 -2.620174C1.833126 -2.789539 0.816936 -2.789539 0.816936 -3.516812C0.816936 -4.124533 1.484433 -4.273973 1.92279 -4.273973C2.410959 -4.273973 3.088418 -4.004981 3.088418 -3.148194C3.088418 -3.058531 3.098381 -2.978829 3.20797 -2.978829C3.307597 -2.978829 3.347447 -3.058531 3.347447 -3.158157C3.347447 -3.20797 3.337484 -3.257783 3.337484 -3.297634V-4.323786C3.337484 -4.383562 3.307597 -4.463263 3.227895 -4.463263C3.068493 -4.463263 2.968867 -4.214197 2.859278 -4.214197H2.849315C2.769614 -4.214197 2.610212 -4.343711 2.49066 -4.383562C2.311333 -4.443337 2.11208 -4.473225 1.92279 -4.473225C1.863014 -4.473225 1.8132 -4.463263 1.753425 -4.463263Z'/>
+</defs>
+<g id='page1'>
+<use x='130.227832' y='0' xlink:href='#g1-84'/>
+<use x='137.421316' y='0' xlink:href='#g1-104'/>
+<use x='142.954764' y='0' xlink:href='#g1-105'/>
+<use x='145.721489' y='0' xlink:href='#g1-115'/>
+<use x='152.970307' y='0' xlink:href='#g1-105'/>
+<use x='155.737031' y='0' xlink:href='#g1-115'/>
+<use x='162.985849' y='0' xlink:href='#g1-115'/>
+<use x='166.914598' y='0' xlink:href='#g1-111'/>
+<use x='171.894702' y='0' xlink:href='#g1-109'/>
+<use x='180.194875' y='0' xlink:href='#g1-101'/>
+<use x='187.941703' y='0' xlink:href='#g1-76'/>
+<use x='190.581158' y='-2.058443' xlink:href='#g0-65'/>
+<use x='194.966477' y='0' xlink:href='#g1-84'/>
+<use x='200.499576' y='2.144211' xlink:href='#g1-69'/>
+<use x='206.033025' y='0' xlink:href='#g1-88'/>
+</g>
+</svg>

+ 2 - 1
examples/webgl_loader_svg.html

@@ -105,6 +105,7 @@
 					"Defs2": 'models/svg/tests/testDefs/Svg-defs2.svg',
 					"Defs3": 'models/svg/tests/testDefs/Wave-defs.svg',
 					"Defs4": 'models/svg/tests/testDefs/defs4.svg',
+					"Defs5": 'models/svg/tests/testDefs/defs5.svg',
 					"Zero Radius": 'models/svg/zero-radius.svg'
 
 				} ).name( 'SVG File' ).onChange( update );
@@ -168,7 +169,7 @@
 								wireframe: guiData.fillShapesWireframe
 							} );
 
-							const shapes = path.toShapes( true );
+							const shapes = SVGLoader.createShapes( path );
 
 							for ( let j = 0; j < shapes.length; j ++ ) {