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/_units.py
ADDED
|
@@ -0,0 +1,1924 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit system for grid_py -- Python port of R's grid unit infrastructure.
|
|
3
|
+
|
|
4
|
+
This module provides the fundamental ``Unit`` class and associated helper
|
|
5
|
+
functions that mirror R's ``grid::unit()`` family. A ``Unit`` stores one or
|
|
6
|
+
more scalar values together with their unit types and optional reference data
|
|
7
|
+
(used by contextual units such as ``"strwidth"`` or ``"grobwidth"``).
|
|
8
|
+
|
|
9
|
+
Arithmetic on units follows R semantics:
|
|
10
|
+
|
|
11
|
+
* ``unit + unit`` produces a compound *sum* unit.
|
|
12
|
+
* ``scalar * unit`` (or ``unit * scalar``) scales the numeric values.
|
|
13
|
+
* ``-unit`` negates the numeric values.
|
|
14
|
+
* ``unit / scalar`` divides the numeric values.
|
|
15
|
+
|
|
16
|
+
Absolute-unit conversions (cm, inches, mm, points, picas, ...) are carried
|
|
17
|
+
out eagerly. Context-dependent conversions (npc, native, lines, ...) are
|
|
18
|
+
deferred -- the unit is returned unchanged when no viewport context is
|
|
19
|
+
available.
|
|
20
|
+
|
|
21
|
+
Notes
|
|
22
|
+
-----
|
|
23
|
+
The module is intentionally self-contained so that it can be imported very
|
|
24
|
+
early during package initialisation without circular-dependency issues.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import copy
|
|
30
|
+
import math
|
|
31
|
+
from numbers import Number
|
|
32
|
+
from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
|
|
33
|
+
|
|
34
|
+
import numpy as np
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"Unit",
|
|
38
|
+
"is_unit",
|
|
39
|
+
"unit_type",
|
|
40
|
+
"unit_c",
|
|
41
|
+
"unit_length",
|
|
42
|
+
"unit_pmax",
|
|
43
|
+
"unit_pmin",
|
|
44
|
+
"unit_psum",
|
|
45
|
+
"unit_rep",
|
|
46
|
+
"string_width",
|
|
47
|
+
"string_height",
|
|
48
|
+
"string_ascent",
|
|
49
|
+
"string_descent",
|
|
50
|
+
"absolute_size",
|
|
51
|
+
"convert_unit",
|
|
52
|
+
"convert_x",
|
|
53
|
+
"convert_y",
|
|
54
|
+
"convert_width",
|
|
55
|
+
"convert_height",
|
|
56
|
+
"device_loc",
|
|
57
|
+
"device_dim",
|
|
58
|
+
"convert_theta",
|
|
59
|
+
"unit_summary_min",
|
|
60
|
+
"unit_summary_max",
|
|
61
|
+
"unit_summary_sum",
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
# ---------------------------------------------------------------------------
|
|
65
|
+
# Valid unit type strings
|
|
66
|
+
# ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
VALID_UNIT_TYPES: Tuple[str, ...] = (
|
|
69
|
+
"npc",
|
|
70
|
+
"cm",
|
|
71
|
+
"inches",
|
|
72
|
+
"mm",
|
|
73
|
+
"points",
|
|
74
|
+
"picas",
|
|
75
|
+
"bigpts",
|
|
76
|
+
"dida",
|
|
77
|
+
"cicero",
|
|
78
|
+
"scaledpts",
|
|
79
|
+
"lines",
|
|
80
|
+
"char",
|
|
81
|
+
"native",
|
|
82
|
+
"null",
|
|
83
|
+
"snpc",
|
|
84
|
+
"strwidth",
|
|
85
|
+
"strheight",
|
|
86
|
+
"strdescent",
|
|
87
|
+
"strascent",
|
|
88
|
+
"vplayoutwidth",
|
|
89
|
+
"vplayoutheight",
|
|
90
|
+
"grobx",
|
|
91
|
+
"groby",
|
|
92
|
+
"grobwidth",
|
|
93
|
+
"grobheight",
|
|
94
|
+
"grobascent",
|
|
95
|
+
"grobdescent",
|
|
96
|
+
"mylines",
|
|
97
|
+
"mychar",
|
|
98
|
+
"mystrwidth",
|
|
99
|
+
"mystrheight",
|
|
100
|
+
"sum",
|
|
101
|
+
"min",
|
|
102
|
+
"max",
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# Convenient lookup set for O(1) membership tests
|
|
106
|
+
_VALID_UNIT_SET: frozenset = frozenset(VALID_UNIT_TYPES)
|
|
107
|
+
|
|
108
|
+
# Aliases accepted on input (mapped to canonical names)
|
|
109
|
+
_UNIT_ALIASES: Dict[str, str] = {
|
|
110
|
+
"in": "inches",
|
|
111
|
+
"inch": "inches",
|
|
112
|
+
"centimetre": "cm",
|
|
113
|
+
"centimetres": "cm",
|
|
114
|
+
"centimeter": "cm",
|
|
115
|
+
"centimeters": "cm",
|
|
116
|
+
"millimetre": "mm",
|
|
117
|
+
"millimetres": "mm",
|
|
118
|
+
"millimeter": "mm",
|
|
119
|
+
"millimeters": "mm",
|
|
120
|
+
"point": "points",
|
|
121
|
+
"pt": "points",
|
|
122
|
+
"line": "lines",
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
# ---------------------------------------------------------------------------
|
|
126
|
+
# Absolute-unit conversion factors (everything relative to inches)
|
|
127
|
+
# ---------------------------------------------------------------------------
|
|
128
|
+
# Reference:
|
|
129
|
+
# 1 inch = 2.54 cm = 25.4 mm = 72.27 pt (TeX point)
|
|
130
|
+
# 1 pica = 12 pt
|
|
131
|
+
# 1 bigpt = 1/72 inch (PostScript point)
|
|
132
|
+
# 1 dida = 1238/1157 pt
|
|
133
|
+
# 1 cicero = 12 dida
|
|
134
|
+
# 1 scaledpt = 1/65536 pt
|
|
135
|
+
|
|
136
|
+
_INCHES_PER: Dict[str, float] = {
|
|
137
|
+
"inches": 1.0,
|
|
138
|
+
"cm": 1.0 / 2.54,
|
|
139
|
+
"mm": 1.0 / 25.4,
|
|
140
|
+
"points": 1.0 / 72.27,
|
|
141
|
+
"picas": 12.0 / 72.27,
|
|
142
|
+
"bigpts": 1.0 / 72.0,
|
|
143
|
+
"dida": (1238.0 / 1157.0) / 72.27,
|
|
144
|
+
"cicero": 12.0 * (1238.0 / 1157.0) / 72.27,
|
|
145
|
+
"scaledpts": 1.0 / (72.27 * 65536.0),
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
# Set of unit types that can be converted without a viewport context
|
|
149
|
+
_ABSOLUTE_UNIT_TYPES: frozenset = frozenset(_INCHES_PER.keys())
|
|
150
|
+
|
|
151
|
+
# Unit types resolved by measuring a string or querying a grob
|
|
152
|
+
_STR_METRIC_TYPES: frozenset = frozenset(
|
|
153
|
+
{"strwidth", "strheight", "strascent", "strdescent"}
|
|
154
|
+
)
|
|
155
|
+
_GROB_METRIC_TYPES: frozenset = frozenset(
|
|
156
|
+
{"grobwidth", "grobheight", "grobascent", "grobdescent"}
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _eval_str_metric(unit_type: str, data: Any, scale: float = 1.0) -> float:
|
|
161
|
+
"""Evaluate a string-metric unit to an inch value.
|
|
162
|
+
|
|
163
|
+
Mirrors R's ``GEStrWidth`` / ``GEStrHeight`` (src/main/engine.c), which
|
|
164
|
+
back ``stringWidth`` / ``stringHeight`` for text units:
|
|
165
|
+
|
|
166
|
+
- split on ``\\n`` into lines,
|
|
167
|
+
- width = max(per-line widths),
|
|
168
|
+
- height = ink(first line) + (n - 1) × cex × lineheight × fontsize × 1.2 / 72
|
|
169
|
+
|
|
170
|
+
Uses the current viewport's gpar (fontsize, cex, lineheight) to match
|
|
171
|
+
R's behaviour where ``stringWidth`` inherits typography from the
|
|
172
|
+
enclosing gpar context.
|
|
173
|
+
|
|
174
|
+
Uses a lazy import of :func:`._size.calc_string_metric` to avoid
|
|
175
|
+
circular dependencies (``_size`` imports ``Unit`` from this module).
|
|
176
|
+
|
|
177
|
+
Parameters
|
|
178
|
+
----------
|
|
179
|
+
unit_type : str
|
|
180
|
+
One of ``"strwidth"``, ``"strheight"``, ``"strascent"``,
|
|
181
|
+
``"strdescent"``.
|
|
182
|
+
data : object
|
|
183
|
+
The string stored as auxiliary data in the unit.
|
|
184
|
+
scale : float
|
|
185
|
+
Multiplicative factor (the unit's numeric value).
|
|
186
|
+
|
|
187
|
+
Returns
|
|
188
|
+
-------
|
|
189
|
+
float
|
|
190
|
+
Measurement in inches, scaled by *scale*.
|
|
191
|
+
"""
|
|
192
|
+
from ._size import calc_string_metric # lazy – avoids circular import
|
|
193
|
+
|
|
194
|
+
text = str(data) if data is not None else ""
|
|
195
|
+
|
|
196
|
+
# Inherit fontsize / cex / lineheight from the current viewport gpar
|
|
197
|
+
# stack, matching R's ``stringWidth`` which uses the enclosing gpar.
|
|
198
|
+
try:
|
|
199
|
+
from ._gpar import get_gpar
|
|
200
|
+
gp = get_gpar()
|
|
201
|
+
except Exception:
|
|
202
|
+
gp = None
|
|
203
|
+
|
|
204
|
+
fontsize = 12.0
|
|
205
|
+
cex = 1.0
|
|
206
|
+
lineheight = 1.2
|
|
207
|
+
if gp is not None:
|
|
208
|
+
fs = gp.get("fontsize", None)
|
|
209
|
+
if fs is not None:
|
|
210
|
+
fontsize = float(fs[0] if isinstance(fs, (list, tuple)) else fs)
|
|
211
|
+
cx = gp.get("cex", None)
|
|
212
|
+
if cx is not None:
|
|
213
|
+
cex = float(cx[0] if isinstance(cx, (list, tuple)) else cx)
|
|
214
|
+
lh = gp.get("lineheight", None)
|
|
215
|
+
if lh is not None:
|
|
216
|
+
lineheight = float(lh[0] if isinstance(lh, (list, tuple)) else lh)
|
|
217
|
+
|
|
218
|
+
lines = text.split("\n") if text else [""]
|
|
219
|
+
n = len(lines)
|
|
220
|
+
m0 = calc_string_metric(lines[0], gp=gp)
|
|
221
|
+
|
|
222
|
+
if unit_type == "strwidth":
|
|
223
|
+
w = max(calc_string_metric(ln, gp=gp)["width"] for ln in lines)
|
|
224
|
+
return w * scale
|
|
225
|
+
elif unit_type == "strheight":
|
|
226
|
+
ink_first = m0["ascent"] + m0["descent"]
|
|
227
|
+
inter_line_gap = cex * lineheight * fontsize * 1.2 / 72.0
|
|
228
|
+
return (ink_first + (n - 1) * inter_line_gap) * scale
|
|
229
|
+
elif unit_type == "strascent":
|
|
230
|
+
return m0["ascent"] * scale
|
|
231
|
+
elif unit_type == "strdescent":
|
|
232
|
+
return m0["descent"] * scale
|
|
233
|
+
return 0.0 # pragma: no cover
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _eval_grob_metric(unit_type: str, grob: Any) -> Optional["Unit"]:
|
|
237
|
+
"""Evaluate a grob-metric unit by calling the appropriate detail dispatcher.
|
|
238
|
+
|
|
239
|
+
Uses a lazy import of dispatchers from :mod:`._size` to avoid
|
|
240
|
+
circular dependencies.
|
|
241
|
+
|
|
242
|
+
Parameters
|
|
243
|
+
----------
|
|
244
|
+
unit_type : str
|
|
245
|
+
One of ``"grobwidth"``, ``"grobheight"``, ``"grobascent"``,
|
|
246
|
+
``"grobdescent"``.
|
|
247
|
+
grob : object
|
|
248
|
+
The grob stored as auxiliary data in the unit.
|
|
249
|
+
|
|
250
|
+
Returns
|
|
251
|
+
-------
|
|
252
|
+
Unit or None
|
|
253
|
+
The measured dimension as a :class:`Unit`, or ``None`` if the
|
|
254
|
+
grob is ``None``.
|
|
255
|
+
"""
|
|
256
|
+
from ._size import ( # lazy – avoids circular import
|
|
257
|
+
width_details,
|
|
258
|
+
height_details,
|
|
259
|
+
ascent_details,
|
|
260
|
+
descent_details,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
if grob is None:
|
|
264
|
+
return None
|
|
265
|
+
_dispatch = {
|
|
266
|
+
"grobwidth": width_details,
|
|
267
|
+
"grobheight": height_details,
|
|
268
|
+
"grobascent": ascent_details,
|
|
269
|
+
"grobdescent": descent_details,
|
|
270
|
+
}
|
|
271
|
+
return _dispatch[unit_type](grob)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _try_resolve_with_renderer(
|
|
275
|
+
x: "Unit",
|
|
276
|
+
i: int,
|
|
277
|
+
src_unit: str,
|
|
278
|
+
target: str,
|
|
279
|
+
axis: str,
|
|
280
|
+
type_: str,
|
|
281
|
+
) -> Optional[float]:
|
|
282
|
+
"""Resolve a context-dependent unit via the active renderer.
|
|
283
|
+
|
|
284
|
+
Implements R's ``L_convert`` two-stage pipeline (grid.c:1384-1575):
|
|
285
|
+
Stage 1: any unit → inches (via renderer._resolve_to_inches_idx)
|
|
286
|
+
Stage 2: inches → target (via _vp_calc inverse transforms)
|
|
287
|
+
|
|
288
|
+
Returns the converted value in *target* units, or ``None`` if no
|
|
289
|
+
renderer is available.
|
|
290
|
+
"""
|
|
291
|
+
from ._state import get_state
|
|
292
|
+
from ._vp_calc import (
|
|
293
|
+
_transform_xy_from_inches,
|
|
294
|
+
_transform_wh_from_inches,
|
|
295
|
+
_transform_xy_to_npc,
|
|
296
|
+
_transform_wh_to_npc,
|
|
297
|
+
_transform_xy_from_npc,
|
|
298
|
+
_transform_wh_from_npc,
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
state = get_state()
|
|
302
|
+
renderer = state.get_renderer()
|
|
303
|
+
|
|
304
|
+
if renderer is None or not hasattr(renderer, "_resolve_to_inches_idx"):
|
|
305
|
+
return None
|
|
306
|
+
|
|
307
|
+
# Build a single-element Unit for the source
|
|
308
|
+
elem = Unit(x._values[i], src_unit, data=x._data[i])
|
|
309
|
+
is_dim = type_ in ("dimension",)
|
|
310
|
+
|
|
311
|
+
# Get viewport context from renderer
|
|
312
|
+
vtr = renderer._vp_transform_stack[-1]
|
|
313
|
+
vpc = vtr.vpc
|
|
314
|
+
|
|
315
|
+
# Determine axis parameters (R grid.c:1426-1427)
|
|
316
|
+
# axis encoding: 0=x-loc, 1=y-loc, 2=x-dim, 3=y-dim
|
|
317
|
+
if axis == "x":
|
|
318
|
+
scalemin, scalemax = vpc.xscalemin, vpc.xscalemax
|
|
319
|
+
this_cm = vtr.width_cm
|
|
320
|
+
other_cm = vtr.height_cm
|
|
321
|
+
else:
|
|
322
|
+
scalemin, scalemax = vpc.yscalemin, vpc.yscalemax
|
|
323
|
+
this_cm = vtr.height_cm
|
|
324
|
+
other_cm = vtr.width_cm
|
|
325
|
+
|
|
326
|
+
# R grid.c:1438-1444 -- special case: relative-to-relative with zero dim
|
|
327
|
+
from_is_relative = src_unit in ("native", "npc")
|
|
328
|
+
to_is_relative = target in ("native", "npc")
|
|
329
|
+
rel_convert = (from_is_relative and to_is_relative
|
|
330
|
+
and this_cm < 1e-6)
|
|
331
|
+
|
|
332
|
+
# Stage 1: convert source → inches (or NPC for relConvert)
|
|
333
|
+
if rel_convert:
|
|
334
|
+
if is_dim:
|
|
335
|
+
stage1 = _transform_wh_to_npc(
|
|
336
|
+
float(x._values[i]), src_unit, scalemin, scalemax)
|
|
337
|
+
else:
|
|
338
|
+
stage1 = _transform_xy_to_npc(
|
|
339
|
+
float(x._values[i]), src_unit, scalemin, scalemax)
|
|
340
|
+
else:
|
|
341
|
+
# Use renderer's full pipeline to get inches
|
|
342
|
+
gp = state.get_gpar()
|
|
343
|
+
stage1 = renderer._resolve_to_inches_idx(elem, 0, axis, is_dim, gp)
|
|
344
|
+
|
|
345
|
+
# Stage 2: inches (or NPC) → target unit
|
|
346
|
+
if rel_convert:
|
|
347
|
+
if is_dim:
|
|
348
|
+
return _transform_wh_from_npc(stage1, target, scalemin, scalemax)
|
|
349
|
+
else:
|
|
350
|
+
return _transform_xy_from_npc(stage1, target, scalemin, scalemax)
|
|
351
|
+
else:
|
|
352
|
+
fontsize, cex, lineheight = renderer._gpar_font_params(state.get_gpar())
|
|
353
|
+
scale = renderer._get_scale()
|
|
354
|
+
if is_dim:
|
|
355
|
+
return _transform_wh_from_inches(
|
|
356
|
+
stage1, target, scalemin, scalemax,
|
|
357
|
+
fontsize, cex, lineheight,
|
|
358
|
+
this_cm, other_cm, scale,
|
|
359
|
+
)
|
|
360
|
+
else:
|
|
361
|
+
return _transform_xy_from_inches(
|
|
362
|
+
stage1, target, scalemin, scalemax,
|
|
363
|
+
fontsize, cex, lineheight,
|
|
364
|
+
this_cm, other_cm, scale,
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _resolve_alias(unit_str: str) -> str:
|
|
369
|
+
"""Return the canonical unit-type string, resolving common aliases.
|
|
370
|
+
|
|
371
|
+
Parameters
|
|
372
|
+
----------
|
|
373
|
+
unit_str : str
|
|
374
|
+
Raw unit name (e.g. ``"in"``, ``"pt"``, ``"centimeters"``).
|
|
375
|
+
|
|
376
|
+
Returns
|
|
377
|
+
-------
|
|
378
|
+
str
|
|
379
|
+
Canonical unit name (e.g. ``"inches"``, ``"points"``, ``"cm"``).
|
|
380
|
+
|
|
381
|
+
Raises
|
|
382
|
+
------
|
|
383
|
+
ValueError
|
|
384
|
+
If *unit_str* is not a recognised unit type or alias.
|
|
385
|
+
"""
|
|
386
|
+
low = unit_str.strip().lower()
|
|
387
|
+
if low in _VALID_UNIT_SET:
|
|
388
|
+
return low
|
|
389
|
+
if low in _UNIT_ALIASES:
|
|
390
|
+
return _UNIT_ALIASES[low]
|
|
391
|
+
raise ValueError(
|
|
392
|
+
f"Unknown unit type {unit_str!r}. "
|
|
393
|
+
f"Valid types: {', '.join(sorted(VALID_UNIT_TYPES))}"
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
# ---------------------------------------------------------------------------
|
|
398
|
+
# The Unit class
|
|
399
|
+
# ---------------------------------------------------------------------------
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
class Unit:
|
|
403
|
+
"""Representation of one or more grid unit values.
|
|
404
|
+
|
|
405
|
+
A ``Unit`` bundles numeric *values* with their *unit types* and optional
|
|
406
|
+
*data* references (e.g. a string for ``"strwidth"`` or a grob for
|
|
407
|
+
``"grobwidth"``). It mirrors R's ``grid::unit`` objects.
|
|
408
|
+
|
|
409
|
+
Parameters
|
|
410
|
+
----------
|
|
411
|
+
x : float, int, Sequence[float], or np.ndarray
|
|
412
|
+
Numeric value(s). Scalars are promoted to length-1 arrays.
|
|
413
|
+
units : str or Sequence[str]
|
|
414
|
+
Unit type(s). A single string is recycled to match the length of *x*.
|
|
415
|
+
data : Any or Sequence[Any], optional
|
|
416
|
+
Auxiliary data attached to each element (used by contextual units
|
|
417
|
+
such as ``"strwidth"``). ``None`` entries are allowed.
|
|
418
|
+
|
|
419
|
+
Raises
|
|
420
|
+
------
|
|
421
|
+
ValueError
|
|
422
|
+
If *x* or *units* are empty, or if *units* contains an unknown type.
|
|
423
|
+
|
|
424
|
+
Examples
|
|
425
|
+
--------
|
|
426
|
+
>>> u = Unit(1, "cm")
|
|
427
|
+
>>> u
|
|
428
|
+
Unit([1.0], ['cm'])
|
|
429
|
+
>>> Unit([0.5, 1.0], ["npc", "cm"])
|
|
430
|
+
Unit([0.5, 1.0], ['npc', 'cm'])
|
|
431
|
+
"""
|
|
432
|
+
|
|
433
|
+
# ---- internal slots ----------------------------------------------------
|
|
434
|
+
__slots__ = ("_values", "_units", "_data", "_is_absolute")
|
|
435
|
+
|
|
436
|
+
# ------------------------------------------------------------------ init
|
|
437
|
+
def __init__(
|
|
438
|
+
self,
|
|
439
|
+
x: Union[float, int, Sequence[float], np.ndarray],
|
|
440
|
+
units: Union[str, Sequence[str]],
|
|
441
|
+
data: Optional[Union[Any, Sequence[Any]]] = None,
|
|
442
|
+
) -> None:
|
|
443
|
+
# If *x* is already a Unit, return a shallow copy (mirrors R behaviour
|
|
444
|
+
# where ``unit(u)`` simply returns *u*).
|
|
445
|
+
if isinstance(x, Unit):
|
|
446
|
+
self._values = x._values.copy()
|
|
447
|
+
self._units = list(x._units)
|
|
448
|
+
self._data = list(x._data)
|
|
449
|
+
self._is_absolute = x._is_absolute
|
|
450
|
+
return
|
|
451
|
+
|
|
452
|
+
# Coerce values --------------------------------------------------
|
|
453
|
+
if isinstance(x, np.ndarray):
|
|
454
|
+
vals = x.astype(np.float64).ravel()
|
|
455
|
+
elif isinstance(x, (list, tuple)):
|
|
456
|
+
vals = np.asarray(x, dtype=np.float64)
|
|
457
|
+
else:
|
|
458
|
+
vals = np.asarray([x], dtype=np.float64)
|
|
459
|
+
|
|
460
|
+
if vals.size == 0:
|
|
461
|
+
raise ValueError("'x' must have length > 0")
|
|
462
|
+
|
|
463
|
+
# Coerce units ---------------------------------------------------
|
|
464
|
+
if isinstance(units, str):
|
|
465
|
+
resolved = _resolve_alias(units)
|
|
466
|
+
unit_list = [resolved] * len(vals)
|
|
467
|
+
else:
|
|
468
|
+
unit_list = [_resolve_alias(u) for u in units]
|
|
469
|
+
if len(unit_list) == 0:
|
|
470
|
+
raise ValueError("'units' must have length > 0")
|
|
471
|
+
# Recycle to match length of vals
|
|
472
|
+
if len(unit_list) < len(vals):
|
|
473
|
+
reps = math.ceil(len(vals) / len(unit_list))
|
|
474
|
+
unit_list = (unit_list * reps)[: len(vals)]
|
|
475
|
+
elif len(unit_list) > len(vals):
|
|
476
|
+
# Recycle values to match units length
|
|
477
|
+
reps = math.ceil(len(unit_list) / len(vals))
|
|
478
|
+
vals = np.tile(vals, reps)[: len(unit_list)]
|
|
479
|
+
|
|
480
|
+
# Coerce data ----------------------------------------------------
|
|
481
|
+
if data is None:
|
|
482
|
+
data_list: List[Any] = [None] * len(unit_list)
|
|
483
|
+
elif isinstance(data, (list, tuple)):
|
|
484
|
+
data_list = list(data)
|
|
485
|
+
if len(data_list) < len(unit_list):
|
|
486
|
+
reps = math.ceil(len(unit_list) / max(len(data_list), 1))
|
|
487
|
+
data_list = (data_list * reps)[: len(unit_list)]
|
|
488
|
+
else:
|
|
489
|
+
data_list = [data] * len(unit_list)
|
|
490
|
+
|
|
491
|
+
self._values: np.ndarray = vals
|
|
492
|
+
self._units: List[str] = unit_list
|
|
493
|
+
self._data: List[Any] = data_list
|
|
494
|
+
self._is_absolute: bool = all(u in _ABSOLUTE_UNIT_TYPES for u in unit_list)
|
|
495
|
+
|
|
496
|
+
# ---------------------------------------------------------------- properties
|
|
497
|
+
@property
|
|
498
|
+
def values(self) -> np.ndarray:
|
|
499
|
+
"""Numeric values as a 1-D ``numpy.float64`` array."""
|
|
500
|
+
return self._values
|
|
501
|
+
|
|
502
|
+
@property
|
|
503
|
+
def units_list(self) -> List[str]:
|
|
504
|
+
"""List of unit-type strings (one per element)."""
|
|
505
|
+
return self._units
|
|
506
|
+
|
|
507
|
+
@property
|
|
508
|
+
def data(self) -> List[Any]:
|
|
509
|
+
"""Auxiliary data list (one entry per element; may contain ``None``)."""
|
|
510
|
+
return self._data
|
|
511
|
+
|
|
512
|
+
# ---------------------------------------------------------------- length
|
|
513
|
+
def __len__(self) -> int:
|
|
514
|
+
return len(self._values)
|
|
515
|
+
|
|
516
|
+
# ---------------------------------------------------------------- repr / str
|
|
517
|
+
def __repr__(self) -> str:
|
|
518
|
+
vals = [float(v) for v in self._values]
|
|
519
|
+
return f"Unit({vals}, {self._units})"
|
|
520
|
+
|
|
521
|
+
def __str__(self) -> str:
|
|
522
|
+
return self.as_character()
|
|
523
|
+
|
|
524
|
+
def as_character(self) -> str:
|
|
525
|
+
"""Return an R-compatible character representation.
|
|
526
|
+
|
|
527
|
+
Returns
|
|
528
|
+
-------
|
|
529
|
+
str
|
|
530
|
+
A string such as ``"1cm"`` or ``"0.5npc+1cm"`` when the unit
|
|
531
|
+
contains a compound *sum* / *min* / *max* type.
|
|
532
|
+
|
|
533
|
+
Examples
|
|
534
|
+
--------
|
|
535
|
+
>>> Unit(2.5, "cm").as_character()
|
|
536
|
+
'2.5cm'
|
|
537
|
+
"""
|
|
538
|
+
parts: List[str] = []
|
|
539
|
+
for i in range(len(self._values)):
|
|
540
|
+
parts.append(self._desc_element(i))
|
|
541
|
+
return ", ".join(parts)
|
|
542
|
+
|
|
543
|
+
def _desc_element(self, idx: int) -> str:
|
|
544
|
+
"""Format a single element as an R-style string."""
|
|
545
|
+
val = self._values[idx]
|
|
546
|
+
utype = self._units[idx]
|
|
547
|
+
d = self._data[idx]
|
|
548
|
+
|
|
549
|
+
if utype in ("sum", "min", "max"):
|
|
550
|
+
# Compound unit -- data should be a Unit
|
|
551
|
+
if isinstance(d, Unit):
|
|
552
|
+
inner = ", ".join(d._desc_element(j) for j in range(len(d)))
|
|
553
|
+
prefix = "" if val == 1.0 else f"{val}*"
|
|
554
|
+
return f"{prefix}{utype}({inner})"
|
|
555
|
+
# Fallback
|
|
556
|
+
return f"{val}{utype}"
|
|
557
|
+
|
|
558
|
+
# String-based units include data in the representation
|
|
559
|
+
if utype in (
|
|
560
|
+
"strwidth",
|
|
561
|
+
"strheight",
|
|
562
|
+
"strascent",
|
|
563
|
+
"strdescent",
|
|
564
|
+
"mystrwidth",
|
|
565
|
+
"mystrheight",
|
|
566
|
+
) and d is not None:
|
|
567
|
+
return f"{val}{utype}({d!r})"
|
|
568
|
+
|
|
569
|
+
# Grob-based units
|
|
570
|
+
if utype in (
|
|
571
|
+
"grobx",
|
|
572
|
+
"groby",
|
|
573
|
+
"grobwidth",
|
|
574
|
+
"grobheight",
|
|
575
|
+
"grobascent",
|
|
576
|
+
"grobdescent",
|
|
577
|
+
) and d is not None:
|
|
578
|
+
return f"{val}{utype}({d!r})"
|
|
579
|
+
|
|
580
|
+
return f"{val}{utype}"
|
|
581
|
+
|
|
582
|
+
# ---------------------------------------------------------------- indexing
|
|
583
|
+
def __getitem__(self, index: Union[int, slice, Sequence[int]]) -> "Unit":
|
|
584
|
+
"""Return a new ``Unit`` containing the selected element(s).
|
|
585
|
+
|
|
586
|
+
Parameters
|
|
587
|
+
----------
|
|
588
|
+
index : int, slice, or sequence of int
|
|
589
|
+
Element selector.
|
|
590
|
+
|
|
591
|
+
Returns
|
|
592
|
+
-------
|
|
593
|
+
Unit
|
|
594
|
+
A new unit with the selected elements.
|
|
595
|
+
|
|
596
|
+
Raises
|
|
597
|
+
------
|
|
598
|
+
IndexError
|
|
599
|
+
If *index* is out of range.
|
|
600
|
+
"""
|
|
601
|
+
if isinstance(index, (int, np.integer)):
|
|
602
|
+
if index < 0:
|
|
603
|
+
index = len(self) + index
|
|
604
|
+
if index < 0 or index >= len(self):
|
|
605
|
+
raise IndexError(
|
|
606
|
+
f"index {index} is out of bounds for Unit of length {len(self)}"
|
|
607
|
+
)
|
|
608
|
+
new = Unit.__new__(Unit)
|
|
609
|
+
new._values = self._values[index : index + 1].copy()
|
|
610
|
+
new._units = [self._units[index]]
|
|
611
|
+
new._data = [self._data[index]]
|
|
612
|
+
new._is_absolute = self._units[index] in _ABSOLUTE_UNIT_TYPES
|
|
613
|
+
return new
|
|
614
|
+
|
|
615
|
+
if isinstance(index, slice):
|
|
616
|
+
indices = range(*index.indices(len(self)))
|
|
617
|
+
else:
|
|
618
|
+
indices = list(index)
|
|
619
|
+
|
|
620
|
+
vals = self._values[index] if isinstance(index, slice) else self._values[list(indices)]
|
|
621
|
+
u_list = [self._units[i] for i in indices]
|
|
622
|
+
d_list = [self._data[i] for i in indices]
|
|
623
|
+
|
|
624
|
+
new = Unit.__new__(Unit)
|
|
625
|
+
new._values = vals.copy() if isinstance(vals, np.ndarray) else np.asarray(vals, dtype=np.float64)
|
|
626
|
+
new._units = u_list
|
|
627
|
+
new._data = d_list
|
|
628
|
+
new._is_absolute = all(u in _ABSOLUTE_UNIT_TYPES for u in u_list)
|
|
629
|
+
return new
|
|
630
|
+
|
|
631
|
+
def __setitem__(
|
|
632
|
+
self, index: Union[int, slice], value: "Unit"
|
|
633
|
+
) -> None:
|
|
634
|
+
"""Set element(s) of this unit in-place.
|
|
635
|
+
|
|
636
|
+
Parameters
|
|
637
|
+
----------
|
|
638
|
+
index : int or slice
|
|
639
|
+
Element selector.
|
|
640
|
+
value : Unit
|
|
641
|
+
Replacement unit value(s).
|
|
642
|
+
"""
|
|
643
|
+
if not isinstance(value, Unit):
|
|
644
|
+
raise TypeError("replacement value must be a Unit")
|
|
645
|
+
|
|
646
|
+
if isinstance(index, (int, np.integer)):
|
|
647
|
+
if index < 0:
|
|
648
|
+
index = len(self) + index
|
|
649
|
+
self._values[index] = value._values[0]
|
|
650
|
+
self._units[index] = value._units[0]
|
|
651
|
+
self._data[index] = value._data[0]
|
|
652
|
+
elif isinstance(index, slice):
|
|
653
|
+
indices = range(*index.indices(len(self)))
|
|
654
|
+
for j, i in enumerate(indices):
|
|
655
|
+
src = j % len(value)
|
|
656
|
+
self._values[i] = value._values[src]
|
|
657
|
+
self._units[i] = value._units[src]
|
|
658
|
+
self._data[i] = value._data[src]
|
|
659
|
+
else:
|
|
660
|
+
raise TypeError(f"unsupported index type {type(index)}")
|
|
661
|
+
|
|
662
|
+
self._is_absolute = all(u in _ABSOLUTE_UNIT_TYPES for u in self._units)
|
|
663
|
+
|
|
664
|
+
# ================================================================
|
|
665
|
+
# Arithmetic operators
|
|
666
|
+
# ================================================================
|
|
667
|
+
|
|
668
|
+
# ---- addition: unit + unit -> compound sum -------------------------
|
|
669
|
+
def __add__(self, other: Any) -> "Unit":
|
|
670
|
+
if isinstance(other, Unit):
|
|
671
|
+
return _make_compound("sum", self, other)
|
|
672
|
+
return NotImplemented
|
|
673
|
+
|
|
674
|
+
def __radd__(self, other: Any) -> "Unit":
|
|
675
|
+
if isinstance(other, Unit):
|
|
676
|
+
return _make_compound("sum", other, self)
|
|
677
|
+
if other == 0:
|
|
678
|
+
# Supports sum() on iterables of Units
|
|
679
|
+
return self.copy()
|
|
680
|
+
return NotImplemented
|
|
681
|
+
|
|
682
|
+
# ---- subtraction: unit - unit -> compound sum (with negated rhs) ---
|
|
683
|
+
def __sub__(self, other: Any) -> "Unit":
|
|
684
|
+
if isinstance(other, Unit):
|
|
685
|
+
return _make_compound("sum", self, -other)
|
|
686
|
+
return NotImplemented
|
|
687
|
+
|
|
688
|
+
def __rsub__(self, other: Any) -> "Unit":
|
|
689
|
+
if isinstance(other, Unit):
|
|
690
|
+
return _make_compound("sum", other, -self)
|
|
691
|
+
return NotImplemented
|
|
692
|
+
|
|
693
|
+
# ---- negation ------------------------------------------------------
|
|
694
|
+
def __neg__(self) -> "Unit":
|
|
695
|
+
new = self.copy()
|
|
696
|
+
new._values = -new._values
|
|
697
|
+
return new
|
|
698
|
+
|
|
699
|
+
def __pos__(self) -> "Unit":
|
|
700
|
+
return self.copy()
|
|
701
|
+
|
|
702
|
+
# ---- multiplication: scalar * unit or unit * scalar ----------------
|
|
703
|
+
def __mul__(self, other: Any) -> "Unit":
|
|
704
|
+
if isinstance(other, Number) and not isinstance(other, bool):
|
|
705
|
+
new = self.copy()
|
|
706
|
+
new._values = new._values * float(other)
|
|
707
|
+
return new
|
|
708
|
+
if isinstance(other, Unit):
|
|
709
|
+
raise TypeError("Cannot multiply two Unit objects; one operand must be numeric")
|
|
710
|
+
return NotImplemented
|
|
711
|
+
|
|
712
|
+
def __rmul__(self, other: Any) -> "Unit":
|
|
713
|
+
if isinstance(other, Number) and not isinstance(other, bool):
|
|
714
|
+
return self.__mul__(other)
|
|
715
|
+
return NotImplemented
|
|
716
|
+
|
|
717
|
+
# ---- division: unit / scalar ---------------------------------------
|
|
718
|
+
def __truediv__(self, other: Any) -> "Unit":
|
|
719
|
+
if isinstance(other, Number) and not isinstance(other, bool):
|
|
720
|
+
if float(other) == 0.0:
|
|
721
|
+
raise ZeroDivisionError("division by zero")
|
|
722
|
+
new = self.copy()
|
|
723
|
+
new._values = new._values / float(other)
|
|
724
|
+
return new
|
|
725
|
+
if isinstance(other, Unit):
|
|
726
|
+
raise TypeError("Cannot divide by a Unit object")
|
|
727
|
+
return NotImplemented
|
|
728
|
+
|
|
729
|
+
def __rtruediv__(self, other: Any) -> "Unit":
|
|
730
|
+
raise TypeError("Cannot divide by a Unit object")
|
|
731
|
+
|
|
732
|
+
# ---- equality (element-wise, mainly for testing) -------------------
|
|
733
|
+
def __eq__(self, other: object) -> bool:
|
|
734
|
+
if not isinstance(other, Unit):
|
|
735
|
+
return NotImplemented
|
|
736
|
+
if len(self) != len(other):
|
|
737
|
+
return False
|
|
738
|
+
return (
|
|
739
|
+
np.allclose(self._values, other._values)
|
|
740
|
+
and self._units == other._units
|
|
741
|
+
)
|
|
742
|
+
|
|
743
|
+
def __ne__(self, other: object) -> bool:
|
|
744
|
+
eq = self.__eq__(other)
|
|
745
|
+
if eq is NotImplemented:
|
|
746
|
+
return NotImplemented # type: ignore[return-value]
|
|
747
|
+
return not eq
|
|
748
|
+
|
|
749
|
+
# ================================================================
|
|
750
|
+
# Helpers
|
|
751
|
+
# ================================================================
|
|
752
|
+
|
|
753
|
+
def copy(self) -> "Unit":
|
|
754
|
+
"""Return a shallow copy of this unit.
|
|
755
|
+
|
|
756
|
+
Returns
|
|
757
|
+
-------
|
|
758
|
+
Unit
|
|
759
|
+
Independent copy sharing no mutable state with the original.
|
|
760
|
+
"""
|
|
761
|
+
new = Unit.__new__(Unit)
|
|
762
|
+
new._values = self._values.copy()
|
|
763
|
+
new._units = list(self._units)
|
|
764
|
+
new._data = list(self._data)
|
|
765
|
+
new._is_absolute = self._is_absolute
|
|
766
|
+
return new
|
|
767
|
+
|
|
768
|
+
def is_absolute(self) -> bool:
|
|
769
|
+
"""Return ``True`` if every element is an absolute (physical) unit.
|
|
770
|
+
|
|
771
|
+
Returns
|
|
772
|
+
-------
|
|
773
|
+
bool
|
|
774
|
+
"""
|
|
775
|
+
return self._is_absolute
|
|
776
|
+
|
|
777
|
+
# Allow hashing to fail (mutable container)
|
|
778
|
+
__hash__ = None # type: ignore[assignment]
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
# ---------------------------------------------------------------------------
|
|
782
|
+
# Compound-unit helper
|
|
783
|
+
# ---------------------------------------------------------------------------
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
def _make_compound(op: str, lhs: Unit, rhs: Unit) -> Unit:
|
|
787
|
+
"""Create a compound unit (*sum*, *min*, or *max*) from two operands.
|
|
788
|
+
|
|
789
|
+
If both operands share identical simple absolute unit types the operation
|
|
790
|
+
is performed eagerly (e.g. ``1cm + 2cm -> 3cm``).
|
|
791
|
+
|
|
792
|
+
Parameters
|
|
793
|
+
----------
|
|
794
|
+
op : str
|
|
795
|
+
One of ``"sum"``, ``"min"``, ``"max"``.
|
|
796
|
+
lhs : Unit
|
|
797
|
+
Left-hand operand.
|
|
798
|
+
rhs : Unit
|
|
799
|
+
Right-hand operand.
|
|
800
|
+
|
|
801
|
+
Returns
|
|
802
|
+
-------
|
|
803
|
+
Unit
|
|
804
|
+
Resulting (possibly compound) unit.
|
|
805
|
+
"""
|
|
806
|
+
# Fast path: identical simple unit types -- compute eagerly
|
|
807
|
+
if (
|
|
808
|
+
len(set(lhs._units)) == 1
|
|
809
|
+
and len(set(rhs._units)) == 1
|
|
810
|
+
and lhs._units[0] == rhs._units[0]
|
|
811
|
+
and lhs._units[0] not in ("sum", "min", "max")
|
|
812
|
+
):
|
|
813
|
+
utype = lhs._units[0]
|
|
814
|
+
# Recycle to common length
|
|
815
|
+
n = max(len(lhs), len(rhs))
|
|
816
|
+
lv = np.resize(lhs._values, n)
|
|
817
|
+
rv = np.resize(rhs._values, n)
|
|
818
|
+
if op == "sum":
|
|
819
|
+
vals = lv + rv
|
|
820
|
+
elif op == "min":
|
|
821
|
+
vals = np.minimum(lv, rv)
|
|
822
|
+
else:
|
|
823
|
+
vals = np.maximum(lv, rv)
|
|
824
|
+
new = Unit.__new__(Unit)
|
|
825
|
+
new._values = vals
|
|
826
|
+
new._units = [utype] * n
|
|
827
|
+
new._data = [None] * n
|
|
828
|
+
new._is_absolute = utype in _ABSOLUTE_UNIT_TYPES
|
|
829
|
+
return new
|
|
830
|
+
|
|
831
|
+
# General path: build a compound unit for each parallel pair
|
|
832
|
+
n = max(len(lhs), len(rhs))
|
|
833
|
+
compound_vals = np.ones(n, dtype=np.float64)
|
|
834
|
+
compound_units: List[str] = [op] * n
|
|
835
|
+
compound_data: List[Any] = []
|
|
836
|
+
|
|
837
|
+
for i in range(n):
|
|
838
|
+
li = i % len(lhs)
|
|
839
|
+
ri = i % len(rhs)
|
|
840
|
+
pair = unit_c(lhs[li], rhs[ri])
|
|
841
|
+
compound_data.append(pair)
|
|
842
|
+
|
|
843
|
+
new = Unit.__new__(Unit)
|
|
844
|
+
new._values = compound_vals
|
|
845
|
+
new._units = compound_units
|
|
846
|
+
new._data = compound_data
|
|
847
|
+
new._is_absolute = False
|
|
848
|
+
return new
|
|
849
|
+
|
|
850
|
+
|
|
851
|
+
# ===================================================================
|
|
852
|
+
# Module-level helper functions
|
|
853
|
+
# ===================================================================
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
def is_unit(x: Any) -> bool:
|
|
857
|
+
"""Check whether *x* is a ``Unit`` instance.
|
|
858
|
+
|
|
859
|
+
Parameters
|
|
860
|
+
----------
|
|
861
|
+
x : Any
|
|
862
|
+
Object to test.
|
|
863
|
+
|
|
864
|
+
Returns
|
|
865
|
+
-------
|
|
866
|
+
bool
|
|
867
|
+
``True`` if *x* is a ``Unit``.
|
|
868
|
+
|
|
869
|
+
Examples
|
|
870
|
+
--------
|
|
871
|
+
>>> is_unit(Unit(1, "cm"))
|
|
872
|
+
True
|
|
873
|
+
>>> is_unit(42)
|
|
874
|
+
False
|
|
875
|
+
"""
|
|
876
|
+
return isinstance(x, Unit)
|
|
877
|
+
|
|
878
|
+
|
|
879
|
+
def unit_type(x: Unit, recurse: bool = False) -> Union[str, List[str], List[Any]]:
|
|
880
|
+
"""Return the unit type(s) of *x*.
|
|
881
|
+
|
|
882
|
+
Port of R ``unitType()`` (unit.R:197-226).
|
|
883
|
+
|
|
884
|
+
Parameters
|
|
885
|
+
----------
|
|
886
|
+
x : Unit
|
|
887
|
+
A unit object.
|
|
888
|
+
recurse : bool
|
|
889
|
+
If ``True``, compound units (sum/min/max) are recursively
|
|
890
|
+
expanded to reveal the underlying unit types. Returns a list
|
|
891
|
+
of lists for compound elements. Default ``False``.
|
|
892
|
+
|
|
893
|
+
Returns
|
|
894
|
+
-------
|
|
895
|
+
str or list
|
|
896
|
+
When *recurse* is ``False``: a single string (length 1) or
|
|
897
|
+
list of strings.
|
|
898
|
+
When *recurse* is ``True``: a list where compound elements
|
|
899
|
+
are themselves lists of their constituent unit types.
|
|
900
|
+
|
|
901
|
+
Raises
|
|
902
|
+
------
|
|
903
|
+
TypeError
|
|
904
|
+
If *x* is not a ``Unit``.
|
|
905
|
+
|
|
906
|
+
Examples
|
|
907
|
+
--------
|
|
908
|
+
>>> unit_type(Unit(1, "cm"))
|
|
909
|
+
'cm'
|
|
910
|
+
>>> unit_type(Unit([1, 2], ["cm", "inches"]))
|
|
911
|
+
['cm', 'inches']
|
|
912
|
+
"""
|
|
913
|
+
if not isinstance(x, Unit):
|
|
914
|
+
raise TypeError("x must be a Unit")
|
|
915
|
+
|
|
916
|
+
if not recurse:
|
|
917
|
+
if len(x) == 1:
|
|
918
|
+
return x._units[0]
|
|
919
|
+
return list(x._units)
|
|
920
|
+
|
|
921
|
+
# recurse=True: expand compound units (R unit.R:211-224)
|
|
922
|
+
result = []
|
|
923
|
+
for i in range(len(x)):
|
|
924
|
+
utype = x._units[i]
|
|
925
|
+
if utype in ("sum", "min", "max"):
|
|
926
|
+
# Compound unit: recurse into the child Unit stored in _data
|
|
927
|
+
child = x._data[i]
|
|
928
|
+
if isinstance(child, Unit):
|
|
929
|
+
result.append(unit_type(child, recurse=True))
|
|
930
|
+
else:
|
|
931
|
+
result.append(utype)
|
|
932
|
+
else:
|
|
933
|
+
result.append(utype)
|
|
934
|
+
return result
|
|
935
|
+
|
|
936
|
+
|
|
937
|
+
def unit_c(*args: Unit) -> Unit:
|
|
938
|
+
"""Concatenate one or more ``Unit`` objects into a single ``Unit``.
|
|
939
|
+
|
|
940
|
+
Parameters
|
|
941
|
+
----------
|
|
942
|
+
*args : Unit
|
|
943
|
+
Units to concatenate.
|
|
944
|
+
|
|
945
|
+
Returns
|
|
946
|
+
-------
|
|
947
|
+
Unit
|
|
948
|
+
A new unit containing all elements in order.
|
|
949
|
+
|
|
950
|
+
Raises
|
|
951
|
+
------
|
|
952
|
+
TypeError
|
|
953
|
+
If any argument is not a ``Unit``.
|
|
954
|
+
ValueError
|
|
955
|
+
If no arguments are provided.
|
|
956
|
+
|
|
957
|
+
Examples
|
|
958
|
+
--------
|
|
959
|
+
>>> unit_c(Unit(1, "cm"), Unit(2, "inches"))
|
|
960
|
+
Unit([1.0, 2.0], ['cm', 'inches'])
|
|
961
|
+
"""
|
|
962
|
+
if len(args) == 0:
|
|
963
|
+
raise ValueError("unit_c requires at least one argument")
|
|
964
|
+
|
|
965
|
+
all_vals: List[np.ndarray] = []
|
|
966
|
+
all_units: List[str] = []
|
|
967
|
+
all_data: List[Any] = []
|
|
968
|
+
|
|
969
|
+
for a in args:
|
|
970
|
+
if not isinstance(a, Unit):
|
|
971
|
+
raise TypeError(f"All arguments must be Unit objects, got {type(a)}")
|
|
972
|
+
all_vals.append(a._values)
|
|
973
|
+
all_units.extend(a._units)
|
|
974
|
+
all_data.extend(a._data)
|
|
975
|
+
|
|
976
|
+
new = Unit.__new__(Unit)
|
|
977
|
+
new._values = np.concatenate(all_vals)
|
|
978
|
+
new._units = all_units
|
|
979
|
+
new._data = all_data
|
|
980
|
+
new._is_absolute = all(u in _ABSOLUTE_UNIT_TYPES for u in all_units)
|
|
981
|
+
return new
|
|
982
|
+
|
|
983
|
+
|
|
984
|
+
def unit_length(x: Unit) -> int:
|
|
985
|
+
"""Return the number of elements in a ``Unit``.
|
|
986
|
+
|
|
987
|
+
Parameters
|
|
988
|
+
----------
|
|
989
|
+
x : Unit
|
|
990
|
+
A unit object.
|
|
991
|
+
|
|
992
|
+
Returns
|
|
993
|
+
-------
|
|
994
|
+
int
|
|
995
|
+
Number of unit values.
|
|
996
|
+
|
|
997
|
+
Examples
|
|
998
|
+
--------
|
|
999
|
+
>>> unit_length(Unit([1, 2, 3], "cm"))
|
|
1000
|
+
3
|
|
1001
|
+
"""
|
|
1002
|
+
if not isinstance(x, Unit):
|
|
1003
|
+
raise TypeError("x must be a Unit")
|
|
1004
|
+
return len(x)
|
|
1005
|
+
|
|
1006
|
+
|
|
1007
|
+
def unit_pmax(*args: Unit) -> Unit:
|
|
1008
|
+
"""Parallel (element-wise) maximum of units.
|
|
1009
|
+
|
|
1010
|
+
Parameters
|
|
1011
|
+
----------
|
|
1012
|
+
*args : Unit
|
|
1013
|
+
Two or more units of the same length.
|
|
1014
|
+
|
|
1015
|
+
Returns
|
|
1016
|
+
-------
|
|
1017
|
+
Unit
|
|
1018
|
+
Element-wise maximum (compound *max* units when types differ).
|
|
1019
|
+
|
|
1020
|
+
Examples
|
|
1021
|
+
--------
|
|
1022
|
+
>>> unit_pmax(Unit([1, 4], "cm"), Unit([3, 2], "cm"))
|
|
1023
|
+
Unit([3.0, 4.0], ['cm', 'cm'])
|
|
1024
|
+
"""
|
|
1025
|
+
return _parallel_op("max", *args)
|
|
1026
|
+
|
|
1027
|
+
|
|
1028
|
+
def unit_pmin(*args: Unit) -> Unit:
|
|
1029
|
+
"""Parallel (element-wise) minimum of units.
|
|
1030
|
+
|
|
1031
|
+
Parameters
|
|
1032
|
+
----------
|
|
1033
|
+
*args : Unit
|
|
1034
|
+
Two or more units of the same length.
|
|
1035
|
+
|
|
1036
|
+
Returns
|
|
1037
|
+
-------
|
|
1038
|
+
Unit
|
|
1039
|
+
Element-wise minimum (compound *min* units when types differ).
|
|
1040
|
+
|
|
1041
|
+
Examples
|
|
1042
|
+
--------
|
|
1043
|
+
>>> unit_pmin(Unit([1, 4], "cm"), Unit([3, 2], "cm"))
|
|
1044
|
+
Unit([1.0, 2.0], ['cm', 'cm'])
|
|
1045
|
+
"""
|
|
1046
|
+
return _parallel_op("min", *args)
|
|
1047
|
+
|
|
1048
|
+
|
|
1049
|
+
def unit_psum(*args: Unit) -> Unit:
|
|
1050
|
+
"""Parallel (element-wise) sum of units.
|
|
1051
|
+
|
|
1052
|
+
Parameters
|
|
1053
|
+
----------
|
|
1054
|
+
*args : Unit
|
|
1055
|
+
Two or more units of the same length.
|
|
1056
|
+
|
|
1057
|
+
Returns
|
|
1058
|
+
-------
|
|
1059
|
+
Unit
|
|
1060
|
+
Element-wise sum (compound *sum* units when types differ).
|
|
1061
|
+
|
|
1062
|
+
Examples
|
|
1063
|
+
--------
|
|
1064
|
+
>>> unit_psum(Unit([1, 2], "cm"), Unit([3, 4], "cm"))
|
|
1065
|
+
Unit([4.0, 6.0], ['cm', 'cm'])
|
|
1066
|
+
"""
|
|
1067
|
+
return _parallel_op("sum", *args)
|
|
1068
|
+
|
|
1069
|
+
|
|
1070
|
+
def _parallel_op(op: str, *args: Unit) -> Unit:
|
|
1071
|
+
"""Internal implementation for parallel min / max / sum.
|
|
1072
|
+
|
|
1073
|
+
Parameters
|
|
1074
|
+
----------
|
|
1075
|
+
op : str
|
|
1076
|
+
``"sum"``, ``"min"``, or ``"max"``.
|
|
1077
|
+
*args : Unit
|
|
1078
|
+
Units to combine element-wise.
|
|
1079
|
+
|
|
1080
|
+
Returns
|
|
1081
|
+
-------
|
|
1082
|
+
Unit
|
|
1083
|
+
"""
|
|
1084
|
+
if len(args) == 0:
|
|
1085
|
+
raise ValueError(f"unit_p{op} requires at least one argument")
|
|
1086
|
+
if len(args) == 1:
|
|
1087
|
+
return args[0].copy()
|
|
1088
|
+
|
|
1089
|
+
result = args[0]
|
|
1090
|
+
for a in args[1:]:
|
|
1091
|
+
result = _make_compound(op, result, a)
|
|
1092
|
+
return result
|
|
1093
|
+
|
|
1094
|
+
|
|
1095
|
+
def unit_rep(
|
|
1096
|
+
x: Unit,
|
|
1097
|
+
times: int = 1,
|
|
1098
|
+
length_out: Optional[int] = None,
|
|
1099
|
+
each: int = 1,
|
|
1100
|
+
) -> Unit:
|
|
1101
|
+
"""Repeat a ``Unit`` object.
|
|
1102
|
+
|
|
1103
|
+
Port of R ``rep.unit()`` (unit.R:539-542). Mirrors the semantics
|
|
1104
|
+
of R's ``rep(x, times, length.out, each)``.
|
|
1105
|
+
|
|
1106
|
+
Parameters
|
|
1107
|
+
----------
|
|
1108
|
+
x : Unit
|
|
1109
|
+
The unit to repeat.
|
|
1110
|
+
times : int
|
|
1111
|
+
Number of times to repeat the (possibly each-expanded) unit.
|
|
1112
|
+
length_out : int or None
|
|
1113
|
+
If given, truncate or recycle the result to this length.
|
|
1114
|
+
each : int
|
|
1115
|
+
Replicate each element *each* times before tiling.
|
|
1116
|
+
|
|
1117
|
+
Returns
|
|
1118
|
+
-------
|
|
1119
|
+
Unit
|
|
1120
|
+
A unit whose elements are *x* repeated.
|
|
1121
|
+
|
|
1122
|
+
Examples
|
|
1123
|
+
--------
|
|
1124
|
+
>>> unit_rep(Unit(1, "cm"), 3)
|
|
1125
|
+
Unit([1.0, 1.0, 1.0], ['cm', 'cm', 'cm'])
|
|
1126
|
+
>>> unit_rep(Unit([1, 2], "cm"), each=2)
|
|
1127
|
+
Unit([1.0, 1.0, 2.0, 2.0], ['cm', 'cm', 'cm', 'cm'])
|
|
1128
|
+
>>> unit_rep(Unit([1, 2, 3], "cm"), length_out=5)
|
|
1129
|
+
Unit([1.0, 2.0, 3.0, 1.0, 2.0], ['cm', 'cm', 'cm', 'cm', 'cm'])
|
|
1130
|
+
"""
|
|
1131
|
+
if not isinstance(x, Unit):
|
|
1132
|
+
raise TypeError("x must be a Unit")
|
|
1133
|
+
|
|
1134
|
+
# Build index vector mirroring R's rep(seq_along(x), times, length.out, each)
|
|
1135
|
+
n = len(x)
|
|
1136
|
+
base = list(range(n))
|
|
1137
|
+
|
|
1138
|
+
# Apply each: replicate each element
|
|
1139
|
+
if each > 1:
|
|
1140
|
+
base = [i for i in base for _ in range(each)]
|
|
1141
|
+
|
|
1142
|
+
# Apply times: tile the whole sequence (times=0 → empty)
|
|
1143
|
+
if times == 0:
|
|
1144
|
+
base = []
|
|
1145
|
+
elif times > 1:
|
|
1146
|
+
base = base * times
|
|
1147
|
+
|
|
1148
|
+
# Apply length_out: truncate or recycle
|
|
1149
|
+
if length_out is not None:
|
|
1150
|
+
if length_out <= 0:
|
|
1151
|
+
base = []
|
|
1152
|
+
elif len(base) == 0:
|
|
1153
|
+
# Recycle from original if times made it empty
|
|
1154
|
+
base = list(range(n))
|
|
1155
|
+
full_cycles = length_out // len(base)
|
|
1156
|
+
remainder = length_out % len(base)
|
|
1157
|
+
base = base * full_cycles + base[:remainder]
|
|
1158
|
+
else:
|
|
1159
|
+
full_cycles = length_out // len(base)
|
|
1160
|
+
remainder = length_out % len(base)
|
|
1161
|
+
base = base * full_cycles + base[:remainder]
|
|
1162
|
+
|
|
1163
|
+
if len(base) == 0:
|
|
1164
|
+
# Return empty Unit
|
|
1165
|
+
new = Unit.__new__(Unit)
|
|
1166
|
+
new._values = np.array([], dtype=np.float64)
|
|
1167
|
+
new._units = []
|
|
1168
|
+
new._data = []
|
|
1169
|
+
new._is_absolute = False
|
|
1170
|
+
return new
|
|
1171
|
+
|
|
1172
|
+
return x[base]
|
|
1173
|
+
|
|
1174
|
+
|
|
1175
|
+
# ===================================================================
|
|
1176
|
+
# Summary.unit: min / max / sum (port of R unit.R:300-347)
|
|
1177
|
+
# ===================================================================
|
|
1178
|
+
|
|
1179
|
+
|
|
1180
|
+
def unit_summary_min(*args: Unit) -> Unit:
|
|
1181
|
+
"""Return the minimum across all elements of all input units.
|
|
1182
|
+
|
|
1183
|
+
Port of R ``Summary.unit`` for ``min`` (unit.R:300-347).
|
|
1184
|
+
Unlike :func:`unit_pmin` (element-wise), this returns a single
|
|
1185
|
+
scalar unit representing the global minimum.
|
|
1186
|
+
|
|
1187
|
+
Parameters
|
|
1188
|
+
----------
|
|
1189
|
+
*args : Unit
|
|
1190
|
+
One or more unit objects.
|
|
1191
|
+
|
|
1192
|
+
Returns
|
|
1193
|
+
-------
|
|
1194
|
+
Unit
|
|
1195
|
+
A single-element unit with the minimum value.
|
|
1196
|
+
|
|
1197
|
+
Examples
|
|
1198
|
+
--------
|
|
1199
|
+
>>> unit_summary_min(Unit([3, 1, 4], "cm"))
|
|
1200
|
+
Unit([1.0], ['cm'])
|
|
1201
|
+
"""
|
|
1202
|
+
return _summary_op("min", *args)
|
|
1203
|
+
|
|
1204
|
+
|
|
1205
|
+
def unit_summary_max(*args: Unit) -> Unit:
|
|
1206
|
+
"""Return the maximum across all elements of all input units.
|
|
1207
|
+
|
|
1208
|
+
Port of R ``Summary.unit`` for ``max`` (unit.R:300-347).
|
|
1209
|
+
|
|
1210
|
+
Parameters
|
|
1211
|
+
----------
|
|
1212
|
+
*args : Unit
|
|
1213
|
+
One or more unit objects.
|
|
1214
|
+
|
|
1215
|
+
Returns
|
|
1216
|
+
-------
|
|
1217
|
+
Unit
|
|
1218
|
+
A single-element unit with the maximum value.
|
|
1219
|
+
|
|
1220
|
+
Examples
|
|
1221
|
+
--------
|
|
1222
|
+
>>> unit_summary_max(Unit([3, 1, 4], "cm"))
|
|
1223
|
+
Unit([4.0], ['cm'])
|
|
1224
|
+
"""
|
|
1225
|
+
return _summary_op("max", *args)
|
|
1226
|
+
|
|
1227
|
+
|
|
1228
|
+
def unit_summary_sum(*args: Unit) -> Unit:
|
|
1229
|
+
"""Return the sum of all elements of all input units.
|
|
1230
|
+
|
|
1231
|
+
Port of R ``Summary.unit`` for ``sum`` (unit.R:300-347).
|
|
1232
|
+
|
|
1233
|
+
Parameters
|
|
1234
|
+
----------
|
|
1235
|
+
*args : Unit
|
|
1236
|
+
One or more unit objects.
|
|
1237
|
+
|
|
1238
|
+
Returns
|
|
1239
|
+
-------
|
|
1240
|
+
Unit
|
|
1241
|
+
A single-element unit with the total sum.
|
|
1242
|
+
|
|
1243
|
+
Examples
|
|
1244
|
+
--------
|
|
1245
|
+
>>> unit_summary_sum(Unit([1, 2, 3], "cm"))
|
|
1246
|
+
Unit([6.0], ['cm'])
|
|
1247
|
+
"""
|
|
1248
|
+
return _summary_op("sum", *args)
|
|
1249
|
+
|
|
1250
|
+
|
|
1251
|
+
def _summary_op(op: str, *args: Unit) -> Unit:
|
|
1252
|
+
"""Internal implementation for Summary.unit (min/max/sum).
|
|
1253
|
+
|
|
1254
|
+
Port of R ``Summary.unit`` (unit.R:300-347).
|
|
1255
|
+
|
|
1256
|
+
Optimisation: if all elements across all inputs share the same
|
|
1257
|
+
simple unit type, the operation is applied directly on the numeric
|
|
1258
|
+
values. Otherwise, a compound unit is created.
|
|
1259
|
+
"""
|
|
1260
|
+
if len(args) == 0:
|
|
1261
|
+
raise ValueError(f"unit {op} requires at least one argument")
|
|
1262
|
+
|
|
1263
|
+
# Filter out None args (R: units[!vapply(units, is.null, ...)])
|
|
1264
|
+
units = [a for a in args if a is not None and isinstance(a, Unit)]
|
|
1265
|
+
if len(units) == 0:
|
|
1266
|
+
raise ValueError(f"unit {op} requires at least one Unit argument")
|
|
1267
|
+
|
|
1268
|
+
# Concatenate all elements
|
|
1269
|
+
combined = unit_c(*units)
|
|
1270
|
+
|
|
1271
|
+
# Optimisation: identical simple unit types (R unit.R:308-320)
|
|
1272
|
+
all_types = set(combined._units)
|
|
1273
|
+
if len(all_types) == 1 and list(all_types)[0] not in ("sum", "min", "max"):
|
|
1274
|
+
utype = combined._units[0]
|
|
1275
|
+
vals = combined._values
|
|
1276
|
+
if op == "sum":
|
|
1277
|
+
result_val = float(np.sum(vals))
|
|
1278
|
+
elif op == "min":
|
|
1279
|
+
result_val = float(np.min(vals))
|
|
1280
|
+
else: # max
|
|
1281
|
+
result_val = float(np.max(vals))
|
|
1282
|
+
return Unit(result_val, utype)
|
|
1283
|
+
|
|
1284
|
+
# General case: create compound unit (R unit.R:321-347)
|
|
1285
|
+
# The compound wraps all elements under a single min/max/sum operation
|
|
1286
|
+
op_code = {"sum": "sum", "min": "min", "max": "max"}[op]
|
|
1287
|
+
return Unit(1.0, op_code, data=combined)
|
|
1288
|
+
|
|
1289
|
+
|
|
1290
|
+
# ===================================================================
|
|
1291
|
+
# String-metric convenience constructors
|
|
1292
|
+
# ===================================================================
|
|
1293
|
+
|
|
1294
|
+
|
|
1295
|
+
def string_width(string: Union[str, Sequence[str]]) -> Unit:
|
|
1296
|
+
"""Create a ``"strwidth"`` unit for the given string(s).
|
|
1297
|
+
|
|
1298
|
+
Parameters
|
|
1299
|
+
----------
|
|
1300
|
+
string : str or sequence of str
|
|
1301
|
+
The string(s) whose rendered width the unit represents.
|
|
1302
|
+
|
|
1303
|
+
Returns
|
|
1304
|
+
-------
|
|
1305
|
+
Unit
|
|
1306
|
+
A unit of type ``"strwidth"`` with value 1 for each string.
|
|
1307
|
+
|
|
1308
|
+
Examples
|
|
1309
|
+
--------
|
|
1310
|
+
>>> string_width("hello")
|
|
1311
|
+
Unit([1.0], ['strwidth'])
|
|
1312
|
+
"""
|
|
1313
|
+
if isinstance(string, str):
|
|
1314
|
+
strings = [string]
|
|
1315
|
+
else:
|
|
1316
|
+
strings = list(string)
|
|
1317
|
+
n = len(strings)
|
|
1318
|
+
return Unit(np.ones(n), ["strwidth"] * n, data=strings)
|
|
1319
|
+
|
|
1320
|
+
|
|
1321
|
+
def string_height(string: Union[str, Sequence[str]]) -> Unit:
|
|
1322
|
+
"""Create a ``"strheight"`` unit for the given string(s).
|
|
1323
|
+
|
|
1324
|
+
Parameters
|
|
1325
|
+
----------
|
|
1326
|
+
string : str or sequence of str
|
|
1327
|
+
The string(s) whose rendered height the unit represents.
|
|
1328
|
+
|
|
1329
|
+
Returns
|
|
1330
|
+
-------
|
|
1331
|
+
Unit
|
|
1332
|
+
A unit of type ``"strheight"`` with value 1 for each string.
|
|
1333
|
+
"""
|
|
1334
|
+
if isinstance(string, str):
|
|
1335
|
+
strings = [string]
|
|
1336
|
+
else:
|
|
1337
|
+
strings = list(string)
|
|
1338
|
+
n = len(strings)
|
|
1339
|
+
return Unit(np.ones(n), ["strheight"] * n, data=strings)
|
|
1340
|
+
|
|
1341
|
+
|
|
1342
|
+
def string_ascent(string: Union[str, Sequence[str]]) -> Unit:
|
|
1343
|
+
"""Create a ``"strascent"`` unit for the given string(s).
|
|
1344
|
+
|
|
1345
|
+
Parameters
|
|
1346
|
+
----------
|
|
1347
|
+
string : str or sequence of str
|
|
1348
|
+
The string(s) whose rendered ascent the unit represents.
|
|
1349
|
+
|
|
1350
|
+
Returns
|
|
1351
|
+
-------
|
|
1352
|
+
Unit
|
|
1353
|
+
A unit of type ``"strascent"`` with value 1 for each string.
|
|
1354
|
+
"""
|
|
1355
|
+
if isinstance(string, str):
|
|
1356
|
+
strings = [string]
|
|
1357
|
+
else:
|
|
1358
|
+
strings = list(string)
|
|
1359
|
+
n = len(strings)
|
|
1360
|
+
return Unit(np.ones(n), ["strascent"] * n, data=strings)
|
|
1361
|
+
|
|
1362
|
+
|
|
1363
|
+
def string_descent(string: Union[str, Sequence[str]]) -> Unit:
|
|
1364
|
+
"""Create a ``"strdescent"`` unit for the given string(s).
|
|
1365
|
+
|
|
1366
|
+
Parameters
|
|
1367
|
+
----------
|
|
1368
|
+
string : str or sequence of str
|
|
1369
|
+
The string(s) whose rendered descent the unit represents.
|
|
1370
|
+
|
|
1371
|
+
Returns
|
|
1372
|
+
-------
|
|
1373
|
+
Unit
|
|
1374
|
+
A unit of type ``"strdescent"`` with value 1 for each string.
|
|
1375
|
+
"""
|
|
1376
|
+
if isinstance(string, str):
|
|
1377
|
+
strings = [string]
|
|
1378
|
+
else:
|
|
1379
|
+
strings = list(string)
|
|
1380
|
+
n = len(strings)
|
|
1381
|
+
return Unit(np.ones(n), ["strdescent"] * n, data=strings)
|
|
1382
|
+
|
|
1383
|
+
|
|
1384
|
+
# ===================================================================
|
|
1385
|
+
# Absolute-size helper
|
|
1386
|
+
# ===================================================================
|
|
1387
|
+
|
|
1388
|
+
|
|
1389
|
+
def _is_absolute_unit_type(utype: str) -> bool:
|
|
1390
|
+
"""Check whether a unit type is "absolute" in R's sense.
|
|
1391
|
+
|
|
1392
|
+
R's ``isAbsolute()`` (grid.h:218) treats these as absolute:
|
|
1393
|
+
cm, inches, mm, points, lines, null, char, strwidth, strheight,
|
|
1394
|
+
strascent, strdescent, and all ``my*`` variants (>1000).
|
|
1395
|
+
|
|
1396
|
+
NON-absolute (context-dependent on parent size): npc, native, snpc.
|
|
1397
|
+
Also non-absolute: grobwidth, grobheight, grobx, groby, grobascent,
|
|
1398
|
+
grobdescent (depend on grob measurement in context).
|
|
1399
|
+
|
|
1400
|
+
Note: ``"null"`` IS absolute (it's resolved by GridLayout, not parent size).
|
|
1401
|
+
Note: ``"lines"`` IS absolute (depends on fontsize only, not parent size).
|
|
1402
|
+
"""
|
|
1403
|
+
_NON_ABSOLUTE = frozenset({
|
|
1404
|
+
"npc", "native", "snpc",
|
|
1405
|
+
"grobwidth", "grobheight", "grobx", "groby",
|
|
1406
|
+
"grobascent", "grobdescent",
|
|
1407
|
+
# Compound types need recursion — not absolute in isolation.
|
|
1408
|
+
"sum", "min", "max",
|
|
1409
|
+
})
|
|
1410
|
+
return utype not in _NON_ABSOLUTE
|
|
1411
|
+
|
|
1412
|
+
|
|
1413
|
+
def absolute_size(x: Unit) -> Unit:
|
|
1414
|
+
"""Convert a Unit to its absolute form.
|
|
1415
|
+
|
|
1416
|
+
Absolute units (cm, inches, lines, null, points, etc.) pass through
|
|
1417
|
+
unchanged. Non-absolute units (npc, native, snpc, grobwidth, etc.)
|
|
1418
|
+
are replaced with ``unit(1, "null")``.
|
|
1419
|
+
|
|
1420
|
+
For compound (sum/min/max) units, the function recurses into the
|
|
1421
|
+
operands, preserving absolute leaves and replacing non-absolute
|
|
1422
|
+
leaves with null.
|
|
1423
|
+
|
|
1424
|
+
This matches R's ``absolute.size()`` / ``absolute.units()``
|
|
1425
|
+
(grid/R/size.R:130, grid/src/unit.c:1777-1831).
|
|
1426
|
+
|
|
1427
|
+
Parameters
|
|
1428
|
+
----------
|
|
1429
|
+
x : Unit
|
|
1430
|
+
A unit object.
|
|
1431
|
+
|
|
1432
|
+
Returns
|
|
1433
|
+
-------
|
|
1434
|
+
Unit
|
|
1435
|
+
A copy of *x* with non-absolute elements replaced by ``null``.
|
|
1436
|
+
|
|
1437
|
+
Examples
|
|
1438
|
+
--------
|
|
1439
|
+
>>> absolute_size(Unit(2, "cm"))
|
|
1440
|
+
Unit([2.0], ['cm'])
|
|
1441
|
+
>>> absolute_size(Unit(0.5, "npc"))
|
|
1442
|
+
Unit([1.0], ['null'])
|
|
1443
|
+
"""
|
|
1444
|
+
if not isinstance(x, Unit):
|
|
1445
|
+
raise TypeError("x must be a Unit")
|
|
1446
|
+
|
|
1447
|
+
n = len(x)
|
|
1448
|
+
|
|
1449
|
+
# Fast path: all absolute → return as-is (R: line 1803)
|
|
1450
|
+
if all(_is_absolute_unit_type(x._units[i]) for i in range(n)):
|
|
1451
|
+
return x
|
|
1452
|
+
|
|
1453
|
+
new = x.copy()
|
|
1454
|
+
for i in range(n):
|
|
1455
|
+
utype = new._units[i]
|
|
1456
|
+
if utype in ("sum", "min", "max"):
|
|
1457
|
+
# Arithmetic compound: recurse into operands (R: lines 1814-1818)
|
|
1458
|
+
data = new._data[i]
|
|
1459
|
+
if data is not None and isinstance(data, Unit):
|
|
1460
|
+
new._data[i] = absolute_size(data)
|
|
1461
|
+
elif not _is_absolute_unit_type(utype):
|
|
1462
|
+
# Non-absolute scalar: replace with unit(1, "null") (R: lines 1819-1820)
|
|
1463
|
+
new._values[i] = 1.0
|
|
1464
|
+
new._units[i] = "null"
|
|
1465
|
+
new._data[i] = None
|
|
1466
|
+
return new
|
|
1467
|
+
|
|
1468
|
+
|
|
1469
|
+
# ===================================================================
|
|
1470
|
+
# Unit conversion
|
|
1471
|
+
# ===================================================================
|
|
1472
|
+
|
|
1473
|
+
|
|
1474
|
+
def _convert_absolute(value: float, unit_from: str, unit_to: str) -> float:
|
|
1475
|
+
"""Convert a single value between two absolute unit types.
|
|
1476
|
+
|
|
1477
|
+
Parameters
|
|
1478
|
+
----------
|
|
1479
|
+
value : float
|
|
1480
|
+
Numeric value in *unit_from* units.
|
|
1481
|
+
unit_from : str
|
|
1482
|
+
Source unit type (must be in ``_ABSOLUTE_UNIT_TYPES``).
|
|
1483
|
+
unit_to : str
|
|
1484
|
+
Target unit type (must be in ``_ABSOLUTE_UNIT_TYPES``).
|
|
1485
|
+
|
|
1486
|
+
Returns
|
|
1487
|
+
-------
|
|
1488
|
+
float
|
|
1489
|
+
The value expressed in *unit_to* units.
|
|
1490
|
+
"""
|
|
1491
|
+
if unit_from == unit_to:
|
|
1492
|
+
return value
|
|
1493
|
+
inches = value * _INCHES_PER[unit_from]
|
|
1494
|
+
return inches / _INCHES_PER[unit_to]
|
|
1495
|
+
|
|
1496
|
+
|
|
1497
|
+
def convert_unit(
|
|
1498
|
+
x: Unit,
|
|
1499
|
+
unitTo: str,
|
|
1500
|
+
axisFrom: str = "x",
|
|
1501
|
+
typeFrom: str = "location",
|
|
1502
|
+
axisTo: Optional[str] = None,
|
|
1503
|
+
typeTo: Optional[str] = None,
|
|
1504
|
+
valueOnly: bool = False,
|
|
1505
|
+
) -> Union[Unit, np.ndarray]:
|
|
1506
|
+
"""Convert a ``Unit`` to a different unit type.
|
|
1507
|
+
|
|
1508
|
+
Absolute-to-absolute conversions (e.g. cm to inches) are performed
|
|
1509
|
+
immediately. Context-dependent conversions (involving npc, native,
|
|
1510
|
+
lines, etc.) use the active renderer's viewport context when
|
|
1511
|
+
available; otherwise a warning is issued and the unit is returned
|
|
1512
|
+
unchanged.
|
|
1513
|
+
|
|
1514
|
+
Parameters
|
|
1515
|
+
----------
|
|
1516
|
+
x : Unit
|
|
1517
|
+
The unit to convert.
|
|
1518
|
+
unitTo : str
|
|
1519
|
+
Target unit type string.
|
|
1520
|
+
axisFrom : str, optional
|
|
1521
|
+
Source axis (``"x"`` or ``"y"``). Default ``"x"``.
|
|
1522
|
+
typeFrom : str, optional
|
|
1523
|
+
Source type (``"location"`` or ``"dimension"``). Default
|
|
1524
|
+
``"location"``.
|
|
1525
|
+
axisTo : str, optional
|
|
1526
|
+
Target axis. Defaults to *axisFrom*.
|
|
1527
|
+
typeTo : str, optional
|
|
1528
|
+
Target type. Defaults to *typeFrom*.
|
|
1529
|
+
valueOnly : bool, optional
|
|
1530
|
+
If ``True`` return a bare ``numpy`` array instead of a ``Unit``.
|
|
1531
|
+
|
|
1532
|
+
Returns
|
|
1533
|
+
-------
|
|
1534
|
+
Unit or numpy.ndarray
|
|
1535
|
+
Converted unit, or numeric array when *valueOnly* is ``True``.
|
|
1536
|
+
|
|
1537
|
+
Examples
|
|
1538
|
+
--------
|
|
1539
|
+
>>> convert_unit(Unit(1, "inches"), "cm")
|
|
1540
|
+
Unit([2.54], ['cm'])
|
|
1541
|
+
>>> convert_unit(Unit(2.54, "cm"), "inches", valueOnly=True)
|
|
1542
|
+
array([1.])
|
|
1543
|
+
"""
|
|
1544
|
+
if not isinstance(x, Unit):
|
|
1545
|
+
raise TypeError("x must be a Unit")
|
|
1546
|
+
|
|
1547
|
+
if axisTo is None:
|
|
1548
|
+
axisTo = axisFrom
|
|
1549
|
+
if typeTo is None:
|
|
1550
|
+
typeTo = typeFrom
|
|
1551
|
+
|
|
1552
|
+
target = _resolve_alias(unitTo)
|
|
1553
|
+
n = len(x)
|
|
1554
|
+
result_vals = np.empty(n, dtype=np.float64)
|
|
1555
|
+
converted = True
|
|
1556
|
+
|
|
1557
|
+
for i in range(n):
|
|
1558
|
+
src_unit = x._units[i]
|
|
1559
|
+
if src_unit in _ABSOLUTE_UNIT_TYPES and target in _ABSOLUTE_UNIT_TYPES:
|
|
1560
|
+
# Fast path: absolute → absolute (no context needed)
|
|
1561
|
+
result_vals[i] = _convert_absolute(x._values[i], src_unit, target)
|
|
1562
|
+
elif src_unit == target:
|
|
1563
|
+
# Same unit type -- no conversion needed
|
|
1564
|
+
result_vals[i] = x._values[i]
|
|
1565
|
+
else:
|
|
1566
|
+
# All other conversions go through the full two-stage pipeline
|
|
1567
|
+
# (R grid.c:1384-1575 L_convert):
|
|
1568
|
+
# Stage 1: source → inches (via renderer context)
|
|
1569
|
+
# Stage 2: inches → target (via inverse transforms)
|
|
1570
|
+
# This handles: npc, native, lines, char, snpc, strwidth,
|
|
1571
|
+
# grobwidth, compound units, absolute→context, context→absolute
|
|
1572
|
+
resolved = _try_resolve_with_renderer(
|
|
1573
|
+
x, i, src_unit, target, axisFrom, typeFrom,
|
|
1574
|
+
)
|
|
1575
|
+
if resolved is not None:
|
|
1576
|
+
result_vals[i] = resolved
|
|
1577
|
+
elif src_unit in _STR_METRIC_TYPES:
|
|
1578
|
+
# Fallback without renderer: string metric → inches → target
|
|
1579
|
+
inches_val = _eval_str_metric(src_unit, x._data[i], x._values[i])
|
|
1580
|
+
if target in _ABSOLUTE_UNIT_TYPES:
|
|
1581
|
+
result_vals[i] = inches_val / _INCHES_PER[target]
|
|
1582
|
+
else:
|
|
1583
|
+
result_vals[i] = inches_val
|
|
1584
|
+
converted = False
|
|
1585
|
+
elif src_unit in _GROB_METRIC_TYPES:
|
|
1586
|
+
# Fallback: grob metric → inches → target
|
|
1587
|
+
metric_unit = _eval_grob_metric(src_unit, x._data[i])
|
|
1588
|
+
if (
|
|
1589
|
+
metric_unit is not None
|
|
1590
|
+
and len(metric_unit) > 0
|
|
1591
|
+
and metric_unit._units[0] in _ABSOLUTE_UNIT_TYPES
|
|
1592
|
+
):
|
|
1593
|
+
src_inches = (
|
|
1594
|
+
metric_unit._values[0]
|
|
1595
|
+
* _INCHES_PER[metric_unit._units[0]]
|
|
1596
|
+
)
|
|
1597
|
+
src_inches *= x._values[i]
|
|
1598
|
+
if target in _ABSOLUTE_UNIT_TYPES:
|
|
1599
|
+
result_vals[i] = src_inches / _INCHES_PER[target]
|
|
1600
|
+
else:
|
|
1601
|
+
result_vals[i] = src_inches
|
|
1602
|
+
converted = False
|
|
1603
|
+
else:
|
|
1604
|
+
result_vals[i] = x._values[i]
|
|
1605
|
+
converted = False
|
|
1606
|
+
elif src_unit in _ABSOLUTE_UNIT_TYPES:
|
|
1607
|
+
# Absolute source → context-dependent target (no renderer)
|
|
1608
|
+
result_vals[i] = x._values[i]
|
|
1609
|
+
converted = False
|
|
1610
|
+
else:
|
|
1611
|
+
result_vals[i] = x._values[i]
|
|
1612
|
+
converted = False
|
|
1613
|
+
|
|
1614
|
+
if valueOnly:
|
|
1615
|
+
return result_vals
|
|
1616
|
+
|
|
1617
|
+
if converted:
|
|
1618
|
+
return Unit(result_vals, target)
|
|
1619
|
+
else:
|
|
1620
|
+
# Return original unchanged when full conversion is not possible
|
|
1621
|
+
import warnings
|
|
1622
|
+
|
|
1623
|
+
warnings.warn(
|
|
1624
|
+
f"Cannot convert between {set(x._units)} and {target!r} "
|
|
1625
|
+
"without a viewport context; returning unit unchanged.",
|
|
1626
|
+
stacklevel=2,
|
|
1627
|
+
)
|
|
1628
|
+
return x.copy()
|
|
1629
|
+
|
|
1630
|
+
|
|
1631
|
+
def convert_x(x: Unit, unitTo: str, valueOnly: bool = False) -> Union[Unit, np.ndarray]:
|
|
1632
|
+
"""Convert an x-axis location unit.
|
|
1633
|
+
|
|
1634
|
+
Parameters
|
|
1635
|
+
----------
|
|
1636
|
+
x : Unit
|
|
1637
|
+
Source unit.
|
|
1638
|
+
unitTo : str
|
|
1639
|
+
Target unit type.
|
|
1640
|
+
valueOnly : bool, optional
|
|
1641
|
+
Return bare numeric array if ``True``.
|
|
1642
|
+
|
|
1643
|
+
Returns
|
|
1644
|
+
-------
|
|
1645
|
+
Unit or numpy.ndarray
|
|
1646
|
+
"""
|
|
1647
|
+
return convert_unit(x, unitTo, "x", "location", "x", "location", valueOnly=valueOnly)
|
|
1648
|
+
|
|
1649
|
+
|
|
1650
|
+
def convert_y(x: Unit, unitTo: str, valueOnly: bool = False) -> Union[Unit, np.ndarray]:
|
|
1651
|
+
"""Convert a y-axis location unit.
|
|
1652
|
+
|
|
1653
|
+
Parameters
|
|
1654
|
+
----------
|
|
1655
|
+
x : Unit
|
|
1656
|
+
Source unit.
|
|
1657
|
+
unitTo : str
|
|
1658
|
+
Target unit type.
|
|
1659
|
+
valueOnly : bool, optional
|
|
1660
|
+
Return bare numeric array if ``True``.
|
|
1661
|
+
|
|
1662
|
+
Returns
|
|
1663
|
+
-------
|
|
1664
|
+
Unit or numpy.ndarray
|
|
1665
|
+
"""
|
|
1666
|
+
return convert_unit(x, unitTo, "y", "location", "y", "location", valueOnly=valueOnly)
|
|
1667
|
+
|
|
1668
|
+
|
|
1669
|
+
def convert_width(x: Unit, unitTo: str, valueOnly: bool = False) -> Union[Unit, np.ndarray]:
|
|
1670
|
+
"""Convert a width (x-axis dimension) unit.
|
|
1671
|
+
|
|
1672
|
+
Parameters
|
|
1673
|
+
----------
|
|
1674
|
+
x : Unit
|
|
1675
|
+
Source unit.
|
|
1676
|
+
unitTo : str
|
|
1677
|
+
Target unit type.
|
|
1678
|
+
valueOnly : bool, optional
|
|
1679
|
+
Return bare numeric array if ``True``.
|
|
1680
|
+
|
|
1681
|
+
Returns
|
|
1682
|
+
-------
|
|
1683
|
+
Unit or numpy.ndarray
|
|
1684
|
+
"""
|
|
1685
|
+
return convert_unit(
|
|
1686
|
+
x, unitTo, "x", "dimension", "x", "dimension", valueOnly=valueOnly
|
|
1687
|
+
)
|
|
1688
|
+
|
|
1689
|
+
|
|
1690
|
+
def convert_height(x: Unit, unitTo: str, valueOnly: bool = False) -> Union[Unit, np.ndarray]:
|
|
1691
|
+
"""Convert a height (y-axis dimension) unit.
|
|
1692
|
+
|
|
1693
|
+
Parameters
|
|
1694
|
+
----------
|
|
1695
|
+
x : Unit
|
|
1696
|
+
Source unit.
|
|
1697
|
+
unitTo : str
|
|
1698
|
+
Target unit type.
|
|
1699
|
+
valueOnly : bool, optional
|
|
1700
|
+
Return bare numeric array if ``True``.
|
|
1701
|
+
|
|
1702
|
+
Returns
|
|
1703
|
+
-------
|
|
1704
|
+
Unit or numpy.ndarray
|
|
1705
|
+
"""
|
|
1706
|
+
return convert_unit(
|
|
1707
|
+
x, unitTo, "y", "dimension", "y", "dimension", valueOnly=valueOnly
|
|
1708
|
+
)
|
|
1709
|
+
|
|
1710
|
+
|
|
1711
|
+
# ---------------------------------------------------------------------------
|
|
1712
|
+
# convertTheta -- port of R unit.R:617-629
|
|
1713
|
+
# ---------------------------------------------------------------------------
|
|
1714
|
+
|
|
1715
|
+
|
|
1716
|
+
_THETA_ALIASES: Dict[str, float] = {
|
|
1717
|
+
"east": 0.0,
|
|
1718
|
+
"north": 90.0,
|
|
1719
|
+
"west": 180.0,
|
|
1720
|
+
"south": 270.0,
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
|
|
1724
|
+
def convert_theta(theta: Any) -> float:
|
|
1725
|
+
"""Convert a theta angle to numeric degrees in [0, 360).
|
|
1726
|
+
|
|
1727
|
+
Port of R ``convertTheta()`` (unit.R:617-629).
|
|
1728
|
+
Accepts character shortcuts ``"east"`` (0), ``"north"`` (90),
|
|
1729
|
+
``"west"`` (180), ``"south"`` (270) or numeric values.
|
|
1730
|
+
|
|
1731
|
+
Parameters
|
|
1732
|
+
----------
|
|
1733
|
+
theta : str or float
|
|
1734
|
+
Angle specification.
|
|
1735
|
+
|
|
1736
|
+
Returns
|
|
1737
|
+
-------
|
|
1738
|
+
float
|
|
1739
|
+
Angle in degrees, normalised to [0, 360).
|
|
1740
|
+
|
|
1741
|
+
Raises
|
|
1742
|
+
------
|
|
1743
|
+
ValueError
|
|
1744
|
+
If *theta* is an unrecognised string.
|
|
1745
|
+
|
|
1746
|
+
Examples
|
|
1747
|
+
--------
|
|
1748
|
+
>>> convert_theta("north")
|
|
1749
|
+
90.0
|
|
1750
|
+
>>> convert_theta(450)
|
|
1751
|
+
90.0
|
|
1752
|
+
"""
|
|
1753
|
+
if isinstance(theta, str):
|
|
1754
|
+
val = _THETA_ALIASES.get(theta.lower())
|
|
1755
|
+
if val is None:
|
|
1756
|
+
raise ValueError(f"invalid theta: {theta!r}")
|
|
1757
|
+
return val
|
|
1758
|
+
return float(theta) % 360.0
|
|
1759
|
+
|
|
1760
|
+
|
|
1761
|
+
# ---------------------------------------------------------------------------
|
|
1762
|
+
# deviceLoc / deviceDim -- port of R unit.R:117-151 + grid.c:1580-1677
|
|
1763
|
+
# ---------------------------------------------------------------------------
|
|
1764
|
+
|
|
1765
|
+
|
|
1766
|
+
def device_loc(
|
|
1767
|
+
x: Unit,
|
|
1768
|
+
y: Unit,
|
|
1769
|
+
value_only: bool = False,
|
|
1770
|
+
device: bool = False,
|
|
1771
|
+
) -> dict:
|
|
1772
|
+
"""Convert grid locations to absolute device coordinates.
|
|
1773
|
+
|
|
1774
|
+
Port of R ``deviceLoc()`` (unit.R:117-133) + ``L_devLoc`` (grid.c:1580-1628).
|
|
1775
|
+
For each (x[i], y[i]) pair:
|
|
1776
|
+
1. Convert x to inches via transformXtoINCHES
|
|
1777
|
+
2. Convert y to inches via transformYtoINCHES
|
|
1778
|
+
3. Apply the viewport 3×3 transform (transformLocn: location → trans)
|
|
1779
|
+
4. Optionally convert to device coordinates
|
|
1780
|
+
|
|
1781
|
+
Parameters
|
|
1782
|
+
----------
|
|
1783
|
+
x, y : Unit
|
|
1784
|
+
Location units.
|
|
1785
|
+
value_only : bool
|
|
1786
|
+
If True, return raw numeric arrays. Otherwise return Unit objects.
|
|
1787
|
+
device : bool
|
|
1788
|
+
If True, return in device-native coordinates (pixels).
|
|
1789
|
+
If False, return in absolute inches.
|
|
1790
|
+
|
|
1791
|
+
Returns
|
|
1792
|
+
-------
|
|
1793
|
+
dict
|
|
1794
|
+
``{'x': ..., 'y': ...}`` — each is either a Unit or ndarray.
|
|
1795
|
+
"""
|
|
1796
|
+
from ._state import get_state
|
|
1797
|
+
from ._vp_calc import location, trans
|
|
1798
|
+
|
|
1799
|
+
state = get_state()
|
|
1800
|
+
renderer = state.get_renderer()
|
|
1801
|
+
|
|
1802
|
+
if renderer is None:
|
|
1803
|
+
raise RuntimeError("deviceLoc requires an active renderer")
|
|
1804
|
+
|
|
1805
|
+
vtr = renderer._vp_transform_stack[-1]
|
|
1806
|
+
gp = state.get_gpar()
|
|
1807
|
+
|
|
1808
|
+
nx = len(x)
|
|
1809
|
+
ny = len(y)
|
|
1810
|
+
maxn = max(nx, ny)
|
|
1811
|
+
|
|
1812
|
+
out_x = np.empty(maxn, dtype=np.float64)
|
|
1813
|
+
out_y = np.empty(maxn, dtype=np.float64)
|
|
1814
|
+
|
|
1815
|
+
for i in range(maxn):
|
|
1816
|
+
# Stage 1: resolve to inches within viewport
|
|
1817
|
+
# R grid.c:1612-1616 transformLocn()
|
|
1818
|
+
xx = renderer._resolve_to_inches_idx(x, i % nx, "x", False, gp)
|
|
1819
|
+
yy = renderer._resolve_to_inches_idx(y, i % ny, "y", False, gp)
|
|
1820
|
+
|
|
1821
|
+
# Stage 2: apply viewport 3×3 transform to get absolute inches
|
|
1822
|
+
# R unit.c:1168-1171 location→trans
|
|
1823
|
+
loc = location(xx, yy)
|
|
1824
|
+
abs_loc = trans(loc, vtr.transform)
|
|
1825
|
+
xx = abs_loc[0]
|
|
1826
|
+
yy = abs_loc[1]
|
|
1827
|
+
|
|
1828
|
+
if device:
|
|
1829
|
+
# Convert absolute inches to device pixels
|
|
1830
|
+
# R grid.c:1618-1619 toDeviceX/Y
|
|
1831
|
+
xx = renderer.inches_to_dev_x(xx)
|
|
1832
|
+
yy = renderer.inches_to_dev_y(yy)
|
|
1833
|
+
|
|
1834
|
+
out_x[i] = xx
|
|
1835
|
+
out_y[i] = yy
|
|
1836
|
+
|
|
1837
|
+
if value_only:
|
|
1838
|
+
return {"x": out_x, "y": out_y}
|
|
1839
|
+
else:
|
|
1840
|
+
if device:
|
|
1841
|
+
return {"x": Unit(out_x, "native"), "y": Unit(out_y, "native")}
|
|
1842
|
+
else:
|
|
1843
|
+
return {"x": Unit(out_x, "inches"), "y": Unit(out_y, "inches")}
|
|
1844
|
+
|
|
1845
|
+
|
|
1846
|
+
def device_dim(
|
|
1847
|
+
w: Unit,
|
|
1848
|
+
h: Unit,
|
|
1849
|
+
value_only: bool = False,
|
|
1850
|
+
device: bool = False,
|
|
1851
|
+
) -> dict:
|
|
1852
|
+
"""Convert grid dimensions to absolute device dimensions.
|
|
1853
|
+
|
|
1854
|
+
Port of R ``deviceDim()`` (unit.R:135-151) + ``L_devDim`` (grid.c:1630-1677).
|
|
1855
|
+
For each (w[i], h[i]) pair:
|
|
1856
|
+
1. Convert w to inches via transformWidthtoINCHES
|
|
1857
|
+
2. Convert h to inches via transformHeighttoINCHES
|
|
1858
|
+
3. Apply rotation transform (transformDimn)
|
|
1859
|
+
4. Optionally convert to device units
|
|
1860
|
+
|
|
1861
|
+
Parameters
|
|
1862
|
+
----------
|
|
1863
|
+
w, h : Unit
|
|
1864
|
+
Dimension units.
|
|
1865
|
+
value_only : bool
|
|
1866
|
+
If True, return raw numeric arrays. Otherwise return Unit objects.
|
|
1867
|
+
device : bool
|
|
1868
|
+
If True, return in device-native units (pixels).
|
|
1869
|
+
If False, return in absolute inches.
|
|
1870
|
+
|
|
1871
|
+
Returns
|
|
1872
|
+
-------
|
|
1873
|
+
dict
|
|
1874
|
+
``{'w': ..., 'h': ...}`` — each is either a Unit or ndarray.
|
|
1875
|
+
"""
|
|
1876
|
+
import math
|
|
1877
|
+
from ._state import get_state
|
|
1878
|
+
from ._vp_calc import location, rotation, trans
|
|
1879
|
+
|
|
1880
|
+
state = get_state()
|
|
1881
|
+
renderer = state.get_renderer()
|
|
1882
|
+
|
|
1883
|
+
if renderer is None:
|
|
1884
|
+
raise RuntimeError("deviceDim requires an active renderer")
|
|
1885
|
+
|
|
1886
|
+
vtr = renderer._vp_transform_stack[-1]
|
|
1887
|
+
gp = state.get_gpar()
|
|
1888
|
+
rotation_angle = vtr.rotation_angle
|
|
1889
|
+
|
|
1890
|
+
nw = len(w)
|
|
1891
|
+
nh = len(h)
|
|
1892
|
+
maxn = max(nw, nh)
|
|
1893
|
+
|
|
1894
|
+
out_w = np.empty(maxn, dtype=np.float64)
|
|
1895
|
+
out_h = np.empty(maxn, dtype=np.float64)
|
|
1896
|
+
|
|
1897
|
+
for i in range(maxn):
|
|
1898
|
+
# Stage 1: resolve to inches within viewport
|
|
1899
|
+
ww = renderer._resolve_to_inches_idx(w, i % nw, "x", True, gp)
|
|
1900
|
+
hh = renderer._resolve_to_inches_idx(h, i % nh, "y", True, gp)
|
|
1901
|
+
|
|
1902
|
+
# Stage 2: apply rotation (R unit.c:1208-1212 transformDimn)
|
|
1903
|
+
# R: location(ww,hh,din); rotation(angle,r); trans(din,r,dout);
|
|
1904
|
+
din = location(ww, hh)
|
|
1905
|
+
rot = rotation(rotation_angle)
|
|
1906
|
+
dout = trans(din, rot)
|
|
1907
|
+
ww = dout[0]
|
|
1908
|
+
hh = dout[1]
|
|
1909
|
+
|
|
1910
|
+
if device:
|
|
1911
|
+
# Convert absolute inches to device pixels
|
|
1912
|
+
ww = renderer.inches_to_dev_w(ww)
|
|
1913
|
+
hh = renderer.inches_to_dev_h(hh)
|
|
1914
|
+
|
|
1915
|
+
out_w[i] = ww
|
|
1916
|
+
out_h[i] = hh
|
|
1917
|
+
|
|
1918
|
+
if value_only:
|
|
1919
|
+
return {"w": out_w, "h": out_h}
|
|
1920
|
+
else:
|
|
1921
|
+
if device:
|
|
1922
|
+
return {"w": Unit(out_w, "native"), "h": Unit(out_h, "native")}
|
|
1923
|
+
else:
|
|
1924
|
+
return {"w": Unit(out_w, "inches"), "h": Unit(out_h, "inches")}
|