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

Skip to content

Commit 8f2d344

Browse files
committed
Enable choosing between two fill rules
1 parent ff01737 commit 8f2d344

23 files changed

Lines changed: 469 additions & 12 deletions

doc/api/typing_api.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ Artist styles
2525
=============
2626

2727
.. autodata:: matplotlib.typing.BlendModeType
28+
.. autodata:: matplotlib.typing.FillRuleType
2829
.. autodata:: matplotlib.typing.LineStyleType
2930
.. autodata:: matplotlib.typing.DrawStyleType
3031
.. autodata:: matplotlib.typing.MarkEveryType
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Option to use the even-odd fill rule for patches
2+
------------------------------------------------
3+
4+
By default, patches such as `~.patches.Polygon` are filled according to the
5+
`non-zero winding fill rule <https://en.wikipedia.org/wiki/Nonzero-rule>`__.
6+
There is now the option to instead use the
7+
`even-odd fill rule <https://en.wikipedia.org/wiki/Even%E2%80%93odd_rule>`__,
8+
which is specified by setting the patch's ``fill_rule`` property to "evenodd".
9+
See :doc:`/gallery/shapes_and_collections/fill_rule_demo` for more details and
10+
an illustration of the difference.
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
"""
2+
==============
3+
Fill rule demo
4+
==============
5+
6+
By default, patches such as `~.patches.Polygon` are filled according to the
7+
`non-zero winding fill rule <https://en.wikipedia.org/wiki/Nonzero-rule>`__.
8+
Any given point has a winding number, which is the number of times the path
9+
wraps around the point in the clockwise direction. For this fill rule, the
10+
filled regions are where the winding number is non-zero. See
11+
:doc:`/gallery/shapes_and_collections/donut` for an example of how to leverage
12+
the winding directions in multiple segments of a path under this fill rule.
13+
14+
The other option for fill rule is the
15+
`even-odd fill rule <https://en.wikipedia.org/wiki/Even%E2%80%93odd_rule>`__,
16+
which is specified by setting the patch's ``fill_rule`` property to "evenodd".
17+
For this fill rule, the filled regions are where the winding number is an odd
18+
number. This fill rule allows for the construction of patterns of fill
19+
regions that would otherwise take many more vertices to construct under the
20+
non-zero winding fill rule.
21+
22+
This example demonstrates the difference between the two fill rules for a single
23+
`~.patches.Polygon` that intersects itself multiple times. The winding number
24+
for each closed region is labeled.
25+
26+
"""
27+
28+
import matplotlib.pyplot as plt
29+
import numpy as np
30+
31+
from matplotlib.patches import Polygon
32+
33+
fig, axs = plt.subplots(1, 2)
34+
35+
vertices = np.array([[0, 0, 6, 6, 1, 1, 5, 5, 2, 2, 4, 4, 3, 3, 5, 5],
36+
[2, 5, 5, 0, 0, 7, 7, 3, 3, 4, 4, 6, 6, 1, 1, 2]]).T
37+
38+
labels = ['1', '0', '1', '2', '3', '2', '1', '1', '0']
39+
label_xys = np.array([[2.0, 4.0, 0.5, 1.5, 2.5, 4.0, 3.5, 2.0, 3.5],
40+
[1.5, 1.5, 3.5, 3.5, 3.5, 3.5, 4.5, 5.5, 5.5]]).T
41+
42+
for ax, fill_rule in zip(axs, ['nonzero', 'evenodd']):
43+
polygon = Polygon(vertices, facecolor='green', edgecolor='red',
44+
fill_rule=fill_rule)
45+
ax.add_patch(polygon)
46+
47+
ax.plot(*vertices.T, '.', markersize=10, color='red')
48+
49+
for label, label_xy in zip(labels, label_xys):
50+
ax.text(*label_xy, label, ha='center', va='center')
51+
52+
ax.set_axis_off()
53+
ax.set_title(f'fill_rule={fill_rule}')
54+
55+
plt.show()
56+
57+
# %%
58+
#
59+
# .. admonition:: References
60+
#
61+
# The use of the following functions, methods, classes and modules is shown
62+
# in this example:
63+
#
64+
# - `matplotlib.patches`
65+
# - `matplotlib.patches.Polygon`
66+
# - `matplotlib.axes.Axes.add_patch`
67+
# - `matplotlib.patches.Patch.set_fill_rule`

