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/_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