1212
1313import datetime
1414import os
15+ import pathlib
1516import shutil
1617from typing import TextIO , Callable
1718
2627IMAGE_PATH = "image_path"
2728ITEM = "item"
2829BASE_DIR = "base_dir"
30+ DATAFILES_PATH = "datafiles_path"
31+ LIB_RELPATH = "lib_relpath"
32+ LIB_PATH = "lib_path"
33+ DATA_SRC = "data_src"
34+ DATA_DST = "data_dst"
2935
3036RESERVED_NAMES = {
31- INDEX ,
32- IMAGE_DIR_NAME ,
33- IMAGE_PATH ,
34- ITEM ,
35- BASE_DIR
36- }
37+ INDEX ,
38+ IMAGE_DIR_NAME ,
39+ IMAGE_PATH ,
40+ ITEM ,
41+ BASE_DIR ,
42+ DATAFILES_PATH ,
43+ LIB_RELPATH ,
44+ LIB_PATH ,
45+ DATA_SRC ,
46+ DATA_DST
47+ }
3748
3849#node input sockets that are messy to set default values for
3950DONT_SET_DEFAULTS = {
@@ -120,6 +131,10 @@ def __init__(self, *args, **kwargs):
120131 # Indentation string (default four spaces)
121132 self ._indentation = " "
122133
134+ self ._link_external_node_groups = True
135+
136+ self ._lib_trees : dict [pathlib .Path , list [bpy .types .NodeTree ]] = {}
137+
123138 if bpy .app .version >= (3 , 4 , 0 ):
124139 # Set default values for hidden sockets
125140 self ._set_unavailable_defaults = False
@@ -154,6 +169,8 @@ def _setup_options(self, options: NTP_PG_Options) -> bool:
154169 elif options .indentation_type == 'TABS' :
155170 self ._indentation = "\t "
156171
172+ self ._link_external_node_groups = options .link_external_node_groups
173+
157174 if bpy .app .version >= (3 , 4 , 0 ):
158175 self ._set_unavailable_defaults = options .set_unavailable_defaults
159176
@@ -204,7 +221,7 @@ def _setup_addon_directories(self, context: Context, obj_var: str) -> bool:
204221
205222 return True
206223
207- def _create_header (self , name : str ) -> None :
224+ def _create_bl_info (self , name : str ) -> None :
208225 """
209226 Sets up the bl_info and imports the Blender API
210227
@@ -228,9 +245,12 @@ def _create_header(self, name: str) -> None:
228245 category = self ._custom_category
229246 self ._write (f"\" category\" : { str_to_py_str (category )} ," , 1 )
230247 self ._write ("}\n " , 0 )
248+
249+ def _create_imports (self ) -> None :
231250 self ._write ("import bpy" , 0 )
232251 self ._write ("import mathutils" , 0 )
233- self ._write ("import os\n " , 0 )
252+ self ._write ("import os" , 0 )
253+ self ._write ("\n " , 0 )
234254
235255 def _init_operator (self , idname : str , label : str ) -> None :
236256 """
@@ -282,18 +302,68 @@ def dfs(nt: NodeTree) -> None:
282302 self .report ({'ERROR' }, "NodeToPython: Found an invalid node tree. "
283303 "Are all data blocks valid?" )
284304 return
305+
306+ if self ._link_external_node_groups and nt .library is not None :
307+ bpy_lib_path = bpy .path .abspath (nt .library .filepath )
308+ lib_path = pathlib .Path (os .path .realpath (bpy_lib_path ))
309+ bpy_datafiles_path = bpy .path .abspath (
310+ bpy .utils .system_resource ('DATAFILES' )
311+ )
312+ datafiles_path = pathlib .Path (os .path .realpath (bpy_datafiles_path ))
313+ is_lib_essential = lib_path .is_relative_to (datafiles_path )
314+
315+ if is_lib_essential :
316+ relative_path = lib_path .relative_to (datafiles_path )
317+ if relative_path not in self ._lib_trees :
318+ self ._lib_trees [relative_path ] = []
319+ self ._lib_trees [relative_path ].append (nt )
320+ return
321+ else :
322+ print (f"Library { lib_path } didn't seem essential, copying node groups" )
323+
285324 if nt not in visited :
286325 visited .add (nt )
287326 for group_node in [node for node in nt .nodes
288327 if node .bl_idname == group_node_type ]:
289328 if group_node .node_tree not in visited :
329+ if group_node .node_tree is None :
330+ self .report (
331+ {'ERROR' },
332+ "NodeToPython: Found an invalid node tree. "
333+ "Are all data blocks valid?"
334+ )
290335 dfs (group_node .node_tree )
291336 result .append (nt )
292337
293338 dfs (node_tree )
294339
295340 return result
296341
342+ def _import_essential_libs (self ) -> None :
343+ self ._inner_indent_level -= 1
344+ self ._write ("# Import node groups from Blender essentials library" )
345+ self ._write (f"{ DATAFILES_PATH } = bpy.utils.system_resource('DATAFILES')" )
346+ for path , node_trees in self ._lib_trees .items ():
347+ self ._write (f"{ LIB_RELPATH } = { str_to_py_str (str (path ))} " )
348+ self ._write (f"{ LIB_PATH } = os.path.join({ DATAFILES_PATH } , { LIB_RELPATH } )" )
349+ self ._write (f"with bpy.data.libraries.load({ LIB_PATH } , link=True) "
350+ f" as ({ DATA_SRC } , { DATA_DST } ):" )
351+ self ._write (f"\t { DATA_DST } .node_groups = []" )
352+ for node_tree in node_trees :
353+ name_str = str_to_py_str (node_tree .name )
354+ self ._write (f"\t if { name_str } in { DATA_SRC } .node_groups:" )
355+ self ._write (f"\t \t { DATA_DST } .node_groups.append({ name_str } )" )
356+ # TODO: handle bad case with warning (in both script and addon mode)
357+
358+ for i , node_tree in enumerate (node_trees ):
359+ nt_var = self ._create_var (node_tree .name )
360+ self ._node_tree_vars [node_tree ] = nt_var
361+ self ._write (f"{ nt_var } = { DATA_DST } .node_groups[{ i } ]" )
362+ self ._write ("\n " )
363+ self ._inner_indent_level += 1
364+
365+
366+
297367 def _create_var (self , name : str ) -> str :
298368 """
299369 Creates a unique variable name for a node tree
@@ -1208,13 +1278,17 @@ def _node_tree_settings(self, node: Node, attr_name: str) -> None:
12081278 node_tree = getattr (node , attr_name )
12091279 if node_tree is None :
12101280 return
1281+
12111282 if node_tree in self ._node_tree_vars :
12121283 nt_var = self ._node_tree_vars [node_tree ]
12131284 node_var = self ._node_vars [node ]
12141285 self ._write (f"{ node_var } .{ attr_name } = { nt_var } " )
12151286 else :
1216- self .report ({'WARNING' }, (f"NodeToPython: Node tree dependency graph "
1217- f"wasn't properly initialized" ))
1287+ self .report (
1288+ {'WARNING' },
1289+ f"NodeToPython: Node tree dependency graph "
1290+ f"wasn't properly initialized! Couldn't find "
1291+ f"node tree { node_tree .name } " )
12181292
12191293 def _save_image (self , img : bpy .types .Image ) -> bool :
12201294 """
0 commit comments