galleries/tutorials/artists.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ class in the Matplotlib API, and the one you will be working with most
205205
# animated = False
206206
# antialiased or aa = False
207207
# bbox = Bbox(x0=0.0, y0=0.0, x1=1.0, y1=1.0)
208+
# blend_mode = normal
208209
# capstyle = butt
209210
# children = []
210211
# clip_box = None
@@ -217,6 +218,7 @@ class in the Matplotlib API, and the one you will be working with most
217218
# facecolor or fc = (1.0, 1.0, 1.0, 1.0)
218219
# figure = Figure(640x480)
219220
# fill = True
221+
# fill_rule = nonzero
220222
# gid = None
221223
# hatch = None
222224
# height = 1

lib/matplotlib/artist.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,7 @@ def __init__(self):
245245
self._animated = False
246246
self._alpha = None
247247
self._blend_mode = "normal"
248+
self._fill_rule = "nonzero"
248249
self.clipbox = None
249250
self._clippath = None
250251
self._clipon = True
@@ -1278,6 +1279,7 @@ def update_from(self, other):
12781279
self._visible = other._visible
12791280
self._alpha = other._alpha
12801281
self._blend_mode = other._blend_mode
1282+
self._fill_rule = other._fill_rule
12811283
self.clipbox = other.clipbox
12821284
self._clipon = other._clipon
12831285
self._clippath = other._clippath

lib/matplotlib/backend_bases.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -757,6 +757,7 @@ def __init__(self):
757757
self._alpha = 1.0
758758
self._forced_alpha = False # if True, _alpha overrides A from RGBA
759759
self._blend_mode = "normal"
760+
self._fill_rule = "nonzero"
760761
self._antialiased = 1 # use 0, 1 not True, False for extension code
761762
self._capstyle = CapStyle('butt')
762763
self._cliprect = None
@@ -779,6 +780,7 @@ def copy_properties(self, gc):
779780
self._alpha = gc._alpha
780781
self._forced_alpha = gc._forced_alpha
781782
self._blend_mode = gc._blend_mode
783+
self._fill_rule = gc._fill_rule
782784
self._antialiased = gc._antialiased
783785
self._capstyle = gc._capstyle
784786
self._cliprect = gc._cliprect
@@ -813,6 +815,9 @@ def get_blend_mode(self):
813815
"""Return the blend mode for compositing - not supported on all backends."""
814816
return self._blend_mode
815817

818+
def get_fill_rule(self):
819+
return self._fill_rule
820+
816821
def get_antialiased(self):
817822
"""Return whether the object should try to do antialiased rendering."""
818823
return self._antialiased
@@ -924,6 +929,9 @@ def set_blend_mode(self, blend_mode):
924929
# Backend-independent input validation is done in Artist.set_blend_mode()
925930
self._blend_mode = blend_mode
926931

932+
def set_fill_rule(self, fill_rule):
933+
self._fill_rule = fill_rule
934+
927935
def set_antialiased(self, b):
928936
"""Set whether object should be drawn with antialiased rendering."""
929937
# Use ints to make life easier on extension code trying to read the gc.

lib/matplotlib/backend_bases.pyi

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ from matplotlib import (
66
widgets,
77
_api,
88
)
9-
from matplotlib.artist import Artist, BlendMode
9+
from matplotlib.artist import Artist
1010
from matplotlib.axes import Axes
1111
from matplotlib.backend_managers import ToolManager
1212
from matplotlib.backend_tools import Cursors, ToolBase
@@ -27,6 +27,7 @@ from .typing import (
2727
CloseEventType,
2828
ColorType,
2929
DrawEventType,
30+
FillRuleType,
3031
JoinStyleType,
3132
KeyEventType,
3233
LineStyleType,
@@ -159,6 +160,7 @@ class GraphicsContextBase:
159160
def restore(self) -> None: ...
160161
def get_alpha(self) -> float: ...
161162
def get_blend_mode(self) -> str: ...
163+
def get_fill_rule(self) -> FillRuleType: ...
162164
def get_antialiased(self) -> int: ...
163165
def get_capstyle(self) -> Literal["butt", "projecting", "round"]: ...
164166
def get_clip_rectangle(self) -> Bbox | None: ...
@@ -174,7 +176,8 @@ class GraphicsContextBase:
174176
def get_gid(self) -> int | None: ...
175177
def get_snap(self) -> bool | None: ...
176178
def set_alpha(self, alpha: float) -> None: ...
177-
def set_blend_mode(self, blend_mode: str | BlendMode) -> None: ...
179+
def set_blend_mode(self, blend_mode: BlendModeType) -> None: ...
180+
def set_fill_rule(self, fill_rule: FillRuleType) -> None: ...
178181
def set_antialiased(self, b: bool) -> None: ...
179182
def set_capstyle(self, cs: CapStyleType) -> None: ...
180183
def set_clip_rectangle(self, rectangle: Bbox | None) -> None: ...

lib/matplotlib/backends/backend_cairo.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,11 @@ class GraphicsContextCairo(GraphicsContextBase):
423423
'luminosity': cairo.OPERATOR_HSL_LUMINOSITY,
424424
}
425425

