@@ -11,7 +11,6 @@ fn cross(a: Point<f64>, b: Point<f64>) -> f64 {
1111/// Signed area of a closed subpath using Green's theorem.
1212/// A negative area means clockwise winding.
1313///
14- ///
1514/// Lines and beziers follow kurbo's [`ParamCurveArea`] formulas;
1615/// the elliptical arc contribution is a direct integration of the parametric ellipse.
1716///
@@ -51,23 +50,23 @@ fn signed_area(stroke: &Stroke) -> f64 {
5150 if !stroke. is_closed ( ) {
5251 area += cross ( stroke. end_point ( ) , stroke. start_point ( ) ) ;
5352 }
54- area * 0.5
53+ area / 2.
5554}
5655
5756/// Returns true if `from` => `to` crosses a ray cast from `point` rightwards.
5857///
5958/// Used as the per-segment primitive for ray-casting. The `>= max_y` exclusion on the upper
6059/// endpoint ensures a shared vertex between two segments is counted only once.
6160fn edge_crosses_ray ( from : Point < f64 > , to : Point < f64 > , point : Point < f64 > ) -> bool {
62- let ( min_y, max_y) = if from. y <= to. y {
63- ( from. y , to. y )
64- } else {
65- ( to. y , from. y )
66- } ;
67- if point. y < min_y || point. y >= max_y {
61+ let ( min_y, max_y) = ( from. y . min ( to. y ) , from. y . max ( to. y ) ) ;
62+
63+ // >= max_y implicitly encoded here
64+ if !( min_y..max_y) . contains ( & point. y ) {
6865 return false ;
6966 }
7067 let t = ( point. y - from. y ) / ( to. y - from. y ) ;
68+
69+ // Ray intersects because the edge x is greater
7170 from. x + t * ( to. x - from. x ) > point. x
7271}
7372
@@ -122,73 +121,94 @@ fn stroke_contains_point(stroke: &Stroke, point: Point<f64>) -> bool {
122121 !crossings. is_multiple_of ( 2 )
123122}
124123
125- /// Partitions raw SVG subpaths into [`FillPolygon`]s — one per outer contour — with holes
124+ fn stroke_contains_stroke ( outer : & Stroke , inner : & Stroke ) -> bool {
125+ let outer_bbox = outer. bounding_box ( ) ;
126+ let inner_bbox = inner. bounding_box ( ) ;
127+ if !outer_bbox. contains_box ( & inner_bbox) {
128+ return false ;
129+ }
130+ if !stroke_contains_point ( outer, inner. start_point ( ) ) {
131+ return false ;
132+ }
133+ for cmd in inner. commands ( ) {
134+ if let Some ( to) = cmd. end_point ( )
135+ && !stroke_contains_point ( outer, to)
136+ {
137+ return false ;
138+ }
139+ }
140+ true
141+ }
142+
143+ /// Partitions raw SVG subpaths into [`FillPolygon`]s, one per outer contour, with holes
126144/// assigned to their closest enclosing outer.
127145///
128- /// The SVG `fill-rule ` is consumed here: for `EvenOdd`, nesting depth determines outer vs. hole
129- /// (even depth → outer); for `NonZero`, the cumulative signed winding of enclosing subpaths
130- /// determines it (zero cumulative winding before entering → outer).
146+ /// `fill_rule ` is flattened here:
147+ /// - `EvenOdd`: even = outer
148+ /// - `NonZero`: 0 cumulative winding = outer
131149pub ( crate ) fn into_fill_polygons ( subpaths : Vec < Stroke > , fill_rule : FillRule ) -> Vec < FillPolygon > {
132150 if subpaths. is_empty ( ) {
133151 return vec ! [ ] ;
134152 }
135153
136- let areas: Vec < f64 > = subpaths. iter ( ) . map ( signed_area) . collect ( ) ;
137- let starts: Vec < Point < f64 > > = subpaths. iter ( ) . map ( |s| s. start_point ( ) ) . collect ( ) ;
138-
139- // For each subpath, the indices of all other subpaths that contain its start point.
140- let containers: Vec < Vec < usize > > = ( 0 ..subpaths. len ( ) )
154+ // For each subpath, the indices of all other subpaths that enclose it.
155+ let containers: Vec < Vec < _ > > = ( 0 ..subpaths. len ( ) )
141156 . map ( |i| {
142157 ( 0 ..subpaths. len ( ) )
143158 . filter ( |& j| j != i)
144- . filter ( |& j| stroke_contains_point ( & subpaths[ j] , starts [ i] ) )
159+ . filter ( |& j| stroke_contains_stroke ( & subpaths[ j] , & subpaths [ i] ) )
145160 . collect ( )
146161 } )
147162 . collect ( ) ;
148163
149164 // Classify each subpath as outer (contributes filled area) or hole (removes it).
150- let mut is_outer = vec ! [ false ; subpaths. len( ) ] ;
151- let mut is_hole = vec ! [ false ; subpaths. len( ) ] ;
165+ let is_outer: Vec < Option < bool > > = match fill_rule {
166+ FillRule :: EvenOdd => containers
167+ . iter ( )
168+ . map ( |containing| Some ( containing. len ( ) . is_multiple_of ( 2 ) ) )
169+ . collect ( ) ,
170+ FillRule :: NonZero => {
171+ let areas: Vec < f64 > = subpaths. iter ( ) . map ( signed_area) . collect ( ) ;
152172
153- for i in 0 ..subpaths. len ( ) {
154- match fill_rule {
155- FillRule :: EvenOdd => {
156- if containers[ i] . len ( ) . is_multiple_of ( 2 ) {
157- is_outer[ i] = true ;
158- } else {
159- is_hole[ i] = true ;
160- }
161- }
162- FillRule :: NonZero => {
163- let cumulative_winding: i32 = containers[ i]
164- . iter ( )
165- . map ( |& j| if areas[ j] > 0.0 { 1i32 } else { -1i32 } )
166- . sum ( ) ;
173+ containers
174+ . iter ( )
175+ . zip ( areas. iter ( ) )
176+ . map ( |( container, & area) | {
177+ let cumulative_winding: i32 = container
178+ . iter ( )
179+ . map ( |& j| if areas[ j] > 0.0 { 1i32 } else { -1i32 } )
180+ . sum ( ) ;
167181
168- if cumulative_winding == 0 {
169- is_outer[ i] = true ;
170- } else {
171- let winding_inside = cumulative_winding + if areas[ i] > 0.0 { 1 } else { -1 } ;
172- if winding_inside == 0 {
173- is_hole[ i] = true ;
182+ if cumulative_winding == 0 {
183+ Some ( true )
184+ } else {
185+ let winding_inside = cumulative_winding + if area > 0.0 { 1 } else { -1 } ;
186+ if winding_inside == 0 {
187+ Some ( false )
188+ } else {
189+ // Ignore (why?)
190+ None
191+ }
174192 }
175- }
176- }
193+ } )
194+ . collect ( )
177195 }
178- }
196+ } ;
179197
180- // For each outer, collect its direct holes — holes for which this outer is the innermost
198+ // For each outer, collect its immediate holes — holes for which this outer is the innermost
181199 // enclosing outer (no other outer sits between them).
182200 ( 0 ..subpaths. len ( ) )
183- . filter ( |& i| is_outer[ i] )
201+ . filter ( |& i| is_outer[ i] == Some ( true ) )
184202 . map ( |i| {
185203 let holes = ( 0 ..subpaths. len ( ) )
186- . filter ( |& j| is_hole[ j] && stroke_contains_point ( & subpaths[ i] , starts[ j] ) )
204+ . filter ( |& j| {
205+ is_outer[ j] == Some ( false ) && stroke_contains_stroke ( & subpaths[ i] , & subpaths[ j] )
206+ } )
187207 . filter ( |& j| {
188208 // No other outer k is strictly between outer i and hole j.
189209 !containers[ j]
190210 . iter ( )
191- . any ( |& k| k != i && is_outer[ k] && containers[ k] . contains ( & i) )
211+ . any ( |& k| k != i && is_outer[ k] == Some ( true ) && containers[ k] . contains ( & i) )
192212 } )
193213 . map ( |j| subpaths[ j] . clone ( ) )
194214 . collect ( ) ;
@@ -288,4 +308,59 @@ mod tests {
288308 assert_eq ! ( polygons[ 0 ] . outer. start_point( ) , s0. start_point( ) ) ;
289309 assert_eq ! ( polygons[ 0 ] . holes. len( ) , 0 ) ;
290310 }
311+
312+ #[ test]
313+ fn test_nonzero_overlapping_not_nested ( ) {
314+ // Subpath 0: CCW square from (0,0) to (10,10)
315+ let s0 = Stroke :: new (
316+ Point :: new ( 0.0 , 0.0 ) ,
317+ vec ! [
318+ DrawCommand :: LineTo {
319+ from: Point :: new( 0.0 , 0.0 ) ,
320+ to: Point :: new( 10.0 , 0.0 ) ,
321+ } ,
322+ DrawCommand :: LineTo {
323+ from: Point :: new( 10.0 , 0.0 ) ,
324+ to: Point :: new( 10.0 , 10.0 ) ,
325+ } ,
326+ DrawCommand :: LineTo {
327+ from: Point :: new( 10.0 , 10.0 ) ,
328+ to: Point :: new( 0.0 , 10.0 ) ,
329+ } ,
330+ DrawCommand :: LineTo {
331+ from: Point :: new( 0.0 , 10.0 ) ,
332+ to: Point :: new( 0.0 , 0.0 ) ,
333+ } ,
334+ ] ,
335+ ) ;
336+
337+ // Subpath 1: CCW square from (5,5) to (15,15) - overlapping but not nested
338+ let s1 = Stroke :: new (
339+ Point :: new ( 5.0 , 5.0 ) ,
340+ vec ! [
341+ DrawCommand :: LineTo {
342+ from: Point :: new( 5.0 , 5.0 ) ,
343+ to: Point :: new( 15.0 , 5.0 ) ,
344+ } ,
345+ DrawCommand :: LineTo {
346+ from: Point :: new( 15.0 , 5.0 ) ,
347+ to: Point :: new( 15.0 , 15.0 ) ,
348+ } ,
349+ DrawCommand :: LineTo {
350+ from: Point :: new( 15.0 , 15.0 ) ,
351+ to: Point :: new( 5.0 , 15.0 ) ,
352+ } ,
353+ DrawCommand :: LineTo {
354+ from: Point :: new( 5.0 , 15.0 ) ,
355+ to: Point :: new( 5.0 , 5.0 ) ,
356+ } ,
357+ ] ,
358+ ) ;
359+
360+ let polygons = into_fill_polygons ( vec ! [ s0. clone( ) , s1. clone( ) ] , FillRule :: NonZero ) ;
361+
362+ // Since they overlap but are not nested (neither bbox is inside the other),
363+ // they should be classified as two independent outer contours.
364+ assert_eq ! ( polygons. len( ) , 2 ) ;
365+ }
291366}
0 commit comments