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.
@@ -0,0 +1,248 @@
1
+ """Scene graph data model for the WebRenderer.
2
+
3
+ Provides a JSON-serializable tree representation of the entire grid scene,
4
+ including viewports, grobs, and shared definitions (gradients, patterns,
5
+ clip paths, masks).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import threading
12
+ from dataclasses import dataclass, field
13
+ from typing import Optional
14
+
15
+ __all__ = [
16
+ "SceneNode",
17
+ "ViewportNode",
18
+ "GrobNode",
19
+ "DefsCollection",
20
+ "SceneGraph",
21
+ ]
22
+
23
+
24
+ # ---------------------------------------------------------------------------
25
+ # Auto-incrementing ID generator
26
+ # ---------------------------------------------------------------------------
27
+
28
+ class _IdGenerator:
29
+ """Thread-safe auto-incrementing ID generator.
30
+
31
+ Each call to ``next(prefix)`` returns ``"{prefix}-{counter}"`` where
32
+ *counter* is a monotonically increasing integer starting at 0.
33
+ """
34
+
35
+ def __init__(self) -> None:
36
+ self._counter: int = 0
37
+ self._lock = threading.Lock()
38
+
39
+ def next(self, prefix: str = "node") -> str:
40
+ """Return the next unique ID string with the given *prefix*."""
41
+ with self._lock:
42
+ node_id = f"{prefix}-{self._counter}"
43
+ self._counter += 1
44
+ return node_id
45
+
46
+ def reset(self) -> None:
47
+ """Reset the counter to zero."""
48
+ with self._lock:
49
+ self._counter = 0
50
+
51
+
52
+ # Module-level singleton shared by factory helpers.
53
+ _id_gen = _IdGenerator()
54
+
55
+
56
+ # ---------------------------------------------------------------------------
57
+ # Scene nodes
58
+ # ---------------------------------------------------------------------------
59
+
60
+ @dataclass
61
+ class SceneNode:
62
+ """Base node in the scene graph tree.
63
+
64
+ Parameters
65
+ ----------
66
+ node_id : str
67
+ Unique identifier for this node.
68
+ node_type : str
69
+ Discriminator tag persisted in the serialized form (e.g.
70
+ ``"viewport"``, ``"rect"``, ``"text"``).
71
+ children : list[SceneNode]
72
+ Ordered child nodes.
73
+ metadata : dict
74
+ Arbitrary key/value pairs attached to this node.
75
+ """
76
+
77
+ node_id: str
78
+ node_type: str
79
+ children: list = field(default_factory=list)
80
+ metadata: dict = field(default_factory=dict)
81
+
82
+ def to_dict(self) -> dict:
83
+ """Recursively convert the node tree to a plain dict.
84
+
85
+ The returned structure is directly suitable for
86
+ ``json.dumps(node.to_dict())``. Keys use the short names
87
+ expected by the JavaScript runtime (``id``, ``type``).
88
+ """
89
+ d: dict = {
90
+ "id": self.node_id,
91
+ "type": self.node_type,
92
+ "children": [child.to_dict() for child in self.children],
93
+ }
94
+ if self.metadata:
95
+ d["metadata"] = self.metadata
96
+ return d
97
+
98
+
99
+ @dataclass
100
+ class ViewportNode(SceneNode):
101
+ """A viewport (coordinate-system container) in the scene tree.
102
+
103
+ Parameters
104
+ ----------
105
+ name : str
106
+ The viewport name (mirrors the grid viewport name).
107
+ transform : dict
108
+ Viewport placement expressed as ``{"x0": ..., "y0": ...,
109
+ "w": ..., "h": ...}`` in the parent coordinate system.
110
+ clip : bool
111
+ Whether drawing is clipped to the viewport boundary.
112
+ clip_id : str or None
113
+ Reference to a ``<clipPath>`` element in *DefsCollection*.
114
+ mask_id : str or None
115
+ Reference to a ``<mask>`` element in *DefsCollection*.
116
+ mask_type : str or None
117
+ Mask compositing type (e.g. ``"luminance"``, ``"alpha"``).
118
+ """
119
+
120
+ name: str = ""
121
+ transform: dict = field(default_factory=lambda: {"x0": 0.0, "y0": 0.0, "w": 1.0, "h": 1.0})
122
+ clip: bool = False
123
+ clip_id: Optional[str] = None
124
+ mask_id: Optional[str] = None
125
+ mask_type: Optional[str] = None
126
+
127
+ def to_dict(self) -> dict:
128
+ d = super().to_dict()
129
+ d["name"] = self.name
130
+ d["transform"] = self.transform
131
+ d["clip"] = self.clip
132
+ if self.clip_id is not None:
133
+ d["clip_id"] = self.clip_id
134
+ if self.mask_id is not None:
135
+ d["mask_id"] = self.mask_id
136
+ if self.mask_type is not None:
137
+ d["mask_type"] = self.mask_type
138
+ return d
139
+
140
+
141
+ @dataclass
142
+ class GrobNode(SceneNode):
143
+ """A graphical object (grob) leaf or branch in the scene tree.
144
+
145
+ Parameters
146
+ ----------
147
+ props : dict
148
+ Grob-specific geometric properties (e.g. ``x``, ``y``,
149
+ ``width``, ``height`` for a rect grob).
150
+ gpar : dict
151
+ Serialized graphical parameters (fill, colour, lwd, ...).
152
+ render_hint : str
153
+ Hint for the web renderer: ``"auto"``, ``"svg"``, or
154
+ ``"canvas"``.
155
+ """
156
+
157
+ props: dict = field(default_factory=dict)
158
+ gpar: dict = field(default_factory=dict)
159
+ render_hint: str = "auto"
160
+
161
+ def to_dict(self) -> dict:
162
+ d = super().to_dict()
163
+ d["props"] = self.props
164
+ d["gpar"] = self.gpar
165
+ d["render_hint"] = self.render_hint
166
+ # Pass through metadata as "data" for JS tooltip consumption
167
+ if self.metadata:
168
+ d["data"] = self.metadata
169
+ return d
170
+
171
+
172
+ # ---------------------------------------------------------------------------
173
+ # Shared definitions (gradients, patterns, clip paths, masks)
174
+ # ---------------------------------------------------------------------------
175
+
176
+ @dataclass
177
+ class DefsCollection:
178
+ """Container for shared definitions referenced by ID from scene nodes.
179
+
180
+ Each list stores plain dicts that must contain at least an ``"id"``
181
+ key together with type-specific fields.
182
+ """
183
+
184
+ gradients: list = field(default_factory=list)
185
+ patterns: list = field(default_factory=list)
186
+ clip_paths: list = field(default_factory=list)
187
+ masks: list = field(default_factory=list)
188
+
189
+ def to_dict(self) -> dict:
190
+ return {
191
+ "gradients": list(self.gradients),
192
+ "patterns": list(self.patterns),
193
+ "clip_paths": list(self.clip_paths),
194
+ "masks": list(self.masks),
195
+ }
196
+
197
+
198
+ # ---------------------------------------------------------------------------
199
+ # Top-level scene graph
200
+ # ---------------------------------------------------------------------------
201
+
202
+ @dataclass
203
+ class SceneGraph:
204
+ """Top-level scene graph container.
205
+
206
+ Parameters
207
+ ----------
208
+ version : int
209
+ Schema version (currently ``1``).
210
+ width : float
211
+ Device width in user units.
212
+ height : float
213
+ Device height in user units.
214
+ dpi : float
215
+ Dots-per-inch used for unit conversion.
216
+ root : ViewportNode
217
+ The root viewport of the scene tree.
218
+ defs : DefsCollection
219
+ Shared definitions (gradients, patterns, clips, masks).
220
+ """
221
+
222
+ width: float
223
+ height: float
224
+ dpi: float
225
+ root: ViewportNode
226
+ version: int = 1
227
+ defs: DefsCollection = field(default_factory=DefsCollection)
228
+
229
+ def to_dict(self) -> dict:
230
+ """Return the full scene graph as a plain nested dict."""
231
+ return {
232
+ "version": self.version,
233
+ "width": self.width,
234
+ "height": self.height,
235
+ "dpi": self.dpi,
236
+ "root": self.root.to_dict(),
237
+ "defs": self.defs.to_dict(),
238
+ }
239
+
240
+ def to_json(self, *, indent: int | None = 2) -> str:
241
+ """Serialize the scene graph to a JSON string.
242
+
243
+ Parameters
244
+ ----------
245
+ indent : int or None
246
+ JSON indentation level. Pass ``None`` for compact output.
247
+ """
248
+ return json.dumps(self.to_dict(), indent=indent)