426+
_filld = {
427+
'nonzero': cairo.FILL_RULE_WINDING,
428+
'evenodd': cairo.FILL_RULE_EVEN_ODD,
429+
}
430+
426431
def __init__(self, renderer):
427432
super().__init__()
428433
self.renderer = renderer
@@ -501,6 +506,11 @@ def set_blend_mode(self, blend_mode):
501506
self.ctx.set_operator(_api.getitem_checked(self._operatord,
502507
blend_mode=self._blend_mode))
503508

509+
def set_fill_rule(self, fill_rule):
510+
super().set_fill_rule(fill_rule)
511+
self.ctx.set_fill_rule(_api.getitem_checked(self._filld,
512+
fill_rule=self._fill_rule))
513+
504514

505515
class _CairoRegion:
506516
def __init__(self, slices, data):

lib/matplotlib/backends/backend_pdf.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,9 @@ class Op(Enum):
440440

441441
close_fill_stroke = b'b'
442442
fill_stroke = b'B'
443+
fill_evenodd_stroke = b'B*'
443444
fill = b'f'
445+
fill_evenodd = b'f*'
444446
closepath = b'h'
445447
close_stroke = b's'
446448
stroke = b'S'
@@ -481,7 +483,7 @@ def pdfRepr(self):
481483
return self.value
482484

483485
@classmethod
484-
def paint_path(cls, fill, stroke):
486+
def paint_path(cls, fill, stroke, *, fill_rule="nonzero"):
485487
"""
486488
Return the PDF operator to paint a path.
487489
@@ -494,11 +496,15 @@ def paint_path(cls, fill, stroke):
494496
"""
495497
if stroke:
496498
if fill:
499+
if fill_rule == "evenodd":
500+
return cls.fill_evenodd_stroke
497501
return cls.fill_stroke
498502
else:
499503
return cls.stroke
500504
else:
501505
if fill:
506+
if fill_rule == "evenodd":
507+
return cls.fill_evenodd
502508
return cls.fill
503509
else:
504510
return cls.endpath
@@ -1983,7 +1989,7 @@ def draw_path(self, gc, path, transform, rgbFace=None):
19831989
path, transform,
19841990
rgbFace is None and gc.get_hatch_path() is None,
19851991
gc.get_sketch_params())
1986-
self.file.output(self.gc.paint())
1992+
self.file.output(self.gc.paint(fill_rule=gc._fill_rule))
19871993

19881994
def draw_path_collection(self, gc, master_transform, paths, all_transforms,
19891995
offsets, offset_trans, facecolors, edgecolors,
@@ -2489,12 +2495,12 @@ def fill(self, *args):
24892495
(_fillcolor is not None and
24902496
(len(_fillcolor) <= 3 or _fillcolor[3] != 0.0)))
24912497

2492-
def paint(self):
2498+
def paint(self, *, fill_rule="nonzero"):
24932499
"""
24942500
Return the appropriate pdf operator to cause the path to be
24952501
stroked, filled, or both.
24962502
"""
2497-
return Op.paint_path(self.fill(), self.stroke())
2503+
return Op.paint_path(self.fill(), self.stroke(), fill_rule=fill_rule)
24982504

24992505
capstyles = {'butt': 0, 'round': 1, 'projecting': 2}
25002506
joinstyles = {'miter': 0, 'round': 1, 'bevel': 2}

lib/matplotlib/backends/backend_pgf.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -556,8 +556,10 @@ def _print_pgf_path_styles(self, gc, rgbFace):
556556
r"\definecolor{currentfill}{rgb}{%f,%f,%f}"
557557
% tuple(rgbFace[:3]))
558558
_writeln(self.fh, r"\pgfsetfillcolor{currentfill}")
559-
if has_fill and fillopacity != 1.0:
560-
_writeln(self.fh, r"\pgfsetfillopacity{%f}" % fillopacity)
559+
if fillopacity != 1.0:
560+
_writeln(self.fh, r"\pgfsetfillopacity{%f}" % fillopacity)
561+
if gc.get_fill_rule() == "evenodd":
562+
_writeln(self.fh, r"\pgfseteorule")
561563

562564
# linewidth and color
563565
lw = gc.get_linewidth() * mpl_pt_to_in * latex_in_to_pt

0 commit comments

Comments
 (0)