From 64c8a6b758160d0f959807188cd240a79557661c Mon Sep 17 00:00:00 2001 From: Michael Herzog Date: Wed, 29 Apr 2026 22:56:11 +0200 Subject: [PATCH 1/2] ShapePath: Update `toShapes()`. (#33503) --- editor/js/Loader.js | 2 +- examples/jsm/loaders/SVGLoader.js | 203 +----------- .../webgl_custom_attributes_lines.jpg | Bin 27328 -> 27386 bytes examples/webgl_loader_svg.html | 2 +- src/extras/core/ShapePath.js | 310 +++++++++--------- 5 files changed, 154 insertions(+), 363 deletions(-) diff --git a/editor/js/Loader.js b/editor/js/Loader.js index 4949984079ccce..a7a2beb4f851c4 100644 --- a/editor/js/Loader.js +++ b/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 ++ ) { diff --git a/examples/jsm/loaders/SVGLoader.js b/examples/jsm/loaders/SVGLoader.js index 38306625adbbd7..f26c18ed601b0c 100644 --- a/examples/jsm/loaders/SVGLoader.js +++ b/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} 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(); } diff --git a/examples/screenshots/webgl_custom_attributes_lines.jpg b/examples/screenshots/webgl_custom_attributes_lines.jpg index 9605a3bc3d9c7c81f79b60a16d0455bbfd7fc823..6be39148b59f13104ddb22035fd1f16c1ec035d4 100644 GIT binary patch delta 24173 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 ZRfur~Rhm-!uMg@-_FW&>Bar_l{|ACkMI`_L delta 24109 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 a+bP}OiJRS;H70Jw2QfZw$_F8T$NvXvj+K}I diff --git a/examples/webgl_loader_svg.html b/examples/webgl_loader_svg.html index 4012e0530239f0..5893121916df18 100644 --- a/examples/webgl_loader_svg.html +++ b/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 ) { diff --git a/src/extras/core/ShapePath.js b/src/extras/core/ShapePath.js index 9c0b9919f9bd7a..c2ae18cf95303d 100644 --- a/src/extras/core/ShapePath.js +++ b/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} 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; + } } From 9c6d2b253e528c1fef09c4c64328be13db61d96b Mon Sep 17 00:00:00 2001 From: Michael Herzog Date: Wed, 29 Apr 2026 23:29:17 +0200 Subject: [PATCH 2/2] Examples: Clean up. (#33505) --- examples/jsm/helpers/LightProbeGridHelper.js | 2 +- examples/webgl_lightprobes.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/jsm/helpers/LightProbeGridHelper.js b/examples/jsm/helpers/LightProbeGridHelper.js index 5939817ed56112..0820dc25d5c19d 100644 --- a/examples/jsm/helpers/LightProbeGridHelper.js +++ b/examples/jsm/helpers/LightProbeGridHelper.js @@ -9,7 +9,7 @@ import { /** * Visualizes an {@link LightProbeGrid} by rendering a sphere at each - * probe position, shaded with the probe's L1 spherical harmonics. + * probe position, shaded with the probe's L2 spherical harmonics. * * Uses a single `InstancedMesh` draw call for all probes. * diff --git a/examples/webgl_lightprobes.html b/examples/webgl_lightprobes.html index 03b9431f6d4da9..abef07b62c7852 100644 --- a/examples/webgl_lightprobes.html +++ b/examples/webgl_lightprobes.html @@ -10,7 +10,7 @@
three.js - light probe volume
- Position-dependent diffuse global illumination via L1 SH probe grid + Position-dependent diffuse global illumination via L2 SH probe grid