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/_vp_calc.py
ADDED
|
@@ -0,0 +1,970 @@
|
|
|
1
|
+
"""Viewport coordinate transform calculations -- port of R's matrix.c + viewport.c.
|
|
2
|
+
|
|
3
|
+
This module provides the 3×3 affine transform matrix operations and viewport
|
|
4
|
+
transform computation that form the core of R grid's coordinate pipeline.
|
|
5
|
+
|
|
6
|
+
All transforms use the **row-vector** convention from R's grid:
|
|
7
|
+
point_out = point_in @ matrix
|
|
8
|
+
where point = [x, y, 1]. Translation terms live in row 2 (m[2,0], m[2,1]).
|
|
9
|
+
|
|
10
|
+
References
|
|
11
|
+
----------
|
|
12
|
+
R source: ``grid/src/matrix.c`` (identity, translation, rotation, scaling,
|
|
13
|
+
multiply, location, trans), ``grid/src/viewport.c`` (calcViewportTransform).
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import math
|
|
19
|
+
from typing import Any, Dict, Optional, Tuple
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
# Matrix operations (port of matrix.c)
|
|
25
|
+
"identity",
|
|
26
|
+
"translation",
|
|
27
|
+
"rotation",
|
|
28
|
+
"scaling",
|
|
29
|
+
"multiply",
|
|
30
|
+
"location",
|
|
31
|
+
"trans",
|
|
32
|
+
"inv_transform",
|
|
33
|
+
"copy_transform",
|
|
34
|
+
# Viewport transform (port of viewport.c:calcViewportTransform)
|
|
35
|
+
"calc_viewport_transform",
|
|
36
|
+
# Inverse transforms (port of unit.c:1226-1475)
|
|
37
|
+
"_transform_from_inches",
|
|
38
|
+
"_transform_xy_from_inches",
|
|
39
|
+
"_transform_wh_from_inches",
|
|
40
|
+
"_transform_xy_to_npc",
|
|
41
|
+
"_transform_wh_to_npc",
|
|
42
|
+
"_transform_xy_from_npc",
|
|
43
|
+
"_transform_wh_from_npc",
|
|
44
|
+
# Viewport context
|
|
45
|
+
"ViewportContext",
|
|
46
|
+
"ViewportTransformResult",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# ============================================================================
|
|
51
|
+
# Type aliases
|
|
52
|
+
# ============================================================================
|
|
53
|
+
|
|
54
|
+
# LTransform = double[3][3] --> np.ndarray shape (3,3) float64
|
|
55
|
+
# LLocation = double[3] --> np.ndarray shape (3,) float64
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ============================================================================
|
|
59
|
+
# Matrix operations -- direct port of R grid/src/matrix.c
|
|
60
|
+
# ============================================================================
|
|
61
|
+
# Each function mirrors the corresponding C function exactly.
|
|
62
|
+
# Comments reference R source line numbers for traceability.
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def identity() -> np.ndarray:
|
|
66
|
+
"""Return 3×3 identity matrix.
|
|
67
|
+
|
|
68
|
+
Port of ``matrix.c:62 identity()``.
|
|
69
|
+
"""
|
|
70
|
+
return np.eye(3, dtype=np.float64)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def translation(tx: float, ty: float) -> np.ndarray:
|
|
74
|
+
"""Return 3×3 translation matrix.
|
|
75
|
+
|
|
76
|
+
Port of ``matrix.c:73 translation()``:
|
|
77
|
+
``identity(m); m[2][0] = tx; m[2][1] = ty;``
|
|
78
|
+
"""
|
|
79
|
+
m = np.eye(3, dtype=np.float64)
|
|
80
|
+
m[2, 0] = tx
|
|
81
|
+
m[2, 1] = ty
|
|
82
|
+
return m
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def scaling(sx: float, sy: float) -> np.ndarray:
|
|
86
|
+
"""Return 3×3 scaling matrix.
|
|
87
|
+
|
|
88
|
+
Port of ``matrix.c:80 scaling()``:
|
|
89
|
+
``identity(m); m[0][0] = sx; m[1][1] = sy;``
|
|
90
|
+
"""
|
|
91
|
+
m = np.eye(3, dtype=np.float64)
|
|
92
|
+
m[0, 0] = sx
|
|
93
|
+
m[1, 1] = sy
|
|
94
|
+
return m
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def rotation(theta_degrees: float) -> np.ndarray:
|
|
98
|
+
"""Return 3×3 rotation matrix for *theta_degrees* degrees.
|
|
99
|
+
|
|
100
|
+
Port of ``matrix.c:87 rotation()``:
|
|
101
|
+
``thetarad = theta/180*PI; m[0][0] = cos; m[0][1] = sin;
|
|
102
|
+
m[1][0] = -sin; m[1][1] = cos;``
|
|
103
|
+
"""
|
|
104
|
+
rad = theta_degrees / 180.0 * math.pi
|
|
105
|
+
c = math.cos(rad)
|
|
106
|
+
s = math.sin(rad)
|
|
107
|
+
m = np.eye(3, dtype=np.float64)
|
|
108
|
+
m[0, 0] = c
|
|
109
|
+
m[0, 1] = s
|
|
110
|
+
m[1, 0] = -s
|
|
111
|
+
m[1, 1] = c
|
|
112
|
+
return m
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def multiply(m1: np.ndarray, m2: np.ndarray) -> np.ndarray:
|
|
116
|
+
"""Multiply two 3×3 matrices: ``result = m1 @ m2``.
|
|
117
|
+
|
|
118
|
+
Port of ``matrix.c:99 multiply()``.
|
|
119
|
+
Uses numpy matmul for clarity; result is identical to the
|
|
120
|
+
hand-unrolled R code.
|
|
121
|
+
"""
|
|
122
|
+
return m1 @ m2
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def location(x: float, y: float) -> np.ndarray:
|
|
126
|
+
"""Create a homogeneous location vector [x, y, 1].
|
|
127
|
+
|
|
128
|
+
Port of ``matrix.c:112 location()``.
|
|
129
|
+
"""
|
|
130
|
+
return np.array([x, y, 1.0], dtype=np.float64)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def trans(vin: np.ndarray, m: np.ndarray) -> np.ndarray:
|
|
134
|
+
"""Transform a location vector by a matrix: ``vout = vin @ m``.
|
|
135
|
+
|
|
136
|
+
Port of ``matrix.c:119 trans()``:
|
|
137
|
+
``vout[0] = vin[0]*m[0][0] + vin[1]*m[1][0] + vin[2]*m[2][0]; ...``
|
|
138
|
+
"""
|
|
139
|
+
return vin @ m
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def inv_transform(t: np.ndarray) -> np.ndarray:
|
|
143
|
+
"""Compute the inverse of a 3×3 transform matrix.
|
|
144
|
+
|
|
145
|
+
Port of ``matrix.c:44 invTransform()`` using the explicit
|
|
146
|
+
cofactor/determinant formula.
|
|
147
|
+
"""
|
|
148
|
+
det = (t[0, 0] * (t[2, 2] * t[1, 1] - t[2, 1] * t[1, 2])
|
|
149
|
+
- t[1, 0] * (t[2, 2] * t[0, 1] - t[2, 1] * t[0, 2])
|
|
150
|
+
+ t[2, 0] * (t[1, 2] * t[0, 1] - t[1, 1] * t[0, 2]))
|
|
151
|
+
if det == 0:
|
|
152
|
+
raise ValueError("singular transformation matrix")
|
|
153
|
+
inv = np.empty((3, 3), dtype=np.float64)
|
|
154
|
+
inv[0, 0] = (t[2, 2] * t[1, 1] - t[2, 1] * t[1, 2]) / det
|
|
155
|
+
inv[0, 1] = -(t[2, 2] * t[0, 1] - t[2, 1] * t[0, 2]) / det
|
|
156
|
+
inv[0, 2] = (t[1, 2] * t[0, 1] - t[1, 1] * t[0, 2]) / det
|
|
157
|
+
inv[1, 0] = -(t[2, 2] * t[1, 0] - t[2, 0] * t[1, 2]) / det
|
|
158
|
+
inv[1, 1] = (t[2, 2] * t[0, 0] - t[2, 0] * t[0, 2]) / det
|
|
159
|
+
inv[1, 2] = -(t[1, 2] * t[0, 0] - t[1, 0] * t[0, 2]) / det
|
|
160
|
+
inv[2, 0] = (t[2, 1] * t[1, 0] - t[2, 0] * t[1, 1]) / det
|
|
161
|
+
inv[2, 1] = -(t[2, 1] * t[0, 0] - t[2, 0] * t[0, 1]) / det
|
|
162
|
+
inv[2, 2] = (t[1, 1] * t[0, 0] - t[1, 0] * t[0, 1]) / det
|
|
163
|
+
return inv
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def copy_transform(t: np.ndarray) -> np.ndarray:
|
|
167
|
+
"""Copy a 3×3 transform matrix.
|
|
168
|
+
|
|
169
|
+
Port of ``matrix.c:36 copyTransform()``.
|
|
170
|
+
"""
|
|
171
|
+
return t.copy()
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# ============================================================================
|
|
175
|
+
# Justification helper -- port of R grid/src/viewport.c justification()
|
|
176
|
+
# ============================================================================
|
|
177
|
+
|
|
178
|
+
def justification(width: float, height: float,
|
|
179
|
+
hjust: float, vjust: float) -> Tuple[float, float]:
|
|
180
|
+
"""Compute justification offsets in the same units as width/height.
|
|
181
|
+
|
|
182
|
+
Port of R's ``justification()`` (grid.h / viewport.c).
|
|
183
|
+
Returns (xadj, yadj) where:
|
|
184
|
+
xadj = -hjust * width
|
|
185
|
+
yadj = -vjust * height
|
|
186
|
+
"""
|
|
187
|
+
return (-hjust * width, -vjust * height)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# ============================================================================
|
|
191
|
+
# Viewport context (port of LViewportContext)
|
|
192
|
+
# ============================================================================
|
|
193
|
+
|
|
194
|
+
class ViewportContext:
|
|
195
|
+
"""Stores xscale/yscale ranges for native unit resolution.
|
|
196
|
+
|
|
197
|
+
Port of ``grid.h:260 LViewportContext``.
|
|
198
|
+
"""
|
|
199
|
+
__slots__ = ("xscalemin", "xscalemax", "yscalemin", "yscalemax")
|
|
200
|
+
|
|
201
|
+
def __init__(self, xscale: Tuple[float, float] = (0.0, 1.0),
|
|
202
|
+
yscale: Tuple[float, float] = (0.0, 1.0)):
|
|
203
|
+
self.xscalemin = float(xscale[0])
|
|
204
|
+
self.xscalemax = float(xscale[1])
|
|
205
|
+
self.yscalemin = float(yscale[0])
|
|
206
|
+
self.yscalemax = float(yscale[1])
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class ViewportTransformResult:
|
|
210
|
+
"""Result of calcViewportTransform: the computed viewport state.
|
|
211
|
+
|
|
212
|
+
Stores the same values that R writes back to the viewport SEXP
|
|
213
|
+
(viewport.c:370-380).
|
|
214
|
+
"""
|
|
215
|
+
__slots__ = ("width_cm", "height_cm", "rotation_angle",
|
|
216
|
+
"transform", "vpc")
|
|
217
|
+
|
|
218
|
+
def __init__(self, width_cm: float, height_cm: float,
|
|
219
|
+
rotation_angle: float, transform: np.ndarray,
|
|
220
|
+
vpc: ViewportContext):
|
|
221
|
+
self.width_cm = width_cm
|
|
222
|
+
self.height_cm = height_cm
|
|
223
|
+
self.rotation_angle = rotation_angle
|
|
224
|
+
self.transform = transform
|
|
225
|
+
self.vpc = vpc
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
# ============================================================================
|
|
229
|
+
# Unit-to-inches conversion -- port of unit.c:transform() switch
|
|
230
|
+
# ============================================================================
|
|
231
|
+
# This is the key function that converts any grid unit value to inches
|
|
232
|
+
# within a viewport context, WITHOUT requiring an R graphics device.
|
|
233
|
+
|
|
234
|
+
_INCHES_PER = {
|
|
235
|
+
"inches": 1.0,
|
|
236
|
+
"cm": 1.0 / 2.54,
|
|
237
|
+
"mm": 1.0 / 25.4,
|
|
238
|
+
"points": 1.0 / 72.27, # TeX points
|
|
239
|
+
"picas": 12.0 / 72.27,
|
|
240
|
+
"bigpts": 1.0 / 72.0, # PostScript/CSS points
|
|
241
|
+
"dida": (1238.0 / 1157.0) / 72.27,
|
|
242
|
+
"cicero": 12.0 * (1238.0 / 1157.0) / 72.27,
|
|
243
|
+
"scaledpts": 1.0 / (72.27 * 65536.0),
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def transform_x_to_inches(
|
|
248
|
+
unit_obj: Any,
|
|
249
|
+
index: int,
|
|
250
|
+
vpc: ViewportContext,
|
|
251
|
+
gc_fontsize: float,
|
|
252
|
+
gc_cex: float,
|
|
253
|
+
gc_lineheight: float,
|
|
254
|
+
parent_width_cm: float,
|
|
255
|
+
parent_height_cm: float,
|
|
256
|
+
str_metric_fn: Any = None,
|
|
257
|
+
grob_metric_fn: Any = None,
|
|
258
|
+
) -> float:
|
|
259
|
+
"""Convert a Unit element to inches along the X axis.
|
|
260
|
+
|
|
261
|
+
Port of ``unit.c:transformXtoINCHES`` which calls
|
|
262
|
+
``transform()`` with ``thisCM = parentWidthCM``.
|
|
263
|
+
|
|
264
|
+
Parameters
|
|
265
|
+
----------
|
|
266
|
+
unit_obj : Unit
|
|
267
|
+
The unit object (must have _values, _units, _data).
|
|
268
|
+
index : int
|
|
269
|
+
Element index within the unit vector.
|
|
270
|
+
vpc : ViewportContext
|
|
271
|
+
The parent viewport's native scale ranges.
|
|
272
|
+
gc_fontsize, gc_cex, gc_lineheight : float
|
|
273
|
+
Font metrics from the gpar context (gc->ps, gc->cex, gc->lineheight).
|
|
274
|
+
parent_width_cm, parent_height_cm : float
|
|
275
|
+
Parent viewport dimensions in CM.
|
|
276
|
+
str_metric_fn : callable or None
|
|
277
|
+
Function(text, gp) -> dict with 'width','ascent','descent' in inches.
|
|
278
|
+
grob_metric_fn : callable or None
|
|
279
|
+
Function(grob, what) -> Unit for grob-based metrics.
|
|
280
|
+
|
|
281
|
+
Returns
|
|
282
|
+
-------
|
|
283
|
+
float
|
|
284
|
+
The value in inches.
|
|
285
|
+
"""
|
|
286
|
+
return _transform_to_inches(
|
|
287
|
+
unit_obj, index, vpc,
|
|
288
|
+
gc_fontsize, gc_cex, gc_lineheight,
|
|
289
|
+
this_cm=parent_width_cm,
|
|
290
|
+
other_cm=parent_height_cm,
|
|
291
|
+
axis="x", is_dim=False,
|
|
292
|
+
str_metric_fn=str_metric_fn,
|
|
293
|
+
grob_metric_fn=grob_metric_fn,
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def transform_y_to_inches(
|
|
298
|
+
unit_obj: Any, index: int, vpc: ViewportContext,
|
|
299
|
+
gc_fontsize: float, gc_cex: float, gc_lineheight: float,
|
|
300
|
+
parent_width_cm: float, parent_height_cm: float,
|
|
301
|
+
str_metric_fn: Any = None, grob_metric_fn: Any = None,
|
|
302
|
+
) -> float:
|
|
303
|
+
"""Convert a Unit element to inches along the Y axis.
|
|
304
|
+
|
|
305
|
+
Port of ``unit.c:transformYtoINCHES``.
|
|
306
|
+
"""
|
|
307
|
+
return _transform_to_inches(
|
|
308
|
+
unit_obj, index, vpc,
|
|
309
|
+
gc_fontsize, gc_cex, gc_lineheight,
|
|
310
|
+
this_cm=parent_height_cm,
|
|
311
|
+
other_cm=parent_width_cm,
|
|
312
|
+
axis="y", is_dim=False,
|
|
313
|
+
str_metric_fn=str_metric_fn,
|
|
314
|
+
grob_metric_fn=grob_metric_fn,
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def transform_width_to_inches(
|
|
319
|
+
unit_obj: Any, index: int, vpc: ViewportContext,
|
|
320
|
+
gc_fontsize: float, gc_cex: float, gc_lineheight: float,
|
|
321
|
+
parent_width_cm: float, parent_height_cm: float,
|
|
322
|
+
str_metric_fn: Any = None, grob_metric_fn: Any = None,
|
|
323
|
+
) -> float:
|
|
324
|
+
"""Convert a Unit element to inches as an X-axis dimension.
|
|
325
|
+
|
|
326
|
+
Port of ``unit.c:transformWidthtoINCHES``.
|
|
327
|
+
"""
|
|
328
|
+
return _transform_to_inches(
|
|
329
|
+
unit_obj, index, vpc,
|
|
330
|
+
gc_fontsize, gc_cex, gc_lineheight,
|
|
331
|
+
this_cm=parent_width_cm,
|
|
332
|
+
other_cm=parent_height_cm,
|
|
333
|
+
axis="x", is_dim=True,
|
|
334
|
+
str_metric_fn=str_metric_fn,
|
|
335
|
+
grob_metric_fn=grob_metric_fn,
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def transform_height_to_inches(
|
|
340
|
+
unit_obj: Any, index: int, vpc: ViewportContext,
|
|
341
|
+
gc_fontsize: float, gc_cex: float, gc_lineheight: float,
|
|
342
|
+
parent_width_cm: float, parent_height_cm: float,
|
|
343
|
+
str_metric_fn: Any = None, grob_metric_fn: Any = None,
|
|
344
|
+
) -> float:
|
|
345
|
+
"""Convert a Unit element to inches as a Y-axis dimension.
|
|
346
|
+
|
|
347
|
+
Port of ``unit.c:transformHeighttoINCHES``.
|
|
348
|
+
"""
|
|
349
|
+
return _transform_to_inches(
|
|
350
|
+
unit_obj, index, vpc,
|
|
351
|
+
gc_fontsize, gc_cex, gc_lineheight,
|
|
352
|
+
this_cm=parent_height_cm,
|
|
353
|
+
other_cm=parent_width_cm,
|
|
354
|
+
axis="y", is_dim=True,
|
|
355
|
+
str_metric_fn=str_metric_fn,
|
|
356
|
+
grob_metric_fn=grob_metric_fn,
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _transform_to_inches(
|
|
361
|
+
unit_obj: Any,
|
|
362
|
+
index: int,
|
|
363
|
+
vpc: ViewportContext,
|
|
364
|
+
gc_fontsize: float,
|
|
365
|
+
gc_cex: float,
|
|
366
|
+
gc_lineheight: float,
|
|
367
|
+
this_cm: float,
|
|
368
|
+
other_cm: float,
|
|
369
|
+
axis: str,
|
|
370
|
+
is_dim: bool,
|
|
371
|
+
str_metric_fn: Any = None,
|
|
372
|
+
grob_metric_fn: Any = None,
|
|
373
|
+
scale: float = 1.0,
|
|
374
|
+
) -> float:
|
|
375
|
+
"""Core unit-to-inches conversion -- port of ``unit.c:transform()``.
|
|
376
|
+
|
|
377
|
+
This implements the big switch statement (unit.c:658-800) that
|
|
378
|
+
converts each unit type to inches, plus the GSS_SCALE post-scaling
|
|
379
|
+
for physical units (unit.c:804-814).
|
|
380
|
+
|
|
381
|
+
Parameters
|
|
382
|
+
----------
|
|
383
|
+
unit_obj : Unit
|
|
384
|
+
The unit object.
|
|
385
|
+
index : int
|
|
386
|
+
Element index.
|
|
387
|
+
vpc : ViewportContext
|
|
388
|
+
Parent scale context.
|
|
389
|
+
gc_fontsize, gc_cex, gc_lineheight : float
|
|
390
|
+
Font context (gc->ps, gc->cex, gc->lineheight).
|
|
391
|
+
this_cm, other_cm : float
|
|
392
|
+
Viewport dimension in CM for the primary and orthogonal axis.
|
|
393
|
+
axis : str
|
|
394
|
+
``"x"`` or ``"y"``.
|
|
395
|
+
is_dim : bool
|
|
396
|
+
True if converting a width/height (dimension), False for position.
|
|
397
|
+
str_metric_fn : callable or None
|
|
398
|
+
String metric query function.
|
|
399
|
+
grob_metric_fn : callable or None
|
|
400
|
+
Grob metric query function.
|
|
401
|
+
scale : float
|
|
402
|
+
GSS_SCALE zoom factor (R unit.c:804-814). Default 1.0.
|
|
403
|
+
|
|
404
|
+
Returns
|
|
405
|
+
-------
|
|
406
|
+
float
|
|
407
|
+
Value in inches.
|
|
408
|
+
"""
|
|
409
|
+
from ._units import Unit
|
|
410
|
+
|
|
411
|
+
if not isinstance(unit_obj, Unit):
|
|
412
|
+
return float(unit_obj)
|
|
413
|
+
|
|
414
|
+
idx = index % len(unit_obj)
|
|
415
|
+
value = float(unit_obj._values[idx])
|
|
416
|
+
utype = unit_obj._units[idx]
|
|
417
|
+
data = unit_obj._data[idx] if unit_obj._data is not None else None
|
|
418
|
+
|
|
419
|
+
this_inches = this_cm / 2.54
|
|
420
|
+
|
|
421
|
+
# ---- Absolute physical units (unit.c:670-682) ----
|
|
422
|
+
# R unit.c:804-814: physical units are additionally scaled by GSS_SCALE
|
|
423
|
+
if utype in _INCHES_PER:
|
|
424
|
+
return value * _INCHES_PER[utype] * scale
|
|
425
|
+
|
|
426
|
+
# ---- NPC (unit.c:667) ----
|
|
427
|
+
# L_NPC: result * thisCM / 2.54
|
|
428
|
+
if utype == "npc":
|
|
429
|
+
if is_dim:
|
|
430
|
+
return value * this_inches
|
|
431
|
+
else:
|
|
432
|
+
return value * this_inches
|
|
433
|
+
|
|
434
|
+
# ---- SNPC (unit.c:689) ----
|
|
435
|
+
# L_SNPC: result * min(thisCM, otherCM) / 2.54
|
|
436
|
+
if utype == "snpc":
|
|
437
|
+
return value * min(this_cm, other_cm) / 2.54
|
|
438
|
+
|
|
439
|
+
# ---- Native (unit.c:837) ----
|
|
440
|
+
# Maps value from [scalemin, scalemax] range to [0, thisCM/2.54]
|
|
441
|
+
if utype == "native":
|
|
442
|
+
scalemin = vpc.xscalemin if axis == "x" else vpc.yscalemin
|
|
443
|
+
scalemax = vpc.xscalemax if axis == "x" else vpc.yscalemax
|
|
444
|
+
srange = scalemax - scalemin
|
|
445
|
+
if srange == 0:
|
|
446
|
+
return 0.0
|
|
447
|
+
if is_dim:
|
|
448
|
+
return (value / srange) * this_inches
|
|
449
|
+
else:
|
|
450
|
+
return ((value - scalemin) / srange) * this_inches
|
|
451
|
+
|
|
452
|
+
# ---- Char (unit.c:683) ----
|
|
453
|
+
# L_CHAR: result * gc->ps * gc->cex / 72
|
|
454
|
+
# Note: gc->ps = fontsize * GSS_SCALE (gpar.c:395), so char/lines
|
|
455
|
+
# units are implicitly scaled by GSS_SCALE through gc->ps.
|
|
456
|
+
if utype == "char":
|
|
457
|
+
return value * gc_fontsize * scale * gc_cex / 72.0
|
|
458
|
+
|
|
459
|
+
# ---- Lines (unit.c:687) ----
|
|
460
|
+
# L_LINES: result * gc->ps * gc->cex * gc->lineheight / 72
|
|
461
|
+
if utype == "lines":
|
|
462
|
+
return value * gc_fontsize * scale * gc_cex * gc_lineheight / 72.0
|
|
463
|
+
|
|
464
|
+
# ---- Null (unit.c:693) ----
|
|
465
|
+
# L_NULL: contributes 0 inches in this context
|
|
466
|
+
if utype == "null":
|
|
467
|
+
return 0.0
|
|
468
|
+
|
|
469
|
+
# ---- String metrics (unit.c:720-760) ----
|
|
470
|
+
#
|
|
471
|
+
# Mirrors R's ``GEStrWidth`` / ``GEStrHeight`` / ``GEStrMetric``:
|
|
472
|
+
#
|
|
473
|
+
# - split the label on ``\n`` into lines;
|
|
474
|
+
# - ``strwidth`` = max(per-line widths) (GEStrWidth)
|
|
475
|
+
# - ``strheight`` = ink(first line) + (n−1) × cex × lineheight × ps × 1.2 / 72
|
|
476
|
+
# (GEStrHeight)
|
|
477
|
+
# - ``strascent`` / ``strdescent`` = ink metric of the first line
|
|
478
|
+
# (GEStrMetric)
|
|
479
|
+
if utype in ("strwidth", "strheight", "strascent", "strdescent"):
|
|
480
|
+
text = str(data) if data is not None else ""
|
|
481
|
+
lines = text.split("\n") if text else [""]
|
|
482
|
+
n = len(lines)
|
|
483
|
+
if str_metric_fn is not None:
|
|
484
|
+
m0 = str_metric_fn(lines[0], None)
|
|
485
|
+
if utype == "strwidth":
|
|
486
|
+
w = max(str_metric_fn(ln, None).get("width", 0.0) for ln in lines)
|
|
487
|
+
return value * w
|
|
488
|
+
elif utype == "strheight":
|
|
489
|
+
ink_first = m0.get("ascent", 0.0) + m0.get("descent", 0.0)
|
|
490
|
+
inter_line_gap = (
|
|
491
|
+
gc_cex * gc_lineheight * gc_fontsize * 1.2 / 72.0
|
|
492
|
+
)
|
|
493
|
+
return value * (ink_first + (n - 1) * inter_line_gap)
|
|
494
|
+
elif utype == "strascent":
|
|
495
|
+
return value * m0.get("ascent", 0.0)
|
|
496
|
+
else:
|
|
497
|
+
return value * m0.get("descent", 0.0)
|
|
498
|
+
# Fallback: estimate from font size when no measurement callback is
|
|
499
|
+
# available. ``gc->ps = fontsize * GSS_SCALE`` in R.
|
|
500
|
+
effective = gc_fontsize * scale * gc_cex
|
|
501
|
+
char_width = effective * 0.6 / 72.0
|
|
502
|
+
if utype == "strwidth":
|
|
503
|
+
max_line_len = max((len(ln) for ln in lines), default=0)
|
|
504
|
+
return value * max_line_len * char_width
|
|
505
|
+
elif utype == "strheight":
|
|
506
|
+
# First line approximated as one ``effective`` unit of height;
|
|
507
|
+
# add the inter-line gap for extra lines.
|
|
508
|
+
inter_line_gap = (
|
|
509
|
+
gc_cex * gc_lineheight * gc_fontsize * 1.2 / 72.0
|
|
510
|
+
)
|
|
511
|
+
return value * (effective / 72.0 + (n - 1) * inter_line_gap)
|
|
512
|
+
elif utype == "strascent":
|
|
513
|
+
return value * effective * 0.75 / 72.0
|
|
514
|
+
else:
|
|
515
|
+
return value * effective * 0.25 / 72.0
|
|
516
|
+
|
|
517
|
+
# ---- Grob metrics (unit.c:770-800) ----
|
|
518
|
+
# R's evaluateGrobUnit (unit.c:325-590) does a full preDraw/postDraw
|
|
519
|
+
# cycle on the grob, then calls widthDetails/heightDetails to get a
|
|
520
|
+
# result Unit, converts it to inches *within the grob's viewport
|
|
521
|
+
# context*, and returns the inches value.
|
|
522
|
+
#
|
|
523
|
+
# grob_metric_fn(grob, utype, value) must return **inches** directly
|
|
524
|
+
# (it does the full preDraw/eval/postDraw/restore cycle internally).
|
|
525
|
+
if utype in ("grobwidth", "grobheight", "grobascent", "grobdescent",
|
|
526
|
+
"grobx", "groby"):
|
|
527
|
+
if grob_metric_fn is not None and data is not None:
|
|
528
|
+
inches = grob_metric_fn(data, utype, value)
|
|
529
|
+
if inches is not None:
|
|
530
|
+
# For width / height / ascent / descent, ``value`` is a
|
|
531
|
+
# scaling factor (usually 1) — the result scales linearly.
|
|
532
|
+
# For grobx / groby, ``value`` carries the evaluation angle
|
|
533
|
+
# (degrees) and the callback already returned the absolute
|
|
534
|
+
# x/y coordinate in inches, so no extra scaling applies.
|
|
535
|
+
if utype in ("grobx", "groby"):
|
|
536
|
+
return inches
|
|
537
|
+
return value * inches
|
|
538
|
+
return 0.0
|
|
539
|
+
|
|
540
|
+
# ---- Compound units: sum, min, max ----
|
|
541
|
+
if utype in ("sum", "min", "max"):
|
|
542
|
+
child = data
|
|
543
|
+
if isinstance(child, Unit):
|
|
544
|
+
results = []
|
|
545
|
+
for j in range(len(child)):
|
|
546
|
+
r = _transform_to_inches(
|
|
547
|
+
child, j, vpc,
|
|
548
|
+
gc_fontsize, gc_cex, gc_lineheight,
|
|
549
|
+
this_cm, other_cm, axis, is_dim,
|
|
550
|
+
str_metric_fn, grob_metric_fn,
|
|
551
|
+
)
|
|
552
|
+
results.append(r)
|
|
553
|
+
if utype == "sum":
|
|
554
|
+
return value * sum(results)
|
|
555
|
+
elif utype == "min":
|
|
556
|
+
return value * min(results) if results else 0.0
|
|
557
|
+
elif utype == "max":
|
|
558
|
+
return value * max(results) if results else 0.0
|
|
559
|
+
return 0.0
|
|
560
|
+
|
|
561
|
+
# ---- mychar, mylines, mystrwidth, mystrheight (unit.c:804+) ----
|
|
562
|
+
if utype == "mychar":
|
|
563
|
+
return value * gc_fontsize * gc_cex / 72.0
|
|
564
|
+
if utype == "mylines":
|
|
565
|
+
return value * gc_fontsize * gc_cex * gc_lineheight / 72.0
|
|
566
|
+
if utype in ("mystrwidth", "mystrheight"):
|
|
567
|
+
# These use the grob's own gpar rather than the parent's.
|
|
568
|
+
# Without that context, fall back to the str metric path.
|
|
569
|
+
# R marks these as "FIXME: Remove this when I can" (unit.c:721,734).
|
|
570
|
+
if str_metric_fn is not None and data is not None:
|
|
571
|
+
text = str(data)
|
|
572
|
+
m = str_metric_fn(text, None)
|
|
573
|
+
if utype == "mystrwidth":
|
|
574
|
+
return value * m.get("width", 0.0)
|
|
575
|
+
else:
|
|
576
|
+
return value * (m.get("ascent", 0.0) + m.get("descent", 0.0))
|
|
577
|
+
return 0.0
|
|
578
|
+
|
|
579
|
+
# ---- Fallback: treat as NPC ----
|
|
580
|
+
return value * this_inches
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
# ============================================================================
|
|
584
|
+
# Inverse transform: inches → target unit (port of unit.c:1226-1475)
|
|
585
|
+
# ============================================================================
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def _transform_from_inches(
|
|
589
|
+
value: float,
|
|
590
|
+
unit: str,
|
|
591
|
+
gc_fontsize: float,
|
|
592
|
+
gc_cex: float,
|
|
593
|
+
gc_lineheight: float,
|
|
594
|
+
this_cm: float,
|
|
595
|
+
other_cm: float,
|
|
596
|
+
scale: float = 1.0,
|
|
597
|
+
) -> float:
|
|
598
|
+
"""Convert a value in inches to the given unit type.
|
|
599
|
+
|
|
600
|
+
Port of R ``unit.c:1226-1333 transformFromINCHES()``.
|
|
601
|
+
Handles absolute and font-relative units. For physical units,
|
|
602
|
+
applies the inverse of GSS_SCALE (unit.c:1313-1331).
|
|
603
|
+
|
|
604
|
+
Parameters
|
|
605
|
+
----------
|
|
606
|
+
value : float
|
|
607
|
+
Value in inches.
|
|
608
|
+
unit : str
|
|
609
|
+
Target unit type string.
|
|
610
|
+
gc_fontsize, gc_cex, gc_lineheight : float
|
|
611
|
+
Font context parameters.
|
|
612
|
+
this_cm, other_cm : float
|
|
613
|
+
Viewport dimensions in CM for primary and orthogonal axis.
|
|
614
|
+
scale : float
|
|
615
|
+
GSS_SCALE zoom factor.
|
|
616
|
+
|
|
617
|
+
Returns
|
|
618
|
+
-------
|
|
619
|
+
float
|
|
620
|
+
The value in the target unit.
|
|
621
|
+
"""
|
|
622
|
+
result = value
|
|
623
|
+
|
|
624
|
+
if unit == "npc":
|
|
625
|
+
# unit.c:1237 result/(thisCM/2.54)
|
|
626
|
+
this_inches = this_cm / 2.54
|
|
627
|
+
if this_inches < 1e-10:
|
|
628
|
+
if result != 0:
|
|
629
|
+
raise ValueError("Viewport has zero dimension(s)")
|
|
630
|
+
return 0.0
|
|
631
|
+
result = result / this_inches
|
|
632
|
+
elif unit == "inches":
|
|
633
|
+
pass # unit.c:1243
|
|
634
|
+
elif unit == "cm":
|
|
635
|
+
result = result * 2.54 # unit.c:1240
|
|
636
|
+
elif unit == "mm":
|
|
637
|
+
result = result * 25.4 # unit.c:1270
|
|
638
|
+
elif unit == "points":
|
|
639
|
+
result = result * 72.27 # unit.c:1275
|
|
640
|
+
elif unit == "picas":
|
|
641
|
+
result = result / 12.0 * 72.27 # unit.c:1278
|
|
642
|
+
elif unit == "bigpts":
|
|
643
|
+
result = result * 72.0 # unit.c:1281
|
|
644
|
+
elif unit == "dida":
|
|
645
|
+
result = result / 1238.0 * 1157.0 * 72.27 # unit.c:1284
|
|
646
|
+
elif unit == "cicero":
|
|
647
|
+
result = result / 1238.0 * 1157.0 * 72.27 / 12.0 # unit.c:1287
|
|
648
|
+
elif unit == "scaledpts":
|
|
649
|
+
result = result * 65536.0 * 72.27 # unit.c:1290
|
|
650
|
+
elif unit == "char":
|
|
651
|
+
# unit.c:1253 (result*72)/(gc->ps*gc->cex)
|
|
652
|
+
ps_cex = gc_fontsize * gc_cex
|
|
653
|
+
if ps_cex < 1e-10:
|
|
654
|
+
return 0.0
|
|
655
|
+
result = (result * 72.0) / ps_cex
|
|
656
|
+
elif unit == "lines":
|
|
657
|
+
# unit.c:1256 (result*72)/(gc->ps*gc->cex*gc->lineheight)
|
|
658
|
+
ps_cex_lh = gc_fontsize * gc_cex * gc_lineheight
|
|
659
|
+
if ps_cex_lh < 1e-10:
|
|
660
|
+
return 0.0
|
|
661
|
+
result = (result * 72.0) / ps_cex_lh
|
|
662
|
+
elif unit == "snpc":
|
|
663
|
+
# unit.c:1258-1268
|
|
664
|
+
if this_cm < 1e-6 or other_cm < 1e-6:
|
|
665
|
+
if result != 0:
|
|
666
|
+
raise ValueError("Viewport has zero dimension(s)")
|
|
667
|
+
return 0.0
|
|
668
|
+
min_inches = min(this_cm, other_cm) / 2.54
|
|
669
|
+
result = result / min_inches
|
|
670
|
+
else:
|
|
671
|
+
raise ValueError(f"Cannot convert from inches to unit {unit!r}")
|
|
672
|
+
|
|
673
|
+
# For physical units, reverse the GSS_SCALE (unit.c:1313-1331)
|
|
674
|
+
_PHYSICAL_UNITS = {
|
|
675
|
+
"inches", "cm", "mm", "points", "picas",
|
|
676
|
+
"bigpts", "dida", "cicero", "scaledpts",
|
|
677
|
+
}
|
|
678
|
+
if unit in _PHYSICAL_UNITS and scale != 0:
|
|
679
|
+
result = result / scale
|
|
680
|
+
|
|
681
|
+
return result
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
def _transform_xy_from_inches(
|
|
685
|
+
location_inches: float,
|
|
686
|
+
unit: str,
|
|
687
|
+
scalemin: float,
|
|
688
|
+
scalemax: float,
|
|
689
|
+
gc_fontsize: float,
|
|
690
|
+
gc_cex: float,
|
|
691
|
+
gc_lineheight: float,
|
|
692
|
+
this_cm: float,
|
|
693
|
+
other_cm: float,
|
|
694
|
+
scale: float = 1.0,
|
|
695
|
+
) -> float:
|
|
696
|
+
"""Convert a location in inches to the target unit.
|
|
697
|
+
|
|
698
|
+
Port of R ``unit.c:1348-1377 transformXYFromINCHES()``.
|
|
699
|
+
Handles the special NATIVE case (scale mapping).
|
|
700
|
+
"""
|
|
701
|
+
if unit == "native":
|
|
702
|
+
this_inches = this_cm / 2.54
|
|
703
|
+
if this_inches < 1e-10:
|
|
704
|
+
if location_inches != 0:
|
|
705
|
+
raise ValueError("Viewport has zero dimension(s)")
|
|
706
|
+
return 0.0
|
|
707
|
+
# unit.c:1369 scalemin + (result/(thisCM/2.54))*(scalemax - scalemin)
|
|
708
|
+
return scalemin + (location_inches / this_inches) * (scalemax - scalemin)
|
|
709
|
+
return _transform_from_inches(
|
|
710
|
+
location_inches, unit,
|
|
711
|
+
gc_fontsize, gc_cex, gc_lineheight,
|
|
712
|
+
this_cm, other_cm, scale,
|
|
713
|
+
)
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
def _transform_wh_from_inches(
|
|
717
|
+
dimension_inches: float,
|
|
718
|
+
unit: str,
|
|
719
|
+
scalemin: float,
|
|
720
|
+
scalemax: float,
|
|
721
|
+
gc_fontsize: float,
|
|
722
|
+
gc_cex: float,
|
|
723
|
+
gc_lineheight: float,
|
|
724
|
+
this_cm: float,
|
|
725
|
+
other_cm: float,
|
|
726
|
+
scale: float = 1.0,
|
|
727
|
+
) -> float:
|
|
728
|
+
"""Convert a dimension in inches to the target unit.
|
|
729
|
+
|
|
730
|
+
Port of R ``unit.c:1379-1408 transformWidthHeightFromINCHES()``.
|
|
731
|
+
Handles the special NATIVE case (dimension = range fraction).
|
|
732
|
+
"""
|
|
733
|
+
if unit == "native":
|
|
734
|
+
this_inches = this_cm / 2.54
|
|
735
|
+
if this_inches < 1e-10:
|
|
736
|
+
if dimension_inches != 0:
|
|
737
|
+
raise ValueError("Viewport has zero dimension(s)")
|
|
738
|
+
return 0.0
|
|
739
|
+
# unit.c:1400 (result/(thisCM/2.54))*(scalemax - scalemin)
|
|
740
|
+
return (dimension_inches / this_inches) * (scalemax - scalemin)
|
|
741
|
+
return _transform_from_inches(
|
|
742
|
+
dimension_inches, unit,
|
|
743
|
+
gc_fontsize, gc_cex, gc_lineheight,
|
|
744
|
+
this_cm, other_cm, scale,
|
|
745
|
+
)
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
def _transform_xy_to_npc(
|
|
749
|
+
value: float, from_unit: str,
|
|
750
|
+
scalemin: float, scalemax: float,
|
|
751
|
+
) -> float:
|
|
752
|
+
"""Relative unit to NPC -- port of ``unit.c:1418-1431 transformXYtoNPC()``.
|
|
753
|
+
|
|
754
|
+
Used when viewport has zero width/height to avoid divide-by-zero.
|
|
755
|
+
"""
|
|
756
|
+
if from_unit == "npc":
|
|
757
|
+
return value
|
|
758
|
+
if from_unit == "native":
|
|
759
|
+
srange = scalemax - scalemin
|
|
760
|
+
if srange == 0:
|
|
761
|
+
return 0.0
|
|
762
|
+
return (value - scalemin) / srange
|
|
763
|
+
raise ValueError(f"Cannot convert {from_unit!r} to NPC (zero-dimension special case)")
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
def _transform_wh_to_npc(
|
|
767
|
+
value: float, from_unit: str,
|
|
768
|
+
scalemin: float, scalemax: float,
|
|
769
|
+
) -> float:
|
|
770
|
+
"""Relative dimension to NPC -- port of ``unit.c:1433-1446 transformWHtoNPC()``."""
|
|
771
|
+
if from_unit == "npc":
|
|
772
|
+
return value
|
|
773
|
+
if from_unit == "native":
|
|
774
|
+
srange = scalemax - scalemin
|
|
775
|
+
if srange == 0:
|
|
776
|
+
return 0.0
|
|
777
|
+
return value / srange
|
|
778
|
+
raise ValueError(f"Cannot convert {from_unit!r} to NPC (zero-dimension special case)")
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
def _transform_xy_from_npc(
|
|
782
|
+
value: float, to_unit: str,
|
|
783
|
+
scalemin: float, scalemax: float,
|
|
784
|
+
) -> float:
|
|
785
|
+
"""NPC to relative unit -- port of ``unit.c:1448-1461 transformXYfromNPC()``."""
|
|
786
|
+
if to_unit == "npc":
|
|
787
|
+
return value
|
|
788
|
+
if to_unit == "native":
|
|
789
|
+
return scalemin + value * (scalemax - scalemin)
|
|
790
|
+
raise ValueError(f"Cannot convert NPC to {to_unit!r} (zero-dimension special case)")
|
|
791
|
+
|
|
792
|
+
|
|
793
|
+
def _transform_wh_from_npc(
|
|
794
|
+
value: float, to_unit: str,
|
|
795
|
+
scalemin: float, scalemax: float,
|
|
796
|
+
) -> float:
|
|
797
|
+
"""NPC to relative dimension -- port of ``unit.c:1463-1475 transformWHfromNPC()``."""
|
|
798
|
+
if to_unit == "npc":
|
|
799
|
+
return value
|
|
800
|
+
if to_unit == "native":
|
|
801
|
+
return value * (scalemax - scalemin)
|
|
802
|
+
raise ValueError(f"Cannot convert NPC to {to_unit!r} (zero-dimension special case)")
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
# ============================================================================
|
|
806
|
+
# calcViewportTransform -- port of viewport.c:214-382
|
|
807
|
+
# ============================================================================
|
|
808
|
+
|
|
809
|
+
def calc_viewport_transform(
|
|
810
|
+
vp: Any,
|
|
811
|
+
parent_transform: np.ndarray,
|
|
812
|
+
parent_width_cm: float,
|
|
813
|
+
parent_height_cm: float,
|
|
814
|
+
parent_angle: float,
|
|
815
|
+
parent_context: ViewportContext,
|
|
816
|
+
gc_fontsize: float = 10.0,
|
|
817
|
+
gc_cex: float = 1.0,
|
|
818
|
+
gc_lineheight: float = 1.2,
|
|
819
|
+
str_metric_fn: Any = None,
|
|
820
|
+
grob_metric_fn: Any = None,
|
|
821
|
+
) -> ViewportTransformResult:
|
|
822
|
+
"""Compute the viewport's 3×3 transform matrix.
|
|
823
|
+
|
|
824
|
+
This is the core function that mirrors R's ``calcViewportTransform``
|
|
825
|
+
(viewport.c:214-382). It converts the viewport's position and
|
|
826
|
+
dimensions to inches, applies justification + rotation, and
|
|
827
|
+
combines with the parent's transform.
|
|
828
|
+
|
|
829
|
+
Parameters
|
|
830
|
+
----------
|
|
831
|
+
vp : Viewport
|
|
832
|
+
The viewport being pushed.
|
|
833
|
+
parent_transform : ndarray (3,3)
|
|
834
|
+
The parent viewport's accumulated transform.
|
|
835
|
+
parent_width_cm, parent_height_cm : float
|
|
836
|
+
Parent viewport dimensions in CM.
|
|
837
|
+
parent_angle : float
|
|
838
|
+
Parent's accumulated rotation angle in degrees.
|
|
839
|
+
parent_context : ViewportContext
|
|
840
|
+
Parent's xscale/yscale.
|
|
841
|
+
gc_fontsize, gc_cex, gc_lineheight : float
|
|
842
|
+
Parent gpar font metrics.
|
|
843
|
+
str_metric_fn, grob_metric_fn : callable or None
|
|
844
|
+
Metric query callbacks.
|
|
845
|
+
|
|
846
|
+
Returns
|
|
847
|
+
-------
|
|
848
|
+
ViewportTransformResult
|
|
849
|
+
Contains width_cm, height_cm, rotation_angle, transform, vpc.
|
|
850
|
+
"""
|
|
851
|
+
# -- viewport.c:308-313: convert vp location to INCHES --
|
|
852
|
+
vp_x_unit = getattr(vp, "_x", None)
|
|
853
|
+
vp_y_unit = getattr(vp, "_y", None)
|
|
854
|
+
vp_w_unit = getattr(vp, "_width", None)
|
|
855
|
+
vp_h_unit = getattr(vp, "_height", None)
|
|
856
|
+
|
|
857
|
+
# Default units from viewport
|
|
858
|
+
from ._units import Unit
|
|
859
|
+
if vp_x_unit is None:
|
|
860
|
+
vp_x_unit = Unit(0.5, "npc")
|
|
861
|
+
if vp_y_unit is None:
|
|
862
|
+
vp_y_unit = Unit(0.5, "npc")
|
|
863
|
+
if vp_w_unit is None:
|
|
864
|
+
vp_w_unit = Unit(1.0, "npc")
|
|
865
|
+
if vp_h_unit is None:
|
|
866
|
+
vp_h_unit = Unit(1.0, "npc")
|
|
867
|
+
|
|
868
|
+
x_inches = transform_x_to_inches(
|
|
869
|
+
vp_x_unit, 0, parent_context,
|
|
870
|
+
gc_fontsize, gc_cex, gc_lineheight,
|
|
871
|
+
parent_width_cm, parent_height_cm,
|
|
872
|
+
str_metric_fn, grob_metric_fn,
|
|
873
|
+
)
|
|
874
|
+
y_inches = transform_y_to_inches(
|
|
875
|
+
vp_y_unit, 0, parent_context,
|
|
876
|
+
gc_fontsize, gc_cex, gc_lineheight,
|
|
877
|
+
parent_width_cm, parent_height_cm,
|
|
878
|
+
str_metric_fn, grob_metric_fn,
|
|
879
|
+
)
|
|
880
|
+
|
|
881
|
+
# -- viewport.c:317-324: convert width/height to CM --
|
|
882
|
+
# Note: R stores these in CM, converting from inches * 2.54
|
|
883
|
+
vp_width_cm = transform_width_to_inches(
|
|
884
|
+
vp_w_unit, 0, parent_context,
|
|
885
|
+
gc_fontsize, gc_cex, gc_lineheight,
|
|
886
|
+
parent_width_cm, parent_height_cm,
|
|
887
|
+
str_metric_fn, grob_metric_fn,
|
|
888
|
+
) * 2.54
|
|
889
|
+
|
|
890
|
+
vp_height_cm = transform_height_to_inches(
|
|
891
|
+
vp_h_unit, 0, parent_context,
|
|
892
|
+
gc_fontsize, gc_cex, gc_lineheight,
|
|
893
|
+
parent_width_cm, parent_height_cm,
|
|
894
|
+
str_metric_fn, grob_metric_fn,
|
|
895
|
+
) * 2.54
|
|
896
|
+
|
|
897
|
+
# Non-finite check (viewport.c:327-331)
|
|
898
|
+
if (not math.isfinite(x_inches) or not math.isfinite(y_inches)
|
|
899
|
+
or not math.isfinite(vp_width_cm) or not math.isfinite(vp_height_cm)):
|
|
900
|
+
raise ValueError("non-finite location and/or size for viewport")
|
|
901
|
+
|
|
902
|
+
# -- viewport.c:334-335: justification offsets --
|
|
903
|
+
just = getattr(vp, "_just", (0.5, 0.5))
|
|
904
|
+
if isinstance(just, (list, tuple)) and len(just) >= 2:
|
|
905
|
+
hjust, vjust = float(just[0]), float(just[1])
|
|
906
|
+
else:
|
|
907
|
+
hjust, vjust = 0.5, 0.5
|
|
908
|
+
|
|
909
|
+
# justification() returns offsets in CM, then we convert to inches
|
|
910
|
+
xadj, yadj = justification(vp_width_cm, vp_height_cm, hjust, vjust)
|
|
911
|
+
|
|
912
|
+
# -- viewport.c:341-355: build transform chain --
|
|
913
|
+
# thisLocation: translate to viewport position (in inches)
|
|
914
|
+
this_location = translation(x_inches, y_inches)
|
|
915
|
+
|
|
916
|
+
# thisRotation: viewport rotation
|
|
917
|
+
vp_angle = float(getattr(vp, "_angle", 0))
|
|
918
|
+
if vp_angle != 0:
|
|
919
|
+
this_rotation = rotation(vp_angle)
|
|
920
|
+
else:
|
|
921
|
+
this_rotation = identity()
|
|
922
|
+
|
|
923
|
+
# thisJustification: translate by justification offsets (CM -> inches)
|
|
924
|
+
this_justification = translation(xadj / 2.54, yadj / 2.54)
|
|
925
|
+
|
|
926
|
+
# viewport.c:349: Position relative to origin of rotation THEN rotate
|
|
927
|
+
temp_transform = multiply(this_justification, this_rotation)
|
|
928
|
+
|
|
929
|
+
# viewport.c:352: Translate to bottom-left corner
|
|
930
|
+
this_transform = multiply(temp_transform, this_location)
|
|
931
|
+
|
|
932
|
+
# viewport.c:355: Combine with parent's transform
|
|
933
|
+
transform = multiply(this_transform, parent_transform)
|
|
934
|
+
|
|
935
|
+
# viewport.c:358: Sum up the rotation angles
|
|
936
|
+
rotation_angle = parent_angle + vp_angle
|
|
937
|
+
|
|
938
|
+
# Build viewport context for children
|
|
939
|
+
xscale = getattr(vp, "_xscale", [0.0, 1.0])
|
|
940
|
+
yscale = getattr(vp, "_yscale", [0.0, 1.0])
|
|
941
|
+
vpc = ViewportContext(
|
|
942
|
+
xscale=(float(xscale[0]), float(xscale[1])),
|
|
943
|
+
yscale=(float(yscale[0]), float(yscale[1])),
|
|
944
|
+
)
|
|
945
|
+
|
|
946
|
+
return ViewportTransformResult(
|
|
947
|
+
width_cm=vp_width_cm,
|
|
948
|
+
height_cm=vp_height_cm,
|
|
949
|
+
rotation_angle=rotation_angle,
|
|
950
|
+
transform=transform,
|
|
951
|
+
vpc=vpc,
|
|
952
|
+
)
|
|
953
|
+
|
|
954
|
+
|
|
955
|
+
def calc_root_transform(
|
|
956
|
+
device_width_cm: float,
|
|
957
|
+
device_height_cm: float,
|
|
958
|
+
) -> ViewportTransformResult:
|
|
959
|
+
"""Compute the root (device-level) viewport transform.
|
|
960
|
+
|
|
961
|
+
Port of ``viewport.c:233-260``: when the parent is NULL (top-level
|
|
962
|
+
viewport), the parent is the device itself.
|
|
963
|
+
"""
|
|
964
|
+
return ViewportTransformResult(
|
|
965
|
+
width_cm=device_width_cm,
|
|
966
|
+
height_cm=device_height_cm,
|
|
967
|
+
rotation_angle=0.0,
|
|
968
|
+
transform=identity(),
|
|
969
|
+
vpc=ViewportContext(xscale=(0.0, 1.0), yscale=(0.0, 1.0)),
|
|
970
|
+
)
|