-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathserialize.py
More file actions
340 lines (275 loc) · 10.6 KB
/
serialize.py
File metadata and controls
340 lines (275 loc) · 10.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
"""
Serialization module for Node Runner.
Converts Blender node trees into plain Python dicts that can be
encoded and shared as strings.
"""
import logging
import pickle
import bpy
import mathutils
from .constants import EXCLUDE_NODE_PROPS, SERIALIZE_READONLY_PROPS, PAIRED_NODE_TYPES
log = logging.getLogger(__name__)
# Primitive / math type serializers
def serialize_color(color):
"""Serialize a Color to a list of floats."""
return list(color)
def serialize_vector(vector):
"""Serialize a Vector to a list of floats."""
return list(vector)
def serialize_euler(euler):
"""Serialize an Euler to a list of floats."""
return list(euler)
# Complex type serializers
def serialize_color_ramp(node):
"""Serialize a ColorRamp attached to *node*."""
ramp = node.color_ramp
return {
"color_mode": ramp.color_mode,
"hue_interpolation": ramp.hue_interpolation,
"interpolation": ramp.interpolation,
"elements": [
{"position": el.position, "color": list(el.color)} for el in ramp.elements
],
}
def serialize_color_mapping(node):
"""Serialize a ColorMapping block attached to *node*."""
cm = node.color_mapping
return {
"blend_color": serialize_color(cm.blend_color),
"blend_factor": cm.blend_factor,
"blend_type": cm.blend_type,
"brightness": cm.brightness,
"color_ramp": serialize_color_ramp(cm),
"contrast": cm.contrast,
"saturation": cm.saturation,
"use_color_ramp": cm.use_color_ramp,
}
def serialize_texture_mapping(node):
"""Serialize a TexMapping block attached to *node*."""
tm = node.texture_mapping
return {
"mapping": tm.mapping,
"mapping_x": tm.mapping_x,
"mapping_y": tm.mapping_y,
"mapping_z": tm.mapping_z,
"max": serialize_vector(tm.max),
"min": serialize_vector(tm.min),
"rotation": serialize_vector(tm.rotation),
"scale": serialize_vector(tm.scale),
"translation": serialize_vector(tm.translation),
"use_max": tm.use_max,
"use_min": tm.use_min,
"vector_type": tm.vector_type,
}
def serialize_curve_mapping(node):
"""Serialize a CurveMapping block attached to *node*."""
mapping = node.mapping
return {
"black_level": serialize_attr(node, mapping.black_level),
"clip_max_x": mapping.clip_max_x,
"clip_max_y": mapping.clip_max_y,
"clip_min_x": mapping.clip_min_x,
"clip_min_y": mapping.clip_min_y,
"curves": serialize_attr(node, mapping.curves),
"extend": mapping.extend,
"tone": mapping.tone,
"use_clip": mapping.use_clip,
"white_level": serialize_attr(node, mapping.white_level),
}
def serialize_curve_map(node, curve_map):
"""Serialize a single CurveMap."""
return {
"points": serialize_attr(node, curve_map.points),
}
def serialize_curve_map_point(node, point):
"""Serialize a single CurveMapPoint."""
return {
"handle_type": point.handle_type,
"location": serialize_attr(node, point.location),
"select": point.select,
}
def serialize_image(image):
"""Serialize an Image reference (name + absolute filepath when available)."""
result = {"name": image.name}
raw_path = getattr(image, "filepath", "")
if raw_path:
result["filepath"] = bpy.path.abspath(raw_path)
return result
def serialize_text_line(text_line):
"""Serialize one TextLine."""
return {"body": text_line.body}
def serialize_text(text):
"""Serialize a Text data-block."""
return {
"current_character": text.current_character,
"current_line": serialize_text_line(text.current_line),
"current_line_index": text.current_line_index,
"filepath": text.filepath,
"indentation": text.indentation,
"lines": [serialize_text_line(line) for line in text.lines],
"select_end_character": text.select_end_character,
"select_end_line_index": text.select_end_line_index,
"use_module": text.use_module,
}
# Generic attribute dispatcher
def serialize_attr(node, attr):
"""Serialize an arbitrary node attribute by dispatching on type.
Falls back to returning the value directly if it is pickle-safe.
Logs a warning for unsupported types.
"""
# Dispatch table: type -> serializer callable
_dispatch = {
mathutils.Color: serialize_color,
mathutils.Vector: serialize_vector,
mathutils.Euler: serialize_euler,
bpy.types.ColorRamp: lambda _: serialize_color_ramp(node),
bpy.types.NodeTree: lambda _: serialize_node_tree(node.node_tree),
bpy.types.ColorMapping: lambda _: serialize_color_mapping(node),
bpy.types.TexMapping: lambda _: serialize_texture_mapping(node),
bpy.types.CurveMapping: lambda _: serialize_curve_mapping(node),
bpy.types.CurveMap: lambda d: serialize_curve_map(node, d),
bpy.types.CurveMapPoint: lambda d: serialize_curve_map_point(node, d),
bpy.types.Image: serialize_image,
bpy.types.ImageUser: lambda _: {},
bpy.types.NodeFrame: lambda _: {}, # Handled separately
bpy.types.Text: lambda _: serialize_text(node.script),
bpy.types.Object: lambda _: None,
bpy.types.NodeSocketStandard: lambda d: (
serialize_attr(node, d.default_value)
if hasattr(d, "default_value")
else None
),
bpy.types.bpy_prop_collection: lambda d: [
serialize_attr(node, el) for el in d.values()
],
bpy.types.bpy_prop_array: lambda d: [serialize_attr(node, el) for el in d],
}
for data_type, serializer in _dispatch.items():
if isinstance(attr, data_type):
return serializer(attr)
# Fallback: check if pickle-safe
try:
pickle.dumps(attr)
except (pickle.PicklingError, TypeError, AttributeError):
log.warning(
"Cannot serialize attribute on node '%s': value=%r type=%s",
node.name,
attr,
type(attr).__name__,
)
return None
return attr
# Node serialization
def serialize_node(node):
"""Serialize all properties of a single node into a dict.
Uses ``bl_rna.properties`` for fast, reliable property iteration
instead of ``dir()``.
"""
node_dict = {}
# Use RNA introspection for reliable, fast iteration
for prop in node.bl_rna.properties:
prop_name = prop.identifier
if prop_name in EXCLUDE_NODE_PROPS:
continue
if prop.is_readonly and prop_name not in SERIALIZE_READONLY_PROPS:
continue
try:
attr = getattr(node, prop_name)
except AttributeError:
continue
if attr is None:
continue
# Parent frame: store name only
if prop_name == "parent":
node_dict["parent"] = node.parent.name
continue
# Group input/output socket ordering
if node.bl_idname in ("NodeGroupInput", "NodeGroupOutput"):
if prop_name == "inputs":
node_dict["input_order"] = [
{
"type": s.bl_idname,
"name": s.name,
"identifier": s.identifier,
}
for s in node.inputs
if s.bl_idname != "NodeSocketVirtual"
]
if prop_name == "outputs":
node_dict["output_order"] = [
{
"type": s.bl_idname,
"name": s.name,
"identifier": s.identifier,
}
for s in node.outputs
if s.bl_idname != "NodeSocketVirtual"
]
node_dict[prop_name] = serialize_attr(node, attr)
# Always include bl_idname as "type" and label
node_dict["type"] = node.bl_idname
node_dict["label"] = node.label
# Store absolute location for correct nested-frame positioning
node_dict["location_absolute"] = list(node.location_absolute)
# Store paired output reference for zone nodes (repeat, simulation, etc.)
if node.bl_idname in PAIRED_NODE_TYPES:
attr_name = PAIRED_NODE_TYPES[node.bl_idname]
paired = getattr(node, attr_name, None)
if paired is not None:
node_dict["_paired_output"] = paired.name
return node_dict
def serialize_node_tree(node_tree, selected_node_names=None):
"""Serialize a complete node tree (nodes + links).
Args:
node_tree: The Blender NodeTree to serialize.
selected_node_names: Optional list of node names to include.
If ``None``, all nodes are included.
Returns:
dict with keys ``"nodes"``, ``"links"``, ``"name"``.
"""
nodes = node_tree.nodes
data = {
"nodes": {},
"links": [],
"name": node_tree.name,
"tree_type": node_tree.bl_idname,
}
if selected_node_names is None:
selected_nodes = list(nodes)
else:
selected_nodes = [nodes[name] for name in selected_node_names if name in nodes]
# Also include parent frames of selected nodes
extra_frames = set()
for node in selected_nodes:
parent = node.parent
while parent is not None:
if parent.name not in {n.name for n in selected_nodes}:
extra_frames.add(parent.name)
parent = parent.parent
for frame_name in extra_frames:
if frame_name in nodes:
selected_nodes.append(nodes[frame_name])
selected_names = {n.name for n in selected_nodes}
for node in selected_nodes:
data["nodes"][node.name] = serialize_node(node)
for link in node_tree.links:
# Compare by name - id() is unreliable for bpy_struct wrappers
# since Blender may create new Python objects on each access.
if (
link.from_node.name in selected_names
and link.to_node.name in selected_names
):
data["links"].append(
{
"from_node": link.from_node.name,
"to_node": link.to_node.name,
"from_socket": link.from_socket.name,
"from_socket_type": link.from_socket.bl_idname,
"from_socket_identifier": link.from_socket.identifier,
"to_socket": link.to_socket.name,
"to_socket_type": link.to_socket.bl_idname,
"to_socket_identifier": link.to_socket.identifier,
}
)
log.debug("Serialized %d links", len(data["links"]))
return data