rgrid-python 4.5.3__tar.gz → 4.5.3.post2__tar.gz
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.
- {rgrid_python-4.5.3 → rgrid_python-4.5.3.post2}/PKG-INFO +3 -1
- {rgrid_python-4.5.3 → rgrid_python-4.5.3.post2}/README.md +2 -0
- {rgrid_python-4.5.3 → rgrid_python-4.5.3.post2}/grid_py/__init__.py +5 -3
- {rgrid_python-4.5.3 → rgrid_python-4.5.3.post2}/grid_py/_draw.py +31 -5
- {rgrid_python-4.5.3 → rgrid_python-4.5.3.post2}/grid_py/_gpar.py +30 -3
- {rgrid_python-4.5.3 → rgrid_python-4.5.3.post2}/grid_py/_layout.py +9 -2
- {rgrid_python-4.5.3 → rgrid_python-4.5.3.post2}/grid_py/_renderer_base.py +119 -10
- {rgrid_python-4.5.3 → rgrid_python-4.5.3.post2}/grid_py/_size.py +36 -10
- {rgrid_python-4.5.3 → rgrid_python-4.5.3.post2}/grid_py/_state.py +42 -0
- {rgrid_python-4.5.3 → rgrid_python-4.5.3.post2}/grid_py/_units.py +90 -11
- {rgrid_python-4.5.3 → rgrid_python-4.5.3.post2}/grid_py/renderer.py +8 -6
- {rgrid_python-4.5.3 → rgrid_python-4.5.3.post2}/pyproject.toml +1 -1
- {rgrid_python-4.5.3 → rgrid_python-4.5.3.post2}/.gitattributes +0 -0
- {rgrid_python-4.5.3 → rgrid_python-4.5.3.post2}/.gitignore +0 -0
- {rgrid_python-4.5.3 → rgrid_python-4.5.3.post2}/LICENSE +0 -0
- {rgrid_python-4.5.3 → rgrid_python-4.5.3.post2}/grid_py/_arrow.py +0 -0
- {rgrid_python-4.5.3 → rgrid_python-4.5.3.post2}/grid_py/_clippath.py +0 -0
- {rgrid_python-4.5.3 → rgrid_python-4.5.3.post2}/grid_py/_colour.py +0 -0
- {rgrid_python-4.5.3 → rgrid_python-4.5.3.post2}/grid_py/_coords.py +0 -0
- {rgrid_python-4.5.3 → rgrid_python-4.5.3.post2}/grid_py/_curve.py +0 -0
- {rgrid_python-4.5.3 → rgrid_python-4.5.3.post2}/grid_py/_display_list.py +0 -0
- {rgrid_python-4.5.3 → rgrid_python-4.5.3.post2}/grid_py/_edit.py +0 -0
- {rgrid_python-4.5.3 → rgrid_python-4.5.3.post2}/grid_py/_font_metrics.py +0 -0
- {rgrid_python-4.5.3 → rgrid_python-4.5.3.post2}/grid_py/_grab.py +0 -0
- {rgrid_python-4.5.3 → rgrid_python-4.5.3.post2}/grid_py/_grob.py +0 -0
- {rgrid_python-4.5.3 → rgrid_python-4.5.3.post2}/grid_py/_group.py +0 -0
- {rgrid_python-4.5.3 → rgrid_python-4.5.3.post2}/grid_py/_highlevel.py +0 -0
- {rgrid_python-4.5.3 → rgrid_python-4.5.3.post2}/grid_py/_just.py +0 -0
- {rgrid_python-4.5.3 → rgrid_python-4.5.3.post2}/grid_py/_ls.py +0 -0
- {rgrid_python-4.5.3 → rgrid_python-4.5.3.post2}/grid_py/_mask.py +0 -0
- {rgrid_python-4.5.3 → rgrid_python-4.5.3.post2}/grid_py/_path.py +0 -0
- {rgrid_python-4.5.3 → rgrid_python-4.5.3.post2}/grid_py/_patterns.py +0 -0
- {rgrid_python-4.5.3 → rgrid_python-4.5.3.post2}/grid_py/_primitives.py +0 -0
- {rgrid_python-4.5.3 → rgrid_python-4.5.3.post2}/grid_py/_scene_graph.py +0 -0
- {rgrid_python-4.5.3 → rgrid_python-4.5.3.post2}/grid_py/_transforms.py +0 -0
- {rgrid_python-4.5.3 → rgrid_python-4.5.3.post2}/grid_py/_typeset.py +0 -0
- {rgrid_python-4.5.3 → rgrid_python-4.5.3.post2}/grid_py/_utils.py +0 -0
- {rgrid_python-4.5.3 → rgrid_python-4.5.3.post2}/grid_py/_viewport.py +0 -0
- {rgrid_python-4.5.3 → rgrid_python-4.5.3.post2}/grid_py/_vp_calc.py +0 -0
- {rgrid_python-4.5.3 → rgrid_python-4.5.3.post2}/grid_py/py.typed +0 -0
- {rgrid_python-4.5.3 → rgrid_python-4.5.3.post2}/grid_py/renderer_web.py +0 -0
- {rgrid_python-4.5.3 → rgrid_python-4.5.3.post2}/grid_py/resources/d3.v7.min.js +0 -0
- {rgrid_python-4.5.3 → rgrid_python-4.5.3.post2}/grid_py/resources/gridpy.css +0 -0
- {rgrid_python-4.5.3 → rgrid_python-4.5.3.post2}/grid_py/resources/gridpy.js +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rgrid-python
|
|
3
|
-
Version: 4.5.3
|
|
3
|
+
Version: 4.5.3.post2
|
|
4
4
|
Summary: Python port of the R grid package (tracks R grid 4.5.3)
|
|
5
5
|
Project-URL: Homepage, https://github.com/Bio-Babel/grid_py
|
|
6
6
|
Project-URL: Repository, https://github.com/Bio-Babel/grid_py
|
|
@@ -41,6 +41,8 @@ Description-Content-Type: text/markdown
|
|
|
41
41
|
|
|
42
42
|
# grid_py
|
|
43
43
|
|
|
44
|
+
[](https://pypi.org/project/rgrid-python/)
|
|
45
|
+
|
|
44
46
|
Python port of the R **grid** package.
|
|
45
47
|
|
|
46
48
|
## Installation
|
|
@@ -6,7 +6,7 @@ units, viewports, grobs (graphical objects), layouts, and rendering via
|
|
|
6
6
|
Cairo (pycairo).
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
-
__version__ = "4.5.3"
|
|
9
|
+
__version__ = "4.5.3.post2"
|
|
10
10
|
|
|
11
11
|
# --- Utilities ---
|
|
12
12
|
from grid_py._utils import depth, explode, grid_pretty, n2mfrow
|
|
@@ -18,13 +18,14 @@ from grid_py._just import valid_just, resolve_hjust, resolve_vjust, resolve_rast
|
|
|
18
18
|
from grid_py._units import (
|
|
19
19
|
Unit, is_unit, unit_type, unit_c, unit_length,
|
|
20
20
|
unit_pmax, unit_pmin, unit_psum, unit_rep,
|
|
21
|
+
unit_summary_sum, unit_summary_min, unit_summary_max,
|
|
21
22
|
string_width, string_height, string_ascent, string_descent,
|
|
22
23
|
absolute_size,
|
|
23
24
|
convert_unit, convert_x, convert_y, convert_width, convert_height,
|
|
24
25
|
)
|
|
25
26
|
|
|
26
27
|
# --- Graphical Parameters ---
|
|
27
|
-
from grid_py._gpar import Gpar, get_gpar
|
|
28
|
+
from grid_py._gpar import Gpar, get_gpar, gpar
|
|
28
29
|
|
|
29
30
|
# --- Arrow ---
|
|
30
31
|
from grid_py._arrow import Arrow, arrow
|
|
@@ -226,11 +227,12 @@ __all__ = [
|
|
|
226
227
|
# Units
|
|
227
228
|
"Unit", "is_unit", "unit_type", "unit_c", "unit_length",
|
|
228
229
|
"unit_pmax", "unit_pmin", "unit_psum", "unit_rep",
|
|
230
|
+
"unit_summary_sum", "unit_summary_min", "unit_summary_max",
|
|
229
231
|
"string_width", "string_height", "string_ascent", "string_descent",
|
|
230
232
|
"absolute_size",
|
|
231
233
|
"convert_unit", "convert_x", "convert_y", "convert_width", "convert_height",
|
|
232
234
|
# Gpar
|
|
233
|
-
"Gpar", "get_gpar",
|
|
235
|
+
"Gpar", "gpar", "get_gpar",
|
|
234
236
|
# Arrow
|
|
235
237
|
"Arrow", "arrow",
|
|
236
238
|
# Path
|
|
@@ -109,6 +109,12 @@ def _subset_gpar(gp: Optional[Gpar], i: int) -> Optional[Gpar]:
|
|
|
109
109
|
picked = val[i % len(val)]
|
|
110
110
|
elif isinstance(val, (list, tuple)) and len(val) > 1:
|
|
111
111
|
picked = val[i % len(val)]
|
|
112
|
+
elif isinstance(val, (list, tuple)) and len(val) == 1 \
|
|
113
|
+
and val[0] is None and key in ("col", "fill"):
|
|
114
|
+
# Length-1 [None] NA sentinel from Gpar(col=None)/Gpar(fill=None).
|
|
115
|
+
# Preserve the NA intent across subset — emit "transparent" so
|
|
116
|
+
# the renderer's scalar-colour path parses to (0,0,0,0).
|
|
117
|
+
picked = "transparent"
|
|
112
118
|
else:
|
|
113
119
|
picked = val
|
|
114
120
|
|
|
@@ -684,24 +690,44 @@ def _pop_grob_vp(vp: Any) -> None:
|
|
|
684
690
|
def _vp_depth(vp: Any) -> int:
|
|
685
691
|
"""Return the depth of a viewport (number of levels it adds).
|
|
686
692
|
|
|
693
|
+
Mirrors R's ``depth()`` generic (grid R/grid.R) for every viewport
|
|
694
|
+
container: VpStack pushes ``sum(depth(child))`` levels, VpList
|
|
695
|
+
pushes ``depth(last)``, VpTree pushes ``depth(parent) + depth(last
|
|
696
|
+
child)``, plain Viewport pushes 1, VpPath pushes ``n``.
|
|
697
|
+
|
|
698
|
+
Recognised types are routed through :func:`._viewport.depth` (the
|
|
699
|
+
canonical R-faithful dispatch); this is what guarantees
|
|
700
|
+
``_pop_grob_vp(grob.vp)`` pops the right count when a Gtable's
|
|
701
|
+
``make_context`` returns ``VpStack(orig_vp, layout_vp)``. The
|
|
702
|
+
prior implementation only checked ``hasattr(vp, "depth")`` —
|
|
703
|
+
VpStack/VpList don't expose a method, so it fell through to
|
|
704
|
+
``return 1`` and left every nested layout vp on the layout_stack
|
|
705
|
+
after the gtable rendered, breaking every grob drawn after.
|
|
706
|
+
|
|
707
|
+
Duck-typed objects with a ``.depth()`` method (used by tests and
|
|
708
|
+
user-defined viewport-like objects) are still honoured as a
|
|
709
|
+
fallback for unknown types.
|
|
710
|
+
|
|
687
711
|
Parameters
|
|
688
712
|
----------
|
|
689
713
|
vp : Any
|
|
690
|
-
A viewport, VpPath, VpStack, VpList, or
|
|
714
|
+
A viewport, VpPath, VpStack, VpList, VpTree, or any
|
|
715
|
+
depth-bearing duck-typed object.
|
|
691
716
|
|
|
692
717
|
Returns
|
|
693
718
|
-------
|
|
694
719
|
int
|
|
695
720
|
The depth.
|
|
696
721
|
"""
|
|
722
|
+
from ._viewport import (
|
|
723
|
+
Viewport, VpList, VpStack, VpTree, depth as _depth,
|
|
724
|
+
)
|
|
697
725
|
from ._path import VpPath
|
|
698
726
|
|
|
699
|
-
if isinstance(vp, VpPath):
|
|
700
|
-
|
|
701
|
-
return getattr(vp, "n", 1)
|
|
727
|
+
if isinstance(vp, (Viewport, VpList, VpStack, VpTree, VpPath)):
|
|
728
|
+
return _depth(vp)
|
|
702
729
|
if hasattr(vp, "depth"):
|
|
703
730
|
return vp.depth()
|
|
704
|
-
# Default single viewport depth
|
|
705
731
|
return 1
|
|
706
732
|
|
|
707
733
|
|
|
@@ -14,7 +14,7 @@ from typing import Any, Dict, List, Optional, Sequence, Union
|
|
|
14
14
|
|
|
15
15
|
import numpy as np
|
|
16
16
|
|
|
17
|
-
__all__ = ["Gpar", "get_gpar"]
|
|
17
|
+
__all__ = ["Gpar", "gpar", "get_gpar"]
|
|
18
18
|
|
|
19
19
|
# ---------------------------------------------------------------------------
|
|
20
20
|
# Constants
|
|
@@ -199,6 +199,14 @@ class Gpar:
|
|
|
199
199
|
# -- process remaining parameters ----------------------------------
|
|
200
200
|
for name, value in kwargs.items():
|
|
201
201
|
if value is None:
|
|
202
|
+
# Colour parameters: preserve explicit None as a one-element
|
|
203
|
+
# NA sentinel so the renderer can tell "col absent (inherit,
|
|
204
|
+
# default black)" apart from "col explicitly NA (transparent)".
|
|
205
|
+
# Mirrors R's ``gpar(col=NA)`` / ``gpar(fill=NA)`` semantics
|
|
206
|
+
# (see R grid src/gpar.c gpCol(): isNull(col) → R_TRANWHITE).
|
|
207
|
+
# Other parameters have no NA semantic; drop silently.
|
|
208
|
+
if name in ("col", "fill"):
|
|
209
|
+
params[name] = [None]
|
|
202
210
|
continue
|
|
203
211
|
|
|
204
212
|
vals = _as_list(value)
|
|
@@ -283,8 +291,16 @@ class Gpar:
|
|
|
283
291
|
# backend, matching R's behaviour).
|
|
284
292
|
pass
|
|
285
293
|
|
|
286
|
-
# Store single-element lists as scalars for cleaner repr
|
|
287
|
-
|
|
294
|
+
# Store single-element lists as scalars for cleaner repr,
|
|
295
|
+
# except for the colour NA sentinel [None] which must stay a
|
|
296
|
+
# sequence so the renderer treats it as R's gpar(col=NA).
|
|
297
|
+
if len(vals) == 1:
|
|
298
|
+
if name in ("col", "fill") and vals[0] is None:
|
|
299
|
+
params[name] = [None]
|
|
300
|
+
else:
|
|
301
|
+
params[name] = vals[0]
|
|
302
|
+
else:
|
|
303
|
+
params[name] = vals
|
|
288
304
|
|
|
289
305
|
self._params = params
|
|
290
306
|
|
|
@@ -570,3 +586,14 @@ def get_gpar(names: Optional[Sequence[str]] = None) -> Gpar:
|
|
|
570
586
|
gp = object.__new__(Gpar)
|
|
571
587
|
gp._params = subset
|
|
572
588
|
return gp
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def gpar(**kwargs: Any) -> Gpar:
|
|
592
|
+
"""Factory mirroring R ``grid::gpar(...)``.
|
|
593
|
+
|
|
594
|
+
R's ``gpar`` function constructs a ``gpar`` object from arbitrary
|
|
595
|
+
keyword arguments. ``Gpar(**kwargs)`` does the same, so this is a
|
|
596
|
+
thin alias kept for direct R-to-Python translation of code that
|
|
597
|
+
reads ``gpar(col="red")``.
|
|
598
|
+
"""
|
|
599
|
+
return Gpar(**kwargs)
|
|
@@ -400,16 +400,23 @@ def _calc_layout_sizes(
|
|
|
400
400
|
reduced_h = max(reduced_h, 0.0)
|
|
401
401
|
|
|
402
402
|
# ---- Phase 2: allocate respected null units (layout.c:allocateRespected)
|
|
403
|
+
# R sums ALL relative widths/heights via totalWidth/totalHeight
|
|
404
|
+
# (layout.c:154-194) — not just the respected ones — when computing
|
|
405
|
+
# the aspect-ratio normalisation denominator. This is what lets a
|
|
406
|
+
# respect matrix that marks a single cell coexist with other null
|
|
407
|
+
# cells in the same dimension; restricting the sum to respected
|
|
408
|
+
# cells (the prior Python implementation) over-allocated the
|
|
409
|
+
# respected slot and collapsed everything else to 0.
|
|
403
410
|
if layout._valid_respect > 0 and (reduced_w > 0 or reduced_h > 0):
|
|
404
411
|
sum_w = sum(
|
|
405
412
|
float(widths._values[i])
|
|
406
413
|
for i in range(ncol)
|
|
407
|
-
if relative_w[i]
|
|
414
|
+
if relative_w[i]
|
|
408
415
|
)
|
|
409
416
|
sum_h = sum(
|
|
410
417
|
float(heights._values[j])
|
|
411
418
|
for j in range(nrow)
|
|
412
|
-
if relative_h[j]
|
|
419
|
+
if relative_h[j]
|
|
413
420
|
)
|
|
414
421
|
|
|
415
422
|
temp_w = reduced_w
|
|
@@ -249,9 +249,30 @@ class GridRenderer(ABC):
|
|
|
249
249
|
# Cell position in parent's NPC then inches
|
|
250
250
|
parent_w_dev = parent_vtr.width_cm / 2.54 * self._dev_units_per_inch
|
|
251
251
|
parent_h_dev = parent_vtr.height_cm / 2.54 * self._dev_units_per_inch
|
|
252
|
-
|
|
253
|
-
#
|
|
254
|
-
|
|
252
|
+
|
|
253
|
+
# Apply layout-level hjust / vjust (R: layout.c::subRegion
|
|
254
|
+
# lines 447-450). When the cells' total dimensions don't
|
|
255
|
+
# fill the parent vp (common for chrome-merge slices in
|
|
256
|
+
# patchwork), the leftover space is distributed per the
|
|
257
|
+
# layout's justification (default 0.5 → centred). The
|
|
258
|
+
# prior implementation always pinned cells to top-left
|
|
259
|
+
# (vjust=1, hjust=0), which placed merged chrome at the
|
|
260
|
+
# top of the parent panel cell rather than just outside
|
|
261
|
+
# it, breaking simplify_fixed renders.
|
|
262
|
+
hjust = float(grid.get("hjust", 0.0))
|
|
263
|
+
vjust = float(grid.get("vjust", 1.0))
|
|
264
|
+
total_w_dev = sum(col_widths)
|
|
265
|
+
total_h_dev = sum(row_heights)
|
|
266
|
+
hjust_offset_dev = hjust * (parent_w_dev - total_w_dev)
|
|
267
|
+
vjust_offset_dev = (1.0 - vjust) * (parent_h_dev - total_h_dev)
|
|
268
|
+
|
|
269
|
+
cell_x_in = (cell_x0_dev + hjust_offset_dev) / self._dev_units_per_inch
|
|
270
|
+
# Device y is top-down; convert to bottom-up inches.
|
|
271
|
+
# vjust_offset_dev shifts the block downward in device-y
|
|
272
|
+
# (i.e. upward in bottom-up coords) by the leftover * (1-vjust).
|
|
273
|
+
cell_y_in = parent_h_in - (
|
|
274
|
+
cell_y0_dev + cell_h_dev + vjust_offset_dev
|
|
275
|
+
) / self._dev_units_per_inch
|
|
255
276
|
|
|
256
277
|
# Build a simple translation transform for the cell
|
|
257
278
|
from ._vp_calc import translation, multiply
|
|
@@ -280,9 +301,12 @@ class GridRenderer(ABC):
|
|
|
280
301
|
if layout is not None:
|
|
281
302
|
w_dev = cell_w_dev
|
|
282
303
|
h_dev = cell_h_dev
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
304
|
+
# R: ``calcViewportLayout`` (layout.c:492-590) reads
|
|
305
|
+
# ``layoutRespect(layout)`` / ``layoutRespectMat(layout)``
|
|
306
|
+
# straight off the layout object — there is no caller-side
|
|
307
|
+
# respect argument. Mirror that: ``_calc_layout_sizes``
|
|
308
|
+
# consumes ``layout._valid_respect`` directly.
|
|
309
|
+
grid_info = self._compute_grid(layout, w_dev, h_dev)
|
|
286
310
|
self._layout_stack.append(grid_info)
|
|
287
311
|
self._layout_depth_stack.append(
|
|
288
312
|
len(self._vp_transform_stack))
|
|
@@ -294,8 +318,9 @@ class GridRenderer(ABC):
|
|
|
294
318
|
# Compute grid in device units for layout children.
|
|
295
319
|
w_dev = parent_vtr.width_cm / 2.54 * self._dev_units_per_inch
|
|
296
320
|
h_dev = parent_vtr.height_cm / 2.54 * self._dev_units_per_inch
|
|
297
|
-
|
|
298
|
-
|
|
321
|
+
# See R layout.c:492-590 — respect lives on the layout object;
|
|
322
|
+
# callers don't pass it down.
|
|
323
|
+
grid_info = self._compute_grid(layout, w_dev, h_dev)
|
|
299
324
|
|
|
300
325
|
# The layout viewport itself has the same transform as parent
|
|
301
326
|
# but we create a new VTR with the vp's xscale/yscale
|
|
@@ -388,9 +413,15 @@ class GridRenderer(ABC):
|
|
|
388
413
|
|
|
389
414
|
def _compute_grid(
|
|
390
415
|
self, layout: Any, parent_w: float, parent_h: float,
|
|
391
|
-
respect: bool = False,
|
|
392
416
|
) -> dict:
|
|
393
|
-
"""Compute row/column positions for a GridLayout within the parent.
|
|
417
|
+
"""Compute row/column positions for a GridLayout within the parent.
|
|
418
|
+
|
|
419
|
+
R reference: ``layout.c:calcViewportLayout`` (lines 492-590).
|
|
420
|
+
Respect (full or matrix-form) lives on the layout object itself —
|
|
421
|
+
``_calc_layout_sizes`` reads ``layout._valid_respect`` directly,
|
|
422
|
+
so this signature has no ``respect`` argument (matches R, where
|
|
423
|
+
there is no caller-side respect parameter either).
|
|
424
|
+
"""
|
|
394
425
|
from ._layout import _calc_layout_sizes, GridLayout
|
|
395
426
|
|
|
396
427
|
if isinstance(layout, GridLayout):
|
|
@@ -412,9 +443,24 @@ class GridRenderer(ABC):
|
|
|
412
443
|
col_starts = [sum(col_widths[:i]) for i in range(ncol)]
|
|
413
444
|
row_starts = [sum(row_heights[:i]) for i in range(nrow)]
|
|
414
445
|
|
|
446
|
+
# Layout justification (R: layout.c::subRegion lines 433-450).
|
|
447
|
+
# When the cells' total size is less than the parent vp, hjust /
|
|
448
|
+
# vjust shift the layout block within the parent (default 0.5 →
|
|
449
|
+
# centred). ``GridLayout`` carries this on ``_valid_just`` as
|
|
450
|
+
# (hjust, vjust); fall back to (0, 1) for non-GridLayout
|
|
451
|
+
# containers (matching the prior top-left alignment).
|
|
452
|
+
valid_just = getattr(layout, "_valid_just", None)
|
|
453
|
+
if valid_just is not None:
|
|
454
|
+
hjust = float(valid_just[0])
|
|
455
|
+
vjust = float(valid_just[1])
|
|
456
|
+
else:
|
|
457
|
+
hjust, vjust = 0.0, 1.0
|
|
458
|
+
|
|
415
459
|
return {
|
|
416
460
|
"col_starts": col_starts, "col_widths": col_widths,
|
|
417
461
|
"row_starts": row_starts, "row_heights": row_heights,
|
|
462
|
+
"parent_w": parent_w, "parent_h": parent_h,
|
|
463
|
+
"hjust": hjust, "vjust": vjust,
|
|
418
464
|
}
|
|
419
465
|
|
|
420
466
|
def _resolve_sizes(self, unit_obj: Any, n: int, total: float,
|
|
@@ -492,6 +538,28 @@ class GridRenderer(ABC):
|
|
|
492
538
|
# evaluateGrobUnit -- port of R unit.c:325-590 #
|
|
493
539
|
# ===================================================================== #
|
|
494
540
|
|
|
541
|
+
# Per-renderer cycle guard — populated with id(grob) for every grob
|
|
542
|
+
# currently being measured. ``_evaluate_grob_unit`` consults this
|
|
543
|
+
# before pushing the grob's vp; re-entry for the same grob means
|
|
544
|
+
# the vp itself references its own grob*Details (e.g.
|
|
545
|
+
# ``viewport(width = grobWidth(self))``), which would otherwise
|
|
546
|
+
# infinite-recurse through calcViewportTransform → grob_metric_fn
|
|
547
|
+
# → _evaluate_grob_unit → push vp → ...
|
|
548
|
+
#
|
|
549
|
+
# R bounds the same recursion implicitly because its
|
|
550
|
+
# widthDetails.gtable returns an absolute mm sum, and R's
|
|
551
|
+
# setviewport/calcViewportTransform doesn't re-trigger the metric
|
|
552
|
+
# path during the recursive pushViewport (verified empirically:
|
|
553
|
+
# preDraw.gtable runs exactly 3 times for one toplevel grid.draw —
|
|
554
|
+
# not unbounded). We mirror the bound explicitly.
|
|
555
|
+
@property
|
|
556
|
+
def _measuring_grobs(self) -> set:
|
|
557
|
+
s = getattr(self, "_measuring_grobs_set", None)
|
|
558
|
+
if s is None:
|
|
559
|
+
s = set()
|
|
560
|
+
self._measuring_grobs_set = s
|
|
561
|
+
return s
|
|
562
|
+
|
|
495
563
|
def _evaluate_grob_unit(
|
|
496
564
|
self,
|
|
497
565
|
grob: Any,
|
|
@@ -545,6 +613,46 @@ class GridRenderer(ABC):
|
|
|
545
613
|
if not isinstance(grob, Grob):
|
|
546
614
|
return 0.0
|
|
547
615
|
|
|
616
|
+
# --- Cycle break for self-referential grobwidth/grobheight ---
|
|
617
|
+
# When we're already measuring this grob and the grob's vp
|
|
618
|
+
# references its own width/height (e.g. patchwork's
|
|
619
|
+
# ``as_patch.GT`` builds ``viewport(width=grobWidth(grob))``),
|
|
620
|
+
# the recursive vp push would trigger _evaluate_grob_unit for
|
|
621
|
+
# the same grob again, etc. Resolve widthDetails/heightDetails
|
|
622
|
+
# directly without the recursive push — for non-context-
|
|
623
|
+
# dependent grobs (gtable widthDetails returns absolute_size of
|
|
624
|
+
# the widths sum) this gives the same answer as the full path,
|
|
625
|
+
# matching R's behaviour where preDraw.gtable runs a bounded
|
|
626
|
+
# number of times rather than unboundedly.
|
|
627
|
+
if id(grob) in self._measuring_grobs:
|
|
628
|
+
if unit_type == "grobwidth":
|
|
629
|
+
ru = width_details(grob)
|
|
630
|
+
axis = "x"
|
|
631
|
+
elif unit_type == "grobheight":
|
|
632
|
+
ru = height_details(grob)
|
|
633
|
+
axis = "y"
|
|
634
|
+
elif unit_type == "grobascent":
|
|
635
|
+
ru = ascent_details(grob)
|
|
636
|
+
axis = "y"
|
|
637
|
+
elif unit_type == "grobdescent":
|
|
638
|
+
ru = descent_details(grob)
|
|
639
|
+
axis = "y"
|
|
640
|
+
else:
|
|
641
|
+
return 0.0
|
|
642
|
+
if ru is None:
|
|
643
|
+
return 0.0
|
|
644
|
+
from ._units import Unit as _U
|
|
645
|
+
if not isinstance(ru, _U):
|
|
646
|
+
return 0.0
|
|
647
|
+
if len(ru) == 1 and ru._units[0] == "null":
|
|
648
|
+
return 0.0
|
|
649
|
+
return float(self._resolve_to_inches(ru, axis, True))
|
|
650
|
+
|
|
651
|
+
# Capture id BEFORE make_context() rebinds ``grob`` to the
|
|
652
|
+
# context-wrapped copy — we need to release the same id we added.
|
|
653
|
+
_measuring_id = id(grob)
|
|
654
|
+
self._measuring_grobs.add(_measuring_id)
|
|
655
|
+
|
|
548
656
|
# --- Save state (R unit.c:355-377) ---
|
|
549
657
|
saved_dl_on = state._dl_on
|
|
550
658
|
state.set_display_list_on(False)
|
|
@@ -620,6 +728,7 @@ class GridRenderer(ABC):
|
|
|
620
728
|
state.replace_gpar(saved_gpar)
|
|
621
729
|
state._current_grob = saved_current_grob
|
|
622
730
|
state.set_display_list_on(saved_dl_on)
|
|
731
|
+
self._measuring_grobs.discard(_measuring_id)
|
|
623
732
|
|
|
624
733
|
return result
|
|
625
734
|
|
|
@@ -232,13 +232,36 @@ def _resolve_grob_gp(grob: Any) -> "Optional[Gpar]":
|
|
|
232
232
|
def _text_bbox(grob: Any) -> tuple:
|
|
233
233
|
"""Compute (width, height) of the text bounding box in inches.
|
|
234
234
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
235
|
+
Ports R's ``heightDetails.text`` / ``widthDetails.text`` (grid's
|
|
236
|
+
``primitives.R:1430-1452``) — R's ``grobHeight`` on a text grob
|
|
237
|
+
returns the **ascent only** (glyph extent above baseline), never
|
|
238
|
+
the descent. ``grobDescent`` is a separate method (see
|
|
239
|
+
``descentDetails.text``) and is exposed independently so that
|
|
240
|
+
callers can add it when needed. ggplot2's ``titleGrob``
|
|
241
|
+
(margins.R:115-132) relies on this convention — it uses
|
|
242
|
+
``unit(1, "grobheight", grob) + y_descent`` to assemble the final
|
|
243
|
+
height — so any deviation here double-counts the descent.
|
|
244
|
+
|
|
245
|
+
Empirical verification (R 4.4 ``cairo_png`` at 150 dpi, default
|
|
246
|
+
Helvetica, fontsize 13.2 pt):
|
|
247
|
+
|
|
248
|
+
grobHeight(textGrob("A long title", fs=13.2)) = 3.293 mm
|
|
249
|
+
grobHeight(textGrob("gjpqy", fs=13.2)) = 3.293 mm
|
|
250
|
+
grobHeight(textGrob("y", fs=13.2)) = 3.293 mm
|
|
251
|
+
|
|
252
|
+
— i.e. the value is a font-level constant, independent of the
|
|
253
|
+
label content. Our cairo-backed ``calc_string_metric`` returns a
|
|
254
|
+
per-label ascent that varies slightly with glyph mix, which is the
|
|
255
|
+
closest we can get without implementing a full AFM font-metric
|
|
256
|
+
path; residual ≤ 0.3 mm discrepancy is a font-file difference
|
|
257
|
+
(AFM Helvetica vs. cairo's "Sans" fallback) and outside the
|
|
258
|
+
scope of this bbox function.
|
|
259
|
+
|
|
260
|
+
Height formula (port of R's ``GEStrHeight``):
|
|
261
|
+
|
|
262
|
+
width = max(per-line ink widths)
|
|
263
|
+
height = ascent(first line)
|
|
240
264
|
+ (n - 1) × cex × lineheight × fontsize × 1.2 / 72
|
|
241
|
-
— R's ``GEStrHeight``
|
|
242
265
|
|
|
243
266
|
The per-extra-line gap ``1.2 × fontsize / 72`` is R's device-level
|
|
244
267
|
``cra[1] × ipr[1] / default_ps`` collapsed for the standard cairo /
|
|
@@ -287,11 +310,14 @@ def _text_bbox(grob: Any) -> tuple:
|
|
|
287
310
|
n_lines = len(lines)
|
|
288
311
|
# Width: max per-line width (R's ``GEStrWidth`` walks each line).
|
|
289
312
|
w = max(calc_string_metric(ln, gp=gp)["width"] for ln in lines)
|
|
290
|
-
# Height:
|
|
291
|
-
#
|
|
292
|
-
#
|
|
313
|
+
# Height: ascent of the first line + per-line gap × (n - 1).
|
|
314
|
+
# R's ``heightDetails.text`` / ``GEStrHeight`` returns ASCENT
|
|
315
|
+
# only — never descent. Descent is a separate method
|
|
316
|
+
# (``descentDetails.text``). Including descent here would
|
|
317
|
+
# double-count it downstream in ggplot2 ``titleGrob``, which
|
|
318
|
+
# adds ``grobDescent`` manually to form the rendered height.
|
|
293
319
|
m0 = calc_string_metric(lines[0], gp=gp)
|
|
294
|
-
h = m0["ascent"] +
|
|
320
|
+
h = m0["ascent"] + (n_lines - 1) * inter_line_gap
|
|
295
321
|
|
|
296
322
|
if rot == 0.0:
|
|
297
323
|
# No rotation: bbox is just the text extent
|
|
@@ -224,6 +224,12 @@ class GridState:
|
|
|
224
224
|
# GSS_GROUPS: group registry for define/use (R grid.h:63, state.c:51)
|
|
225
225
|
# Maps group name → dict with keys: ref, xy, xyin, wh, r, etc.
|
|
226
226
|
self._groups: Dict[str, Any] = {}
|
|
227
|
+
# Per-push counter of redundant self-pushes (the same vp pushed
|
|
228
|
+
# again while already current). Each genuine push appends a 0;
|
|
229
|
+
# a redundant push increments the top entry; pops decrement
|
|
230
|
+
# before actually unwinding. Prevents the cyclic parent chain
|
|
231
|
+
# that would otherwise hang ``current_vp_path``.
|
|
232
|
+
self._redundant_push_count: List[int] = [0]
|
|
227
233
|
|
|
228
234
|
# ---- reset ------------------------------------------------------------
|
|
229
235
|
|
|
@@ -244,7 +250,27 @@ class GridState:
|
|
|
244
250
|
vp : Any
|
|
245
251
|
A viewport-like object. Must expose ``name``, ``parent``,
|
|
246
252
|
and ``children`` attributes (or dict keys).
|
|
253
|
+
|
|
254
|
+
Notes
|
|
255
|
+
-----
|
|
256
|
+
Pushing the same viewport object twice (e.g. when a Gtable's
|
|
257
|
+
``make_context`` builds ``VpStack(orig_vp, layout_vp)`` and
|
|
258
|
+
the iterating push hits ``orig_vp`` while ``orig_vp`` is
|
|
259
|
+
already the current vp — happens in ``_evaluate_grob_unit``'s
|
|
260
|
+
preDraw for self-referential ``viewport(width = grobWidth(self))``
|
|
261
|
+
setups) would otherwise set ``vp.parent = vp`` and produce a
|
|
262
|
+
cyclic parent chain that deadlocks ``current_vp_path``. The
|
|
263
|
+
Python port stores parent on the vp object itself (rather than
|
|
264
|
+
on a separate per-push record like R's grid does), so we
|
|
265
|
+
track redundant self-pushes on a counter stack and treat the
|
|
266
|
+
matching ``up_viewport`` / ``pop_viewport`` as no-ops so depth
|
|
267
|
+
stays balanced.
|
|
247
268
|
"""
|
|
269
|
+
if vp is self._current_vp:
|
|
270
|
+
self._redundant_push_count[-1] = (
|
|
271
|
+
self._redundant_push_count[-1] + 1
|
|
272
|
+
)
|
|
273
|
+
return
|
|
248
274
|
_vp_set_attr(vp, "parent", self._current_vp)
|
|
249
275
|
children = _vp_children(self._current_vp)
|
|
250
276
|
if children is None:
|
|
@@ -252,6 +278,7 @@ class GridState:
|
|
|
252
278
|
_vp_set_attr(self._current_vp, "children", children)
|
|
253
279
|
children.append(vp)
|
|
254
280
|
self._current_vp = vp
|
|
281
|
+
self._redundant_push_count.append(0)
|
|
255
282
|
|
|
256
283
|
def pop_viewport(self, n: int = 1) -> None:
|
|
257
284
|
"""Pop *n* viewports, navigating back toward the root.
|
|
@@ -272,8 +299,14 @@ class GridState:
|
|
|
272
299
|
if n == 0:
|
|
273
300
|
# Pop to root.
|
|
274
301
|
self._current_vp = self._vp_tree
|
|
302
|
+
self._redundant_push_count = [0]
|
|
275
303
|
return
|
|
276
304
|
for _ in range(n):
|
|
305
|
+
# First absorb any redundant self-pushes recorded for the
|
|
306
|
+
# current vp (see ``push_viewport`` notes).
|
|
307
|
+
if self._redundant_push_count and self._redundant_push_count[-1] > 0:
|
|
308
|
+
self._redundant_push_count[-1] -= 1
|
|
309
|
+
continue
|
|
277
310
|
parent = _vp_parent(self._current_vp)
|
|
278
311
|
if parent is None:
|
|
279
312
|
raise ValueError(
|
|
@@ -286,6 +319,8 @@ class GridState:
|
|
|
286
319
|
except ValueError:
|
|
287
320
|
pass
|
|
288
321
|
self._current_vp = parent
|
|
322
|
+
if len(self._redundant_push_count) > 1:
|
|
323
|
+
self._redundant_push_count.pop()
|
|
289
324
|
|
|
290
325
|
def up_viewport(self, n: int = 1) -> None:
|
|
291
326
|
"""Navigate up *n* levels without removing viewports from the tree.
|
|
@@ -305,14 +340,21 @@ class GridState:
|
|
|
305
340
|
raise ValueError(f"'n' must be non-negative, got {n}")
|
|
306
341
|
if n == 0:
|
|
307
342
|
self._current_vp = self._vp_tree
|
|
343
|
+
self._redundant_push_count = [0]
|
|
308
344
|
return
|
|
309
345
|
for _ in range(n):
|
|
346
|
+
# Absorb redundant self-pushes first.
|
|
347
|
+
if self._redundant_push_count and self._redundant_push_count[-1] > 0:
|
|
348
|
+
self._redundant_push_count[-1] -= 1
|
|
349
|
+
continue
|
|
310
350
|
parent = _vp_parent(self._current_vp)
|
|
311
351
|
if parent is None:
|
|
312
352
|
raise ValueError(
|
|
313
353
|
"Cannot navigate above the root viewport."
|
|
314
354
|
)
|
|
315
355
|
self._current_vp = parent
|
|
356
|
+
if len(self._redundant_push_count) > 1:
|
|
357
|
+
self._redundant_push_count.pop()
|
|
316
358
|
|
|
317
359
|
def down_viewport(self, name: str, strict: bool = False) -> int:
|
|
318
360
|
"""Navigate down to a named viewport (breadth-first search).
|
|
@@ -301,7 +301,20 @@ def _try_resolve_with_renderer(
|
|
|
301
301
|
state = get_state()
|
|
302
302
|
renderer = state.get_renderer()
|
|
303
303
|
|
|
304
|
-
if renderer is None
|
|
304
|
+
if renderer is None:
|
|
305
|
+
# R parity: `convertUnit` (unit.R:59-75) dispatches to `L_convert` in
|
|
306
|
+
# src/unit.c, which resolves context via `GEcurrentDevice()`. When no
|
|
307
|
+
# device is open, R's graphics system auto-opens its default device
|
|
308
|
+
# (PDF, 7×7 in) and converts against it. We replicate that here by
|
|
309
|
+
# lazily installing a default 7×7 in CairoRenderer the first time a
|
|
310
|
+
# context-dependent conversion is requested — matching R's observed
|
|
311
|
+
# behaviour: `convertHeight(unit(0.3,"npc"),"mm") == 53.34` (=0.3×7in
|
|
312
|
+
# in mm) without any prior `grid_newpage()` / `pdf()` call.
|
|
313
|
+
from .renderer import CairoRenderer
|
|
314
|
+
renderer = CairoRenderer(width=7.0, height=7.0)
|
|
315
|
+
state.init_device(renderer)
|
|
316
|
+
|
|
317
|
+
if not hasattr(renderer, "_resolve_to_inches_idx"):
|
|
305
318
|
return None
|
|
306
319
|
|
|
307
320
|
# Build a single-element Unit for the source
|
|
@@ -1574,6 +1587,56 @@ def convert_unit(
|
|
|
1574
1587
|
)
|
|
1575
1588
|
if resolved is not None:
|
|
1576
1589
|
result_vals[i] = resolved
|
|
1590
|
+
elif src_unit in ("sum", "min", "max"):
|
|
1591
|
+
# R ``grid/src/unit.c: L_convert`` dispatches ``sum.unit`` /
|
|
1592
|
+
# ``min.unit`` / ``max.unit`` via ``L_sumUnits`` /
|
|
1593
|
+
# ``L_minUnits`` / ``L_maxUnits``, each of which calls
|
|
1594
|
+
# ``convertUnit`` recursively on every child in the
|
|
1595
|
+
# compound's list and combines the resulting absolute
|
|
1596
|
+
# lengths. Without this branch the fallback at the end of
|
|
1597
|
+
# this loop returns the outer scalar (1.0) unchanged —
|
|
1598
|
+
# which is why ``convertWidth(unit(1,'grobwidth',g) +
|
|
1599
|
+
# unit(0.05,'inches'),'cm')`` returned 1.0 regardless of
|
|
1600
|
+
# the text's actual rendered width.
|
|
1601
|
+
child = x._data[i]
|
|
1602
|
+
if (
|
|
1603
|
+
child is not None
|
|
1604
|
+
and isinstance(child, Unit)
|
|
1605
|
+
and len(child) > 0
|
|
1606
|
+
):
|
|
1607
|
+
child_inches: List[float] = []
|
|
1608
|
+
for j in range(len(child)):
|
|
1609
|
+
sub = Unit.__new__(Unit)
|
|
1610
|
+
sub._values = np.array(
|
|
1611
|
+
[child._values[j]], dtype=np.float64,
|
|
1612
|
+
)
|
|
1613
|
+
sub._units = [child._units[j]]
|
|
1614
|
+
sub._data = [child._data[j]]
|
|
1615
|
+
sub._is_absolute = (
|
|
1616
|
+
child._units[j] in _ABSOLUTE_UNIT_TYPES
|
|
1617
|
+
)
|
|
1618
|
+
inches_arr = convert_unit(
|
|
1619
|
+
sub, "inches",
|
|
1620
|
+
axisFrom=axisFrom, typeFrom=typeFrom,
|
|
1621
|
+
axisTo=axisTo, typeTo=typeTo,
|
|
1622
|
+
valueOnly=True,
|
|
1623
|
+
)
|
|
1624
|
+
child_inches.append(float(inches_arr[0]))
|
|
1625
|
+
if src_unit == "sum":
|
|
1626
|
+
combined = float(np.sum(child_inches))
|
|
1627
|
+
elif src_unit == "min":
|
|
1628
|
+
combined = float(np.min(child_inches))
|
|
1629
|
+
else:
|
|
1630
|
+
combined = float(np.max(child_inches))
|
|
1631
|
+
combined *= float(x._values[i])
|
|
1632
|
+
if target in _ABSOLUTE_UNIT_TYPES:
|
|
1633
|
+
result_vals[i] = combined / _INCHES_PER[target]
|
|
1634
|
+
else:
|
|
1635
|
+
result_vals[i] = combined
|
|
1636
|
+
converted = False
|
|
1637
|
+
else:
|
|
1638
|
+
result_vals[i] = x._values[i]
|
|
1639
|
+
converted = False
|
|
1577
1640
|
elif src_unit in _STR_METRIC_TYPES:
|
|
1578
1641
|
# Fallback without renderer: string metric → inches → target
|
|
1579
1642
|
inches_val = _eval_str_metric(src_unit, x._data[i], x._values[i])
|
|
@@ -1583,18 +1646,34 @@ def convert_unit(
|
|
|
1583
1646
|
result_vals[i] = inches_val
|
|
1584
1647
|
converted = False
|
|
1585
1648
|
elif src_unit in _GROB_METRIC_TYPES:
|
|
1586
|
-
# Fallback: grob metric → inches → target
|
|
1649
|
+
# Fallback: grob metric → inches → target.
|
|
1650
|
+
# Mirrors R ``grid/src/unit.c``:
|
|
1651
|
+
# evaluateGrobUnit(..., evalType=2)
|
|
1652
|
+
# unitx <- widthDetails(grob)
|
|
1653
|
+
# result = transformWidthtoINCHES(unitx, 0, ...)
|
|
1654
|
+
# R takes *only index 0* of ``widthDetails``'s return,
|
|
1655
|
+
# relying on its methods (e.g. ``widthDetails.titleGrob``
|
|
1656
|
+
# ``<- sum(x$widths)``) to wrap multi-element units as
|
|
1657
|
+
# a single sum.unit. The recursive ``transformWidthtoINCHES``
|
|
1658
|
+
# then unwraps L_SUM via the compound branch above.
|
|
1587
1659
|
metric_unit = _eval_grob_metric(src_unit, x._data[i])
|
|
1588
|
-
if (
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1660
|
+
if metric_unit is not None and len(metric_unit) > 0:
|
|
1661
|
+
head = Unit.__new__(Unit)
|
|
1662
|
+
head._values = np.array(
|
|
1663
|
+
[metric_unit._values[0]], dtype=np.float64,
|
|
1664
|
+
)
|
|
1665
|
+
head._units = [metric_unit._units[0]]
|
|
1666
|
+
head._data = [metric_unit._data[0]]
|
|
1667
|
+
head._is_absolute = (
|
|
1668
|
+
metric_unit._units[0] in _ABSOLUTE_UNIT_TYPES
|
|
1669
|
+
)
|
|
1670
|
+
inches_arr = convert_unit(
|
|
1671
|
+
head, "inches",
|
|
1672
|
+
axisFrom=axisFrom, typeFrom=typeFrom,
|
|
1673
|
+
axisTo=axisTo, typeTo=typeTo,
|
|
1674
|
+
valueOnly=True,
|
|
1596
1675
|
)
|
|
1597
|
-
src_inches
|
|
1676
|
+
src_inches = float(inches_arr[0]) * float(x._values[i])
|
|
1598
1677
|
if target in _ABSOLUTE_UNIT_TYPES:
|
|
1599
1678
|
result_vals[i] = src_inches / _INCHES_PER[target]
|
|
1600
1679
|
else:
|
|
@@ -336,12 +336,14 @@ class CairoRenderer(GridRenderer):
|
|
|
336
336
|
|
|
337
337
|
col = gp.get("col", None)
|
|
338
338
|
# R semantics:
|
|
339
|
-
# * col
|
|
340
|
-
# * col=
|
|
341
|
-
#
|
|
342
|
-
#
|
|
343
|
-
#
|
|
344
|
-
#
|
|
339
|
+
# * col absent from gp → inherit parent, default "black"
|
|
340
|
+
# * Gpar(col=None) → stored as [None] (NA sentinel)
|
|
341
|
+
# → transparent (≡ R gpar(col=NA))
|
|
342
|
+
# * sequence with first=None → transparent (ggplot2 colour=NA)
|
|
343
|
+
# * scalar string "NA"/"none"/ → transparent (resolved by
|
|
344
|
+
# "transparent" _parse_colour)
|
|
345
|
+
# The [None] sentinel is produced by Gpar(col=None) — see
|
|
346
|
+
# _gpar.py Gpar.__init__.
|
|
345
347
|
_is_seq = hasattr(col, "__len__") and not isinstance(col, str)
|
|
346
348
|
if _is_seq:
|
|
347
349
|
col_val = col[0]
|
|
@@ -12,7 +12,7 @@ name = "rgrid-python"
|
|
|
12
12
|
# a corresponding R update, bump a PEP 440 post-release suffix:
|
|
13
13
|
# "4.5.3" → "4.5.3.post1" → "4.5.3.post2" → ...
|
|
14
14
|
# When R grid itself ships a new upstream version, move to e.g. "4.5.4".
|
|
15
|
-
version = "4.5.3"
|
|
15
|
+
version = "4.5.3.post2"
|
|
16
16
|
description = "Python port of the R grid package (tracks R grid 4.5.3)"
|
|
17
17
|
readme = "README.md"
|
|
18
18
|
requires-python = ">=3.10"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|