Просмотр исходного кода

ShapePath: Update `toShapes()`. (#33503)

Michael Herzog 1 месяц назад
Родитель
Сommit
64c8a6b758

+ 1 - 1
editor/js/Loader.js

@@ -716,7 +716,7 @@ function Loader( editor ) {
 
 						if ( fillMaterial ) {
 
-							const shapes = SVGLoader.createShapes( path );
+							const shapes = path.toShapes();
 
 							for ( let j = 0; j < shapes.length; j ++ ) {
 

+ 3 - 200
examples/jsm/loaders/SVGLoader.js

@@ -13,7 +13,6 @@ import {
 	MirroredRepeatWrapping,
 	Path,
 	RepeatWrapping,
-	Shape,
 	ShapePath,
 	ShapeUtils,
 	SRGBColorSpace,
@@ -2238,211 +2237,15 @@ class SVGLoader extends Loader {
 	/**
 	 * Creates from the given shape path and array of shapes.
 	 *
+	 * @deprecated since 185.
 	 * @param {ShapePath} shapePath - The shape path.
 	 * @return {Array<Shape>} An array of shapes.
 	 */
 	static createShapes( shapePath ) {
 
-		// Point-in-polygon test using the even-odd ray-casting rule. Valid for
-		// simple (non self-intersecting) polygons.
-		function pointInPolygon( p, polygon ) {
+		console.warn( 'SVGLoader: createShapes() is deprecated. Use shapePath.toShapes() instead.' ); // @deprecated, r185
 
-			let inside = false;
-			const n = polygon.length;
-
-			for ( let i = 0, j = n - 1; i < n; j = i ++ ) {
-
-				const a = polygon[ i ];
-				const b = polygon[ j ];
-
-				if ( ( a.y > p.y ) !== ( b.y > p.y ) &&
-					p.x < ( b.x - a.x ) * ( p.y - a.y ) / ( b.y - a.y ) + a.x ) {
-
-					inside = ! inside;
-
-				}
-
-			}
-
-			return inside;
-
-		}
-
-		// Returns a point guaranteed to be strictly inside the given simple
-		// polygon. First tries the bounding-box center; if that falls outside
-		// the polygon, casts a horizontal ray at the center's y and picks the
-		// midpoint between the first two sorted intercepts.
-		//
-		// Port of paper.js' Path#getInteriorPoint()
-		// https://github.com/paperjs/paper.js/blob/develop/src/path/PathItem.Boolean.js
-		function getInteriorPoint( polygon, boundingBox ) {
-
-			const point = boundingBox.getCenter( new Vector2() );
-
-			if ( pointInPolygon( point, polygon ) ) return point;
-
-			const y = point.y;
-			const intercepts = [];
-			const n = polygon.length;
-
-			for ( let i = 0; i < n; i ++ ) {
-
-				const a = polygon[ i ];
-				const b = polygon[ ( i + 1 ) % n ];
-
-				// Half-open crossing rule — counts each vertex exactly once and
-				// skips horizontal edges.
-				if ( ( a.y > y ) !== ( b.y > y ) ) {
-
-					const x = a.x + ( y - a.y ) * ( b.x - a.x ) / ( b.y - a.y );
-					intercepts.push( x );
-
-				}
-
-			}
-
-			if ( intercepts.length > 1 ) {
-
-				intercepts.sort( ( a, b ) => a - b );
-				point.x = ( intercepts[ 0 ] + intercepts[ 1 ] ) / 2;
-
-			}
-
-			return point;
-
-		}
-
-		// Resolve fill-rule. SVG defaults to 'nonzero'.
-		let fillRule = ( shapePath.userData && shapePath.userData.style && shapePath.userData.style.fillRule ) || 'nonzero';
-
-		if ( fillRule !== 'nonzero' && fillRule !== 'evenodd' ) {
-
-			console.warn( 'THREE.SVGLoader: fill-rule "' + fillRule + '" is not supported, falling back to "nonzero".' );
-			fillRule = 'nonzero';
-
-		}
-
-		// Predicate that decides whether a winding number falls inside the fill
-		// region, per the SVG fill-rule spec. Works for negative windings too,
-		// because JavaScript's bitwise AND preserves odd/even under two's
-		// complement.
-		const isInside = fillRule === 'nonzero'
-			? ( w => w !== 0 )
-			: ( w => ( w & 1 ) !== 0 );
-
-		// Build an entry per usable subpath. Self-winding follows the standard
-		// convention used by ShapeUtils: counter-clockwise (signed area > 0)
-		// contributes +1 to the winding number at an interior point,
-		// clockwise contributes -1.
-		const entries = [];
-
-		for ( const subPath of shapePath.subPaths ) {
-
-			const points = subPath.getPoints();
-			if ( points.length < 3 ) continue;
-
-			const area = ShapeUtils.area( points );
-			if ( area === 0 ) continue;
-
-			const boundingBox = new Box2();
-			for ( let i = 0; i < points.length; i ++ ) boundingBox.expandByPoint( points[ i ] );
-
-			entries.push( {
-				subPath: subPath,
-				points: points,
-				boundingBox: boundingBox,
-				interiorPoint: getInteriorPoint( points, boundingBox ),
-				absArea: Math.abs( area ),
-				winding: area < 0 ? - 1 : 1,
-				container: null,
-				exclude: false,
-				role: null
-			} );
-
-		}
-
-		// Sort by area descending. This guarantees that any subpath that could
-		// contain `entries[i]` is located at a smaller index and has already
-		// been processed when it's entries[i]'s turn. Port of paper.js'
-		// reorientPaths() algorithm.
-		entries.sort( ( a, b ) => b.absArea - a.absArea );
-
-		// Walk already-processed entries from closest-in-size to largest,
-		// stopping at the innermost container. Accumulate the container's
-		// cumulative winding into this entry's winding so that the final value
-		// equals the winding number at this entry's interior point.
-		//
-		// A subpath only contributes to the fill boundary when crossing it
-		// actually flips the "insideness" per the fill rule; otherwise it's a
-		// redundant overlap and gets excluded to avoid double-counting.
-		for ( let i = 0; i < entries.length; i ++ ) {
-
-			const entry = entries[ i ];
-			let containerWinding = 0;
-
-			for ( let j = i - 1; j >= 0; j -- ) {
-
-				const candidate = entries[ j ];
-				if ( ! candidate.boundingBox.containsPoint( entry.interiorPoint ) ) continue;
-				if ( ! pointInPolygon( entry.interiorPoint, candidate.points ) ) continue;
-
-				entry.container = candidate.exclude ? candidate.container : candidate;
-				containerWinding = candidate.winding;
-				entry.winding += containerWinding;
-				break;
-
-			}
-
-			if ( isInside( entry.winding ) === isInside( containerWinding ) ) {
-
-				entry.exclude = true;
-
-			}
-
-		}
-
-		// Classify retained entries. An entry is an outer shape if it has no
-		// container or if its container is itself a hole (a solid nested inside
-		// a hole becomes a new top-level shape); otherwise it's a hole in its
-		// container. Entries were already sorted outermost-first, so each
-		// container's role is known by the time we look at it.
-		for ( const entry of entries ) {
-
-			if ( entry.exclude ) continue;
-			entry.role = ( entry.container === null || entry.container.role === 'hole' ) ? 'outer' : 'hole';
-
-		}
-
-		// Build Shapes for outers first, then attach holes to their container's
-		// Shape.
-		const shapes = [];
-		const shapeByEntry = new Map();
-
-		for ( const entry of entries ) {
-
-			if ( entry.exclude || entry.role !== 'outer' ) continue;
-
-			const shape = new Shape();
-			shape.curves = entry.subPath.curves;
-			shapes.push( shape );
-			shapeByEntry.set( entry, shape );
-
-		}
-
-		for ( const entry of entries ) {
-
-			if ( entry.exclude || entry.role !== 'hole' ) continue;
-
-			const shape = shapeByEntry.get( entry.container );
-			if ( ! shape ) continue;
-
-			const hole = new Path();
-			hole.curves = entry.subPath.curves;
-			shape.holes.push( hole );
-
-		}
-
-		return shapes;
+		return shapePath.toShapes();
 
 	}
 

BIN
examples/screenshots/webgl_custom_attributes_lines.jpg


+ 1 - 1
examples/webgl_loader_svg.html

@@ -183,7 +183,7 @@
 
 								material.wireframe = guiData.fillShapesWireframe;
 
-								const shapes = SVGLoader.createShapes( path );
+								const shapes = path.toShapes();
 
 								for ( const shape of shapes ) {
 

+ 149 - 161
src/extras/core/ShapePath.js

@@ -1,7 +1,10 @@
 import { Color } from '../../math/Color.js';
+import { Box2 } from '../../math/Box2.js';
+import { Vector2 } from '../../math/Vector2.js';
 import { Path } from './Path.js';
 import { Shape } from './Shape.js';
 import { ShapeUtils } from '../ShapeUtils.js';
+import { warn } from '../../utils.js';
 
 /**
  * This class is used to convert a series of paths to an array of
@@ -39,6 +42,14 @@ class ShapePath {
 		 */
 		this.currentPath = null;
 
+		/**
+		 * An object that can be used to store custom data about the shape path.
+		 * Mainly used by SVGLoader to store style information.
+		 *
+		 * @type {Object}
+		 */
+		this.userData = {};
+
 	}
 
 	/**
@@ -130,235 +141,212 @@ class ShapePath {
 	/**
 	 * Converts the paths into an array of shapes.
 	 *
-	 * @param {boolean} isCCW - By default solid shapes are  defined clockwise (CW) and holes are defined counterclockwise (CCW).
-	 * If this flag is set to `true`, then those are flipped.
 	 * @return {Array<Shape>} An array of shapes.
 	 */
-	toShapes( isCCW ) {
-
-		function toShapesNoHoles( inSubpaths ) {
-
-			const shapes = [];
-
-			for ( let i = 0, l = inSubpaths.length; i < l; i ++ ) {
-
-				const tmpPath = inSubpaths[ i ];
-
-				const tmpShape = new Shape();
-				tmpShape.curves = tmpPath.curves;
-
-				shapes.push( tmpShape );
-
-			}
-
-			return shapes;
-
-		}
+	toShapes() {
 
-		function isPointInsidePolygon( inPt, inPolygon ) {
+		// Point-in-polygon test using the even-odd ray-casting rule. Valid for
+		// simple (non self-intersecting) polygons.
+		function pointInPolygon( p, polygon ) {
 
-			const polyLen = inPolygon.length;
-
-			// inPt on polygon contour => immediate success    or
-			// toggling of inside/outside at every single! intersection point of an edge
-			//  with the horizontal line through inPt, left of inPt
-			//  not counting lowerY endpoints of edges and whole edges on that line
 			let inside = false;
-			for ( let p = polyLen - 1, q = 0; q < polyLen; p = q ++ ) {
-
-				let edgeLowPt = inPolygon[ p ];
-				let edgeHighPt = inPolygon[ q ];
-
-				let edgeDx = edgeHighPt.x - edgeLowPt.x;
-				let edgeDy = edgeHighPt.y - edgeLowPt.y;
-
-				if ( Math.abs( edgeDy ) > Number.EPSILON ) {
-
-					// not parallel
-					if ( edgeDy < 0 ) {
-
-						edgeLowPt = inPolygon[ q ]; edgeDx = - edgeDx;
-						edgeHighPt = inPolygon[ p ]; edgeDy = - edgeDy;
-
-					}
+			const n = polygon.length;
 
-					if ( ( inPt.y < edgeLowPt.y ) || ( inPt.y > edgeHighPt.y ) ) 		continue;
+			for ( let i = 0, j = n - 1; i < n; j = i ++ ) {
 
-					if ( inPt.y === edgeLowPt.y ) {
+				const a = polygon[ i ];
+				const b = polygon[ j ];
 
-						if ( inPt.x === edgeLowPt.x )		return	true;		// inPt is on contour ?
-						// continue;				// no intersection or edgeLowPt => doesn't count !!!
+				if ( ( a.y > p.y ) !== ( b.y > p.y ) &&
+							p.x < ( b.x - a.x ) * ( p.y - a.y ) / ( b.y - a.y ) + a.x ) {
 
-					} else {
-
-						const perpEdge = edgeDy * ( inPt.x - edgeLowPt.x ) - edgeDx * ( inPt.y - edgeLowPt.y );
-						if ( perpEdge === 0 )				return	true;		// inPt is on contour ?
-						if ( perpEdge < 0 ) 				continue;
-						inside = ! inside;		// true intersection left of inPt
-
-					}
-
-				} else {
-
-					// parallel or collinear
-					if ( inPt.y !== edgeLowPt.y ) 		continue;			// parallel
-					// edge lies on the same horizontal line as inPt
-					if ( ( ( edgeHighPt.x <= inPt.x ) && ( inPt.x <= edgeLowPt.x ) ) ||
-						 ( ( edgeLowPt.x <= inPt.x ) && ( inPt.x <= edgeHighPt.x ) ) )		return	true;	// inPt: Point on contour !
-					// continue;
+					inside = ! inside;
 
 				}
 
 			}
 
-			return	inside;
+			return inside;
 
 		}
 
-		const isClockWise = ShapeUtils.isClockWise;
+		// Returns a point guaranteed to be strictly inside the given simple
+		// polygon. First tries the bounding-box center; if that falls outside
+		// the polygon, casts a horizontal ray at the center's y and picks the
+		// midpoint between the first two sorted intercepts.
+		//
+		// Port of paper.js' Path#getInteriorPoint()
+		// https://github.com/paperjs/paper.js/blob/develop/src/path/PathItem.Boolean.js
+		function getInteriorPoint( polygon, boundingBox ) {
 
-		const subPaths = this.subPaths;
-		if ( subPaths.length === 0 ) return [];
+			const point = boundingBox.getCenter( new Vector2() );
 
-		let solid, tmpPath, tmpShape;
-		const shapes = [];
-
-		if ( subPaths.length === 1 ) {
+			if ( pointInPolygon( point, polygon ) ) return point;
 
-			tmpPath = subPaths[ 0 ];
-			tmpShape = new Shape();
-			tmpShape.curves = tmpPath.curves;
-			shapes.push( tmpShape );
-			return shapes;
+			const y = point.y;
+			const intercepts = [];
+			const n = polygon.length;
 
-		}
+			for ( let i = 0; i < n; i ++ ) {
 
-		let holesFirst = ! isClockWise( subPaths[ 0 ].getPoints() );
-		holesFirst = isCCW ? ! holesFirst : holesFirst;
+				const a = polygon[ i ];
+				const b = polygon[ ( i + 1 ) % n ];
 
-		// log("Holes first", holesFirst);
+				// Half-open crossing rule — counts each vertex exactly once and
+				// skips horizontal edges.
+				if ( ( a.y > y ) !== ( b.y > y ) ) {
 
-		const betterShapeHoles = [];
-		const newShapes = [];
-		let newShapeHoles = [];
-		let mainIdx = 0;
-		let tmpPoints;
+					const x = a.x + ( y - a.y ) * ( b.x - a.x ) / ( b.y - a.y );
+					intercepts.push( x );
 
-		newShapes[ mainIdx ] = undefined;
-		newShapeHoles[ mainIdx ] = [];
+				}
 
-		for ( let i = 0, l = subPaths.length; i < l; i ++ ) {
+			}
 
-			tmpPath = subPaths[ i ];
-			tmpPoints = tmpPath.getPoints();
-			solid = isClockWise( tmpPoints );
-			solid = isCCW ? ! solid : solid;
+			if ( intercepts.length > 1 ) {
 
-			if ( solid ) {
+				intercepts.sort( ( a, b ) => a - b );
+				point.x = ( intercepts[ 0 ] + intercepts[ 1 ] ) / 2;
 
-				if ( ( ! holesFirst ) && ( newShapes[ mainIdx ] ) )	mainIdx ++;
+			}
 
-				newShapes[ mainIdx ] = { s: new Shape(), p: tmpPoints };
-				newShapes[ mainIdx ].s.curves = tmpPath.curves;
+			return point;
 
-				if ( holesFirst )	mainIdx ++;
-				newShapeHoles[ mainIdx ] = [];
+		}
 
-				//log('cw', i);
+		// Resolve fill-rule. Defaults to 'nonzero'.
+		let fillRule = ( this.userData.style && this.userData.style.fillRule ) || 'nonzero';
 
-			} else {
+		if ( fillRule !== 'nonzero' && fillRule !== 'evenodd' ) {
 
-				newShapeHoles[ mainIdx ].push( { h: tmpPath, p: tmpPoints[ 0 ] } );
+			warn( 'Fill-rule "' + fillRule + '" is not supported, falling back to "nonzero".' );
+			fillRule = 'nonzero';
 
-				//log('ccw', i);
+		}
 
-			}
+		// Predicate that decides whether a winding number falls inside the fill
+		// region, per the SVG fill-rule spec. Works for negative windings too,
+		// because JavaScript's bitwise AND preserves odd/even under two's
+		// complement.
+		const isInside = fillRule === 'nonzero'
+			? ( w => w !== 0 )
+			: ( w => ( w & 1 ) !== 0 );
+
+		// Build an entry per usable subpath. Self-winding follows the standard
+		// convention used by ShapeUtils: counter-clockwise (signed area > 0)
+		// contributes +1 to the winding number at an interior point,
+		// clockwise contributes -1.
+		const entries = [];
+
+		for ( const subPath of this.subPaths ) {
+
+			const points = subPath.getPoints();
+			if ( points.length < 3 ) continue;
+
+			const area = ShapeUtils.area( points );
+			if ( area === 0 ) continue;
+
+			const boundingBox = new Box2();
+			for ( let i = 0; i < points.length; i ++ ) boundingBox.expandByPoint( points[ i ] );
+
+			entries.push( {
+				subPath: subPath,
+				points: points,
+				boundingBox: boundingBox,
+				interiorPoint: getInteriorPoint( points, boundingBox ),
+				absArea: Math.abs( area ),
+				winding: area < 0 ? - 1 : 1,
+				container: null,
+				exclude: false,
+				role: null
+			} );
 
 		}
 
-		// only Holes? -> probably all Shapes with wrong orientation
-		if ( ! newShapes[ 0 ] )	return	toShapesNoHoles( subPaths );
+		// Sort by area descending. This guarantees that any subpath that could
+		// contain `entries[i]` is located at a smaller index and has already
+		// been processed when it's entries[i]'s turn. Port of paper.js'
+		// reorientPaths() algorithm.
+		entries.sort( ( a, b ) => b.absArea - a.absArea );
 
+		// Walk already-processed entries from closest-in-size to largest,
+		// stopping at the innermost container. Accumulate the container's
+		// cumulative winding into this entry's winding so that the final value
+		// equals the winding number at this entry's interior point.
+		//
+		// A subpath only contributes to the fill boundary when crossing it
+		// actually flips the "insideness" per the fill rule; otherwise it's a
+		// redundant overlap and gets excluded to avoid double-counting.
+		for ( let i = 0; i < entries.length; i ++ ) {
 
-		if ( newShapes.length > 1 ) {
+			const entry = entries[ i ];
+			let containerWinding = 0;
 
-			let ambiguous = false;
-			let toChange = 0;
+			for ( let j = i - 1; j >= 0; j -- ) {
 
-			for ( let sIdx = 0, sLen = newShapes.length; sIdx < sLen; sIdx ++ ) {
+				const candidate = entries[ j ];
+				if ( ! candidate.boundingBox.containsPoint( entry.interiorPoint ) ) continue;
+				if ( ! pointInPolygon( entry.interiorPoint, candidate.points ) ) continue;
 
-				betterShapeHoles[ sIdx ] = [];
+				entry.container = candidate.exclude ? candidate.container : candidate;
+				containerWinding = candidate.winding;
+				entry.winding += containerWinding;
+				break;
 
 			}
 
-			for ( let sIdx = 0, sLen = newShapes.length; sIdx < sLen; sIdx ++ ) {
-
-				const sho = newShapeHoles[ sIdx ];
-
-				for ( let hIdx = 0; hIdx < sho.length; hIdx ++ ) {
-
-					const ho = sho[ hIdx ];
-					let hole_unassigned = true;
-
-					for ( let s2Idx = 0; s2Idx < newShapes.length; s2Idx ++ ) {
-
-						if ( isPointInsidePolygon( ho.p, newShapes[ s2Idx ].p ) ) {
-
-							if ( sIdx !== s2Idx )	toChange ++;
-
-							if ( hole_unassigned ) {
-
-								hole_unassigned = false;
-								betterShapeHoles[ s2Idx ].push( ho );
-
-							} else {
+			if ( isInside( entry.winding ) === isInside( containerWinding ) ) {
 
-								ambiguous = true;
+				entry.exclude = true;
 
-							}
-
-						}
-
-					}
+			}
 
-					if ( hole_unassigned ) {
+		}
 
-						betterShapeHoles[ sIdx ].push( ho );
+		// Classify retained entries. An entry is an outer shape if it has no
+		// container or if its container is itself a hole (a solid nested inside
+		// a hole becomes a new top-level shape); otherwise it's a hole in its
+		// container. Entries were already sorted outermost-first, so each
+		// container's role is known by the time we look at it.
+		for ( const entry of entries ) {
 
-					}
+			if ( entry.exclude ) continue;
+			entry.role = ( entry.container === null || entry.container.role === 'hole' ) ? 'outer' : 'hole';
 
-				}
+		}
 
-			}
+		// Build Shapes for outers first, then attach holes to their container's
+		// Shape.
+		const shapes = [];
+		const shapeByEntry = new Map();
 
-			if ( toChange > 0 && ambiguous === false ) {
+		for ( const entry of entries ) {
 
-				newShapeHoles = betterShapeHoles;
+			if ( entry.exclude || entry.role !== 'outer' ) continue;
 
-			}
+			const shape = new Shape();
+			shape.curves = entry.subPath.curves;
+			shapes.push( shape );
+			shapeByEntry.set( entry, shape );
 
 		}
 
-		let tmpHoles;
+		for ( const entry of entries ) {
 
-		for ( let i = 0, il = newShapes.length; i < il; i ++ ) {
+			if ( entry.exclude || entry.role !== 'hole' ) continue;
 
-			tmpShape = newShapes[ i ].s;
-			shapes.push( tmpShape );
-			tmpHoles = newShapeHoles[ i ];
+			const shape = shapeByEntry.get( entry.container );
+			if ( ! shape ) continue;
 
-			for ( let j = 0, jl = tmpHoles.length; j < jl; j ++ ) {
-
-				tmpShape.holes.push( tmpHoles[ j ].h );
-
-			}
+			const hole = new Path();
+			hole.curves = entry.subPath.curves;
+			shape.holes.push( hole );
 
 		}
 
-		//log("shape", shapes);
-
 		return shapes;
 
+
 	}
 
 }

粤ICP备19079148号