rgrid-python 4.5.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- grid_py/__init__.py +340 -0
- grid_py/_arrow.py +331 -0
- grid_py/_clippath.py +170 -0
- grid_py/_colour.py +815 -0
- grid_py/_coords.py +1534 -0
- grid_py/_curve.py +1668 -0
- grid_py/_display_list.py +507 -0
- grid_py/_draw.py +1397 -0
- grid_py/_edit.py +756 -0
- grid_py/_font_metrics.py +319 -0
- grid_py/_gpar.py +572 -0
- grid_py/_grab.py +501 -0
- grid_py/_grob.py +1377 -0
- grid_py/_group.py +798 -0
- grid_py/_highlevel.py +2176 -0
- grid_py/_just.py +361 -0
- grid_py/_layout.py +593 -0
- grid_py/_ls.py +895 -0
- grid_py/_mask.py +196 -0
- grid_py/_path.py +414 -0
- grid_py/_patterns.py +1049 -0
- grid_py/_primitives.py +2198 -0
- grid_py/_renderer_base.py +1184 -0
- grid_py/_scene_graph.py +248 -0
- grid_py/_size.py +1352 -0
- grid_py/_state.py +683 -0
- grid_py/_transforms.py +448 -0
- grid_py/_typeset.py +384 -0
- grid_py/_units.py +1924 -0
- grid_py/_utils.py +310 -0
- grid_py/_viewport.py +1649 -0
- grid_py/_vp_calc.py +970 -0
- grid_py/py.typed +0 -0
- grid_py/renderer.py +1762 -0
- grid_py/renderer_web.py +764 -0
- grid_py/resources/d3.v7.min.js +2 -0
- grid_py/resources/gridpy.css +80 -0
- grid_py/resources/gridpy.js +813 -0
- rgrid_python-4.5.3.dist-info/METADATA +489 -0
- rgrid_python-4.5.3.dist-info/RECORD +42 -0
- rgrid_python-4.5.3.dist-info/WHEEL +4 -0
- rgrid_python-4.5.3.dist-info/licenses/LICENSE +3 -0
grid_py/_mask.py
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""Mask system for grid_py.
|
|
2
|
+
|
|
3
|
+
Python port of R's ``grid`` package mask infrastructure
|
|
4
|
+
(``grid/R/mask.R``). Provides the :class:`GridMask` class and the
|
|
5
|
+
:func:`as_mask` factory function for converting a grob (or a boolean
|
|
6
|
+
sentinel) into an alpha or luminance mask.
|
|
7
|
+
|
|
8
|
+
Classes
|
|
9
|
+
-------
|
|
10
|
+
GridMask
|
|
11
|
+
A mask defined by a reference grob and a compositing type.
|
|
12
|
+
|
|
13
|
+
Functions
|
|
14
|
+
---------
|
|
15
|
+
as_mask
|
|
16
|
+
Factory for :class:`GridMask`.
|
|
17
|
+
is_mask
|
|
18
|
+
Type-check predicate.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from typing import Any, List, Union
|
|
24
|
+
|
|
25
|
+
__all__: List[str] = [
|
|
26
|
+
"GridMask",
|
|
27
|
+
"as_mask",
|
|
28
|
+
"is_mask",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
# Valid mask types, matching R's ``match.arg(type)`` in ``as.mask()``.
|
|
32
|
+
_VALID_TYPES: tuple[str, ...] = ("alpha", "luminance")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ======================================================================
|
|
36
|
+
# GridMask
|
|
37
|
+
# ======================================================================
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class GridMask:
|
|
41
|
+
"""A mask wrapping a grob for alpha or luminance compositing.
|
|
42
|
+
|
|
43
|
+
This is the Python equivalent of the R S3 class ``"GridMask"``
|
|
44
|
+
produced by ``as.mask()``.
|
|
45
|
+
|
|
46
|
+
Parameters
|
|
47
|
+
----------
|
|
48
|
+
ref : Any
|
|
49
|
+
The reference object that defines the mask. Typically a grob,
|
|
50
|
+
but may also be ``True`` (use the default mask) or ``False``
|
|
51
|
+
(disable masking).
|
|
52
|
+
type : str, optional
|
|
53
|
+
Compositing type, either ``"alpha"`` (default) or
|
|
54
|
+
``"luminance"``.
|
|
55
|
+
|
|
56
|
+
Raises
|
|
57
|
+
------
|
|
58
|
+
ValueError
|
|
59
|
+
If *type* is not one of the accepted values.
|
|
60
|
+
|
|
61
|
+
Examples
|
|
62
|
+
--------
|
|
63
|
+
>>> m = GridMask("some_grob", type="alpha")
|
|
64
|
+
>>> m.type
|
|
65
|
+
'alpha'
|
|
66
|
+
>>> m.ref
|
|
67
|
+
'some_grob'
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
__slots__ = ("_ref", "_type")
|
|
71
|
+
|
|
72
|
+
def __init__(self, ref: Any, type: str = "alpha") -> None: # noqa: A002
|
|
73
|
+
if type not in _VALID_TYPES:
|
|
74
|
+
raise ValueError(
|
|
75
|
+
f"'type' must be one of {list(_VALID_TYPES)}, got {type!r}"
|
|
76
|
+
)
|
|
77
|
+
self._ref: Any = ref
|
|
78
|
+
self._type: str = type
|
|
79
|
+
|
|
80
|
+
# -- properties ---------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def ref(self) -> Any:
|
|
84
|
+
"""The grob (or boolean sentinel) that defines the mask.
|
|
85
|
+
|
|
86
|
+
Returns
|
|
87
|
+
-------
|
|
88
|
+
Any
|
|
89
|
+
"""
|
|
90
|
+
return self._ref
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def type(self) -> str:
|
|
94
|
+
"""Compositing type (``"alpha"`` or ``"luminance"``).
|
|
95
|
+
|
|
96
|
+
Returns
|
|
97
|
+
-------
|
|
98
|
+
str
|
|
99
|
+
"""
|
|
100
|
+
return self._type
|
|
101
|
+
|
|
102
|
+
# -- dunder methods -----------------------------------------------------
|
|
103
|
+
|
|
104
|
+
def __repr__(self) -> str:
|
|
105
|
+
return f"GridMask(ref={self._ref!r}, type={self._type!r})"
|
|
106
|
+
|
|
107
|
+
def __eq__(self, other: object) -> bool:
|
|
108
|
+
if isinstance(other, GridMask):
|
|
109
|
+
return self._ref == other._ref and self._type == other._type
|
|
110
|
+
return NotImplemented
|
|
111
|
+
|
|
112
|
+
def __hash__(self) -> int:
|
|
113
|
+
return hash((id(self._ref), self._type))
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# ======================================================================
|
|
117
|
+
# Factory function
|
|
118
|
+
# ======================================================================
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def as_mask(
|
|
122
|
+
x: Union[Any, bool],
|
|
123
|
+
type: str = "alpha", # noqa: A002
|
|
124
|
+
) -> GridMask:
|
|
125
|
+
"""Convert a grob (or boolean) to a :class:`GridMask`.
|
|
126
|
+
|
|
127
|
+
This mirrors R's ``as.mask()`` function from the *grid* package.
|
|
128
|
+
|
|
129
|
+
When *x* is a grob the resulting :class:`GridMask` will use that grob
|
|
130
|
+
as the mask definition. Passing ``True`` or ``False`` creates a
|
|
131
|
+
sentinel mask that signals default or disabled masking behaviour to
|
|
132
|
+
the graphics device.
|
|
133
|
+
|
|
134
|
+
Parameters
|
|
135
|
+
----------
|
|
136
|
+
x : Any
|
|
137
|
+
A grob instance, or ``True``/``False`` for default/disabled
|
|
138
|
+
masking. ``None`` is **not** accepted.
|
|
139
|
+
type : str, optional
|
|
140
|
+
Compositing type -- ``"alpha"`` (default) or ``"luminance"``.
|
|
141
|
+
|
|
142
|
+
Returns
|
|
143
|
+
-------
|
|
144
|
+
GridMask
|
|
145
|
+
A new mask object wrapping *x*.
|
|
146
|
+
|
|
147
|
+
Raises
|
|
148
|
+
------
|
|
149
|
+
TypeError
|
|
150
|
+
If *x* is ``None``.
|
|
151
|
+
ValueError
|
|
152
|
+
If *type* is not ``"alpha"`` or ``"luminance"``.
|
|
153
|
+
|
|
154
|
+
Examples
|
|
155
|
+
--------
|
|
156
|
+
>>> mask = as_mask("placeholder_grob")
|
|
157
|
+
>>> mask.type
|
|
158
|
+
'alpha'
|
|
159
|
+
>>> mask.ref
|
|
160
|
+
'placeholder_grob'
|
|
161
|
+
|
|
162
|
+
>>> mask_lum = as_mask("grob", type="luminance")
|
|
163
|
+
>>> mask_lum.type
|
|
164
|
+
'luminance'
|
|
165
|
+
"""
|
|
166
|
+
if x is None:
|
|
167
|
+
raise TypeError("only a grob (or True/False) can be converted to a mask")
|
|
168
|
+
return GridMask(ref=x, type=type)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# ======================================================================
|
|
172
|
+
# Predicate
|
|
173
|
+
# ======================================================================
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def is_mask(x: Any) -> bool:
|
|
177
|
+
"""Return whether *x* is a :class:`GridMask` instance.
|
|
178
|
+
|
|
179
|
+
Parameters
|
|
180
|
+
----------
|
|
181
|
+
x : Any
|
|
182
|
+
Object to test.
|
|
183
|
+
|
|
184
|
+
Returns
|
|
185
|
+
-------
|
|
186
|
+
bool
|
|
187
|
+
``True`` if *x* is a :class:`GridMask`, ``False`` otherwise.
|
|
188
|
+
|
|
189
|
+
Examples
|
|
190
|
+
--------
|
|
191
|
+
>>> is_mask(as_mask("grob"))
|
|
192
|
+
True
|
|
193
|
+
>>> is_mask("not a mask")
|
|
194
|
+
False
|
|
195
|
+
"""
|
|
196
|
+
return isinstance(x, GridMask)
|
grid_py/_path.py
ADDED
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
"""Path-related classes for grid_py (port of R's grid path system).
|
|
2
|
+
|
|
3
|
+
This module provides :class:`GPath` and :class:`VpPath` for addressing grobs
|
|
4
|
+
and viewports by hierarchical name paths, as well as :class:`GridPath` and
|
|
5
|
+
the helper :func:`as_path` for treating a grob as a single filled/stroked
|
|
6
|
+
path. These are direct ports of the R ``gPath()``, ``vpPath()``, and
|
|
7
|
+
``as.path()`` facilities in the *grid* package.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import Any, Optional, Sequence, Union
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"GPath",
|
|
16
|
+
"VpPath",
|
|
17
|
+
"GridPath",
|
|
18
|
+
"as_path",
|
|
19
|
+
"is_closed",
|
|
20
|
+
"PATH_SEP",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
# Constants
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
PATH_SEP: str = "::"
|
|
28
|
+
"""Separator used between components in both grob and viewport paths."""
|
|
29
|
+
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
# Grob types considered "open" (i.e. not closed shapes).
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
_OPEN_TYPES: frozenset[str] = frozenset(
|
|
35
|
+
{
|
|
36
|
+
"move.to",
|
|
37
|
+
"line.to",
|
|
38
|
+
"lines",
|
|
39
|
+
"polyline",
|
|
40
|
+
"segments",
|
|
41
|
+
"beziergrob",
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
# GPath
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class GPath:
|
|
52
|
+
"""A path addressing a grob inside a grob hierarchy.
|
|
53
|
+
|
|
54
|
+
Components are joined with the ``"::"`` separator, mirroring R's
|
|
55
|
+
``gPath()`` constructor.
|
|
56
|
+
|
|
57
|
+
Parameters
|
|
58
|
+
----------
|
|
59
|
+
*args : str
|
|
60
|
+
One or more path component strings. Each string may itself
|
|
61
|
+
contain ``"::"`` separators which will be split automatically.
|
|
62
|
+
|
|
63
|
+
Examples
|
|
64
|
+
--------
|
|
65
|
+
>>> p = GPath("a", "b", "c")
|
|
66
|
+
>>> str(p)
|
|
67
|
+
'a::b::c'
|
|
68
|
+
>>> p.name
|
|
69
|
+
'c'
|
|
70
|
+
>>> p.path
|
|
71
|
+
'a::b'
|
|
72
|
+
>>> p.n
|
|
73
|
+
3
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
__slots__ = ("_components",)
|
|
77
|
+
|
|
78
|
+
def __init__(self, *args: str) -> None:
|
|
79
|
+
components: list[str] = []
|
|
80
|
+
for a in args:
|
|
81
|
+
if not isinstance(a, str):
|
|
82
|
+
raise TypeError(f"invalid grob name: expected str, got {type(a).__name__}")
|
|
83
|
+
components.extend(a.split(PATH_SEP))
|
|
84
|
+
if len(components) < 1:
|
|
85
|
+
raise ValueError("a grob path must contain at least one grob name")
|
|
86
|
+
for c in components:
|
|
87
|
+
if not c:
|
|
88
|
+
raise ValueError("invalid grob name (empty string)")
|
|
89
|
+
self._components: tuple[str, ...] = tuple(components)
|
|
90
|
+
|
|
91
|
+
# -- properties ---------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def name(self) -> str:
|
|
95
|
+
"""Last (leaf) component of the path.
|
|
96
|
+
|
|
97
|
+
Returns
|
|
98
|
+
-------
|
|
99
|
+
str
|
|
100
|
+
"""
|
|
101
|
+
return self._components[-1]
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def path(self) -> Optional[str]:
|
|
105
|
+
"""Parent path as a ``"::"``-separated string, or ``None``.
|
|
106
|
+
|
|
107
|
+
Returns
|
|
108
|
+
-------
|
|
109
|
+
str or None
|
|
110
|
+
``None`` when the path has only one component.
|
|
111
|
+
"""
|
|
112
|
+
if self._components.__len__() == 1:
|
|
113
|
+
return None
|
|
114
|
+
return PATH_SEP.join(self._components[:-1])
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def n(self) -> int:
|
|
118
|
+
"""Depth (number of components) of the path.
|
|
119
|
+
|
|
120
|
+
Returns
|
|
121
|
+
-------
|
|
122
|
+
int
|
|
123
|
+
"""
|
|
124
|
+
return len(self._components)
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def components(self) -> tuple[str, ...]:
|
|
128
|
+
"""All path components as a tuple of strings.
|
|
129
|
+
|
|
130
|
+
Returns
|
|
131
|
+
-------
|
|
132
|
+
tuple[str, ...]
|
|
133
|
+
"""
|
|
134
|
+
return self._components
|
|
135
|
+
|
|
136
|
+
# -- dunder methods -----------------------------------------------------
|
|
137
|
+
|
|
138
|
+
def __str__(self) -> str:
|
|
139
|
+
return PATH_SEP.join(self._components)
|
|
140
|
+
|
|
141
|
+
def __repr__(self) -> str:
|
|
142
|
+
return f"GPath({PATH_SEP.join(self._components)})"
|
|
143
|
+
|
|
144
|
+
def __eq__(self, other: object) -> bool:
|
|
145
|
+
if isinstance(other, GPath):
|
|
146
|
+
return self._components == other._components
|
|
147
|
+
return NotImplemented
|
|
148
|
+
|
|
149
|
+
def __hash__(self) -> int:
|
|
150
|
+
return hash(self._components)
|
|
151
|
+
|
|
152
|
+
def __len__(self) -> int:
|
|
153
|
+
return self.n
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# ---------------------------------------------------------------------------
|
|
157
|
+
# VpPath
|
|
158
|
+
# ---------------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class VpPath:
|
|
162
|
+
"""A path addressing a viewport inside the viewport tree.
|
|
163
|
+
|
|
164
|
+
Components are joined with ``"::"`` separators, mirroring R's
|
|
165
|
+
``vpPath()`` constructor.
|
|
166
|
+
|
|
167
|
+
Parameters
|
|
168
|
+
----------
|
|
169
|
+
*args : str
|
|
170
|
+
One or more path component strings. Each string may itself
|
|
171
|
+
contain ``"::"`` separators which will be split automatically.
|
|
172
|
+
|
|
173
|
+
Examples
|
|
174
|
+
--------
|
|
175
|
+
>>> vp = VpPath("root", "panel", "strip")
|
|
176
|
+
>>> str(vp)
|
|
177
|
+
'root::panel::strip'
|
|
178
|
+
>>> vp.name
|
|
179
|
+
'strip'
|
|
180
|
+
>>> vp[0:2]
|
|
181
|
+
VpPath(root::panel)
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
__slots__ = ("_components",)
|
|
185
|
+
|
|
186
|
+
def __init__(self, *args: str) -> None:
|
|
187
|
+
components: list[str] = []
|
|
188
|
+
for a in args:
|
|
189
|
+
if not isinstance(a, str):
|
|
190
|
+
raise TypeError(
|
|
191
|
+
f"invalid viewport name: expected str, got {type(a).__name__}"
|
|
192
|
+
)
|
|
193
|
+
components.extend(a.split(PATH_SEP))
|
|
194
|
+
if len(components) < 1:
|
|
195
|
+
raise ValueError(
|
|
196
|
+
"a viewport path must contain at least one viewport name"
|
|
197
|
+
)
|
|
198
|
+
for c in components:
|
|
199
|
+
if not c:
|
|
200
|
+
raise ValueError("invalid viewport name (empty string)")
|
|
201
|
+
self._components: tuple[str, ...] = tuple(components)
|
|
202
|
+
|
|
203
|
+
# -- properties ---------------------------------------------------------
|
|
204
|
+
|
|
205
|
+
@property
|
|
206
|
+
def name(self) -> str:
|
|
207
|
+
"""Last (leaf) component of the path.
|
|
208
|
+
|
|
209
|
+
Returns
|
|
210
|
+
-------
|
|
211
|
+
str
|
|
212
|
+
"""
|
|
213
|
+
return self._components[-1]
|
|
214
|
+
|
|
215
|
+
@property
|
|
216
|
+
def path(self) -> Optional[str]:
|
|
217
|
+
"""Parent path as a ``"::"``-separated string, or ``None``.
|
|
218
|
+
|
|
219
|
+
Returns
|
|
220
|
+
-------
|
|
221
|
+
str or None
|
|
222
|
+
``None`` when the path has only one component.
|
|
223
|
+
"""
|
|
224
|
+
if len(self._components) == 1:
|
|
225
|
+
return None
|
|
226
|
+
return PATH_SEP.join(self._components[:-1])
|
|
227
|
+
|
|
228
|
+
@property
|
|
229
|
+
def n(self) -> int:
|
|
230
|
+
"""Depth (number of components) of the path.
|
|
231
|
+
|
|
232
|
+
Returns
|
|
233
|
+
-------
|
|
234
|
+
int
|
|
235
|
+
"""
|
|
236
|
+
return len(self._components)
|
|
237
|
+
|
|
238
|
+
@property
|
|
239
|
+
def components(self) -> tuple[str, ...]:
|
|
240
|
+
"""All path components as a tuple of strings.
|
|
241
|
+
|
|
242
|
+
Returns
|
|
243
|
+
-------
|
|
244
|
+
tuple[str, ...]
|
|
245
|
+
"""
|
|
246
|
+
return self._components
|
|
247
|
+
|
|
248
|
+
# -- indexing ------------------------------------------------------------
|
|
249
|
+
|
|
250
|
+
def __getitem__(self, index: Union[int, slice]) -> "VpPath":
|
|
251
|
+
"""Index or slice the path to obtain a sub-path.
|
|
252
|
+
|
|
253
|
+
Parameters
|
|
254
|
+
----------
|
|
255
|
+
index : int or slice
|
|
256
|
+
Integer index or slice applied to the ordered components.
|
|
257
|
+
|
|
258
|
+
Returns
|
|
259
|
+
-------
|
|
260
|
+
VpPath
|
|
261
|
+
A new ``VpPath`` built from the selected components.
|
|
262
|
+
|
|
263
|
+
Raises
|
|
264
|
+
------
|
|
265
|
+
IndexError
|
|
266
|
+
If the resulting selection is empty.
|
|
267
|
+
"""
|
|
268
|
+
selected = self._components[index]
|
|
269
|
+
if isinstance(selected, str):
|
|
270
|
+
selected = (selected,)
|
|
271
|
+
if len(selected) == 0:
|
|
272
|
+
raise IndexError("resulting viewport path is empty")
|
|
273
|
+
return VpPath(*selected)
|
|
274
|
+
|
|
275
|
+
# -- dunder methods -----------------------------------------------------
|
|
276
|
+
|
|
277
|
+
def __str__(self) -> str:
|
|
278
|
+
return PATH_SEP.join(self._components)
|
|
279
|
+
|
|
280
|
+
def __repr__(self) -> str:
|
|
281
|
+
return f"VpPath({PATH_SEP.join(self._components)})"
|
|
282
|
+
|
|
283
|
+
def __eq__(self, other: object) -> bool:
|
|
284
|
+
if isinstance(other, VpPath):
|
|
285
|
+
return self._components == other._components
|
|
286
|
+
return NotImplemented
|
|
287
|
+
|
|
288
|
+
def __hash__(self) -> int:
|
|
289
|
+
return hash(self._components)
|
|
290
|
+
|
|
291
|
+
def __len__(self) -> int:
|
|
292
|
+
return self.n
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
# ---------------------------------------------------------------------------
|
|
296
|
+
# GridPath (wraps a grob as a stroke/fill path)
|
|
297
|
+
# ---------------------------------------------------------------------------
|
|
298
|
+
|
|
299
|
+
_VALID_RULES: frozenset[str] = frozenset({"winding", "evenodd"})
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
class GridPath:
|
|
303
|
+
"""Wrapper that marks a grob for path-based stroke/fill operations.
|
|
304
|
+
|
|
305
|
+
This is the Python equivalent of R's ``GridPath`` S3 class produced by
|
|
306
|
+
``as.path()``.
|
|
307
|
+
|
|
308
|
+
Parameters
|
|
309
|
+
----------
|
|
310
|
+
grob : Any
|
|
311
|
+
The grob object to treat as a path.
|
|
312
|
+
gp : Any, optional
|
|
313
|
+
Graphical parameters (``Gpar`` instance or ``None``).
|
|
314
|
+
rule : str, optional
|
|
315
|
+
Fill rule, either ``"winding"`` (default) or ``"evenodd"``.
|
|
316
|
+
|
|
317
|
+
Raises
|
|
318
|
+
------
|
|
319
|
+
ValueError
|
|
320
|
+
If *rule* is not one of the accepted values.
|
|
321
|
+
"""
|
|
322
|
+
|
|
323
|
+
__slots__ = ("grob", "gp", "rule")
|
|
324
|
+
|
|
325
|
+
def __init__(
|
|
326
|
+
self,
|
|
327
|
+
grob: Any,
|
|
328
|
+
gp: Any = None,
|
|
329
|
+
rule: str = "winding",
|
|
330
|
+
) -> None:
|
|
331
|
+
if rule not in _VALID_RULES:
|
|
332
|
+
raise ValueError(
|
|
333
|
+
f"'rule' must be one of {sorted(_VALID_RULES)}, got {rule!r}"
|
|
334
|
+
)
|
|
335
|
+
self.grob: Any = grob
|
|
336
|
+
self.gp: Any = gp
|
|
337
|
+
self.rule: str = rule
|
|
338
|
+
|
|
339
|
+
def __repr__(self) -> str:
|
|
340
|
+
return f"GridPath(grob={self.grob!r}, rule={self.rule!r})"
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
# ---------------------------------------------------------------------------
|
|
344
|
+
# as_path (factory function)
|
|
345
|
+
# ---------------------------------------------------------------------------
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def as_path(
|
|
349
|
+
x: Any,
|
|
350
|
+
gp: Any = None,
|
|
351
|
+
rule: str = "winding",
|
|
352
|
+
) -> GridPath:
|
|
353
|
+
"""Convert a grob to a :class:`GridPath` for stroke/fill operations.
|
|
354
|
+
|
|
355
|
+
This mirrors R's ``as.path()`` function from the *grid* package.
|
|
356
|
+
|
|
357
|
+
Parameters
|
|
358
|
+
----------
|
|
359
|
+
x : Any
|
|
360
|
+
A grob instance. In the current implementation no strict type check
|
|
361
|
+
is enforced beyond ensuring *x* is not ``None``.
|
|
362
|
+
gp : Any, optional
|
|
363
|
+
Graphical parameters to associate with the path.
|
|
364
|
+
rule : str, optional
|
|
365
|
+
Fill rule — ``"winding"`` (default) or ``"evenodd"``.
|
|
366
|
+
|
|
367
|
+
Returns
|
|
368
|
+
-------
|
|
369
|
+
GridPath
|
|
370
|
+
|
|
371
|
+
Raises
|
|
372
|
+
------
|
|
373
|
+
TypeError
|
|
374
|
+
If *x* is ``None``.
|
|
375
|
+
ValueError
|
|
376
|
+
If *rule* is invalid.
|
|
377
|
+
"""
|
|
378
|
+
if x is None:
|
|
379
|
+
raise TypeError("only a grob can be converted to a path")
|
|
380
|
+
return GridPath(grob=x, gp=gp, rule=rule)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
# ---------------------------------------------------------------------------
|
|
384
|
+
# is_closed (S3-style dispatch)
|
|
385
|
+
# ---------------------------------------------------------------------------
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def is_closed(x: Any) -> bool:
|
|
389
|
+
"""Return whether a grob represents a closed shape.
|
|
390
|
+
|
|
391
|
+
Mimics R's ``isClosed()`` generic with method dispatch based on a
|
|
392
|
+
``_grid_class`` attribute (or, failing that, the Python class name).
|
|
393
|
+
|
|
394
|
+
Open shapes (returning ``False``):
|
|
395
|
+
``move.to``, ``line.to``, ``lines``, ``polyline``, ``segments``,
|
|
396
|
+
``beziergrob``.
|
|
397
|
+
|
|
398
|
+
Everything else (including ``rect``, ``circle``, ``polygon``, and any
|
|
399
|
+
unknown type) defaults to ``True``, matching R's ``isClosed.default``.
|
|
400
|
+
|
|
401
|
+
Parameters
|
|
402
|
+
----------
|
|
403
|
+
x : Any
|
|
404
|
+
A grob or grob-like object. The function inspects
|
|
405
|
+
``x._grid_class`` first; if that attribute is absent it falls back
|
|
406
|
+
to ``type(x).__name__``.
|
|
407
|
+
|
|
408
|
+
Returns
|
|
409
|
+
-------
|
|
410
|
+
bool
|
|
411
|
+
``True`` for closed shapes, ``False`` for open ones.
|
|
412
|
+
"""
|
|
413
|
+
cls_name: str = getattr(x, "_grid_class", type(x).__name__)
|
|
414
|
+
return cls_name not in _OPEN_TYPES
|