nodeeditor-qtpy 0.8.1__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.
Files changed (40) hide show
  1. nodeeditor/__init__.py +93 -0
  2. nodeeditor/node_colors_config.py +343 -0
  3. nodeeditor/node_content_widget.py +129 -0
  4. nodeeditor/node_decorators.py +65 -0
  5. nodeeditor/node_edge.py +518 -0
  6. nodeeditor/node_edge_dragging.py +185 -0
  7. nodeeditor/node_edge_intersect.py +219 -0
  8. nodeeditor/node_edge_rerouting.py +216 -0
  9. nodeeditor/node_edge_snapping.py +95 -0
  10. nodeeditor/node_edge_validators.py +82 -0
  11. nodeeditor/node_editor_widget.py +257 -0
  12. nodeeditor/node_editor_window.py +448 -0
  13. nodeeditor/node_graph.py +583 -0
  14. nodeeditor/node_graphics_cutline.py +57 -0
  15. nodeeditor/node_graphics_edge.py +375 -0
  16. nodeeditor/node_graphics_edge_path.py +226 -0
  17. nodeeditor/node_graphics_node.py +693 -0
  18. nodeeditor/node_graphics_scene.py +369 -0
  19. nodeeditor/node_graphics_socket.py +262 -0
  20. nodeeditor/node_graphics_view.py +907 -0
  21. nodeeditor/node_group.py +1286 -0
  22. nodeeditor/node_group_node.py +5 -0
  23. nodeeditor/node_icon_content_widget.py +176 -0
  24. nodeeditor/node_icon_graphics_node.py +524 -0
  25. nodeeditor/node_multi_input_node.py +450 -0
  26. nodeeditor/node_node.py +848 -0
  27. nodeeditor/node_scene.py +1106 -0
  28. nodeeditor/node_scene_clipboard.py +314 -0
  29. nodeeditor/node_scene_history.py +556 -0
  30. nodeeditor/node_serializable.py +43 -0
  31. nodeeditor/node_socket.py +445 -0
  32. nodeeditor/node_socket_shapes.py +261 -0
  33. nodeeditor/py.typed +0 -0
  34. nodeeditor/qss/nodestyle.qss +3 -0
  35. nodeeditor/utils.py +108 -0
  36. nodeeditor/utils_no_qt.py +21 -0
  37. nodeeditor_qtpy-0.8.1.dist-info/METADATA +165 -0
  38. nodeeditor_qtpy-0.8.1.dist-info/RECORD +40 -0
  39. nodeeditor_qtpy-0.8.1.dist-info/WHEEL +4 -0
  40. nodeeditor_qtpy-0.8.1.dist-info/licenses/LICENCE +23 -0
