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/_transforms.py ADDED
@@ -0,0 +1,448 @@
1
+ """Affine transformation matrices for grid_py (port of R's grid group transforms).
2
+
3
+ This module provides functions that construct 3x3 affine transformation
4
+ matrices used by the group/define/use grob system. Each function returns a
5
+ :class:`numpy.ndarray` of shape ``(3, 3)`` and dtype ``float64``.
6
+
7
+ The matrices follow the **row-vector** convention used by R's *grid* package:
8
+ a point ``[x, y, 1]`` is transformed via ``point @ matrix``. Translation
9
+ terms therefore live in the bottom row (indices ``[2, 0]`` and ``[2, 1]``).
10
+
11
+ Group transforms
12
+ ----------------
13
+ Low-level building blocks that mirror R's ``groupTranslate``,
14
+ ``groupRotate``, ``groupScale``, ``groupShear``, and ``groupFlip``.
15
+
16
+ Definition transforms
17
+ ---------------------
18
+ Transforms applied when defining a group (``defineGrob``).
19
+
20
+ Use transforms
21
+ --------------
22
+ Transforms applied when reusing a group (``useGrob``).
23
+
24
+ Viewport transforms
25
+ -------------------
26
+ Combined transforms that map from one viewport to another.
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import math
32
+ from typing import Union
33
+
34
+ import numpy as np
35
+ from numpy.typing import NDArray
36
+
37
+ __all__ = [
38
+ # Group (primitive) transforms
39
+ "group_translate",
40
+ "group_rotate",
41
+ "group_scale",
42
+ "group_shear",
43
+ "group_flip",
44
+ # Definition transforms
45
+ "defn_translate",
46
+ "defn_rotate",
47
+ "defn_scale",
48
+ # Use transforms
49
+ "use_translate",
50
+ "use_rotate",
51
+ "use_scale",
52
+ # Viewport transforms
53
+ "viewport_translate",
54
+ "viewport_rotate",
55
+ "viewport_scale",
56
+ "viewport_transform",
57
+ ]
58
+
59
+ # ---------------------------------------------------------------------------
60
+ # Type alias
61
+ # ---------------------------------------------------------------------------
62
+ Matrix3x3 = NDArray[np.float64]
63
+
64
+ # ============================================================================
65
+ # Group (primitive) transforms
66
+ # ============================================================================
67
+
68
+
69
+ def group_translate(dx: float = 0, dy: float = 0) -> Matrix3x3:
70
+ """Return a 3x3 translation matrix.
71
+
72
+ Parameters
73
+ ----------
74
+ dx : float, optional
75
+ Horizontal translation (default ``0``).
76
+ dy : float, optional
77
+ Vertical translation (default ``0``).
78
+
79
+ Returns
80
+ -------
81
+ numpy.ndarray
82
+ A 3x3 affine translation matrix.
83
+
84
+ Examples
85
+ --------
86
+ >>> group_translate(10, 20)
87
+ array([[ 1., 0., 0.],
88
+ [ 0., 1., 0.],
89
+ [10., 20., 1.]])
90
+ """
91
+ mat = np.eye(3, dtype=np.float64)
92
+ mat[2, 0] = dx
93
+ mat[2, 1] = dy
94
+ return mat
95
+
96
+
97
+ def group_rotate(angle: float = 0, device: bool = True) -> Matrix3x3:
98
+ """Return a 3x3 rotation matrix.
99
+
100
+ Parameters
101
+ ----------
102
+ angle : float, optional
103
+ Rotation angle in **degrees** (default ``0``). Positive values
104
+ rotate counter-clockwise in the standard mathematical sense.
105
+ device : bool, optional
106
+ If ``True`` (default) the rotation follows device conventions
107
+ (identical to R's ``groupRotate`` with ``device=TRUE`` when the
108
+ device origin is at the bottom-left).
109
+
110
+ Returns
111
+ -------
112
+ numpy.ndarray
113
+ A 3x3 affine rotation matrix.
114
+
115
+ Examples
116
+ --------
117
+ >>> import numpy as np
118
+ >>> np.allclose(group_rotate(90) @ np.array([1, 0, 1]),
119
+ ... np.array([0, 1, 1]), atol=1e-15)
120
+ True
121
+ """
122
+ theta = math.radians(angle)
123
+ cos_t = math.cos(theta)
124
+ sin_t = math.sin(theta)
125
+ mat = np.eye(3, dtype=np.float64)
126
+ mat[0, 0] = cos_t
127
+ mat[0, 1] = sin_t
128
+ mat[1, 0] = -sin_t
129
+ mat[1, 1] = cos_t
130
+ return mat
131
+
132
+
133
+ def group_scale(sx: float = 1, sy: float = 1) -> Matrix3x3:
134
+ """Return a 3x3 scaling matrix.
135
+
136
+ Parameters
137
+ ----------
138
+ sx : float, optional
139
+ Horizontal scale factor (default ``1``).
140
+ sy : float, optional
141
+ Vertical scale factor (default ``1``).
142
+
143
+ Returns
144
+ -------
145
+ numpy.ndarray
146
+ A 3x3 affine scaling matrix.
147
+
148
+ Examples
149
+ --------
150
+ >>> group_scale(2, 3)
151
+ array([[2., 0., 0.],
152
+ [0., 3., 0.],
153
+ [0., 0., 1.]])
154
+ """
155
+ mat = np.eye(3, dtype=np.float64)
156
+ mat[0, 0] = sx
157
+ mat[1, 1] = sy
158
+ return mat
159
+
160
+
161
+ def group_shear(sx: float = 0, sy: float = 0) -> Matrix3x3:
162
+ """Return a 3x3 shear matrix.
163
+
164
+ Parameters
165
+ ----------
166
+ sx : float, optional
167
+ Shear factor along the *x*-axis (default ``0``). This is placed
168
+ at matrix position ``[1, 0]``, matching R's ``groupShear``.
169
+ sy : float, optional
170
+ Shear factor along the *y*-axis (default ``0``). This is placed
171
+ at matrix position ``[0, 1]``, matching R's ``groupShear``.
172
+
173
+ Returns
174
+ -------
175
+ numpy.ndarray
176
+ A 3x3 affine shear matrix.
177
+
178
+ Examples
179
+ --------
180
+ >>> group_shear(0.5, 0)
181
+ array([[1. , 0. , 0. ],
182
+ [0.5, 1. , 0. ],
183
+ [0. , 0. , 1. ]])
184
+ """
185
+ mat = np.eye(3, dtype=np.float64)
186
+ mat[0, 1] = sy
187
+ mat[1, 0] = sx
188
+ return mat
189
+
190
+
191
+ def group_flip(flipX: bool = False, flipY: bool = False) -> Matrix3x3:
192
+ """Return a 3x3 flip (reflection) matrix.
193
+
194
+ Parameters
195
+ ----------
196
+ flipX : bool, optional
197
+ If ``True``, negate the *x*-component (reflect across the *y*-axis).
198
+ flipY : bool, optional
199
+ If ``True``, negate the *y*-component (reflect across the *x*-axis).
200
+
201
+ Returns
202
+ -------
203
+ numpy.ndarray
204
+ A 3x3 affine flip matrix.
205
+
206
+ Examples
207
+ --------
208
+ >>> group_flip(True, False)
209
+ array([[-1., 0., 0.],
210
+ [ 0., 1., 0.],
211
+ [ 0., 0., 1.]])
212
+ """
213
+ mat = np.eye(3, dtype=np.float64)
214
+ if flipX:
215
+ mat[0, 0] = -1.0
216
+ if flipY:
217
+ mat[1, 1] = -1.0
218
+ return mat
219
+
220
+
221
+ # ============================================================================
222
+ # Definition transforms (for defineGrob)
223
+ # ============================================================================
224
+
225
+
226
+ def defn_translate(dx: float = 0, dy: float = 0) -> Matrix3x3:
227
+ """Return a 3x3 translation matrix for a group definition.
228
+
229
+ In R this retrieves the definition viewport location from the group
230
+ object. Here we accept the displacements directly.
231
+
232
+ Parameters
233
+ ----------
234
+ dx : float, optional
235
+ Horizontal displacement (default ``0``).
236
+ dy : float, optional
237
+ Vertical displacement (default ``0``).
238
+
239
+ Returns
240
+ -------
241
+ numpy.ndarray
242
+ A 3x3 affine translation matrix.
243
+ """
244
+ return group_translate(dx, dy)
245
+
246
+
247
+ def defn_rotate(angle: float = 0) -> Matrix3x3:
248
+ """Return a 3x3 rotation matrix for a group definition.
249
+
250
+ Parameters
251
+ ----------
252
+ angle : float, optional
253
+ Rotation angle in degrees (default ``0``).
254
+
255
+ Returns
256
+ -------
257
+ numpy.ndarray
258
+ A 3x3 affine rotation matrix.
259
+ """
260
+ return group_rotate(angle, device=True)
261
+
262
+
263
+ def defn_scale(sx: float = 1, sy: float = 1) -> Matrix3x3:
264
+ """Return a 3x3 scaling matrix for a group definition.
265
+
266
+ Parameters
267
+ ----------
268
+ sx : float, optional
269
+ Horizontal scale factor (default ``1``).
270
+ sy : float, optional
271
+ Vertical scale factor (default ``1``).
272
+
273
+ Returns
274
+ -------
275
+ numpy.ndarray
276
+ A 3x3 affine scaling matrix.
277
+ """
278
+ return group_scale(sx, sy)
279
+
280
+
281
+ # ============================================================================
282
+ # Use transforms (for useGrob)
283
+ # ============================================================================
284
+
285
+
286
+ def use_translate(dx: float = 0, dy: float = 0) -> Matrix3x3:
287
+ """Return a 3x3 translation matrix for a group use.
288
+
289
+ In R this retrieves the current viewport location. Here we accept
290
+ the displacements directly so the matrix can be composed offline.
291
+
292
+ Parameters
293
+ ----------
294
+ dx : float, optional
295
+ Horizontal displacement (default ``0``).
296
+ dy : float, optional
297
+ Vertical displacement (default ``0``).
298
+
299
+ Returns
300
+ -------
301
+ numpy.ndarray
302
+ A 3x3 affine translation matrix.
303
+ """
304
+ return group_translate(dx, dy)
305
+
306
+
307
+ def use_rotate(angle: float = 0) -> Matrix3x3:
308
+ """Return a 3x3 rotation matrix for a group use.
309
+
310
+ Parameters
311
+ ----------
312
+ angle : float, optional
313
+ Rotation angle in degrees (default ``0``).
314
+
315
+ Returns
316
+ -------
317
+ numpy.ndarray
318
+ A 3x3 affine rotation matrix.
319
+ """
320
+ return group_rotate(angle, device=True)
321
+
322
+
323
+ def use_scale(sx: float = 1, sy: float = 1) -> Matrix3x3:
324
+ """Return a 3x3 scaling matrix for a group use.
325
+
326
+ Parameters
327
+ ----------
328
+ sx : float, optional
329
+ Horizontal scale factor (default ``1``).
330
+ sy : float, optional
331
+ Vertical scale factor (default ``1``).
332
+
333
+ Returns
334
+ -------
335
+ numpy.ndarray
336
+ A 3x3 affine scaling matrix.
337
+ """
338
+ return group_scale(sx, sy)
339
+
340
+
341
+ # ============================================================================
342
+ # Viewport transforms
343
+ # ============================================================================
344
+
345
+
346
+ def viewport_translate(dx: float = 0, dy: float = 0) -> Matrix3x3:
347
+ """Return a 3x3 translation suitable for a viewport transform.
348
+
349
+ This is the combined inverse-definition-translate followed by the
350
+ use-translate, collapsed into a single translation of the difference.
351
+
352
+ Parameters
353
+ ----------
354
+ dx : float, optional
355
+ Net horizontal displacement (default ``0``).
356
+ dy : float, optional
357
+ Net vertical displacement (default ``0``).
358
+
359
+ Returns
360
+ -------
361
+ numpy.ndarray
362
+ A 3x3 affine translation matrix.
363
+ """
364
+ return group_translate(dx, dy)
365
+
366
+
367
+ def viewport_rotate(angle: float = 0) -> Matrix3x3:
368
+ """Return a 3x3 rotation suitable for a viewport transform.
369
+
370
+ Parameters
371
+ ----------
372
+ angle : float, optional
373
+ Net rotation angle in degrees (default ``0``).
374
+
375
+ Returns
376
+ -------
377
+ numpy.ndarray
378
+ A 3x3 affine rotation matrix.
379
+ """
380
+ return group_rotate(angle, device=True)
381
+
382
+
383
+ def viewport_scale(sx: float = 1, sy: float = 1) -> Matrix3x3:
384
+ """Return a 3x3 scaling suitable for a viewport transform.
385
+
386
+ Parameters
387
+ ----------
388
+ sx : float, optional
389
+ Horizontal scale factor (default ``1``).
390
+ sy : float, optional
391
+ Vertical scale factor (default ``1``).
392
+
393
+ Returns
394
+ -------
395
+ numpy.ndarray
396
+ A 3x3 affine scaling matrix.
397
+ """
398
+ return group_scale(sx, sy)
399
+
400
+
401
+ def viewport_transform(
402
+ dx: float = 0,
403
+ dy: float = 0,
404
+ rotation: float = 0,
405
+ sx: float = 1,
406
+ sy: float = 1,
407
+ ) -> Matrix3x3:
408
+ """Return a combined 3x3 viewport transform matrix.
409
+
410
+ The resulting matrix applies translation, rotation, and scaling in a
411
+ single affine transform, following the composition order used by R's
412
+ ``viewportTransform``:
413
+
414
+ translate(dx, dy) @ rotate(rotation) @ scale(sx, sy)
415
+
416
+ Because we use the row-vector convention (``point @ matrix``), the
417
+ operations are applied left-to-right: first translate, then rotate,
418
+ then scale.
419
+
420
+ Parameters
421
+ ----------
422
+ dx : float, optional
423
+ Horizontal translation (default ``0``).
424
+ dy : float, optional
425
+ Vertical translation (default ``0``).
426
+ rotation : float, optional
427
+ Rotation angle in degrees (default ``0``).
428
+ sx : float, optional
429
+ Horizontal scale factor (default ``1``).
430
+ sy : float, optional
431
+ Vertical scale factor (default ``1``).
432
+
433
+ Returns
434
+ -------
435
+ numpy.ndarray
436
+ A 3x3 combined affine transform matrix.
437
+
438
+ Examples
439
+ --------
440
+ >>> import numpy as np
441
+ >>> T = viewport_transform(dx=10, dy=20, rotation=0, sx=2, sy=3)
442
+ >>> np.allclose(T, group_translate(10, 20) @ group_scale(2, 3))
443
+ True
444
+ """
445
+ T = group_translate(dx, dy)
446
+ R = group_rotate(rotation, device=True)
447
+ S = group_scale(sx, sy)
448
+ return T @ R @ S