Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Enable choosing the even-odd rule for filling shapes - #32253

Merged
greglucas merged 1 commit into
matplotlib:mainfrom
ayshih:fill_rule
Sep 10, 2026
Merged

Enable choosing the even-odd rule for filling shapes#32253
greglucas merged 1 commit into
matplotlib:mainfrom
ayshih:fill_rule

Conversation

@ayshih

@ayshih ayshih commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

PR summary

This PR enables choosing the even-odd rule for filling a shape, as an alternative to the existing behavior of the non-zero winding rule. The choice of fill rule is implemented as an Patch property, and defaults to the non-zero winding rule. All backends are supported.

Examples

Agg
agg

Cairo
cairo

SVG
Figure_1

PDF
Figure_1.pdf

PGF
Figure_1.pgf.pdf

PS
Figure_1.ps.pdf

Generating script

import matplotlib
#matplotlib.use('TkCairo')

import numpy as np

import matplotlib.pyplot as plt
from matplotlib.patches import PathPatch, Polygon
from matplotlib.path import Path

deg = np.arange(6) * 144
x = np.sin(deg * np.pi / 180)
y = np.cos(deg * np.pi / 180)
star = np.stack([x, y], axis=1)

square_vertices = np.array([[-1, -1], [-1, 1], [1, 1], [1, -1], [-1, -1]])
square_codes = [Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.LINETO]

fig, axs = plt.subplots(1, 2, figsize=(4, 5))

for ax, fill_rule in zip(axs, ['nonzero', 'evenodd']):
    stroked_star = Polygon(star + [0, 4], closed=False, ec='b', lw=5, ls=(0, (5, 1)),
                           fc='r', hatch='xx', fill_rule=fill_rule)
    ax.add_patch(stroked_star)

    nonstroked_star = Polygon(star + [0, 2], closed=False, ec='none',
                              fc='r', hatch='xx', fill_rule=fill_rule)
    ax.add_patch(nonstroked_star)

    squares = Path(np.vstack([square_vertices * 0.9,
                              square_vertices / 3 + [0, 0.5],
                              square_vertices / 3 + [0.3, 0],
                              (square_vertices / 3)[::-1, :] + [0, -0.5]]),
                   square_codes * 4)
    
    ax.add_patch(PathPatch(squares, fc='g', ec='m', fill_rule=fill_rule))

    ax.set_xlim(-1, 1)
    ax.set_ylim(-1, 5.1)
    ax.set_aspect('equal')
    ax.set_axis_off()
    ax.set_title(f'fill_rule={fill_rule}')

plt.show()

AI Disclosure

No AI was used

PR quality check

  • Use an expressive title, e.g. "Fix title font property precedence"
  • New and changed code is tested
  • Plotting related features are demonstrated in an example
  • New features and API changes have release notes
  • Documentation complies with general and docstring guidelines

@ayshih

ayshih commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

This PR is ready for review! It includes a gallery example showing the difference between the two fill rules:
sphx_glr_fill_rule_demo_001

@greglucas

Copy link
Copy Markdown
Contributor

How do the two cases behave with contains() and mouse checking on the various artists? Does the hover work only over the filled areas or is it everything within any enclosing line? I think I'd expect it to be just the filled portion, but I'm also not entirely clear whether that is always desired. It might be nice to add some of the mouse event contains checks as tests for this as well to verify the filling assertions other than with just the images.

@ayshih

ayshih commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

How do the two cases behave with contains() and mouse checking on the various artists?

I've investigated this, and amusingly enough, contains() is not fully consistent with either fill rule. The current logic (in this function) is essentially (and possibly unintentionally) consistent with the even-odd rule on any individual subpath (and if the subpath does not self-intersect, the non-zero winding rule is the same). However, if there are multiple subpaths, a point is considered "inside" the combined path if it is inside at least one subpath, even if the combined effect of all subpaths mean that the point should actually be "outside". This means:

  • If all subpaths do not self-intersect and do not intersect with each other, contains() is consistent with both the non-zero winding rule and the even-odd rule (that is, both rules have the same result)
  • If there is a single subpath that self-intersects, contains() is consistent with the even-odd rule
  • If there are multiple subpaths that do not self-intersect but do intersect with each other, and all subpaths are the same orientation, contains() is consistent with the non-zero winding rule
  • If none of the above, contains() gives a "wrong" result: it is not consistent with either rule

My inclination is that fixing contains() should be deferred to a future PR.

@iccir iccir left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional comments: I feel that there may be lurking bugs in GraphicsContextBase.restore() where we call restore() on a wrapped Cairo context, but we never actually restore the associated properties. I need to deepen my understanding of GraphicsContextBase, however.

Comment thread lib/matplotlib/patches.pyi Outdated
Comment thread lib/matplotlib/backends/backend_svg.py

@iccir iccir left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The included changes look good. I'm not sure if additional changes are needed or if there are edge cases.

@greglucas

Copy link
Copy Markdown
Contributor

My inclination is that fixing contains() should be deferred to a future PR.

I agree, I opened #32329 from that comment.

@greglucas
greglucas merged commit f8791a8 into matplotlib:main Sep 10, 2026
41 of 42 checks passed
@QuLogic QuLogic added this to the v3.12.0 milestone Sep 10, 2026
@ayshih
ayshih deleted the fill_rule branch September 10, 2026 13:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants