From 8f2d3444b6a324430a782d561c4b2a2a2d95aca1 Mon Sep 17 00:00:00 2001 From: "Albert Y. Shih" Date: Fri, 8 May 2026 13:58:11 -0400 Subject: [PATCH] Enable choosing between two fill rules --- doc/api/typing_api.rst | 1 + doc/release/next_whats_new/fill_rules.rst | 10 + .../shapes_and_collections/fill_rule_demo.py | 67 +++++ galleries/tutorials/artists.py | 2 + lib/matplotlib/artist.py | 2 + lib/matplotlib/backend_bases.py | 8 + lib/matplotlib/backend_bases.pyi | 7 +- lib/matplotlib/backends/backend_cairo.py | 10 + lib/matplotlib/backends/backend_pdf.py | 14 +- lib/matplotlib/backends/backend_pgf.py | 6 +- lib/matplotlib/backends/backend_ps.py | 5 +- lib/matplotlib/backends/backend_svg.py | 2 + lib/matplotlib/patches.py | 24 ++ lib/matplotlib/patches.pyi | 4 +- .../test_backends_rendering/fill_rules.pdf | Bin 0 -> 2839 bytes .../test_backends_rendering/fill_rules.png | Bin 0 -> 16196 bytes .../test_backends_rendering/fill_rules.svg | 240 ++++++++++++++++++ .../fill_rules_cairo.png | Bin 0 -> 16397 bytes .../fill_rules_pgf.pdf | Bin 0 -> 8046 bytes .../tests/test_backends_rendering.py | 56 +++- lib/matplotlib/typing.py | 3 + src/_backend_agg.h | 4 + src/_backend_agg_basic_types.h | 16 ++ 23 files changed, 469 insertions(+), 12 deletions(-) create mode 100644 doc/release/next_whats_new/fill_rules.rst create mode 100644 galleries/examples/shapes_and_collections/fill_rule_demo.py create mode 100644 lib/matplotlib/tests/baseline_images/test_backends_rendering/fill_rules.pdf create mode 100644 lib/matplotlib/tests/baseline_images/test_backends_rendering/fill_rules.png create mode 100644 lib/matplotlib/tests/baseline_images/test_backends_rendering/fill_rules.svg create mode 100644 lib/matplotlib/tests/baseline_images/test_backends_rendering/fill_rules_cairo.png create mode 100644 lib/matplotlib/tests/baseline_images/test_backends_rendering/fill_rules_pgf.pdf diff --git a/doc/api/typing_api.rst b/doc/api/typing_api.rst index 0eafe849a0d0..8cd4023167e6 100644 --- a/doc/api/typing_api.rst +++ b/doc/api/typing_api.rst @@ -25,6 +25,7 @@ Artist styles ============= .. autodata:: matplotlib.typing.BlendModeType +.. autodata:: matplotlib.typing.FillRuleType .. autodata:: matplotlib.typing.LineStyleType .. autodata:: matplotlib.typing.DrawStyleType .. autodata:: matplotlib.typing.MarkEveryType diff --git a/doc/release/next_whats_new/fill_rules.rst b/doc/release/next_whats_new/fill_rules.rst new file mode 100644 index 000000000000..ccbc3669d535 --- /dev/null +++ b/doc/release/next_whats_new/fill_rules.rst @@ -0,0 +1,10 @@ +Option to use the even-odd fill rule for patches +------------------------------------------------ + +By default, patches such as `~.patches.Polygon` are filled according to the +`non-zero winding fill rule `__. +There is now the option to instead use the +`even-odd fill rule `__, +which is specified by setting the patch's ``fill_rule`` property to "evenodd". +See :doc:`/gallery/shapes_and_collections/fill_rule_demo` for more details and +an illustration of the difference. diff --git a/galleries/examples/shapes_and_collections/fill_rule_demo.py b/galleries/examples/shapes_and_collections/fill_rule_demo.py new file mode 100644 index 000000000000..09782401d1b0 --- /dev/null +++ b/galleries/examples/shapes_and_collections/fill_rule_demo.py @@ -0,0 +1,67 @@ +""" +============== +Fill rule demo +============== + +By default, patches such as `~.patches.Polygon` are filled according to the +`non-zero winding fill rule `__. +Any given point has a winding number, which is the number of times the path +wraps around the point in the clockwise direction. For this fill rule, the +filled regions are where the winding number is non-zero. See +:doc:`/gallery/shapes_and_collections/donut` for an example of how to leverage +the winding directions in multiple segments of a path under this fill rule. + +The other option for fill rule is the +`even-odd fill rule `__, +which is specified by setting the patch's ``fill_rule`` property to "evenodd". +For this fill rule, the filled regions are where the winding number is an odd +number. This fill rule allows for the construction of patterns of fill +regions that would otherwise take many more vertices to construct under the +non-zero winding fill rule. + +This example demonstrates the difference between the two fill rules for a single +`~.patches.Polygon` that intersects itself multiple times. The winding number +for each closed region is labeled. + +""" + +import matplotlib.pyplot as plt +import numpy as np + +from matplotlib.patches import Polygon + +fig, axs = plt.subplots(1, 2) + +vertices = np.array([[0, 0, 6, 6, 1, 1, 5, 5, 2, 2, 4, 4, 3, 3, 5, 5], + [2, 5, 5, 0, 0, 7, 7, 3, 3, 4, 4, 6, 6, 1, 1, 2]]).T + +labels = ['1', '0', '1', '2', '3', '2', '1', '1', '0'] +label_xys = np.array([[2.0, 4.0, 0.5, 1.5, 2.5, 4.0, 3.5, 2.0, 3.5], + [1.5, 1.5, 3.5, 3.5, 3.5, 3.5, 4.5, 5.5, 5.5]]).T + +for ax, fill_rule in zip(axs, ['nonzero', 'evenodd']): + polygon = Polygon(vertices, facecolor='green', edgecolor='red', + fill_rule=fill_rule) + ax.add_patch(polygon) + + ax.plot(*vertices.T, '.', markersize=10, color='red') + + for label, label_xy in zip(labels, label_xys): + ax.text(*label_xy, label, ha='center', va='center') + + ax.set_axis_off() + ax.set_title(f'fill_rule={fill_rule}') + +plt.show() + +# %% +# +# .. admonition:: References +# +# The use of the following functions, methods, classes and modules is shown +# in this example: +# +# - `matplotlib.patches` +# - `matplotlib.patches.Polygon` +# - `matplotlib.axes.Axes.add_patch` +# - `matplotlib.patches.Patch.set_fill_rule` diff --git a/galleries/tutorials/artists.py b/galleries/tutorials/artists.py index 21ba6ee0a9de..08f65079fd89 100644 --- a/galleries/tutorials/artists.py +++ b/galleries/tutorials/artists.py @@ -205,6 +205,7 @@ class in the Matplotlib API, and the one you will be working with most # animated = False # antialiased or aa = False # bbox = Bbox(x0=0.0, y0=0.0, x1=1.0, y1=1.0) +# blend_mode = normal # capstyle = butt # children = [] # clip_box = None @@ -217,6 +218,7 @@ class in the Matplotlib API, and the one you will be working with most # facecolor or fc = (1.0, 1.0, 1.0, 1.0) # figure = Figure(640x480) # fill = True +# fill_rule = nonzero # gid = None # hatch = None # height = 1 diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index 5123817507e0..7b7947b2cf90 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -245,6 +245,7 @@ def __init__(self): self._animated = False self._alpha = None self._blend_mode = "normal" + self._fill_rule = "nonzero" self.clipbox = None self._clippath = None self._clipon = True @@ -1278,6 +1279,7 @@ def update_from(self, other): self._visible = other._visible self._alpha = other._alpha self._blend_mode = other._blend_mode + self._fill_rule = other._fill_rule self.clipbox = other.clipbox self._clipon = other._clipon self._clippath = other._clippath diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index d73dd81c32fc..ddb1425b02cf 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -757,6 +757,7 @@ def __init__(self): self._alpha = 1.0 self._forced_alpha = False # if True, _alpha overrides A from RGBA self._blend_mode = "normal" + self._fill_rule = "nonzero" self._antialiased = 1 # use 0, 1 not True, False for extension code self._capstyle = CapStyle('butt') self._cliprect = None @@ -779,6 +780,7 @@ def copy_properties(self, gc): self._alpha = gc._alpha self._forced_alpha = gc._forced_alpha self._blend_mode = gc._blend_mode + self._fill_rule = gc._fill_rule self._antialiased = gc._antialiased self._capstyle = gc._capstyle self._cliprect = gc._cliprect @@ -813,6 +815,9 @@ def get_blend_mode(self): """Return the blend mode for compositing - not supported on all backends.""" return self._blend_mode + def get_fill_rule(self): + return self._fill_rule + def get_antialiased(self): """Return whether the object should try to do antialiased rendering.""" return self._antialiased @@ -924,6 +929,9 @@ def set_blend_mode(self, blend_mode): # Backend-independent input validation is done in Artist.set_blend_mode() self._blend_mode = blend_mode + def set_fill_rule(self, fill_rule): + self._fill_rule = fill_rule + def set_antialiased(self, b): """Set whether object should be drawn with antialiased rendering.""" # Use ints to make life easier on extension code trying to read the gc. diff --git a/lib/matplotlib/backend_bases.pyi b/lib/matplotlib/backend_bases.pyi index 399607063f15..64cea7f8da7f 100644 --- a/lib/matplotlib/backend_bases.pyi +++ b/lib/matplotlib/backend_bases.pyi @@ -6,7 +6,7 @@ from matplotlib import ( widgets, _api, ) -from matplotlib.artist import Artist, BlendMode +from matplotlib.artist import Artist from matplotlib.axes import Axes from matplotlib.backend_managers import ToolManager from matplotlib.backend_tools import Cursors, ToolBase @@ -27,6 +27,7 @@ from .typing import ( CloseEventType, ColorType, DrawEventType, + FillRuleType, JoinStyleType, KeyEventType, LineStyleType, @@ -159,6 +160,7 @@ class GraphicsContextBase: def restore(self) -> None: ... def get_alpha(self) -> float: ... def get_blend_mode(self) -> str: ... + def get_fill_rule(self) -> FillRuleType: ... def get_antialiased(self) -> int: ... def get_capstyle(self) -> Literal["butt", "projecting", "round"]: ... def get_clip_rectangle(self) -> Bbox | None: ... @@ -174,7 +176,8 @@ class GraphicsContextBase: def get_gid(self) -> int | None: ... def get_snap(self) -> bool | None: ... def set_alpha(self, alpha: float) -> None: ... - def set_blend_mode(self, blend_mode: str | BlendMode) -> None: ... + def set_blend_mode(self, blend_mode: BlendModeType) -> None: ... + def set_fill_rule(self, fill_rule: FillRuleType) -> None: ... def set_antialiased(self, b: bool) -> None: ... def set_capstyle(self, cs: CapStyleType) -> None: ... def set_clip_rectangle(self, rectangle: Bbox | None) -> None: ... diff --git a/lib/matplotlib/backends/backend_cairo.py b/lib/matplotlib/backends/backend_cairo.py index c034949c6a07..be17fa693ae3 100644 --- a/lib/matplotlib/backends/backend_cairo.py +++ b/lib/matplotlib/backends/backend_cairo.py @@ -423,6 +423,11 @@ class GraphicsContextCairo(GraphicsContextBase): 'luminosity': cairo.OPERATOR_HSL_LUMINOSITY, } + _filld = { + 'nonzero': cairo.FILL_RULE_WINDING, + 'evenodd': cairo.FILL_RULE_EVEN_ODD, + } + def __init__(self, renderer): super().__init__() self.renderer = renderer @@ -501,6 +506,11 @@ def set_blend_mode(self, blend_mode): self.ctx.set_operator(_api.getitem_checked(self._operatord, blend_mode=self._blend_mode)) + def set_fill_rule(self, fill_rule): + super().set_fill_rule(fill_rule) + self.ctx.set_fill_rule(_api.getitem_checked(self._filld, + fill_rule=self._fill_rule)) + class _CairoRegion: def __init__(self, slices, data): diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index 7e480f84fc67..67ff38fb325f 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -440,7 +440,9 @@ class Op(Enum): close_fill_stroke = b'b' fill_stroke = b'B' + fill_evenodd_stroke = b'B*' fill = b'f' + fill_evenodd = b'f*' closepath = b'h' close_stroke = b's' stroke = b'S' @@ -481,7 +483,7 @@ def pdfRepr(self): return self.value @classmethod - def paint_path(cls, fill, stroke): + def paint_path(cls, fill, stroke, *, fill_rule="nonzero"): """ Return the PDF operator to paint a path. @@ -494,11 +496,15 @@ def paint_path(cls, fill, stroke): """ if stroke: if fill: + if fill_rule == "evenodd": + return cls.fill_evenodd_stroke return cls.fill_stroke else: return cls.stroke else: if fill: + if fill_rule == "evenodd": + return cls.fill_evenodd return cls.fill else: return cls.endpath @@ -1983,7 +1989,7 @@ def draw_path(self, gc, path, transform, rgbFace=None): path, transform, rgbFace is None and gc.get_hatch_path() is None, gc.get_sketch_params()) - self.file.output(self.gc.paint()) + self.file.output(self.gc.paint(fill_rule=gc._fill_rule)) def draw_path_collection(self, gc, master_transform, paths, all_transforms, offsets, offset_trans, facecolors, edgecolors, @@ -2489,12 +2495,12 @@ def fill(self, *args): (_fillcolor is not None and (len(_fillcolor) <= 3 or _fillcolor[3] != 0.0))) - def paint(self): + def paint(self, *, fill_rule="nonzero"): """ Return the appropriate pdf operator to cause the path to be stroked, filled, or both. """ - return Op.paint_path(self.fill(), self.stroke()) + return Op.paint_path(self.fill(), self.stroke(), fill_rule=fill_rule) capstyles = {'butt': 0, 'round': 1, 'projecting': 2} joinstyles = {'miter': 0, 'round': 1, 'bevel': 2} diff --git a/lib/matplotlib/backends/backend_pgf.py b/lib/matplotlib/backends/backend_pgf.py index 06853b13dba7..7ea6eb09b557 100644 --- a/lib/matplotlib/backends/backend_pgf.py +++ b/lib/matplotlib/backends/backend_pgf.py @@ -556,8 +556,10 @@ def _print_pgf_path_styles(self, gc, rgbFace): r"\definecolor{currentfill}{rgb}{%f,%f,%f}" % tuple(rgbFace[:3])) _writeln(self.fh, r"\pgfsetfillcolor{currentfill}") - if has_fill and fillopacity != 1.0: - _writeln(self.fh, r"\pgfsetfillopacity{%f}" % fillopacity) + if fillopacity != 1.0: + _writeln(self.fh, r"\pgfsetfillopacity{%f}" % fillopacity) + if gc.get_fill_rule() == "evenodd": + _writeln(self.fh, r"\pgfseteorule") # linewidth and color lw = gc.get_linewidth() * mpl_pt_to_in * latex_in_to_pt diff --git a/lib/matplotlib/backends/backend_ps.py b/lib/matplotlib/backends/backend_ps.py index 67a02b3fb064..7061ee6e2758 100644 --- a/lib/matplotlib/backends/backend_ps.py +++ b/lib/matplotlib/backends/backend_ps.py @@ -924,6 +924,7 @@ def _draw_ps(self, ps, gc, rgbFace, *, fill=True, stroke=True): if self._is_transparent(rgbFace): fill = False hatch = gc.get_hatch() + fill_op = "eofill" if gc.get_fill_rule() == "evenodd" else "fill" if mightstroke: self.set_linewidth(gc.get_linewidth()) @@ -943,7 +944,7 @@ def _draw_ps(self, ps, gc, rgbFace, *, fill=True, stroke=True): if stroke or hatch: write("gsave\n") self.set_color(*rgbFace[:3], store=False) - write("fill\n") + write(f"{fill_op}\n") if stroke or hatch: write("grestore\n") @@ -951,7 +952,7 @@ def _draw_ps(self, ps, gc, rgbFace, *, fill=True, stroke=True): hatch_name = self.create_hatch(hatch, gc.get_hatch_linewidth()) write("gsave\n") write(_nums_to_str(*gc.get_hatch_color()[:3])) - write(f" {hatch_name} setpattern fill grestore\n") + write(f" {hatch_name} setpattern {fill_op} grestore\n") if stroke: write("stroke\n") diff --git a/lib/matplotlib/backends/backend_svg.py b/lib/matplotlib/backends/backend_svg.py index 8373ffecb5b9..00790ab698de 100644 --- a/lib/matplotlib/backends/backend_svg.py +++ b/lib/matplotlib/backends/backend_svg.py @@ -620,6 +620,8 @@ def _get_style_dict(self, gc, rgbFace): attrib['opacity'] = _short_float_fmt(gc.get_alpha()) if (blend_mode := _svg_blend_mode(gc.get_blend_mode())) != "normal": attrib["mix-blend-mode"] = blend_mode + if (fill_rule := gc.get_fill_rule()) != "nonzero": + attrib["fill-rule"] = fill_rule offset, seq = gc.get_dashes() if seq is not None: diff --git a/lib/matplotlib/patches.py b/lib/matplotlib/patches.py index cae9e43a752a..c88d98f94a77 100644 --- a/lib/matplotlib/patches.py +++ b/lib/matplotlib/patches.py @@ -634,6 +634,29 @@ def get_hatch_linewidth(self): """Return the hatch linewidth.""" return self._hatch_linewidth + def set_fill_rule(self, fill_rule): + """ + Set the rule for filling a shape. + + See :doc:`/gallery/shapes_and_collections/fill_rule_demo`. + + Parameters + ---------- + fill_rule : {'nonzero', 'evenodd'} + 'nonzero' for the non-zero winding rule (the default), or + 'evenodd' for the even-odd rule + + References + ---------- + * `Wikipedia: Non-zero winding rule `__ + * `Wikipedia: Even-odd rule `__ + """ + _api.check_in_list(["nonzero", "evenodd"], fill_rule=fill_rule) + self._fill_rule = fill_rule + + def get_fill_rule(self): + return self._fill_rule + def _has_dashed_edge(self): """ Return whether the patch edge has a dashed linestyle. @@ -673,6 +696,7 @@ def _draw_paths_with_artist_properties( gc.set_alpha(self._alpha) gc.set_blend_mode(self.get_blend_mode()) + gc.set_fill_rule(self.get_fill_rule()) if self._hatch: gc.set_hatch(self._hatch) diff --git a/lib/matplotlib/patches.pyi b/lib/matplotlib/patches.pyi index 60b03b0f1e0c..8dd128ec7462 100644 --- a/lib/matplotlib/patches.pyi +++ b/lib/matplotlib/patches.pyi @@ -8,7 +8,7 @@ from typing import Any, Literal, overload import numpy as np from numpy.typing import ArrayLike -from .typing import ColorType, LineStyleType, CapStyleType, JoinStyleType +from .typing import ColorType, FillRuleType, LineStyleType, CapStyleType, JoinStyleType class Patch(artist.Artist): zorder: float @@ -68,6 +68,8 @@ class Patch(artist.Artist): def set_hatch_linewidth(self, lw: float) -> None: ... def get_hatch_linewidth(self) -> float: ... def get_hatch(self) -> str: ... + def set_fill_rule(self, fill_rule: FillRuleType) -> None: ... + def get_fill_rule(self) -> FillRuleType: ... def get_path(self) -> Path: ... class Shadow(Patch): diff --git a/lib/matplotlib/tests/baseline_images/test_backends_rendering/fill_rules.pdf b/lib/matplotlib/tests/baseline_images/test_backends_rendering/fill_rules.pdf new file mode 100644 index 0000000000000000000000000000000000000000..3882344bb9fbcbc4f344a3f1acefd7b28b13e4e0 GIT binary patch literal 2839 zcmdT`YgAKL7M>YuOH(YkDpqS}c^1k`L+&FFqJlgPVu(N@A_ZFWxP)NH4ao&#MTS~h zTm=yyC2D}74T>%ld32zNQY}=erBI5!!>{ z#Dtzq*2&>{X#2$g5fKWN)SooK9Z~qh`Fz0Up&*ul1i(YVnR>8xO4CoHA>jz4)n+?_ z{ygou5JID*;sFoi0z{xv!-K-78lFCokdq36wpUN-2x%h2kY5lXB45KCE{#VvA8%-U ze7j)5z6AV{doE`Cneod_1x2 z;@HxZiNSuBL(F(`hHYos#Ebhby>rcBVlb#roa*gSy1g#TeyGNw?90$Fe^$a<`sx?N zZAAj6bH%AQzE0N*KJ2zh-M8FT^I1*Mrnd*#+D9fNCx&&$wq>Mz&*Xo8|6RN1Uq9>^ z$?1F0?~)-q*5&gT|LlBmUjM!QFGN%tU(DMdzGitX&oQK8z%C|6xH<15bE9MFsr>eA z)8t`hbV2a8orV7VS61}Z*6%uh>Ey27w2ZyS4m^^Zn==lZPIS5L_P`Gu6_~g7n7$Bs zEiV|>P)Ea)n2+$)Jq-`ALh5qEqO2 zR(7U7;#+x;yP)`H<5#aZ-)dGL;yrAsZ1KMLUG~w5rq9~jxsw5|uJ!LHBnn+WOr~}i zmM++p74EdrF=Z{%oUVO&vh`0n3zFBK*4vh^Dt2OWT9Mf+1*Z(<>lf~=S+5MNS>JZnn5Y_dstawkv3K#2t-sXZHwG~A{qPtnOV?jay81YnV3 zG(J@tYI?9$rG{iMHDVzCGr07`0sO}TWV?hSwDda?Wqo7J0yS-97^DzA0g?Lr9D<0X zluku+0L>z(S~xhvkT!KU&vzgFFwnd%ju2NFVa^8K}D6lsDK zJqw7oMDxRocdK`|b9=JmN@KS__%Z%Yj(HC+syB#AB8osZ9uZe^#1GT&ue1#XP%H`wlKNx1R;Cnd)8 zIvf8BEk(JT+e5tXetp15?XM|*!#)W{x~Kznu||4}Clf~c-*Y2VC6Xw&|SP&E~T`Jy4D-in_YbI+j64#4#WXaT(~-3-GvHHW$Q!r!oxsn7)8!T83kM7^1l{ z3}r*NKP%(H?q{wH$KXoyWE>dRXZ3i{>2qZ$%A2<@%7%>jj2?z!^K3zJbe=63hRv7p z=)yI3UmWMm+LzKvRcb0l+QcM0e}fQU(wP8mz97p#dRfhA=u?VFyG>L`n=aSjh< LI6H3)6)^q_n92a| literal 0 HcmV?d00001 diff --git a/lib/matplotlib/tests/baseline_images/test_backends_rendering/fill_rules.png b/lib/matplotlib/tests/baseline_images/test_backends_rendering/fill_rules.png new file mode 100644 index 0000000000000000000000000000000000000000..746b706225f824ab118a919a0da70c158f4e33f4 GIT binary patch literal 16196 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�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 literal 0 HcmV?d00001 diff --git a/lib/matplotlib/tests/baseline_images/test_backends_rendering/fill_rules.svg b/lib/matplotlib/tests/baseline_images/test_backends_rendering/fill_rules.svg new file mode 100644 index 000000000000..188ac12f3fd8 --- /dev/null +++ b/lib/matplotlib/tests/baseline_images/test_backends_rendering/fill_rules.svg @@ -0,0 +1,240 @@ + + + + + + + + 2026-09-03T23:38:18.563086 + image/svg+xml + + + Matplotlib v3.12.0.dev540+gbdec8821e.d20260904, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_backends_rendering/fill_rules_cairo.png b/lib/matplotlib/tests/baseline_images/test_backends_rendering/fill_rules_cairo.png new file mode 100644 index 0000000000000000000000000000000000000000..0b0d1658518c229a8c572ce36d010b0f86665e43 GIT binary patch literal 16397 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 I5D?{m0VJ$#m;e9( literal 0 HcmV?d00001 diff --git a/lib/matplotlib/tests/test_backends_rendering.py b/lib/matplotlib/tests/test_backends_rendering.py index 48d20054c92b..72de3238706c 100644 --- a/lib/matplotlib/tests/test_backends_rendering.py +++ b/lib/matplotlib/tests/test_backends_rendering.py @@ -11,7 +11,8 @@ from matplotlib.backends.backend_pgf import RendererPgf from matplotlib.backends.backend_svg import RendererSVG from matplotlib.figure import Figure -from matplotlib.patches import Circle, Rectangle +from matplotlib.patches import Circle, PathPatch, Polygon, Rectangle +from matplotlib.path import Path from matplotlib.testing._markers import needs_pgf_pdflatex from matplotlib.testing.decorators import image_comparison @@ -277,3 +278,56 @@ def test_group_invalid_blend_mode(renderer): with pytest.raises(ValueError, match="not a valid value for blend_mode"): renderer_instance.open_blend_group("invalid_blend_mode") + + +def plot_fill_rule_comparison(): + 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() + + +@image_comparison(['fill_rules'], extensions=['png', 'svg', 'pdf'], style='mpl20') +def test_fill_rules(): + plot_fill_rule_comparison() + + +@pytest.mark.backend('cairo') +@image_comparison(['fill_rules_cairo.png'], style='mpl20') +def test_fill_rules_cairo(): + plot_fill_rule_comparison() + + +@needs_pgf_pdflatex +@pytest.mark.backend('pgf') +@image_comparison(['fill_rules_pgf.pdf'], style='mpl20') +def test_fill_rules_pgf(): + plot_fill_rule_comparison() diff --git a/lib/matplotlib/typing.py b/lib/matplotlib/typing.py index 4b87915ce303..1e2bec055395 100644 --- a/lib/matplotlib/typing.py +++ b/lib/matplotlib/typing.py @@ -61,6 +61,9 @@ ) """Blend modes. See :ref:`blend-modes`.""" +type FillRuleType = Literal["nonzero", "evenodd"] +"""Fill rule options.""" + type LineStyleType = ( Literal["-", "solid", "--", "dashed", "-.", "dashdot", ":", "dotted", "", "none", " ", "None"] | diff --git a/src/_backend_agg.h b/src/_backend_agg.h index 36b0dc49c20a..78b4b2fdfc2e 100644 --- a/src/_backend_agg.h +++ b/src/_backend_agg.h @@ -295,6 +295,8 @@ template inline void RendererAgg::_draw_path(path_t &path, bool has_clippath, const std::optional &face, GCAgg &gc) { + theRasterizer.filling_rule(gc.filling_rule); + // Render face if (face) { theRasterizer.add_path(path); @@ -380,6 +382,8 @@ RendererAgg::_draw_path(path_t &path, bool has_clippath, const std::optional struct type_caster { + public: + PYBIND11_TYPE_CASTER(agg::filling_rule_e, const_name("filling_rule_e")); + + bool load(handle src, bool) { + const std::unordered_map enum_values = { + {"nonzero", agg::fill_non_zero}, + {"evenodd", agg::fill_even_odd}, + }; + value = enum_values.at(src.cast()); + return true; + } + }; + template <> struct type_caster { public: PYBIND11_TYPE_CASTER(agg::line_cap_e, const_name("line_cap_e")); @@ -278,6 +293,7 @@ namespace PYBIND11_NAMESPACE { namespace detail { value.alpha = src.attr("_alpha").cast(); value.forced_alpha = src.attr("_forced_alpha").cast(); value.comp_op = src.attr("_blend_mode").cast(); + value.filling_rule = src.attr("_fill_rule").cast(); value.color = src.attr("_rgb").cast(); value.isaa = src.attr("_antialiased").cast(); value.cap = src.attr("_capstyle").cast();