|
@@ -1,11 +1,14 @@
|
|
import {
|
|
import {
|
|
|
|
+ Box2,
|
|
BufferGeometry,
|
|
BufferGeometry,
|
|
FileLoader,
|
|
FileLoader,
|
|
Float32BufferAttribute,
|
|
Float32BufferAttribute,
|
|
Loader,
|
|
Loader,
|
|
Matrix3,
|
|
Matrix3,
|
|
Path,
|
|
Path,
|
|
|
|
+ Shape,
|
|
ShapePath,
|
|
ShapePath,
|
|
|
|
+ ShapeUtils,
|
|
Vector2,
|
|
Vector2,
|
|
Vector3
|
|
Vector3
|
|
} from '../../../build/three.module.js';
|
|
} 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 ) {
|
|
SVGLoader.getStrokeStyle = function ( width, color, lineJoin, lineCap, miterLimit ) {
|
|
|
|
|
|
// Param width: Stroke width
|
|
// Param width: Stroke width
|