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/_patterns.py
ADDED
|
@@ -0,0 +1,1049 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Gradient and pattern fill system for grid_py.
|
|
3
|
+
|
|
4
|
+
Python port of R's ``grid`` package pattern infrastructure
|
|
5
|
+
(``grid/R/patterns.R``). Provides linear gradients, radial gradients,
|
|
6
|
+
and tiling patterns that can be used as fill values in graphical
|
|
7
|
+
parameter (gpar) objects.
|
|
8
|
+
|
|
9
|
+
Classes
|
|
10
|
+
-------
|
|
11
|
+
LinearGradient
|
|
12
|
+
A two-point linear colour gradient.
|
|
13
|
+
RadialGradient
|
|
14
|
+
A two-circle radial colour gradient.
|
|
15
|
+
Pattern
|
|
16
|
+
A tiling pattern based on an arbitrary grob.
|
|
17
|
+
|
|
18
|
+
Functions
|
|
19
|
+
---------
|
|
20
|
+
linear_gradient
|
|
21
|
+
Factory for :class:`LinearGradient`.
|
|
22
|
+
radial_gradient
|
|
23
|
+
Factory for :class:`RadialGradient`.
|
|
24
|
+
pattern
|
|
25
|
+
Factory for :class:`Pattern`.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
from typing import Any, List, Optional, Sequence, Union
|
|
31
|
+
|
|
32
|
+
import numpy as np
|
|
33
|
+
|
|
34
|
+
from ._units import Unit, is_unit
|
|
35
|
+
|
|
36
|
+
__all__: List[str] = [
|
|
37
|
+
"LinearGradient",
|
|
38
|
+
"RadialGradient",
|
|
39
|
+
"Pattern",
|
|
40
|
+
"ResolvedPattern",
|
|
41
|
+
"linear_gradient",
|
|
42
|
+
"radial_gradient",
|
|
43
|
+
"pattern",
|
|
44
|
+
"is_pattern",
|
|
45
|
+
"is_pattern_list",
|
|
46
|
+
"is_resolved_pattern",
|
|
47
|
+
"resolve_fill",
|
|
48
|
+
"resolve_pattern",
|
|
49
|
+
"record_grob_for_pattern_resolution",
|
|
50
|
+
"record_gtree_for_pattern_resolution",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
# Valid *extend* modes, matching R's ``match.arg`` choices.
|
|
54
|
+
_VALID_EXTEND: tuple[str, ...] = ("pad", "repeat", "reflect", "none")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _validate_extend(extend: str) -> str:
|
|
58
|
+
"""Return *extend* if valid, otherwise raise ``ValueError``.
|
|
59
|
+
|
|
60
|
+
Parameters
|
|
61
|
+
----------
|
|
62
|
+
extend : str
|
|
63
|
+
One of ``"pad"``, ``"repeat"``, ``"reflect"``, or ``"none"``.
|
|
64
|
+
|
|
65
|
+
Returns
|
|
66
|
+
-------
|
|
67
|
+
str
|
|
68
|
+
The validated extend string.
|
|
69
|
+
|
|
70
|
+
Raises
|
|
71
|
+
------
|
|
72
|
+
ValueError
|
|
73
|
+
If *extend* is not one of the four recognised modes.
|
|
74
|
+
"""
|
|
75
|
+
if extend not in _VALID_EXTEND:
|
|
76
|
+
raise ValueError(
|
|
77
|
+
f"extend must be one of {_VALID_EXTEND!r}, got {extend!r}"
|
|
78
|
+
)
|
|
79
|
+
return extend
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _ensure_unit(value: Any, default_units: str) -> Unit:
|
|
83
|
+
"""Coerce *value* to a :class:`Unit` if it is not already one.
|
|
84
|
+
|
|
85
|
+
Parameters
|
|
86
|
+
----------
|
|
87
|
+
value : Any
|
|
88
|
+
A :class:`Unit` instance, or a numeric scalar that will be
|
|
89
|
+
wrapped in ``Unit(value, default_units)``.
|
|
90
|
+
default_units : str
|
|
91
|
+
Unit type used when *value* is not already a :class:`Unit`.
|
|
92
|
+
|
|
93
|
+
Returns
|
|
94
|
+
-------
|
|
95
|
+
Unit
|
|
96
|
+
The (possibly newly created) unit.
|
|
97
|
+
"""
|
|
98
|
+
if is_unit(value):
|
|
99
|
+
return value
|
|
100
|
+
return Unit(value, default_units)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _make_stops(
|
|
104
|
+
colours: Sequence[str],
|
|
105
|
+
stops: Optional[Sequence[float]],
|
|
106
|
+
) -> tuple[list[str], list[float]]:
|
|
107
|
+
"""Normalise *colours* and *stops* following R semantics.
|
|
108
|
+
|
|
109
|
+
Both sequences are recycled to the length of the longer one. If
|
|
110
|
+
*stops* is ``None``, evenly-spaced values in [0, 1] are generated.
|
|
111
|
+
|
|
112
|
+
Parameters
|
|
113
|
+
----------
|
|
114
|
+
colours : Sequence[str]
|
|
115
|
+
Colour specification strings.
|
|
116
|
+
stops : Sequence[float] or None
|
|
117
|
+
Gradient stop positions in [0, 1].
|
|
118
|
+
|
|
119
|
+
Returns
|
|
120
|
+
-------
|
|
121
|
+
tuple[list[str], list[float]]
|
|
122
|
+
``(colours, stops)`` recycled to the same length.
|
|
123
|
+
|
|
124
|
+
Raises
|
|
125
|
+
------
|
|
126
|
+
ValueError
|
|
127
|
+
If the resulting length is less than 1.
|
|
128
|
+
"""
|
|
129
|
+
n_colours = len(colours)
|
|
130
|
+
if stops is None:
|
|
131
|
+
n_stops = n_colours
|
|
132
|
+
stops_arr: np.ndarray = np.linspace(0.0, 1.0, n_stops)
|
|
133
|
+
else:
|
|
134
|
+
stops_arr = np.asarray(stops, dtype=float)
|
|
135
|
+
n_stops = len(stops_arr)
|
|
136
|
+
|
|
137
|
+
nstops = max(n_colours, n_stops)
|
|
138
|
+
if nstops < 1:
|
|
139
|
+
raise ValueError("colours and stops must be at least length 1")
|
|
140
|
+
|
|
141
|
+
# Recycle both to *nstops* (mirroring R's ``rep(x, length.out=n)``).
|
|
142
|
+
colours_out: list[str] = [
|
|
143
|
+
colours[i % n_colours] for i in range(nstops)
|
|
144
|
+
]
|
|
145
|
+
if len(stops_arr) < nstops:
|
|
146
|
+
stops_arr = np.resize(stops_arr, nstops)
|
|
147
|
+
stops_out: list[float] = stops_arr[:nstops].tolist()
|
|
148
|
+
|
|
149
|
+
return colours_out, stops_out
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
# ======================================================================
|
|
153
|
+
# LinearGradient
|
|
154
|
+
# ======================================================================
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class LinearGradient:
|
|
158
|
+
"""A linear colour gradient defined by two endpoints.
|
|
159
|
+
|
|
160
|
+
This corresponds to R's ``grid::linearGradient()`` and the internal
|
|
161
|
+
class ``GridLinearGradient``.
|
|
162
|
+
|
|
163
|
+
Parameters
|
|
164
|
+
----------
|
|
165
|
+
colours : list[str]
|
|
166
|
+
Colour strings (e.g. ``["black", "white"]``).
|
|
167
|
+
stops : list[float] or None
|
|
168
|
+
Gradient stop positions in [0, 1]. ``None`` (default) produces
|
|
169
|
+
evenly-spaced stops matching the length of *colours*.
|
|
170
|
+
x1 : Unit or float or None
|
|
171
|
+
Horizontal start of the gradient line. Defaults to
|
|
172
|
+
``Unit(0, "npc")``.
|
|
173
|
+
y1 : Unit or float or None
|
|
174
|
+
Vertical start of the gradient line. Defaults to
|
|
175
|
+
``Unit(0, "npc")``.
|
|
176
|
+
x2 : Unit or float or None
|
|
177
|
+
Horizontal end of the gradient line. Defaults to
|
|
178
|
+
``Unit(1, "npc")``.
|
|
179
|
+
y2 : Unit or float or None
|
|
180
|
+
Vertical end of the gradient line. Defaults to
|
|
181
|
+
``Unit(1, "npc")``.
|
|
182
|
+
default_units : str
|
|
183
|
+
Unit type applied when a coordinate is given as a plain number.
|
|
184
|
+
extend : str
|
|
185
|
+
One of ``"pad"``, ``"repeat"``, ``"reflect"``, or ``"none"``.
|
|
186
|
+
group : bool
|
|
187
|
+
If ``True`` the gradient is resolved relative to the bounding
|
|
188
|
+
box of *all* shapes; if ``False`` it is resolved per shape.
|
|
189
|
+
|
|
190
|
+
Raises
|
|
191
|
+
------
|
|
192
|
+
ValueError
|
|
193
|
+
If *extend* is invalid, or *colours*/*stops* have length < 1,
|
|
194
|
+
or any coordinate has length != 1.
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
def __init__(
|
|
198
|
+
self,
|
|
199
|
+
colours: list[str],
|
|
200
|
+
stops: Optional[list[float]] = None,
|
|
201
|
+
x1: Optional[Union[Unit, float]] = None,
|
|
202
|
+
y1: Optional[Union[Unit, float]] = None,
|
|
203
|
+
x2: Optional[Union[Unit, float]] = None,
|
|
204
|
+
y2: Optional[Union[Unit, float]] = None,
|
|
205
|
+
default_units: str = "npc",
|
|
206
|
+
extend: str = "pad",
|
|
207
|
+
group: bool = True,
|
|
208
|
+
) -> None:
|
|
209
|
+
self.colours, self.stops = _make_stops(colours, stops)
|
|
210
|
+
|
|
211
|
+
self.x1: Unit = _ensure_unit(
|
|
212
|
+
x1 if x1 is not None else 0.0, default_units
|
|
213
|
+
)
|
|
214
|
+
self.y1: Unit = _ensure_unit(
|
|
215
|
+
y1 if y1 is not None else 0.0, default_units
|
|
216
|
+
)
|
|
217
|
+
self.x2: Unit = _ensure_unit(
|
|
218
|
+
x2 if x2 is not None else 1.0, default_units
|
|
219
|
+
)
|
|
220
|
+
self.y2: Unit = _ensure_unit(
|
|
221
|
+
y2 if y2 is not None else 1.0, default_units
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
# Each coordinate must be scalar (length 1).
|
|
225
|
+
for name, val in (
|
|
226
|
+
("x1", self.x1),
|
|
227
|
+
("y1", self.y1),
|
|
228
|
+
("x2", self.x2),
|
|
229
|
+
("y2", self.y2),
|
|
230
|
+
):
|
|
231
|
+
if len(val) != 1:
|
|
232
|
+
raise ValueError(
|
|
233
|
+
f"{name} must be length 1, got length {len(val)}"
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
self.extend: str = _validate_extend(extend)
|
|
237
|
+
self.group: bool = bool(group)
|
|
238
|
+
|
|
239
|
+
# ------------------------------------------------------------------
|
|
240
|
+
# Dunder helpers
|
|
241
|
+
# ------------------------------------------------------------------
|
|
242
|
+
|
|
243
|
+
def __repr__(self) -> str: # noqa: D105
|
|
244
|
+
return (
|
|
245
|
+
f"LinearGradient(colours={self.colours!r}, "
|
|
246
|
+
f"stops={self.stops!r}, "
|
|
247
|
+
f"x1={self.x1!r}, y1={self.y1!r}, "
|
|
248
|
+
f"x2={self.x2!r}, y2={self.y2!r}, "
|
|
249
|
+
f"extend={self.extend!r}, group={self.group!r})"
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
# ======================================================================
|
|
254
|
+
# RadialGradient
|
|
255
|
+
# ======================================================================
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
class RadialGradient:
|
|
259
|
+
"""A radial colour gradient defined by two circles.
|
|
260
|
+
|
|
261
|
+
This corresponds to R's ``grid::radialGradient()`` and the internal
|
|
262
|
+
class ``GridRadialGradient``.
|
|
263
|
+
|
|
264
|
+
Parameters
|
|
265
|
+
----------
|
|
266
|
+
colours : list[str]
|
|
267
|
+
Colour strings.
|
|
268
|
+
stops : list[float] or None
|
|
269
|
+
Gradient stop positions in [0, 1].
|
|
270
|
+
cx1 : Unit or float or None
|
|
271
|
+
Horizontal centre of the inner circle. Default ``0.5 npc``.
|
|
272
|
+
cy1 : Unit or float or None
|
|
273
|
+
Vertical centre of the inner circle. Default ``0.5 npc``.
|
|
274
|
+
r1 : Unit or float or None
|
|
275
|
+
Radius of the inner circle. Default ``0 npc``.
|
|
276
|
+
cx2 : Unit or float or None
|
|
277
|
+
Horizontal centre of the outer circle. Default ``0.5 npc``.
|
|
278
|
+
cy2 : Unit or float or None
|
|
279
|
+
Vertical centre of the outer circle. Default ``0.5 npc``.
|
|
280
|
+
r2 : Unit or float or None
|
|
281
|
+
Radius of the outer circle. Default ``0.5 npc``.
|
|
282
|
+
default_units : str
|
|
283
|
+
Unit type applied when a parameter is given as a plain number.
|
|
284
|
+
extend : str
|
|
285
|
+
One of ``"pad"``, ``"repeat"``, ``"reflect"``, or ``"none"``.
|
|
286
|
+
group : bool
|
|
287
|
+
If ``True`` the gradient is resolved relative to the bounding
|
|
288
|
+
box of *all* shapes; if ``False`` it is resolved per shape.
|
|
289
|
+
|
|
290
|
+
Raises
|
|
291
|
+
------
|
|
292
|
+
ValueError
|
|
293
|
+
If *extend* is invalid, colours/stops have length < 1, or any
|
|
294
|
+
coordinate/radius has length != 1.
|
|
295
|
+
"""
|
|
296
|
+
|
|
297
|
+
def __init__(
|
|
298
|
+
self,
|
|
299
|
+
colours: list[str],
|
|
300
|
+
stops: Optional[list[float]] = None,
|
|
301
|
+
cx1: Optional[Union[Unit, float]] = None,
|
|
302
|
+
cy1: Optional[Union[Unit, float]] = None,
|
|
303
|
+
r1: Optional[Union[Unit, float]] = None,
|
|
304
|
+
cx2: Optional[Union[Unit, float]] = None,
|
|
305
|
+
cy2: Optional[Union[Unit, float]] = None,
|
|
306
|
+
r2: Optional[Union[Unit, float]] = None,
|
|
307
|
+
default_units: str = "npc",
|
|
308
|
+
extend: str = "pad",
|
|
309
|
+
group: bool = True,
|
|
310
|
+
) -> None:
|
|
311
|
+
self.colours, self.stops = _make_stops(colours, stops)
|
|
312
|
+
|
|
313
|
+
self.cx1: Unit = _ensure_unit(
|
|
314
|
+
cx1 if cx1 is not None else 0.5, default_units
|
|
315
|
+
)
|
|
316
|
+
self.cy1: Unit = _ensure_unit(
|
|
317
|
+
cy1 if cy1 is not None else 0.5, default_units
|
|
318
|
+
)
|
|
319
|
+
self.r1: Unit = _ensure_unit(
|
|
320
|
+
r1 if r1 is not None else 0.0, default_units
|
|
321
|
+
)
|
|
322
|
+
self.cx2: Unit = _ensure_unit(
|
|
323
|
+
cx2 if cx2 is not None else 0.5, default_units
|
|
324
|
+
)
|
|
325
|
+
self.cy2: Unit = _ensure_unit(
|
|
326
|
+
cy2 if cy2 is not None else 0.5, default_units
|
|
327
|
+
)
|
|
328
|
+
self.r2: Unit = _ensure_unit(
|
|
329
|
+
r2 if r2 is not None else 0.5, default_units
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
for name, val in (
|
|
333
|
+
("cx1", self.cx1),
|
|
334
|
+
("cy1", self.cy1),
|
|
335
|
+
("r1", self.r1),
|
|
336
|
+
("cx2", self.cx2),
|
|
337
|
+
("cy2", self.cy2),
|
|
338
|
+
("r2", self.r2),
|
|
339
|
+
):
|
|
340
|
+
if len(val) != 1:
|
|
341
|
+
raise ValueError(
|
|
342
|
+
f"{name} must be length 1, got length {len(val)}"
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
self.extend: str = _validate_extend(extend)
|
|
346
|
+
self.group: bool = bool(group)
|
|
347
|
+
|
|
348
|
+
def __repr__(self) -> str: # noqa: D105
|
|
349
|
+
return (
|
|
350
|
+
f"RadialGradient(colours={self.colours!r}, "
|
|
351
|
+
f"stops={self.stops!r}, "
|
|
352
|
+
f"cx1={self.cx1!r}, cy1={self.cy1!r}, r1={self.r1!r}, "
|
|
353
|
+
f"cx2={self.cx2!r}, cy2={self.cy2!r}, r2={self.r2!r}, "
|
|
354
|
+
f"extend={self.extend!r}, group={self.group!r})"
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
# ======================================================================
|
|
359
|
+
# Pattern (tiling pattern)
|
|
360
|
+
# ======================================================================
|
|
361
|
+
|
|
362
|
+
# Justification helpers -- resolve a single *just* string to (hjust, vjust).
|
|
363
|
+
_JUST_H: dict[str, float] = {
|
|
364
|
+
"left": 0.0,
|
|
365
|
+
"right": 1.0,
|
|
366
|
+
"centre": 0.5,
|
|
367
|
+
"center": 0.5,
|
|
368
|
+
"top": 0.5,
|
|
369
|
+
"bottom": 0.5,
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
_JUST_V: dict[str, float] = {
|
|
373
|
+
"left": 0.5,
|
|
374
|
+
"right": 0.5,
|
|
375
|
+
"centre": 0.5,
|
|
376
|
+
"center": 0.5,
|
|
377
|
+
"top": 1.0,
|
|
378
|
+
"bottom": 0.0,
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _resolve_just(
|
|
383
|
+
just: Union[str, tuple[float, float]],
|
|
384
|
+
) -> tuple[float, float]:
|
|
385
|
+
"""Return ``(hjust, vjust)`` from a justification specification.
|
|
386
|
+
|
|
387
|
+
Parameters
|
|
388
|
+
----------
|
|
389
|
+
just : str or tuple[float, float]
|
|
390
|
+
A justification string (``"centre"``, ``"left"``, etc.) or an
|
|
391
|
+
explicit ``(hjust, vjust)`` pair.
|
|
392
|
+
|
|
393
|
+
Returns
|
|
394
|
+
-------
|
|
395
|
+
tuple[float, float]
|
|
396
|
+
Numeric ``(hjust, vjust)`` in [0, 1].
|
|
397
|
+
|
|
398
|
+
Raises
|
|
399
|
+
------
|
|
400
|
+
ValueError
|
|
401
|
+
If *just* is an unrecognised string.
|
|
402
|
+
"""
|
|
403
|
+
if isinstance(just, str):
|
|
404
|
+
j = just.lower()
|
|
405
|
+
if j not in _JUST_H:
|
|
406
|
+
raise ValueError(
|
|
407
|
+
f"Unrecognised justification string: {just!r}"
|
|
408
|
+
)
|
|
409
|
+
return _JUST_H[j], _JUST_V[j]
|
|
410
|
+
# Assume numeric pair.
|
|
411
|
+
return (float(just[0]), float(just[1]))
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
class Pattern:
|
|
415
|
+
"""A tiling pattern fill based on an arbitrary grob.
|
|
416
|
+
|
|
417
|
+
This corresponds to R's ``grid::pattern()`` and the internal class
|
|
418
|
+
``GridTilingPattern``.
|
|
419
|
+
|
|
420
|
+
Parameters
|
|
421
|
+
----------
|
|
422
|
+
grob : Any
|
|
423
|
+
A grob (graphical object) to use as the repeating tile.
|
|
424
|
+
x : Unit or float or None
|
|
425
|
+
Horizontal position of the tile. Default ``0.5 npc``.
|
|
426
|
+
y : Unit or float or None
|
|
427
|
+
Vertical position of the tile. Default ``0.5 npc``.
|
|
428
|
+
width : Unit or float or None
|
|
429
|
+
Width of the tile. Default ``1 npc``.
|
|
430
|
+
height : Unit or float or None
|
|
431
|
+
Height of the tile. Default ``1 npc``.
|
|
432
|
+
default_units : str
|
|
433
|
+
Unit type applied when a dimension is given as a plain number.
|
|
434
|
+
just : str or tuple[float, float]
|
|
435
|
+
Justification of the tile relative to ``(x, y)``. Accepts
|
|
436
|
+
standard strings such as ``"centre"``, ``"left"``, etc., or an
|
|
437
|
+
explicit ``(hjust, vjust)`` pair.
|
|
438
|
+
extend : str
|
|
439
|
+
One of ``"pad"``, ``"repeat"``, ``"reflect"``, or ``"none"``.
|
|
440
|
+
group : bool
|
|
441
|
+
If ``True`` the pattern is resolved relative to the bounding
|
|
442
|
+
box of *all* shapes; if ``False`` it is resolved per shape.
|
|
443
|
+
|
|
444
|
+
Raises
|
|
445
|
+
------
|
|
446
|
+
ValueError
|
|
447
|
+
If *extend* is invalid or any coordinate/dimension has
|
|
448
|
+
length != 1.
|
|
449
|
+
"""
|
|
450
|
+
|
|
451
|
+
def __init__(
|
|
452
|
+
self,
|
|
453
|
+
grob: Any,
|
|
454
|
+
x: Optional[Union[Unit, float]] = None,
|
|
455
|
+
y: Optional[Union[Unit, float]] = None,
|
|
456
|
+
width: Optional[Union[Unit, float]] = None,
|
|
457
|
+
height: Optional[Union[Unit, float]] = None,
|
|
458
|
+
default_units: str = "npc",
|
|
459
|
+
just: Union[str, tuple[float, float]] = "centre",
|
|
460
|
+
extend: str = "pad",
|
|
461
|
+
group: bool = True,
|
|
462
|
+
) -> None:
|
|
463
|
+
self.grob: Any = grob
|
|
464
|
+
|
|
465
|
+
self.x: Unit = _ensure_unit(
|
|
466
|
+
x if x is not None else 0.5, default_units
|
|
467
|
+
)
|
|
468
|
+
self.y: Unit = _ensure_unit(
|
|
469
|
+
y if y is not None else 0.5, default_units
|
|
470
|
+
)
|
|
471
|
+
self.width: Unit = _ensure_unit(
|
|
472
|
+
width if width is not None else 1.0, default_units
|
|
473
|
+
)
|
|
474
|
+
self.height: Unit = _ensure_unit(
|
|
475
|
+
height if height is not None else 1.0, default_units
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
for name, val in (
|
|
479
|
+
("x", self.x),
|
|
480
|
+
("y", self.y),
|
|
481
|
+
("width", self.width),
|
|
482
|
+
("height", self.height),
|
|
483
|
+
):
|
|
484
|
+
if len(val) != 1:
|
|
485
|
+
raise ValueError(
|
|
486
|
+
f"{name} must be length 1, got length {len(val)}"
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
self.hjust: float
|
|
490
|
+
self.vjust: float
|
|
491
|
+
self.hjust, self.vjust = _resolve_just(just)
|
|
492
|
+
|
|
493
|
+
self.extend: str = _validate_extend(extend)
|
|
494
|
+
self.group: bool = bool(group)
|
|
495
|
+
|
|
496
|
+
def __repr__(self) -> str: # noqa: D105
|
|
497
|
+
return (
|
|
498
|
+
f"Pattern(grob={self.grob!r}, "
|
|
499
|
+
f"x={self.x!r}, y={self.y!r}, "
|
|
500
|
+
f"width={self.width!r}, height={self.height!r}, "
|
|
501
|
+
f"hjust={self.hjust!r}, vjust={self.vjust!r}, "
|
|
502
|
+
f"extend={self.extend!r}, group={self.group!r})"
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
# ======================================================================
|
|
507
|
+
# Factory functions
|
|
508
|
+
# ======================================================================
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def linear_gradient(
|
|
512
|
+
colours: list[str] = None,
|
|
513
|
+
stops: Optional[list[float]] = None,
|
|
514
|
+
x1: Optional[Union[Unit, float]] = None,
|
|
515
|
+
y1: Optional[Union[Unit, float]] = None,
|
|
516
|
+
x2: Optional[Union[Unit, float]] = None,
|
|
517
|
+
y2: Optional[Union[Unit, float]] = None,
|
|
518
|
+
default_units: str = "npc",
|
|
519
|
+
extend: str = "pad",
|
|
520
|
+
group: bool = True,
|
|
521
|
+
) -> LinearGradient:
|
|
522
|
+
"""Create a :class:`LinearGradient`.
|
|
523
|
+
|
|
524
|
+
This is a convenience wrapper matching R's
|
|
525
|
+
``grid::linearGradient()`` function signature.
|
|
526
|
+
|
|
527
|
+
Parameters
|
|
528
|
+
----------
|
|
529
|
+
colours : list[str], optional
|
|
530
|
+
Colour strings. Defaults to ``["black", "white"]``.
|
|
531
|
+
stops : list[float] or None
|
|
532
|
+
Gradient stop positions in [0, 1].
|
|
533
|
+
x1 : Unit or float or None
|
|
534
|
+
Horizontal start of the gradient line.
|
|
535
|
+
y1 : Unit or float or None
|
|
536
|
+
Vertical start of the gradient line.
|
|
537
|
+
x2 : Unit or float or None
|
|
538
|
+
Horizontal end of the gradient line.
|
|
539
|
+
y2 : Unit or float or None
|
|
540
|
+
Vertical end of the gradient line.
|
|
541
|
+
default_units : str
|
|
542
|
+
Unit type used for bare numeric coordinates.
|
|
543
|
+
extend : str
|
|
544
|
+
Gradient extension mode.
|
|
545
|
+
group : bool
|
|
546
|
+
Resolve gradient relative to all shapes (``True``) or per
|
|
547
|
+
shape (``False``).
|
|
548
|
+
|
|
549
|
+
Returns
|
|
550
|
+
-------
|
|
551
|
+
LinearGradient
|
|
552
|
+
A new linear gradient object.
|
|
553
|
+
|
|
554
|
+
Examples
|
|
555
|
+
--------
|
|
556
|
+
>>> lg = linear_gradient(["red", "blue"])
|
|
557
|
+
>>> lg.colours
|
|
558
|
+
['red', 'blue']
|
|
559
|
+
"""
|
|
560
|
+
if colours is None:
|
|
561
|
+
colours = ["black", "white"]
|
|
562
|
+
return LinearGradient(
|
|
563
|
+
colours=colours,
|
|
564
|
+
stops=stops,
|
|
565
|
+
x1=x1,
|
|
566
|
+
y1=y1,
|
|
567
|
+
x2=x2,
|
|
568
|
+
y2=y2,
|
|
569
|
+
default_units=default_units,
|
|
570
|
+
extend=extend,
|
|
571
|
+
group=group,
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
def radial_gradient(
|
|
576
|
+
colours: list[str] = None,
|
|
577
|
+
stops: Optional[list[float]] = None,
|
|
578
|
+
cx1: Optional[Union[Unit, float]] = None,
|
|
579
|
+
cy1: Optional[Union[Unit, float]] = None,
|
|
580
|
+
r1: Optional[Union[Unit, float]] = None,
|
|
581
|
+
cx2: Optional[Union[Unit, float]] = None,
|
|
582
|
+
cy2: Optional[Union[Unit, float]] = None,
|
|
583
|
+
r2: Optional[Union[Unit, float]] = None,
|
|
584
|
+
default_units: str = "npc",
|
|
585
|
+
extend: str = "pad",
|
|
586
|
+
group: bool = True,
|
|
587
|
+
) -> RadialGradient:
|
|
588
|
+
"""Create a :class:`RadialGradient`.
|
|
589
|
+
|
|
590
|
+
This is a convenience wrapper matching R's
|
|
591
|
+
``grid::radialGradient()`` function signature.
|
|
592
|
+
|
|
593
|
+
Parameters
|
|
594
|
+
----------
|
|
595
|
+
colours : list[str], optional
|
|
596
|
+
Colour strings. Defaults to ``["black", "white"]``.
|
|
597
|
+
stops : list[float] or None
|
|
598
|
+
Gradient stop positions in [0, 1].
|
|
599
|
+
cx1 : Unit or float or None
|
|
600
|
+
Horizontal centre of the inner circle.
|
|
601
|
+
cy1 : Unit or float or None
|
|
602
|
+
Vertical centre of the inner circle.
|
|
603
|
+
r1 : Unit or float or None
|
|
604
|
+
Radius of the inner circle.
|
|
605
|
+
cx2 : Unit or float or None
|
|
606
|
+
Horizontal centre of the outer circle.
|
|
607
|
+
cy2 : Unit or float or None
|
|
608
|
+
Vertical centre of the outer circle.
|
|
609
|
+
r2 : Unit or float or None
|
|
610
|
+
Radius of the outer circle.
|
|
611
|
+
default_units : str
|
|
612
|
+
Unit type used for bare numeric values.
|
|
613
|
+
extend : str
|
|
614
|
+
Gradient extension mode.
|
|
615
|
+
group : bool
|
|
616
|
+
Resolve gradient relative to all shapes (``True``) or per
|
|
617
|
+
shape (``False``).
|
|
618
|
+
|
|
619
|
+
Returns
|
|
620
|
+
-------
|
|
621
|
+
RadialGradient
|
|
622
|
+
A new radial gradient object.
|
|
623
|
+
|
|
624
|
+
Examples
|
|
625
|
+
--------
|
|
626
|
+
>>> rg = radial_gradient(["white", "black"])
|
|
627
|
+
>>> rg.r2
|
|
628
|
+
Unit([0.5], ['npc'])
|
|
629
|
+
"""
|
|
630
|
+
if colours is None:
|
|
631
|
+
colours = ["black", "white"]
|
|
632
|
+
return RadialGradient(
|
|
633
|
+
colours=colours,
|
|
634
|
+
stops=stops,
|
|
635
|
+
cx1=cx1,
|
|
636
|
+
cy1=cy1,
|
|
637
|
+
r1=r1,
|
|
638
|
+
cx2=cx2,
|
|
639
|
+
cy2=cy2,
|
|
640
|
+
r2=r2,
|
|
641
|
+
default_units=default_units,
|
|
642
|
+
extend=extend,
|
|
643
|
+
group=group,
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
def pattern(
|
|
648
|
+
grob: Any,
|
|
649
|
+
x: Optional[Union[Unit, float]] = None,
|
|
650
|
+
y: Optional[Union[Unit, float]] = None,
|
|
651
|
+
width: Optional[Union[Unit, float]] = None,
|
|
652
|
+
height: Optional[Union[Unit, float]] = None,
|
|
653
|
+
default_units: str = "npc",
|
|
654
|
+
just: Union[str, tuple[float, float]] = "centre",
|
|
655
|
+
extend: str = "pad",
|
|
656
|
+
group: bool = True,
|
|
657
|
+
) -> Pattern:
|
|
658
|
+
"""Create a :class:`Pattern` (tiling fill).
|
|
659
|
+
|
|
660
|
+
This is a convenience wrapper matching R's ``grid::pattern()``
|
|
661
|
+
function signature.
|
|
662
|
+
|
|
663
|
+
Parameters
|
|
664
|
+
----------
|
|
665
|
+
grob : Any
|
|
666
|
+
A grob to use as the repeating tile.
|
|
667
|
+
x : Unit or float or None
|
|
668
|
+
Horizontal position of the tile.
|
|
669
|
+
y : Unit or float or None
|
|
670
|
+
Vertical position of the tile.
|
|
671
|
+
width : Unit or float or None
|
|
672
|
+
Width of the tile.
|
|
673
|
+
height : Unit or float or None
|
|
674
|
+
Height of the tile.
|
|
675
|
+
default_units : str
|
|
676
|
+
Unit type used for bare numeric values.
|
|
677
|
+
just : str or tuple[float, float]
|
|
678
|
+
Tile justification relative to ``(x, y)``.
|
|
679
|
+
extend : str
|
|
680
|
+
Pattern extension mode.
|
|
681
|
+
group : bool
|
|
682
|
+
Resolve pattern relative to all shapes (``True``) or per
|
|
683
|
+
shape (``False``).
|
|
684
|
+
|
|
685
|
+
Returns
|
|
686
|
+
-------
|
|
687
|
+
Pattern
|
|
688
|
+
A new tiling pattern object.
|
|
689
|
+
|
|
690
|
+
Examples
|
|
691
|
+
--------
|
|
692
|
+
>>> pat = pattern("placeholder_grob")
|
|
693
|
+
>>> pat.hjust
|
|
694
|
+
0.5
|
|
695
|
+
"""
|
|
696
|
+
return Pattern(
|
|
697
|
+
grob=grob,
|
|
698
|
+
x=x,
|
|
699
|
+
y=y,
|
|
700
|
+
width=width,
|
|
701
|
+
height=height,
|
|
702
|
+
default_units=default_units,
|
|
703
|
+
just=just,
|
|
704
|
+
extend=extend,
|
|
705
|
+
group=group,
|
|
706
|
+
)
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
# ============================================================================
|
|
710
|
+
# Pattern resolution pipeline
|
|
711
|
+
# Port of R patterns.R:140-429
|
|
712
|
+
# ============================================================================
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
def is_pattern(fill: Any) -> bool:
|
|
716
|
+
"""Test whether *fill* is a grid pattern (gradient or tiling).
|
|
717
|
+
|
|
718
|
+
Mirrors R ``is.pattern()``.
|
|
719
|
+
"""
|
|
720
|
+
return isinstance(fill, (LinearGradient, RadialGradient, Pattern))
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
def is_pattern_list(fill: Any) -> bool:
|
|
724
|
+
"""Test whether *fill* is a list of patterns."""
|
|
725
|
+
if isinstance(fill, (list, tuple)):
|
|
726
|
+
return len(fill) > 0 and all(is_pattern(f) for f in fill)
|
|
727
|
+
return False
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
def is_resolved_pattern(fill: Any) -> bool:
|
|
731
|
+
"""Test whether a pattern has already been resolved."""
|
|
732
|
+
return isinstance(fill, dict) and fill.get("_resolved", False)
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
class ResolvedPattern:
|
|
736
|
+
"""A pattern that has been resolved to renderer-specific form.
|
|
737
|
+
|
|
738
|
+
Port of R ``resolvedPattern()`` (patterns.R:192-196).
|
|
739
|
+
Wraps the original pattern with a ``ref`` (renderer handle) and
|
|
740
|
+
marks it as resolved.
|
|
741
|
+
"""
|
|
742
|
+
|
|
743
|
+
def __init__(self, pattern: Any, ref: Any) -> None:
|
|
744
|
+
self.pattern = pattern
|
|
745
|
+
self.ref = ref
|
|
746
|
+
self._resolved = True
|
|
747
|
+
|
|
748
|
+
def __repr__(self) -> str:
|
|
749
|
+
return f"ResolvedPattern(ref={self.ref!r})"
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
# ---------------------------------------------------------------------------
|
|
753
|
+
# resolveFill -- S3-like dispatcher
|
|
754
|
+
# Port of R patterns.R:198-385
|
|
755
|
+
# ---------------------------------------------------------------------------
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
def resolve_fill(fill: Any, index: int = 1, grob: Any = None) -> Any:
|
|
759
|
+
"""Resolve a fill value to a renderer-ready form.
|
|
760
|
+
|
|
761
|
+
Port of R ``resolveFill()`` (patterns.R:198-385).
|
|
762
|
+
Dispatches based on fill type:
|
|
763
|
+
- Simple colour strings pass through unchanged
|
|
764
|
+
- Already-resolved patterns pass through
|
|
765
|
+
- Unresolved patterns → resolvePattern()
|
|
766
|
+
- Pattern lists → resolve each element
|
|
767
|
+
|
|
768
|
+
Parameters
|
|
769
|
+
----------
|
|
770
|
+
fill : Any
|
|
771
|
+
Fill value from gpar. May be a string, LinearGradient,
|
|
772
|
+
RadialGradient, Pattern, ResolvedPattern, list, etc.
|
|
773
|
+
index : int
|
|
774
|
+
Shape index (1-based, for per-shape pattern lists).
|
|
775
|
+
grob : Grob or None
|
|
776
|
+
The grob being filled (attached for coordinate queries).
|
|
777
|
+
|
|
778
|
+
Returns
|
|
779
|
+
-------
|
|
780
|
+
Any
|
|
781
|
+
Resolved fill value (string, ResolvedPattern, or "transparent").
|
|
782
|
+
"""
|
|
783
|
+
# Already resolved (R patterns.R:210-212)
|
|
784
|
+
if isinstance(fill, ResolvedPattern):
|
|
785
|
+
return fill
|
|
786
|
+
|
|
787
|
+
# Simple fill (R patterns.R:205-207)
|
|
788
|
+
if not is_pattern(fill) and not is_pattern_list(fill):
|
|
789
|
+
return fill
|
|
790
|
+
|
|
791
|
+
# Single pattern (R patterns.R:217-280)
|
|
792
|
+
if is_pattern(fill):
|
|
793
|
+
return _resolve_fill_pattern(fill, index, grob)
|
|
794
|
+
|
|
795
|
+
# Pattern list (R patterns.R:222-334)
|
|
796
|
+
if is_pattern_list(fill):
|
|
797
|
+
return _resolve_fill_pattern_list(fill, grob)
|
|
798
|
+
|
|
799
|
+
return fill
|
|
800
|
+
|
|
801
|
+
|
|
802
|
+
def _resolve_fill_pattern(fill: Any, index: int, grob: Any) -> Any:
|
|
803
|
+
"""Resolve a single pattern fill.
|
|
804
|
+
|
|
805
|
+
Port of R ``resolveFill.GridGrobPattern`` (patterns.R:237-280).
|
|
806
|
+
"""
|
|
807
|
+
from ._coords import grob_points, is_empty_coords, coords_bbox
|
|
808
|
+
from ._viewport import Viewport, push_viewport, pop_viewport
|
|
809
|
+
from ._gpar import Gpar
|
|
810
|
+
|
|
811
|
+
# If grob is available, compute bounding box
|
|
812
|
+
if grob is not None:
|
|
813
|
+
pts = grob_points(grob, closed=True)
|
|
814
|
+
if not is_empty_coords(pts):
|
|
815
|
+
if getattr(fill, "group", True) or len(pts) <= 1:
|
|
816
|
+
bbox = coords_bbox(pts)
|
|
817
|
+
else:
|
|
818
|
+
# Per-shape bounding box
|
|
819
|
+
idx = (index - 1) % len(pts)
|
|
820
|
+
bbox = coords_bbox(pts[idx] if hasattr(pts, '__getitem__') else pts)
|
|
821
|
+
|
|
822
|
+
# Push temporary viewport for pattern resolution
|
|
823
|
+
# (R patterns.R:266-271)
|
|
824
|
+
vp = Viewport(
|
|
825
|
+
x=bbox["left"], y=bbox["bottom"],
|
|
826
|
+
width=bbox["width"], height=bbox["height"],
|
|
827
|
+
default_units="inches",
|
|
828
|
+
just=("left", "bottom"),
|
|
829
|
+
clip="off", mask="none",
|
|
830
|
+
)
|
|
831
|
+
push_viewport(vp, recording=False)
|
|
832
|
+
result = resolve_pattern(fill)
|
|
833
|
+
pop_viewport(recording=False)
|
|
834
|
+
return result
|
|
835
|
+
else:
|
|
836
|
+
import warnings
|
|
837
|
+
warnings.warn("Pattern fill applied to object with no inside")
|
|
838
|
+
return "transparent"
|
|
839
|
+
|
|
840
|
+
# No grob context — resolve in current viewport
|
|
841
|
+
return resolve_pattern(fill)
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
def _resolve_fill_pattern_list(fill: list, grob: Any) -> Any:
|
|
845
|
+
"""Resolve a list of patterns.
|
|
846
|
+
|
|
847
|
+
Port of R ``resolveFill.GridGrobPatternList`` (patterns.R:283-334).
|
|
848
|
+
"""
|
|
849
|
+
resolved = []
|
|
850
|
+
for i, f in enumerate(fill):
|
|
851
|
+
if isinstance(f, ResolvedPattern):
|
|
852
|
+
resolved.append(f)
|
|
853
|
+
elif is_pattern(f):
|
|
854
|
+
resolved.append(_resolve_fill_pattern(f, i + 1, grob))
|
|
855
|
+
else:
|
|
856
|
+
resolved.append(f)
|
|
857
|
+
return resolved
|
|
858
|
+
|
|
859
|
+
|
|
860
|
+
# ---------------------------------------------------------------------------
|
|
861
|
+
# resolvePattern -- type-specific resolution
|
|
862
|
+
# Port of R patterns.R:387-429
|
|
863
|
+
# ---------------------------------------------------------------------------
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
def resolve_pattern(pattern: Any) -> Any:
|
|
867
|
+
"""Resolve a pattern object to renderer-specific form.
|
|
868
|
+
|
|
869
|
+
Port of R ``resolvePattern()`` (patterns.R:387-429).
|
|
870
|
+
Dispatches based on pattern type: LinearGradient, RadialGradient,
|
|
871
|
+
or Pattern (tiling).
|
|
872
|
+
|
|
873
|
+
The resolved pattern contains a renderer-specific ``ref`` that can
|
|
874
|
+
be used directly by the rendering backend.
|
|
875
|
+
|
|
876
|
+
Parameters
|
|
877
|
+
----------
|
|
878
|
+
pattern : LinearGradient or RadialGradient or Pattern
|
|
879
|
+
The pattern to resolve.
|
|
880
|
+
|
|
881
|
+
Returns
|
|
882
|
+
-------
|
|
883
|
+
ResolvedPattern
|
|
884
|
+
The pattern with renderer-specific reference attached.
|
|
885
|
+
"""
|
|
886
|
+
if isinstance(pattern, LinearGradient):
|
|
887
|
+
return _resolve_linear_gradient(pattern)
|
|
888
|
+
elif isinstance(pattern, RadialGradient):
|
|
889
|
+
return _resolve_radial_gradient(pattern)
|
|
890
|
+
elif isinstance(pattern, Pattern):
|
|
891
|
+
return _resolve_tiling_pattern(pattern)
|
|
892
|
+
return pattern
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
def _resolve_linear_gradient(grad: LinearGradient) -> ResolvedPattern:
|
|
896
|
+
"""Resolve a linear gradient to renderer-specific form.
|
|
897
|
+
|
|
898
|
+
Port of R ``resolvePattern.GridLinearGradient`` (patterns.R:391-399).
|
|
899
|
+
Converts endpoints to device coordinates, creates a renderer pattern.
|
|
900
|
+
"""
|
|
901
|
+
# The renderer will handle the actual gradient creation when it
|
|
902
|
+
# encounters this object in gpar$fill. We store the resolved
|
|
903
|
+
# coordinates for the renderer to use.
|
|
904
|
+
from ._units import device_loc
|
|
905
|
+
|
|
906
|
+
try:
|
|
907
|
+
p1 = device_loc(grad.x1, grad.y1, value_only=True, device=True)
|
|
908
|
+
p2 = device_loc(grad.x2, grad.y2, value_only=True, device=True)
|
|
909
|
+
ref = {
|
|
910
|
+
"type": "linear_gradient",
|
|
911
|
+
"x1": float(p1["x"][0]), "y1": float(p1["y"][0]),
|
|
912
|
+
"x2": float(p2["x"][0]), "y2": float(p2["y"][0]),
|
|
913
|
+
"colours": grad.colours,
|
|
914
|
+
"stops": grad.stops,
|
|
915
|
+
"extend": grad.extend,
|
|
916
|
+
}
|
|
917
|
+
except Exception:
|
|
918
|
+
# Fallback: store the original gradient for later resolution
|
|
919
|
+
ref = {"type": "linear_gradient", "pattern": grad}
|
|
920
|
+
|
|
921
|
+
return ResolvedPattern(grad, ref)
|
|
922
|
+
|
|
923
|
+
|
|
924
|
+
def _resolve_radial_gradient(grad: RadialGradient) -> ResolvedPattern:
|
|
925
|
+
"""Resolve a radial gradient to renderer-specific form.
|
|
926
|
+
|
|
927
|
+
Port of R ``resolvePattern.GridRadialGradient`` (patterns.R:401-418).
|
|
928
|
+
"""
|
|
929
|
+
from ._units import device_loc, device_dim, Unit
|
|
930
|
+
import math
|
|
931
|
+
|
|
932
|
+
try:
|
|
933
|
+
c1 = device_loc(grad.cx1, grad.cy1, value_only=True, device=True)
|
|
934
|
+
c2 = device_loc(grad.cx2, grad.cy2, value_only=True, device=True)
|
|
935
|
+
# R computes r as min of two axis scalings (patterns.R:403-411)
|
|
936
|
+
dim1a = device_dim(Unit(0, "inches"), grad.r1, value_only=True, device=True)
|
|
937
|
+
dim1b = device_dim(grad.r1, Unit(0, "inches"), value_only=True, device=True)
|
|
938
|
+
r1 = min(math.sqrt(dim1a["w"][0]**2 + dim1a["h"][0]**2),
|
|
939
|
+
math.sqrt(dim1b["w"][0]**2 + dim1b["h"][0]**2))
|
|
940
|
+
dim2a = device_dim(Unit(0, "inches"), grad.r2, value_only=True, device=True)
|
|
941
|
+
dim2b = device_dim(grad.r2, Unit(0, "inches"), value_only=True, device=True)
|
|
942
|
+
r2 = min(math.sqrt(dim2a["w"][0]**2 + dim2a["h"][0]**2),
|
|
943
|
+
math.sqrt(dim2b["w"][0]**2 + dim2b["h"][0]**2))
|
|
944
|
+
ref = {
|
|
945
|
+
"type": "radial_gradient",
|
|
946
|
+
"cx1": float(c1["x"][0]), "cy1": float(c1["y"][0]), "r1": r1,
|
|
947
|
+
"cx2": float(c2["x"][0]), "cy2": float(c2["y"][0]), "r2": r2,
|
|
948
|
+
"colours": grad.colours,
|
|
949
|
+
"stops": grad.stops,
|
|
950
|
+
"extend": grad.extend,
|
|
951
|
+
}
|
|
952
|
+
except Exception:
|
|
953
|
+
ref = {"type": "radial_gradient", "pattern": grad}
|
|
954
|
+
|
|
955
|
+
return ResolvedPattern(grad, ref)
|
|
956
|
+
|
|
957
|
+
|
|
958
|
+
def _resolve_tiling_pattern(pat: Pattern) -> ResolvedPattern:
|
|
959
|
+
"""Resolve a tiling pattern to renderer-specific form.
|
|
960
|
+
|
|
961
|
+
Port of R ``resolvePattern.GridTilingPattern`` (patterns.R:420-429).
|
|
962
|
+
"""
|
|
963
|
+
from ._units import device_loc, device_dim
|
|
964
|
+
|
|
965
|
+
try:
|
|
966
|
+
xy = device_loc(pat.x, pat.y, value_only=True, device=True)
|
|
967
|
+
wh = device_dim(pat.width, pat.height, value_only=True, device=True)
|
|
968
|
+
left = float(xy["x"][0]) - pat.hjust * float(wh["w"][0])
|
|
969
|
+
bottom = float(xy["y"][0]) - pat.vjust * float(wh["h"][0])
|
|
970
|
+
ref = {
|
|
971
|
+
"type": "tiling_pattern",
|
|
972
|
+
"grob": pat.grob,
|
|
973
|
+
"x": left, "y": bottom,
|
|
974
|
+
"width": float(wh["w"][0]), "height": float(wh["h"][0]),
|
|
975
|
+
"extend": pat.extend,
|
|
976
|
+
}
|
|
977
|
+
except Exception:
|
|
978
|
+
ref = {"type": "tiling_pattern", "pattern": pat}
|
|
979
|
+
|
|
980
|
+
return ResolvedPattern(pat, ref)
|
|
981
|
+
|
|
982
|
+
|
|
983
|
+
# ---------------------------------------------------------------------------
|
|
984
|
+
# recordGrobForPatternResolution / recordGTreeForPatternResolution
|
|
985
|
+
# Port of R patterns.R:150-190
|
|
986
|
+
# ---------------------------------------------------------------------------
|
|
987
|
+
|
|
988
|
+
|
|
989
|
+
def record_grob_for_pattern_resolution(grob: Any) -> None:
|
|
990
|
+
"""Attach the built grob to gpar$fill for pattern resolution.
|
|
991
|
+
|
|
992
|
+
Port of R ``recordGrobForPatternResolution`` (patterns.R:150-161).
|
|
993
|
+
Called after ``makeContent()`` in the drawing pipeline.
|
|
994
|
+
If the current gpar has a pattern fill, the grob is attached so
|
|
995
|
+
that subsequent ``resolveFill()`` can compute the bounding box.
|
|
996
|
+
"""
|
|
997
|
+
from ._state import get_state
|
|
998
|
+
|
|
999
|
+
state = get_state()
|
|
1000
|
+
gp = state.get_gpar()
|
|
1001
|
+
if gp is None:
|
|
1002
|
+
return
|
|
1003
|
+
|
|
1004
|
+
fill = gp.get("fill", None)
|
|
1005
|
+
if fill is None:
|
|
1006
|
+
return
|
|
1007
|
+
|
|
1008
|
+
if is_pattern(fill):
|
|
1009
|
+
# Attach grob to the pattern for later resolution
|
|
1010
|
+
fill._attached_grob = grob
|
|
1011
|
+
elif is_pattern_list(fill):
|
|
1012
|
+
for f in fill:
|
|
1013
|
+
if is_pattern(f):
|
|
1014
|
+
f._attached_grob = grob
|
|
1015
|
+
|
|
1016
|
+
|
|
1017
|
+
def record_gtree_for_pattern_resolution(grob: Any) -> None:
|
|
1018
|
+
"""Resolve gTree-level pattern fill immediately.
|
|
1019
|
+
|
|
1020
|
+
Port of R ``recordGTreeForPatternResolution`` (patterns.R:176-190).
|
|
1021
|
+
If the gTree's own gp$fill is a pattern with group=TRUE,
|
|
1022
|
+
resolve it now (using the gTree's bounding box).
|
|
1023
|
+
"""
|
|
1024
|
+
from ._state import get_state
|
|
1025
|
+
|
|
1026
|
+
gp = getattr(grob, "gp", None)
|
|
1027
|
+
if gp is None:
|
|
1028
|
+
return
|
|
1029
|
+
|
|
1030
|
+
fill = gp.get("fill", None) if hasattr(gp, "get") else None
|
|
1031
|
+
if fill is None:
|
|
1032
|
+
return
|
|
1033
|
+
|
|
1034
|
+
should_resolve = False
|
|
1035
|
+
if is_pattern(fill) and getattr(fill, "group", False):
|
|
1036
|
+
should_resolve = True
|
|
1037
|
+
elif is_pattern_list(fill):
|
|
1038
|
+
should_resolve = True
|
|
1039
|
+
|
|
1040
|
+
if should_resolve:
|
|
1041
|
+
state = get_state()
|
|
1042
|
+
current_gp = state.get_gpar()
|
|
1043
|
+
current_fill = current_gp.get("fill", None) if current_gp else None
|
|
1044
|
+
if current_fill is not None and (is_pattern(current_fill) or is_pattern_list(current_fill)):
|
|
1045
|
+
resolved = resolve_fill(current_fill, grob=grob)
|
|
1046
|
+
if resolved is None:
|
|
1047
|
+
current_gp._params["fill"] = "transparent"
|
|
1048
|
+
else:
|
|
1049
|
+
current_gp._params["fill"] = resolved
|