nodeeditor/__init__.py ADDED
@@ -0,0 +1,93 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from importlib.metadata import PackageNotFoundError
4
+ from importlib.metadata import version as _dist_version
5
+
6
+ __name__ = 'Node Editor'
7
+ __author__ = 'Azad Kshitij'
8
+
9
+ try:
10
+ __version__ = _dist_version("nodeeditor-qtpy")
11
+ except PackageNotFoundError: # running from a source checkout
12
+ __version__ = "0.8.1"
13
+
14
+
15
+ _QT_API_NAME, _QT_API_VERSION = None, None
16
+
17
+ if _QT_API_NAME is None:
18
+ try:
19
+ from PyQt5.QtCore import PYQT_VERSION_STR # type: ignore
20
+ _QT_API_NAME, _QT_API_VERSION = "pyqt5", PYQT_VERSION_STR
21
+ except ImportError:
22
+ pass
23
+
24
+ if _QT_API_NAME is None:
25
+ try:
26
+ from PySide2 import __version__ # type: ignore
27
+ _QT_API_NAME, _QT_API_VERSION = "pyside2", __version__
28
+ except ImportError:
29
+ pass
30
+
31
+ if _QT_API_NAME is None:
32
+ try:
33
+ from PyQt6.QtCore import PYQT_VERSION_STR
34
+ _QT_API_NAME, _QT_API_VERSION = "pyqt6", PYQT_VERSION_STR
35
+ except ImportError:
36
+ pass
37
+
38
+ if _QT_API_NAME is None:
39
+ try:
40
+ from PySide6 import __version__ # type: ignore
41
+ _QT_API_NAME, _QT_API_VERSION = "pyside6", __version__
42
+ except ImportError:
43
+ pass
44
+
45
+ if _QT_API_NAME is None:
46
+ raise ImportError(
47
+ "No Qt binding found. Install nodeeditor-qtpy with one, e.g. "
48
+ "'pip install nodeeditor-qtpy[pyqt6]' or "
49
+ "'pip install nodeeditor-qtpy[pyside6]', "
50
+ "or install PyQt5/PySide2/PyQt6/PySide6 directly."
51
+ )
52
+
53
+ # Export color configuration classes for easier access
54
+ from nodeeditor.node_colors_config import (
55
+ NodeEditorColorScheme,
56
+ EdgeColors,
57
+ SocketColors,
58
+ get_color_scheme,
59
+ )
60
+
61
+ # Export the socket shape system for easier access
62
+ from nodeeditor.node_socket_shapes import (
63
+ SocketShape,
64
+ SocketShapeContext,
65
+ CircleShape,
66
+ SquareShape,
67
+ TriangleShape,
68
+ ChevronShape,
69
+ register_socket_shape,
70
+ get_socket_shape,
71
+ SOCKET_SHAPES,
72
+ )
73
+
74
+ # Export the graph algorithm exception
75
+ from nodeeditor.node_graph import GraphCycleError
76
+
77
+
78
+ __all__ = [
79
+ "NodeEditorColorScheme",
80
+ "EdgeColors",
81
+ "SocketColors",
82
+ "get_color_scheme",
83
+ "SocketShape",
84
+ "SocketShapeContext",
85
+ "CircleShape",
86
+ "SquareShape",
87
+ "TriangleShape",
88
+ "ChevronShape",
89
+ "register_socket_shape",
90
+ "get_socket_shape",
91
+ "SOCKET_SHAPES",
92
+ "GraphCycleError",
93
+ ]
@@ -0,0 +1,343 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ A module for centralized color configuration for the Node Editor.
4
+
5
+ This module provides a single point to manage and update all colors used in:
6
+ - Edge graphics (default, selected, hovered, dragging)
7
+ - Socket graphics (background colors per type, highlighting, outlines)
8
+ """
9
+
10
+ from qtpy.QtGui import QColor
11
+ from typing import Dict, List
12
+
13
+
14
+ class BaseColors:
15
+ """Base class for color configurations
16
+ _Parameters:
17
+ - **_default**: Default color
18
+ - **_selected**: Selected color
19
+ - **_hovered**: Hovered color
20
+ - **_dragging**: Dragging color
21
+ """
22
+ def __init__(self):
23
+ """Initialize default colors
24
+ """
25
+ self._default: QColor = QColor("#B08848")
26
+ self._selected: QColor = QColor("#00ff00")
27
+ self._hovered: QColor = QColor("#FF37A6FF")
28
+ self._dragging: QColor = QColor("#333334")
29
+
30
+ @property
31
+ def default(self) -> QColor:
32
+ """Get default edge color"""
33
+ return QColor(self._default)
34
+
35
+ @default.setter
36
+ def default(self, color):
37
+ """Set default edge color"""
38
+ self._default = self._parse_color(color)
39
+
40
+ @property
41
+ def selected(self) -> QColor:
42
+ """Get selected edge color"""
43
+ return QColor(self._selected)
44
+
45
+ @selected.setter
46
+ def selected(self, color):
47
+ """Set selected edge color"""
48
+ self._selected = self._parse_color(color)
49
+
50
+ @property
51
+ def hovered(self) -> QColor:
52
+ """Get hovered edge color"""
53
+ return QColor(self._hovered)
54
+
55
+ @hovered.setter
56
+ def hovered(self, color):
57
+ """Set hovered edge color"""
58
+ self._hovered = self._parse_color(color)
59
+
60
+ @staticmethod
61
+ def _parse_color(color) -> QColor:
62
+ """Parse color from various formats to QColor"""
63
+ if isinstance(color, QColor):
64
+ return QColor(color)
65
+ elif isinstance(color, str):
66
+ return QColor(color)
67
+ elif isinstance(color, tuple) and len(color) >= 3:
68
+ # RGB or RGBA tuple
69
+ return QColor(*color)
70
+ else:
71
+ raise ValueError(f"Invalid color format: {color}")
72
+
73
+
74
+ class EdgeColors(BaseColors):
75
+ """Configuration for Edge colors"""
76
+
77
+ def __init__(self):
78
+ """Initialize default edge colors"""
79
+ self._default = QColor("#B08848")
80
+ self._selected = QColor("#ff00d0")
81
+ self._hovered = QColor("#FF37A6FF")
82
+ self._dragging = QColor("#333334")
83
+
84
+ @property
85
+ def default(self) -> QColor:
86
+ """Get default edge color"""
87
+ return QColor(self._default)
88
+
89
+ @default.setter
90
+ def default(self, color):
91
+ """Set default edge color"""
92
+ self._default = self._parse_color(color)
93
+
94
+ @property
95
+ def selected(self) -> QColor:
96
+ """Get selected edge color"""
97
+ return QColor(self._selected)
98
+
99
+ @selected.setter
100
+ def selected(self, color):
101
+ """Set selected edge color"""
102
+ self._selected = self._parse_color(color)
103
+
104
+ @property
105
+ def hovered(self) -> QColor:
106
+ """Get hovered edge color"""
107
+ return QColor(self._hovered)
108
+
109
+ @hovered.setter
110
+ def hovered(self, color):
111
+ """Set hovered edge color"""
112
+ self._hovered = self._parse_color(color)
113
+
114
+ @property
115
+ def dragging(self) -> QColor:
116
+ """Get dragging edge color"""
117
+ return QColor(self._dragging)
118
+
119
+ @dragging.setter
120
+ def dragging(self, color):
121
+ """Set dragging edge color"""
122
+ self._dragging = self._parse_color(color)
123
+
124
+ def set_all(self, default=None, selected=None, hovered=None, dragging=None):
125
+ """Set all edge colors at once
126
+
127
+ :param default: Default edge color
128
+ :param selected: Selected edge color
129
+ :param hovered: Hovered edge color
130
+ :param dragging: Dragging edge color
131
+ """
132
+ if default is not None:
133
+ self.default = default
134
+ if selected is not None:
135
+ self.selected = selected
136
+ if hovered is not None:
137
+ self.hovered = hovered
138
+ if dragging is not None:
139
+ self.dragging = dragging
140
+
141
+
142
+ class SocketColors(BaseColors):
143
+ """Configuration for Socket colors"""
144
+
145
+ def __init__(self):
146
+ """Initialize default socket colors"""
147
+ self._socket_type_colors = [
148
+ QColor("#FFFF7700"), # Orange
149
+ QColor("#FF52e220"), # Green
150
+ QColor("#FF0056a6"), # Blue
151
+ QColor("#FFa86db1"), # Purple
152
+ QColor("#FFb54747"), # Red
153
+ QColor("#FFdbe220"), # Yellow
154
+ QColor("#FF888888"), # Gray
155
+ QColor("#FFFF7700"), # Orange (duplicate)
156
+ QColor("#FF52e220"), # Green (duplicate)
157
+ QColor("#FF0056a6"), # Blue (duplicate)
158
+ QColor("#FFa86db1"), # Purple (duplicate)
159
+ QColor("#FFb54747"), # Red (duplicate)
160
+ QColor("#FFdbe220"), # Yellow (duplicate)
161
+ QColor("#FF888888"), # Gray (duplicate)
162
+ ]
163
+ self._outline = QColor("#FF000000")
164
+ self._highlight = QColor("#FF37A6FF")
165
+
166
+ @property
167
+ def outline(self) -> QColor:
168
+ """Get socket outline color"""
169
+ return QColor(self._outline)
170
+
171
+ @outline.setter
172
+ def outline(self, color):
173
+ """Set socket outline color"""
174
+ self._outline = self._parse_color(color)
175
+
176
+ @property
177
+ def highlight(self) -> QColor:
178
+ """Get socket highlight color"""
179
+ return QColor(self._highlight)
180
+
181
+ @highlight.setter
182
+ def highlight(self, color):
183
+ """Set socket highlight color"""
184
+ self._highlight = self._parse_color(color)
185
+
186
+ def get_type_color(self, socket_type: int) -> QColor:
187
+ """Get color for a specific socket type
188
+
189
+ :param socket_type: Socket type index
190
+ :return: QColor for the socket type
191
+ """
192
+ if isinstance(socket_type, int):
193
+ return QColor(self._socket_type_colors[socket_type % len(self._socket_type_colors)])
194
+ elif isinstance(socket_type, str):
195
+ return QColor(socket_type)
196
+ return QColor(0, 0, 0, 0)
197
+
198
+ def set_type_color(self, socket_type: int, color):
199
+ """Set color for a specific socket type
200
+
201
+ :param socket_type: Socket type index
202
+ :param color: Color to set
203
+ """
204
+ self._socket_type_colors[socket_type] = self._parse_color(color)
205
+
206
+ def set_type_colors(self, colors: List) -> None:
207
+ """Set all socket type colors at once
208
+
209
+ :param colors: List of colors
210
+ """
211
+ self._socket_type_colors = [self._parse_color(c) for c in colors]
212
+
213
+ def get_all_type_colors(self) -> List[QColor]:
214
+ """Get all socket type colors
215
+
216
+ :return: List of QColor objects
217
+ """
218
+ return [QColor(c) for c in self._socket_type_colors]
219
+
220
+ def set_all(self, outline=None, highlight=None, type_colors=None):
221
+ """Set all socket colors at once
222
+
223
+ :param outline: Socket outline color
224
+ :param highlight: Socket highlight color
225
+ :param type_colors: List of socket type colors
226
+ """
227
+ if outline is not None:
228
+ self.outline = outline
229
+ if highlight is not None:
230
+ self.highlight = highlight
231
+ if type_colors is not None:
232
+ self.set_type_colors(type_colors)
233
+
234
+ class NodesColors(BaseColors):
235
+ """Configuration for Node colors
236
+
237
+ This class reuses the EdgeColors structure to define colors for nodes.
238
+ """
239
+
240
+ def __init__(self):
241
+ """Initialize default node colors"""
242
+ super().__init__()
243
+
244
+
245
+ class NodeEditorColorScheme:
246
+ """Global color scheme configuration for Node Editor
247
+
248
+ This class provides a centralized way to manage and update all colors
249
+ used in the node editor, including edge colors and socket colors.
250
+
251
+ Example:
252
+ >>> scheme = NodeEditorColorScheme()
253
+ >>> scheme.edges.default = "#FF0000" # Red edges
254
+ >>> scheme.sockets.highlight = "#00FF00" # Green highlights
255
+ """
256
+
257
+ _instance = None
258
+
259
+ def __new__(cls):
260
+ """Singleton pattern - ensure only one instance exists"""
261
+ if cls._instance is None:
262
+ cls._instance = super().__new__(cls)
263
+ cls._instance._initialized = False
264
+ return cls._instance
265
+
266
+ def __init__(self):
267
+ """Initialize color scheme (only once)"""
268
+ if self._initialized:
269
+ return
270
+
271
+ self.edges = EdgeColors()
272
+ self.sockets = SocketColors()
273
+ self.nodes = NodesColors() # Reusing EdgeColors for node colors
274
+ self._initialized = True
275
+
276
+ @classmethod
277
+ def get_instance(cls) -> 'NodeEditorColorScheme':
278
+ """Get the singleton instance of the color scheme
279
+
280
+ :return: NodeEditorColorScheme instance
281
+ """
282
+ return cls()
283
+
284
+ def reset_to_defaults(self):
285
+ """Reset all colors to their default values"""
286
+ self.edges = EdgeColors()
287
+ self.sockets = SocketColors()
288
+ self.nodes = NodesColors()
289
+
290
+ def to_dict(self) -> Dict:
291
+ """Export current color scheme to dictionary
292
+
293
+ :return: Dictionary representation of the color scheme
294
+ """
295
+ return {
296
+ 'edges': {
297
+ 'default': self.edges.default.name(),
298
+ 'selected': self.edges.selected.name(),
299
+ 'hovered': self.edges.hovered.name(),
300
+ 'dragging': self.edges.dragging.name(),
301
+ },
302
+ 'sockets': {
303
+ 'outline': self.sockets.outline.name(),
304
+ 'highlight': self.sockets.highlight.name(),
305
+ 'type_colors': [c.name() for c in self.sockets.get_all_type_colors()],
306
+ }
307
+ }
308
+
309
+ def from_dict(self, config: Dict):
310
+ """Import color scheme from dictionary
311
+
312
+ :param config: Dictionary with color configuration
313
+ """
314
+ if 'edges' in config:
315
+ edge_config = config['edges']
316
+ self.edges.set_all(
317
+ default=edge_config.get('default'),
318
+ selected=edge_config.get('selected'),
319
+ hovered=edge_config.get('hovered'),
320
+ dragging=edge_config.get('dragging')
321
+ )
322
+
323
+ if 'sockets' in config:
324
+ socket_config = config['sockets']
325
+ self.sockets.set_all(
326
+ outline=socket_config.get('outline'),
327
+ highlight=socket_config.get('highlight'),
328
+ type_colors=socket_config.get('type_colors')
329
+ )
330
+
331
+ # Global singleton instance
332
+ _global_scheme = None
333
+
334
+
335
+ def get_color_scheme() -> NodeEditorColorScheme:
336
+ """Get the global color scheme instance
337
+
338
+ :return: NodeEditorColorScheme instance
339
+ """
340
+ global _global_scheme
341
+ if _global_scheme is None:
342
+ _global_scheme = NodeEditorColorScheme()
343
+ return _global_scheme
@@ -0,0 +1,129 @@
1
+ # -*- coding: utf-8 -*-
2
+ """A module containing the base class for the Node's content graphical representation. It also contains an example of
3
+ an overridden Text Widget, which can pass a notification to it's parent about being modified."""
4
+ from collections import OrderedDict
5
+ from nodeeditor.node_serializable import Serializable
6
+ from qtpy.QtWidgets import QWidget, QLabel, QVBoxLayout, QTextEdit
7
+ from qtpy.QtGui import QFocusEvent
8
+
9
+ from typing import TYPE_CHECKING
10
+
11
+ if TYPE_CHECKING:
12
+ from nodeeditor.node_node import Node
13
+
14
+
15
+ class QDMNodeContentWidget(QWidget, Serializable):
16
+ """Base class for representation of the Node's graphics content. This class also provides layout
17
+ for other widgets inside of a :py:class:`~nodeeditor.node_node.Node`"""
18
+
19
+ def __init__(self, node: 'Node', parent: QWidget = None) -> None:
20
+ """
21
+ :param node: reference to the :py:class:`~nodeeditor.node_node.Node`
22
+ :type node: :py:class:`~nodeeditor.node_node.Node`
23
+ :param parent: parent widget
24
+ :type parent: QWidget
25
+
26
+ :Instance Attributes:
27
+ - **node** - reference to the :class:`~nodeeditor.node_node.Node`
28
+ - **layout** - ``QLayout`` container
29
+ """
30
+ self.node = node
31
+ super().__init__(parent)
32
+
33
+ self.initUI()
34
+
35
+ def initUI(self) -> None:
36
+ """Sets up layouts and widgets to be rendered in :py:class:`~nodeeditor.node_graphics_node.QDMGraphicsNode` class.
37
+ """
38
+ self.layout = QVBoxLayout()
39
+ self.layout.setContentsMargins(0, 0, 0, 0)
40
+ self.setLayout(self.layout)
41
+
42
+ self.wdg_label = QLabel("Some Title")
43
+ self.layout.addWidget(self.wdg_label)
44
+ self.layout.addWidget(QDMTextEdit("foo"))
45
+
46
+ def setEditingFlag(self, value: bool) -> None:
47
+ """
48
+ .. note::
49
+
50
+ If you are handling keyPress events by default Qt Window's shortcuts and ``QActions``, you will not
51
+ need to use this method.
52
+
53
+ Helper function which sets editingFlag inside :py:class:`~nodeeditor.node_graphics_view.QDMGraphicsView` class.
54
+
55
+ This is a helper function to handle keys inside nodes with ``QLineEdits`` or ``QTextEdits`` (you can
56
+ use overridden :py:class:`QDMTextEdit` class) and with QGraphicsView class method ``keyPressEvent``.
57
+
58
+ :param value: new value for editing flag
59
+ """
60
+ self.node.scene.getView().editingFlag = value
61
+
62
+ def serialize(self) -> OrderedDict:
63
+ """Serialization method to serialize this class data into ``OrderedDict``.
64
+
65
+ The content widget holds no state of its own, so there is nothing to store.
66
+
67
+ :return: data serialized in ``OrderedDict``
68
+ :rtype: ``OrderedDict``
69
+ """
70
+ return OrderedDict([
71
+ ])
72
+
73
+ def deserialize(self, data: dict, hashmap: dict = {}, restore_id: bool = True) -> bool:
74
+ """Deserialization method which takes data in python ``dict`` format.
75
+
76
+ The content widget holds no state of its own, so there is nothing to
77
+ restore and the call always succeeds.
78
+
79
+ :param data: Dictionary containing serialized data
80
+ :type data: ``dict``
81
+ :param hashmap: Helper dictionary containing references (by id == key) to existing objects
82
+ :type hashmap: ``dict``
83
+ :param restore_id: True if we are creating new objects. False is useful when loading existing
84
+ objects of which we want to keep the existing object's `id`.
85
+ :type restore_id: bool
86
+ :return: ``True`` if deserialization was successful, otherwise ``False``
87
+ :rtype: ``bool``
88
+ """
89
+ return True
90
+
91
+ # NOTE: deliberately no `history_stamp_callback` here. SceneHistory's
92
+ # dispatch resolves the hook with getattr and tolerates its absence, and
93
+ # defining a no-op base would shadow the real implementation in any
94
+ # content class whose MRO puts this Qt base ahead of its own mixin.
95
+
96
+
97
+ class QDMTextEdit(QTextEdit):
98
+ """
99
+ .. note::
100
+
101
+ This class is an example of a ``QTextEdit`` modification that handles the `Delete` key event with an overridden
102
+ Qt's ``keyPressEvent`` (when not using ``QActions`` in menu or toolbar)
103
+
104
+ Overridden ``QTextEdit`` which sends a notification about being edited to its parent's container :py:class:`QDMNodeContentWidget`
105
+ """
106
+
107
+ def focusInEvent(self, event: QFocusEvent | None) -> None:
108
+ """Example of an overridden focusInEvent to mark the start of editing
109
+
110
+ :param event: Qt's focus event
111
+ :type event: QFocusEvent
112
+ """
113
+ parent = self.parentWidget()
114
+ if isinstance(parent, QDMNodeContentWidget):
115
+ parent.setEditingFlag(True)
116
+
117
+ super().focusInEvent(event)
118
+
119
+ def focusOutEvent(self, event: QFocusEvent | None) -> None:
120
+ """Example of an overridden focusOutEvent to mark the end of editing
121
+
122
+ :param event: Qt's focus event
123
+ :type event: QFocusEvent
124
+ """
125
+ parent = self.parentWidget()
126
+ if isinstance(parent, QDMNodeContentWidget):
127
+ parent.setEditingFlag(False)
128
+
129
+ super().focusOutEvent(event)
@@ -0,0 +1,65 @@
1
+ """
2
+ A module containing the decorator which marks classes as automatically serializable
3
+ """
4
+ from typing import Any, Callable, Optional, Type, Dict, TypeVar, List, TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ from nodeeditor.node_scene import Scene
8
+
9
+
10
+ T = TypeVar('T')
11
+
12
+
13
+ class MakeSerializable:
14
+ """Registry for classes that should be automatically serialized"""
15
+ _registry: Dict[str, Type] = {}
16
+ _instances: Dict[str, List['Scene']] = {} # Track instances per scene
17
+
18
+ @classmethod
19
+ def register(cls, key: str = "") -> Callable[[Type[T]], Type[T]]:
20
+ """Class decorator to mark a class as automatically serializable"""
21
+ def decorator(wrapped_class: Type[T]) -> Type[T]:
22
+ # Use class name as key if none provided
23
+ serialize_key = key or wrapped_class.__name__.lower()
24
+
25
+ # Ensure the class has required methods
26
+ if not hasattr(wrapped_class, 'serialize') or not hasattr(wrapped_class, 'deserialize'):
27
+ raise TypeError(
28
+ f"Class {wrapped_class.__name__} must implement serialize and deserialize methods")
29
+
30
+ # Store the original __init__
31
+ original_init = wrapped_class.__init__
32
+
33
+ # Create new __init__ that registers with scene
34
+ def new_init(self, scene: 'Scene', *args, **kwargs):
35
+ original_init(self, scene, *args, **kwargs)
36
+ # Auto-register with scene
37
+ if serialize_key not in cls._instances:
38
+ cls._instances[serialize_key] = []
39
+ cls._instances[serialize_key].append(scene)
40
+ # Force scene to register serializers
41
+ scene._register_auto_serializers()
42
+
43
+ wrapped_class.__init__ = new_init
44
+ cls._registry[serialize_key] = wrapped_class
45
+ return wrapped_class
46
+ return decorator
47
+
48
+ @classmethod
49
+ def get_registered(cls) -> Dict[str, Type]:
50
+ """Get all classes registered as automatically serializable
51
+
52
+ :return: mapping of serialize keys to the registered classes
53
+ :rtype: ``dict``
54
+ """
55
+ return cls._registry
56
+
57
+ @classmethod
58
+ def get_instance(cls, key: str, scene: 'Scene') -> Optional[Any]:
59
+ """Get instance for given key and scene"""
60
+ if key in cls._instances:
61
+ scenes = cls._instances[key]
62
+ if scene in scenes:
63
+ return next((obj for obj in scene.__dict__.values()
64
+ if isinstance(obj, cls._registry[key])), None)
65
+ return None