node-to-json 0.1.0__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.
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.4
2
+ Name: node_to_json
3
+ Version: 0.1.0
4
+ Summary: Library to serialize Blender 5.2 nodes to json files, and from json files to Blender nodes.
5
+ Requires-Python: >=3.13
6
+ Description-Content-Type: text/markdown
7
+
8
+ # Node To JSON
9
+
10
+ ## Convert Blender 5.2 Nodes to JSON files. Convert JSON objects to Blender Nodes.
11
+ ### Created to work with Blender Addons and Extensions to serialize nodes. It is only intended to work within Blender's python interpreter and requires the bpy module.
@@ -0,0 +1,4 @@
1
+ # Node To JSON
2
+
3
+ ## Convert Blender 5.2 Nodes to JSON files. Convert JSON objects to Blender Nodes.
4
+ ### Created to work with Blender Addons and Extensions to serialize nodes. It is only intended to work within Blender's python interpreter and requires the bpy module.
@@ -0,0 +1,5 @@
1
+ from .asset_funcs import asset_nodes, asset_node_group, nodes_dir
2
+ from .json_io import dict_to_json, json_to_dict
3
+ from .node_getters import serialize_node_group, serialize_mat_group, serialize_material, serialize_comp_group, convert_attr, get_node_build_data, get_node_group_interface, get_node_group_names
4
+ from .node_registry import bpy_types_funcs, register_node_setter
5
+ from .node_setters import nested_dict, convert_default_value, create_node_group_from_data, add_node_group, create_material, add_material_to_ob, add_comp_group, process_node, set_interface
@@ -0,0 +1,78 @@
1
+ import bpy
2
+ from pathlib import Path
3
+ from functools import cache
4
+ from typing import Generator
5
+ from .type_util import BGroup, PYDict
6
+
7
+ datafiles_path = Path(bpy.utils.system_resource('DATAFILES'))
8
+ lib_relpath = Path("assets").joinpath("nodes")
9
+ nodes_dir = datafiles_path.joinpath(lib_relpath)
10
+ hair_nodes = nodes_dir.joinpath("procedural_hair_node_assets.blend")
11
+ comp_nodes = nodes_dir.joinpath("compositing_nodes_essentials.blend")
12
+ dyn_nodes = nodes_dir.joinpath("geometry_nodes_dynamics_assets.blend")
13
+ geom_nodes = nodes_dir.joinpath("geometry_nodes_essentials.blend")
14
+ principal_nodes = nodes_dir.joinpath("principal_components.blend")
15
+ shading_nodes = nodes_dir.joinpath("shading_nodes_essentials.blend")
16
+
17
+
18
+ def get_asset_nodes_helper(file: str) -> Generator[str, None, None]:
19
+ with bpy.data.libraries.load(str(file), link=True) as (data_src, data_dst):
20
+ groups = (g for g in data_src.node_groups)
21
+ return groups
22
+
23
+
24
+ @cache
25
+ def get_asset_nodes() -> Generator[str, None, None]:
26
+ for file in nodes_dir.rglob('*blend'):
27
+ yield from get_asset_nodes_helper(file)
28
+
29
+
30
+ asset_nodes = list(get_asset_nodes())
31
+
32
+ @cache
33
+ def get_asset_file(name: str) -> str | None:
34
+ for file in nodes_dir.rglob('*blend'):
35
+ if name in get_asset_nodes_helper(file):
36
+ return file
37
+ return
38
+
39
+
40
+ def append_asset_nodes(*node_groups: str) -> None:
41
+ for group in node_groups:
42
+ name = group.split(".")[0]
43
+ file = get_asset_file(name)
44
+ if file:
45
+ with bpy.data.libraries.load(str(file), link=False) as (data_src, data_dst):
46
+ data_dst.node_groups = [name]
47
+ return
48
+
49
+
50
+ def get_loaded_node_groups() -> Generator[BGroup, None, None]:
51
+ return (group for group in bpy.data.node_groups)
52
+
53
+
54
+ def get_loaded_node_group_names() -> list[str]:
55
+ return [group.name for group in get_loaded_node_groups()]
56
+
57
+
58
+ def load_asset_node(node_group: BGroup) -> Generator[BGroup, None, None] | None:
59
+ global asset_nodes
60
+ loaded = list(get_loaded_node_groups())
61
+ if node_group in asset_nodes:
62
+ append_asset_nodes(node_group)
63
+ new_ = list(get_loaded_node_groups())
64
+ return (n for n in new_ if n not in loaded)
65
+ return
66
+
67
+
68
+ def asset_node_group(node_group: str) -> BGroup | None:
69
+ """Add an asset node group or return None.
70
+
71
+ :param node_group: Node group name to load.
72
+ :type node_group: str
73
+ :return: Return node group if it is an asset node else return None.
74
+ :rtype: Node Group | None"""
75
+ anode = load_asset_node(node_group)
76
+ if anode not in [None, []]:
77
+ return next((g for g in anode if g.name.split(".")[0] == node_group.split(".")[0]))
78
+ return
@@ -0,0 +1,31 @@
1
+ from json import dump, load
2
+ from .type_util import PYDict
3
+
4
+
5
+ def dict_to_json(file: str, data: PYDict, indent: int | None = None) -> None:
6
+ """Convert python dict to json object.
7
+
8
+ :param file: File path of file to save to.
9
+ :type file: str
10
+ :param data: Dict of data to convert.
11
+ :type data: dict[str, Any]
12
+ :param indent: How many spaces to indent json object. (None = no indent.)
13
+ :type indent: int | None"""
14
+ with open(file, 'w') as f:
15
+ if indent:
16
+ dump(data, f, indent=indent)
17
+ else:
18
+ dump(data, f)
19
+
20
+
21
+ def json_to_dict(file: str) -> PYDict:
22
+ """Convert json object to python dict.
23
+
24
+ :param file: File path to load from.
25
+ :type file: str
26
+ :return: Dict of converted json object
27
+ :rtype: dict[str, Any]"""
28
+ with open(file, 'r') as f:
29
+ data = load(f)
30
+ return data
31
+
@@ -0,0 +1,490 @@
1
+ import bpy
2
+ from mathutils import Color, Euler, Vector, Matrix, Quaternion
3
+ from idprop.types import IDPropertyArray
4
+ from collections import defaultdict
5
+ from typing import Any, Generator
6
+ from .type_util import BNode, BGroup, PYDict, PYObject
7
+ from .asset_funcs import asset_nodes
8
+
9
+
10
+ node_attr_exclude = ['rna_type', 'bl_rna', 'inputs', 'internal_links', 'outputs', 'enum_definition', 'interface_items', 'panel_states',
11
+ 'item', 'bl_static_type', 'dimensions', 'color_tag']
12
+
13
+ node_type_exclude = [
14
+ 'bpy_prop_collection',
15
+ 'Object',
16
+ 'Material',
17
+ 'Image',
18
+ 'ImageTexture',
19
+ 'Texture',
20
+ 'Collection',
21
+ 'Scene',
22
+ 'Sound',
23
+ 'Node',
24
+ 'GeometryNodeTree',
25
+ 'ShaderNodeTree',
26
+ 'CompositorNodeTree',
27
+ ]
28
+
29
+ preset_attr_exclude = [
30
+ 'bl_description',
31
+ 'bl_height_default',
32
+ 'bl_height_max',
33
+ 'bl_height_min',
34
+ 'bl_icon',
35
+ 'bl_idname',
36
+ 'bl_label',
37
+ 'bl_static_type',
38
+ 'bl_width_default',
39
+ 'bl_width_max',
40
+ 'bl_width_min',
41
+ 'color',
42
+ 'color_tag',
43
+ 'dimensions',
44
+ 'height',
45
+ 'hide',
46
+ 'label',
47
+ 'location',
48
+ 'location_absolute',
49
+ 'mute',
50
+ 'name',
51
+ 'node_tree',
52
+ 'panel_states',
53
+ 'parent',
54
+ 'select',
55
+ 'show_options',
56
+ 'show_preview',
57
+ 'show_texture',
58
+ 'type',
59
+ 'use_custom_color',
60
+ 'warning_propagation',
61
+ 'width',
62
+ ]
63
+
64
+ socket_attr = [
65
+ 'description',
66
+ 'enabled',
67
+ 'hide',
68
+ 'hide_value',
69
+ 'name',
70
+ 'pin_gizmo',
71
+ 'show_expanded',
72
+ 'type',
73
+ 'is_linked',
74
+ 'default_value',
75
+ ]
76
+
77
+ loop_outputs = [
78
+ 'GeometryNodeRepeatOutput',
79
+ 'GeometryNodeForeachGeometryElementOutput',
80
+ 'GeometryNodeSimulationOutput',
81
+ 'NodeClosureOutput',
82
+ ]
83
+
84
+
85
+ ### TREE NODES ###
86
+
87
+ node_tree_attr = [
88
+ 'bl_description',
89
+ 'bl_icon',
90
+ 'bl_idname',
91
+ 'bl_label',
92
+ 'bl_use_group_interface',
93
+ 'color_tag',
94
+ 'default_group_node_width',
95
+ 'description',
96
+ 'node_tree',
97
+ ]
98
+
99
+ geo_tree_attr = [
100
+ 'is_mode_edit',
101
+ 'is_mode_object',
102
+ 'is_mode_paint',
103
+ 'is_mode_sculpt',
104
+ 'is_modifier',
105
+ 'is_tool',
106
+ 'is_type_curve',
107
+ 'is_type_grease_pencil',
108
+ 'is_type_mesh',
109
+ 'is_type_pointcloud',
110
+ 'node_tool_idname',
111
+ 'show_modifier_manage_panel',
112
+ 'use_wait_for_click',
113
+ ]
114
+
115
+ tree_inter_base = [
116
+ 'item_type',
117
+ 'parent',
118
+ 'position',
119
+ 'description',
120
+ 'name',
121
+ 'index',
122
+ ]
123
+
124
+ tree_inter_sock = [
125
+ 'attribute_domain',
126
+ 'bl_socket_idname',
127
+ 'default_attribute_name',
128
+ 'default_input',
129
+ 'hide_in_modifier',
130
+ 'hide_value',
131
+ 'in_out',
132
+ 'is_inspect_output',
133
+ 'is_panel_toggle',
134
+ 'layer_selection_field',
135
+ 'menu_expanded',
136
+ 'optional_label',
137
+ 'socket_type',
138
+ 'structure_type',
139
+ ]
140
+
141
+ tree_inter_panel = [
142
+ 'default_closed',
143
+ 'persistent_uid',
144
+ ]
145
+
146
+ items_tree_attr = [
147
+ 'default_value',
148
+ 'dimensions',
149
+ 'min_value',
150
+ 'max_value',
151
+ 'subtype',
152
+ ]
153
+
154
+
155
+ ### MATERIAL NODES ###
156
+
157
+ mat_attr = [
158
+ 'alpha_threshold',
159
+ 'blend_method',
160
+ 'diffuse_color',
161
+ 'displacement_method',
162
+ 'line_color',
163
+ 'line_priority',
164
+ 'max_vertex_displacement',
165
+ 'metallic',
166
+ 'paint_active_slot',
167
+ 'paint_clone_slot',
168
+ 'pass_index',
169
+ 'preview_render_type',
170
+ 'refraction_depth',
171
+ 'roughness',
172
+ # 'show_transparent_back', #(may introduce transparency sorting problems) (Deprecated: use ‘use_tranparency_overlap’)
173
+ 'specular_color',
174
+ 'specular_intensity',
175
+ 'surface_render_method',
176
+ 'thickness_mode',
177
+ 'use_backface_culling',
178
+ 'use_backface_culling_lightprobe_volume',
179
+ 'use_backface_culling_shadow',
180
+ # 'use_nodes', #Depricated
181
+ 'use_preview_world',
182
+ 'use_raytrace_refraction',
183
+ 'use_screen_refraction',
184
+ 'use_sss_translucency',
185
+ 'use_thickness_from_shadow',
186
+ 'use_transparency_overlap',
187
+ 'use_transparent_shadow',
188
+ 'volume_intersection_method',
189
+ ]
190
+
191
+
192
+ ###################### HELPER FUNCS #######################
193
+
194
+ def nested_dict() -> defaultdict:
195
+ """Create a nested dict of dicts.
196
+
197
+ :return: dict[str, dict]
198
+ :rtype: defaultdict"""
199
+
200
+ return defaultdict(nested_dict)
201
+
202
+ def convert_attr(val: Any) -> PYObject | list[PYObject]:
203
+ """Convert a value based on its bpy type.
204
+
205
+ :param val: Value to modify if it is not serializable.
206
+ :type val: Any
207
+ :return: Modified value if it is not serializable, else the original value.
208
+ :rtype: Any"""
209
+
210
+ if isinstance(val, (Color, Vector, Euler, Quaternion, bpy.types.bpy_prop_array, IDPropertyArray, set, tuple)):
211
+ return list(val)
212
+ if isinstance(val, (Matrix,)):
213
+ return [list(v) for v in val]
214
+ if isinstance(val, (bpy.types.Object, bpy.types.Material, bpy.types.Image, bpy.types.ImageTexture, bpy.types.Texture, bpy.types.Collection, bpy.types.Scene, bpy.types.Sound, bpy.types.MovieClip, bpy.types.AnimData, bpy.types.Action, bpy.types.Annotation, bpy.types.Mask)):
215
+ return None
216
+ if isinstance(val, (bpy.types.Node, bpy.types.GeometryNodeTree, bpy.types.ShaderNodeTree, bpy.types.CompositorNodeTree)):
217
+ return val.name
218
+ if isinstance(val, (bpy.types.NodeTreeInterfaceItem, bpy.types.NodeTreeInterfacePanel)):
219
+ if hasattr(val, 'persistent_uid'):
220
+ return [val.name, val.persistent_uid]
221
+ return val.name
222
+ return val
223
+
224
+
225
+ ########################## NODE ###########################
226
+
227
+
228
+ def get_node_attr(ob: BNode, attr: list[str]) -> PYDict:
229
+ func = lambda a: [a, convert_attr(getattr(ob, a, None))]
230
+ mfunc = lambda a: hasattr(ob, a) and not isinstance(getattr(ob, a, None), type(None))
231
+ data = map(func, filter(mfunc, attr))
232
+ return dict(data)
233
+
234
+
235
+ def get_node_data(node: BNode) -> PYDict:
236
+ global node_attr_exclude
237
+ a_filter = lambda a: (not a.startswith("__") and not a in node_attr_exclude) and (not getattr(node, a, None) is None and not type(getattr(node, a, None)).__name__ in ['bpy_func', 'method-wrapper', 'builtin_function_or_method', 'EnumProperty'])
238
+ attributes = filter(a_filter, dir(node))
239
+ def _get_attr(attr):
240
+ val = getattr(node, attr, None)
241
+ val_ = convert_attr(val)
242
+ if type(val).__name__ not in node_type_exclude and attr not in ['parent']:
243
+ if type(val).__name__ in dir(bpy.types):
244
+ return [attr, get_node_data(val)]
245
+ else:
246
+ return [attr, val_]
247
+ else:
248
+ if type(val).__name__ in ['Node', 'GeometryNodeTree', 'ShaderNodeTree', 'CompositorNodeTree'] or attr in ['parent']:
249
+ return [attr, val_]
250
+ elif type(val).__name__ in ['bpy_prop_collection']:
251
+ return [attr, [get_node_data(v) for v in val[:]]]
252
+ else:
253
+ pass
254
+ return {k: v for k, v in map(_get_attr, attributes)}
255
+
256
+
257
+ def get_socket_attr(node: BNode) -> dict[str, list[PYDict]]:
258
+ global socket_attr
259
+ i = []
260
+ o = []
261
+ if node.bl_idname not in loop_outputs:
262
+ if hasattr(node, 'inputs'):
263
+ inputs = getattr(node, 'inputs', None)
264
+ if inputs:
265
+ i = [get_node_attr(i_, socket_attr) for i_ in inputs]
266
+ if hasattr(node, 'outputs'):
267
+ outputs = getattr(node, 'outputs', None)
268
+ if outputs:
269
+ o = [get_node_attr(o_, socket_attr) for o_ in outputs]
270
+ return {'inputs': i, 'outputs': o}
271
+
272
+
273
+ def get_node_build_data(node: BNode) -> dict[str, PYDict]:
274
+ """Serialize data to rebuild a node.
275
+
276
+ :param node: Node to get data from.
277
+ :type node: Node
278
+ :return: Dict containing data to rebuild a node and input / output sockets.
279
+ :rtype: dict[str, Any]"""
280
+
281
+ return {**get_node_data(node), **get_socket_attr(node)}
282
+
283
+
284
+ ####################### NODE GROUP ########################
285
+
286
+
287
+ def _get_node_group_names(nodes: BNode) -> Generator[str, None, None]:
288
+ global asset_nodes
289
+ for node in nodes:
290
+ if node.type == 'GROUP':
291
+ yield node.node_tree.name
292
+ if node.node_tree.name.split(".")[0] not in asset_nodes:
293
+ yield from _get_node_group_names(node.node_tree.nodes)
294
+
295
+
296
+ def get_node_group_names(node_group: BGroup) -> PYDict:
297
+ """Retrieve all of the node groups inside a node group.
298
+
299
+ :param node_group: Node group to search internal node groups inside of.
300
+ :type node_group: Node Group
301
+ :return: Dict containing the node group and all internally nested node groups.
302
+ :rtype: dict[str, dict[None]]"""
303
+
304
+ return {**{node_group.name: {}}, **{group: {} for group in _get_node_group_names(node_group.nodes)}}
305
+
306
+
307
+ def get_node_group_attr_list(node_tree: BGroup) -> PYDict:
308
+ global node_tree_attr, geo_tree_attr
309
+ attributes = node_tree_attr + geo_tree_attr if node_tree.bl_idname == 'GeometryNodeTree' else node_tree_attr
310
+ return {a: convert_attr(getattr(node_tree, a, None)) for a in attributes}
311
+
312
+
313
+ def get_items_tree(item: bpy.types.NodeTreeInterfaceItem) -> PYDict:
314
+ global tree_inter_base, tree_inter_sock, tree_inter_panel
315
+ itype = item.item_type
316
+ data = tree_inter_base
317
+ data = data + (tree_inter_panel if itype == 'PANEL' else tree_inter_sock)
318
+ if itype != 'PANEL':
319
+ data = data + items_tree_attr
320
+ return {a: convert_attr(getattr(item, a, None)) for a in data if hasattr(item, a)}
321
+ else:
322
+ return {a: convert_attr(getattr(item, a, None)) for a in data if hasattr(item, a)}
323
+
324
+
325
+ def get_node_group_interface(node_tree: BGroup) -> Generator[PYDict, None, None]:
326
+ """Retrieve data for node group sockets and panels.
327
+
328
+ :param node_tree: Node group to get data from.
329
+ :type node_tree: Node Group
330
+ :return: Generator containing dicts of socket and panel data.
331
+ :rtype: Generator[dict[str, Any], None, None]"""
332
+
333
+ return map(get_items_tree, node_tree.interface.items_tree)
334
+
335
+
336
+ def get_socket_data(node: BNode) -> PYDict:
337
+ global node_attr_exclude
338
+ data = nested_dict()
339
+ attributes = (a for a in dir(node) if not a.startswith("__") and not a in node_attr_exclude)
340
+ for attr in attributes:
341
+ val = getattr(node, attr, None)
342
+ if val is None:
343
+ data[attr] = None
344
+ else:
345
+ if not type(val).__name__ in ['bpy_func', 'method-wrapper', 'builtin_function_or_method', 'EnumProperty']:
346
+ val_ = convert_attr(val)
347
+ data[attr] = val_
348
+ return data
349
+
350
+
351
+ def get_node_tree_nodes_data(node_tree: BGroup) -> PYDict | None:
352
+ nodes = getattr(node_tree, 'nodes', None)
353
+ if nodes:
354
+ return {node.name: get_node_build_data(node) for node in nodes}
355
+ return
356
+
357
+
358
+ def get_node_tree_socket_data(node_tree: BGroup) -> Generator[PYDict, None, None] | None:
359
+ interface = getattr(node_tree, 'interface', None)
360
+ if interface:
361
+ items_tree = getattr(interface, 'items_tree', None)
362
+ if items_tree:
363
+ return (get_socket_data(socket) for socket in items_tree)
364
+ return
365
+
366
+
367
+ def get_links(node_tree: BGroup) -> Generator[PYDict, None, None]:
368
+ link_attr = [
369
+ 'name',
370
+ 'type',
371
+ ]
372
+ links = getattr(node_tree, 'links', None)
373
+ if links:
374
+ for s in ([i.from_socket, i.to_socket] for i in links):
375
+ o_node = s[0].node.name
376
+ o_sock = next(idx for idx, o in enumerate(s[0].node.outputs) if o.identifier == s[0].identifier)
377
+ i_node = s[1].node.name
378
+ i_sock = next(idx for idx, i in enumerate(s[1].node.inputs) if i.identifier == s[1].identifier)
379
+ yield [{**{'node': o_node, 'socket': o_sock}, **{l: getattr(s[0], l, None) for l in link_attr}}, {**{'node': i_node, 'socket': i_sock}, **{l: getattr(s[1], l, None) for l in link_attr}}]
380
+
381
+
382
+ def serialize_node_group(node_group: BGroup) -> PYDict:
383
+ """Serialize data to rebuild a node group.
384
+
385
+ :param node_group: Node group to get data from.
386
+ :type node_group: Node Group
387
+ :return: Dict of data to rebuild a node group.
388
+ :rtype: dict[str, Any]"""
389
+
390
+ global asset_nodes
391
+ data = get_node_group_names(node_group)
392
+ gnames = [g.name for g in bpy.data.node_groups]
393
+ for group in data:
394
+ if group in gnames:
395
+ is_asset = group.split(".")[0] in asset_nodes
396
+ node_tree = bpy.data.node_groups[group]
397
+ data[group] = {**get_node_group_attr_list(node_tree), **{'interface': (None if is_asset else list(get_node_group_interface(node_tree)))}, **{'links': (None if is_asset else list(get_links(node_tree)))}, **{'nodes': (None if is_asset else get_node_tree_nodes_data(node_tree))}}
398
+ return {d: data[d] for d in data if len(data[d]) > 0}
399
+
400
+
401
+ ######################## MATERIAL #########################
402
+
403
+
404
+ def get_mat_attr(material: bpy.types.Material) -> PYDict:
405
+ global mat_attr
406
+ return {a: convert_attr(getattr(material, a, None)) for a in mat_attr if hasattr(material, a)}
407
+
408
+
409
+ def serialize_mat_group(node_group: BGroup) -> PYDict:
410
+ """Serialize data to rebuild a material node group.
411
+
412
+ :param node_group: Node group to get data from.
413
+ :type node_group: Node Group
414
+ :return: Dict of data to rebuild a material node group.
415
+ :rtype: dict[str, Any]"""
416
+
417
+ global asset_nodes
418
+ data = get_node_group_names(node_group)
419
+ data_ = {**{'links': list(get_links(node_group))}, **{'nodes': get_node_tree_nodes_data(node_group)}, **{'node_groups': {}}}
420
+ for group in data:
421
+ if group in [g.name for g in bpy.data.node_groups]:
422
+ is_asset = group.split(".")[0] in asset_nodes
423
+ node_tree = bpy.data.node_groups[group]
424
+ data_['node_groups'] = {group: {**get_node_group_attr_list(node_tree), **{'interface': (None if is_asset else list(get_node_group_interface(node_tree)))}, **{'links': (None if is_asset else list(get_links(node_tree)))}, **{'nodes': (None if is_asset else get_node_tree_nodes_data(node_tree))}}}
425
+ return data_
426
+
427
+
428
+ def serialize_material(material: bpy.types.Material) -> PYDict:
429
+ """Serialize data to rebuild a material.
430
+
431
+ :param material: Material to get data from.
432
+ :type material: Material
433
+ :return: Dict of data to rebuild a material and material node group.
434
+ :rtype: dict[str, Any]"""
435
+
436
+ return {material.name.split(".")[0]: {**get_mat_attr(material), **{'node_tree': serialize_mat_group(material.node_tree)}}}
437
+
438
+
439
+ ####################### COMPOSITOR ########################
440
+
441
+
442
+ def get_comp_node_data(node: BNode) -> PYDict:
443
+ global node_attr_exclude
444
+ a_filter = lambda a: (not a.startswith("__") and not a in node_attr_exclude) and (not getattr(node, a, None) is None and not type(getattr(node, a, None)).__name__ in ['bpy_func', 'method-wrapper', 'builtin_function_or_method', 'EnumProperty'])
445
+ attributes = filter(a_filter, dir(node))
446
+ def _get_attr(attr):
447
+ val = getattr(node, attr, None)
448
+ val_ = convert_attr(val)
449
+ if type(val).__name__ not in node_type_exclude and attr not in ['parent']:
450
+ if type(val).__name__ in dir(bpy.types):
451
+ return [attr, get_node_data(val)]
452
+ else:
453
+ return [attr, val_]
454
+ else:
455
+ if type(val).__name__ in ['Node', 'GeometryNodeTree', 'ShaderNodeTree', 'CompositorNodeTree'] or attr in ['parent']:
456
+ return [attr, val_]
457
+ elif type(val).__name__ in ['bpy_prop_collection']:
458
+ return [attr, [get_node_data(v) for v in val[:]]]
459
+ else:
460
+ return [attr, val_]
461
+ return dict(map(_get_attr, attributes))
462
+
463
+
464
+ def serialize_comp_group(compositor: bpy.types.CompositorNodeTree) -> PYDict:
465
+ """Serialize data to rebuild a compositor node group.
466
+
467
+ :param compositor: Node group to get data from.
468
+ :type compositor: Node Group
469
+ :return: Dict of data to rebuild a compositor node group.
470
+ :rtype: dict[str, Any]"""
471
+
472
+ compositor_exclude = ['bl_rna', 'id_type', 'interface', 'rna_type', 'nodes', 'links']
473
+ a_filter = lambda a: (not a.startswith("__") and not a in compositor_exclude) and (not getattr(compositor, a, None) is None and not type(getattr(compositor, a, None)).__name__ in ['bpy_func', 'method-wrapper', 'builtin_function_or_method', 'EnumProperty'])
474
+ attributes = filter(a_filter, dir(compositor))
475
+ def _get_attr(attr):
476
+ val = getattr(compositor, attr, None)
477
+ val_ = convert_attr(val)
478
+ return attr, val_
479
+ data = map(_get_attr, attributes)
480
+ group_names = get_node_group_names(compositor)
481
+ _nodes = lambda node_tree: {node.name: {**get_comp_node_data(node), **get_socket_attr(node)} for node in node_tree.nodes}
482
+ data = dict(data) | {'node_groups': {}}
483
+ for group in group_names:
484
+ if group in [g.name for g in bpy.data.node_groups]:
485
+ is_asset = group.split(".")[0] in asset_nodes
486
+ node_tree = bpy.data.node_groups[group]
487
+ data['node_groups'][group] = {**get_node_group_attr_list(node_tree), **{'interface': (None if is_asset else list(get_node_group_interface(node_tree)))}, **{'links': (None if is_asset else list(get_links(node_tree)))}, **{'nodes': (None if is_asset else _nodes(node_tree))}}
488
+ return data
489
+
490
+
@@ -0,0 +1,26 @@
1
+ import bpy
2
+ from typing import Callable
3
+ from .type_util import PYDict
4
+ from functools import wraps
5
+
6
+
7
+ type AttrData = PYDict
8
+ type BtypeFn = Callable[[str, list[str]], AttrData]
9
+
10
+
11
+ bpy_types_funcs: dict[str, BtypeFn] = {}
12
+
13
+
14
+ def register_node_setter(*bpy_type: str) -> Callable[[bpy.types.Node, list[str]], AttrData]:
15
+ def decorator(fn: BtypeFn) -> dict[str, BtypeFn]:
16
+ @wraps(fn)
17
+ def wrapper(node: bpy.types.Node, attr: list[str]) -> BtypeFn:
18
+ return fn(node, attr)
19
+ # Assign types to function
20
+ for b in bpy_type:
21
+ bpy_types_funcs[b] = wrapper
22
+ # Return function
23
+ return wrapper
24
+ # Return the decorator
25
+ return decorator
26
+