i_o.py 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962
  1. # this is the I/O part of mantis. I eventually intend to make this a markup language. not right now tho lol
  2. from .utilities import (prRed, prGreen, prPurple, prWhite,
  3. prOrange,
  4. wrapRed, wrapGreen, wrapPurple, wrapWhite,
  5. wrapOrange,)
  6. from mathutils import Vector
  7. NODES_REMOVED=["xFormRootNode"]
  8. # Node bl_idname, # Socket Name
  9. SOCKETS_REMOVED=[("UtilityDriverVariable", "Transform Channel"),
  10. ("xFormRootNode","World Out"),
  11. ("UtilitySwitch","xForm"),
  12. ("LinkDrivenParameter", "Enable")]
  13. # Node Class #Prior bl_idname # prior name # new bl_idname # new name, # Multi
  14. # ignore these because they are either unrelated python stuff or useless or borked
  15. prop_ignore = [ "__dict__", "__doc__", "__module__", "__weakref__",# "name",
  16. "bl_height_default", "bl_height_max", "bl_height_min",
  17. "bl_icon", "bl_rna", "bl_static_type", "bl_description",
  18. "bl_width_default", "bl_width_max", "bl_width_min",
  19. "__annotations__", "original", "rna_type", "view_center",
  20. "links", "nodes", "internal_links", "inputs", "outputs",
  21. "__slots__", "dimensions", "type", "interface",
  22. "library_weak_reference", "parsed_tree", "node_tree_updater",
  23. "asset_data", "preview", # blender asset stuff
  24. "object_reference", # this one is here to hold on to widgets when appending
  25. "color_tag" , # added in blender 4.4, not used by Mantis, readonly.
  26. # more blender properties...
  27. "bl_use_group_interface", "default_group_node_width", "id_type",
  28. # blender runtime stuff
  29. "animation_data", "description", "grease_pencil", "is_editable",
  30. "is_embedded_data", "is_evaluated", "is_library_indirect", "is_missing",
  31. "is_runtime_data", "library", "name_full", "override_library",
  32. "session_uid", "tag", "use_extra_user", "use_fake_user", "users",
  33. # some Mantis stuff I don't need to save
  34. "do_live_update", "is_executing", "is_exporting", "hash", "filepath",
  35. "prevent_next_exec", "execution_id", "num_links", "tree_valid",
  36. "interface_helper",
  37. # node stuff
  38. "mantis_node_class_name", "color", "height", "initialized", "select",
  39. "show_options", "show_preview", "show_texture", "use_custom_color",
  40. "warning_propagation",
  41. # these are in Bone
  42. "socket_count", "display_bb_settings", "display_def_settings",
  43. "display_ik_settings", "display_vp_settings",
  44. ]
  45. # don't ignore: "bl_idname", "bl_label",
  46. # ignore the name, it's the dict - key for the node props
  47. # no that's stupid don't ignore the name good grief
  48. # I am doing this because these are interactions with other addons that cause problems and probably don't exist for any given user
  49. prop_ignore.extend(['keymesh'])
  50. # trees
  51. prop_ignore_tree = prop_ignore.copy()
  52. prop_ignore_tree.extend(["bl_label", "name"])
  53. from bpy.app import version
  54. if version >= (4,5,0):
  55. SOCKETS_REMOVED.append( ("LinkSplineIK", "Use Original Scale"))
  56. add_inputs_bl_idnames = [
  57. "UtilityDriver", "UtilityFCurve", "DeformerMorphTargetDeform",
  58. "LinkArmature",
  59. 'xFormBoneNode', # for custom properties, right?
  60. # For a long time this wasn't in here and I guess there weren't problems
  61. # I really don't know if adding it here is right...
  62. ]
  63. # this works but it is really ugly and probably quite inneficient
  64. # TODO: make hotkeys for export and import and reload from file
  65. # we need to give the tree a filepath attribute and update it on saving
  66. # then we need to use the filepath attribute to load from
  67. # finally we need to use a few operators to choose whether to open a menu or not
  68. # and we need a message to display on save/load so that the user knows it is happening
  69. # TODO:
  70. # Additionally export MetaRig and Curve and other referenced data
  71. # Meshes can be exported as .obj and imported via GN
  72. def TellClasses():
  73. return [ MantisExportNodeTreeSaveAs, MantisExportNodeTreeSave, MantisExportNodeTree, MantisImportNodeTree, MantisReloadNodeTree]
  74. # https://stackoverflow.com/questions/42033142/is-there-an-easy-way-to-check-if-an-object-is-json-serializable-in-python - thanks!
  75. def is_jsonable(x):
  76. import json
  77. try:
  78. json.dumps(x)
  79. return True
  80. except (TypeError, OverflowError):
  81. return False
  82. # https://stackoverflow.com/questions/295135/turn-a-stritree-into-a-valid-filename - thank you user "Sophie Gage"
  83. def remove_special_characters(name):
  84. import re; return re.sub('[^\w_.)( -]', '', name)# re = regular expressions
  85. def fix_custom_parameter(n, property_definition, ):
  86. if n.bl_idname in ['xFormNullNode', 'xFormBoneNode', 'xFormArmatureNode', 'xFormGeometryObjectNode',]:
  87. prop_name = property_definition["name"]
  88. prop_type = property_definition["bl_idname"]
  89. if prop_type in ['ParameterBoolSocket', 'ParameterIntSocket', 'ParameterFloatSocket', 'ParameterVectorSocket' ]:
  90. # is it good to make both of them?
  91. input = n.inputs.new( prop_type, prop_name)
  92. output = n.outputs.new( prop_type, prop_name)
  93. if property_definition["is_output"] == True:
  94. return output
  95. return input
  96. elif n.bl_idname in ['LinkArmature']:
  97. prop_name = property_definition["name"]
  98. prop_type = property_definition["bl_idname"]
  99. input = n.inputs.new( prop_type, prop_name)
  100. return input
  101. return None
  102. def get_socket_data(socket, ignore_if_default=False):
  103. # TODO: don't get stuff in the socket templates
  104. # PROBLEM: I don't have easy access to this from the ui class (or mantis class)
  105. socket_data = {}
  106. socket_data["name"] = socket.name
  107. socket_data["bl_idname"] = socket.bl_idname
  108. socket_data["is_output"] = socket.is_output
  109. socket_data["is_multi_input"] = socket.is_multi_input
  110. # here is where we'll handle a socket_data'socket special data
  111. if socket.bl_idname == "EnumMetaBoneSocket":
  112. socket_data["bone"] = socket.bone
  113. if socket.bl_idname in ["EnumMetaBoneSocket", "EnumMetaRigSocket", "EnumCurveSocket"]:
  114. if sp := socket.get("search_prop"): # may be None
  115. socket_data["search_prop"] = sp.name # this is an object.
  116. #
  117. if hasattr(socket, "default_value"):
  118. value = socket.default_value
  119. else:
  120. value = None
  121. return socket_data # we don't need to store any more.
  122. if not is_jsonable(value): # FIRST try and make a tuple out of it because JSON doesn't like mutables
  123. value = tuple(value)
  124. if not is_jsonable(value): # now see if it worked and crash out if it didn't
  125. raise RuntimeError(f"Error serializing data in {socket.node.name}::{socket.name} for value of type {type(value)}")
  126. socket_data["default_value"] = value
  127. # TODO TODO implement "ignore if default" feature here
  128. # at this point we can get the custom parameter ui hints if we want
  129. if not socket.is_output:
  130. # try and get this data
  131. if value := getattr(socket,'min', None):
  132. socket_data["min"] = value
  133. if value := getattr(socket,'max', None):
  134. socket_data["max"] = value
  135. if value := getattr(socket,'soft_min', None):
  136. socket_data["soft_min"] = value
  137. if value := getattr(socket,'soft_max', None):
  138. socket_data["soft_max"] = value
  139. if value := getattr(socket,'description', None):
  140. socket_data["description"] = value
  141. return socket_data
  142. #
  143. def get_node_data(ui_node):
  144. # if this is a node-group, force it to update its interface, because it may be messed up.
  145. # can remove this HACK when I have stronger guarentees about node-group's keeping the interface
  146. from .base_definitions import node_group_update
  147. if hasattr(ui_node, "node_tree"):
  148. ui_node.is_updating = True
  149. try: # HERE BE DANGER
  150. node_group_update(ui_node, force=True)
  151. finally: # ensure this line is run even if there is an error
  152. ui_node.is_updating = False
  153. node_props, sockets = {}, {}
  154. for propname in dir(ui_node):
  155. value = getattr(ui_node, propname)
  156. if propname in ['fake_fcurve_ob']:
  157. value=value.name
  158. if (propname in prop_ignore) or ( callable(value) ):
  159. continue
  160. if value.__class__.__name__ in ["Vector", "Color"]:
  161. value = tuple(value)
  162. if isinstance(value, bpy.types.NodeTree):
  163. value = value.name
  164. if isinstance(value, bpy.types.bpy_prop_array):
  165. value = tuple(value)
  166. if propname == "parent" and value:
  167. value = value.name
  168. if not is_jsonable(value):
  169. raise RuntimeError(f"Could not export... {ui_node.name}, {propname}, {type(value)}")
  170. if value is None:
  171. continue
  172. node_props[propname] = value
  173. # so we have to accumulate the parent location because the location is not absolute
  174. if propname == "location" and ui_node.parent is not None:
  175. location_acc = Vector((0,0))
  176. parent = ui_node.parent
  177. while (parent):
  178. location_acc += parent.location
  179. parent = parent.parent
  180. location_acc += getattr(ui_node, propname)
  181. node_props[propname] = tuple(location_acc)
  182. # this works!
  183. for i, ui_socket in enumerate(ui_node.inputs):
  184. if ui_socket.is_linked: continue # not necessary to save it since it doesn't affect the tree
  185. socket = get_socket_data(ui_socket)
  186. socket["index"]=i
  187. sockets[ui_socket.identifier] = socket
  188. for i, ui_socket in enumerate(ui_node.outputs):
  189. if ui_socket.is_linked: continue # see above
  190. socket = get_socket_data(ui_socket)
  191. socket["index"]=i
  192. sockets[ui_socket.identifier] = socket
  193. node_props["sockets"] = sockets
  194. return node_props
  195. def get_tree_data(tree):
  196. tree_info = {}
  197. for propname in dir(tree):
  198. # if getattr(tree, propname):
  199. # pass
  200. if (propname in prop_ignore_tree) or ( callable(getattr(tree, propname)) ):
  201. continue
  202. v = getattr(tree, propname)
  203. if isinstance(getattr(tree, propname), bpy.types.bpy_prop_array):
  204. v = tuple(getattr(tree, propname))
  205. if not is_jsonable( v ):
  206. raise RuntimeError(f"Not JSON-able: {propname}, type: {type(v)}")
  207. tree_info[propname] = v
  208. tree_info["name"]=tree.name
  209. return tree_info
  210. def get_interface_data(tree, tree_in_out):
  211. for sock in tree.interface.items_tree:
  212. sock_data={}
  213. if sock.item_type == 'PANEL':
  214. sock_data["name"] = sock.name
  215. sock_data["item_type"] = sock.item_type
  216. sock_data["description"] = sock.description
  217. sock_data["default_closed"] = sock.default_closed
  218. tree_in_out[sock.name] = sock_data
  219. # if it is a socket....
  220. else:
  221. # we need to get the socket class from the bl_idname
  222. bl_socket_idname = sock.bl_socket_idname
  223. # try and import it
  224. from . import socket_definitions
  225. # WANT an attribute error if this fails.
  226. socket_class = getattr(socket_definitions, bl_socket_idname)
  227. sock_parent = None
  228. if sock.parent:
  229. sock_parent = sock.parent.name
  230. for propname in dir(sock):
  231. v = getattr(sock, propname)
  232. if (propname in prop_ignore) or ( callable(v) ):
  233. continue
  234. if (propname == "parent"):
  235. sock_data[propname] = sock_parent
  236. continue
  237. if isinstance(getattr(sock, propname), bpy.types.bpy_prop_array):
  238. v = tuple(getattr(sock, propname))
  239. if not is_jsonable( v ):
  240. raise RuntimeError(f"{propname}, {type(v)}")
  241. sock_data[propname] = v
  242. # this is a property. pain.
  243. sock_data["socket_type"] = socket_class.interface_type.fget(socket_class)
  244. tree_in_out[sock.identifier] = sock_data
  245. def export_to_json(trees, path="", write_file=True, only_selected=False):
  246. export_data = {}
  247. for tree in trees:
  248. current_tree_is_base_tree = False
  249. if tree is trees[-1]:
  250. current_tree_is_base_tree = True
  251. tree_info, tree_in_out = {}, {}
  252. tree_info = get_tree_data(tree)
  253. # if only_selected:
  254. # # all in/out links, relative to the selection, should be marked and used to initialize tree properties
  255. if not only_selected: # we'll handle this later with the links
  256. for sock in tree.interface.items_tree:
  257. get_interface_data(tree, tree_in_out) # it concerns me that this one modifies
  258. # the collection instead of getting the data and returning it. TODO refactor this
  259. nodes = {}
  260. for node in tree.nodes:
  261. if only_selected and node.select == False:
  262. continue
  263. nodes[node.name] = get_node_data(node)
  264. links = []
  265. in_sockets, out_sockets = {}, {}
  266. unique_sockets_from, unique_sockets_to = {}, {}
  267. in_node = {"name":"MANTIS_AUTOGEN_GROUP_INPUT", "bl_idname":"NodeGroupInput", "sockets":in_sockets}
  268. out_node = {"name":"MANTIS_AUTOGEN_GROUP_OUTPUT", "bl_idname":"NodeGroupOutput", "sockets":out_sockets}
  269. add_input_node, add_output_node = False, False
  270. for link in tree.links:
  271. from_node_name, from_socket_id = link.from_node.name, link.from_socket.identifier
  272. to_node_name, to_socket_id = link.to_node.name, link.to_socket.identifier
  273. from_socket_name, to_socket_name = link.from_socket.name, link.to_socket.name
  274. # get the indices of the sockets to be absolutely sure
  275. for from_outoput_index, outp in enumerate(link.from_node.outputs):
  276. # for some reason, 'is' does not return True no matter what...
  277. # so we are gonn compare the memory address directly, this is stupid
  278. if (outp.as_pointer() == link.from_socket.as_pointer()): break
  279. else:
  280. problem=link.from_node.name + "::" + link.from_socket.name
  281. raise RuntimeError(wrapRed(f"Error saving index of socket: {problem}"))
  282. for to_input_index, inp in enumerate(link.to_node.inputs):
  283. if (inp.as_pointer() == link.to_socket.as_pointer()): break
  284. else:
  285. problem = link.to_node.name + "::" + link.to_socket.name
  286. raise RuntimeError(wrapRed(f"Error saving index of socket: {problem}"))
  287. if current_tree_is_base_tree:
  288. if (only_selected and link.from_node.select) and (not link.to_node.select):
  289. # handle an output in the tree
  290. add_output_node=True
  291. if not (sock_name := unique_sockets_to.get(link.from_socket.node.name+link.from_socket.identifier)):
  292. sock_name = link.to_socket.name; name_stub = sock_name
  293. used_names = list(tree_in_out.keys()); i=0
  294. while sock_name in used_names:
  295. sock_name=name_stub+'.'+str(i).zfill(3); i+=1
  296. unique_sockets_to[link.from_socket.node.name+link.from_socket.identifier]=sock_name
  297. out_sock = out_sockets.get(sock_name)
  298. if not out_sock:
  299. out_sock = {}; out_sockets[sock_name] = out_sock
  300. out_sock["index"]=len(out_sockets) # zero indexed, so zero length makes zero the first index and so on, this works
  301. # what in the bad word is happening here?
  302. # why?
  303. # why no de-duplication?
  304. # what was I thinking?
  305. # TODO REFACTOR THIS SOON
  306. out_sock["name"] = sock_name
  307. out_sock["identifier"] = sock_name
  308. out_sock["bl_idname"] = link.to_socket.bl_idname
  309. out_sock["is_output"] = False
  310. out_sock["source"]=[link.to_socket.node.name,link.to_socket.identifier]
  311. out_sock["is_multi_input"] = False # this is not something I can even set on tree interface items, and this code is not intended for making Schema
  312. sock_data={}
  313. sock_data["name"] = sock_name
  314. sock_data["item_type"] = "SOCKET"
  315. sock_data["default_closed"] = False
  316. # record the actual bl_idname and the proper interface type.
  317. sock_data["socket_type"] = link.from_socket.interface_type
  318. sock_data["bl_socket_idname"] = link.from_socket.bl_idname
  319. sock_data["identifier"] = sock_name
  320. sock_data["in_out"]="OUTPUT"
  321. sock_data["index"]=out_sock["index"]
  322. tree_in_out[sock_name] = sock_data
  323. to_node_name=out_node["name"]
  324. to_socket_id=out_sock["identifier"]
  325. to_input_index=out_sock["index"]
  326. to_socket_name=out_sock["name"]
  327. elif (only_selected and (not link.from_node.select)) and link.to_node.select:
  328. add_input_node=True
  329. # we need to get a unique name for this
  330. # use the Tree IN/Out because we are dealing with Group in/out
  331. if not (sock_name := unique_sockets_from.get(link.from_socket.node.name+link.from_socket.identifier)):
  332. sock_name = link.from_socket.name; name_stub = sock_name
  333. used_names = list(tree_in_out.keys()); i=0
  334. while sock_name in used_names:
  335. sock_name=name_stub+'.'+str(i).zfill(3); i+=1
  336. unique_sockets_from[link.from_socket.node.name+link.from_socket.identifier]=sock_name
  337. in_sock = in_sockets.get(sock_name)
  338. if not in_sock:
  339. in_sock = {}; in_sockets[sock_name] = in_sock
  340. in_sock["index"]=len(in_sockets) # zero indexed, so zero length makes zero the first index and so on, this works
  341. #
  342. in_sock["name"] = sock_name
  343. in_sock["identifier"] = sock_name
  344. in_sock["bl_idname"] = link.from_socket.bl_idname
  345. in_sock["is_output"] = True
  346. in_sock["is_multi_input"] = False # this is not something I can even set on tree interface items, and this code is not intended for making Schema
  347. in_sock["source"] = [link.from_socket.node.name,link.from_socket.identifier]
  348. sock_data={}
  349. sock_data["name"] = sock_name
  350. sock_data["item_type"] = "SOCKET"
  351. sock_data["default_closed"] = False
  352. # record the actual bl_idname and the proper interface type.
  353. sock_data["socket_type"] = link.from_socket.interface_type
  354. sock_data["bl_socket_idname"] = link.from_socket.bl_idname
  355. sock_data["identifier"] = sock_name
  356. sock_data["in_out"]="INPUT"
  357. sock_data["index"]=in_sock["index"]
  358. tree_in_out[sock_name] = sock_data
  359. from_node_name=in_node.get("name")
  360. from_socket_id=in_sock["identifier"]
  361. from_outoput_index=in_sock["index"]
  362. from_socket_name=in_node.get("name")
  363. # parentheses matter here...
  364. elif (only_selected and not (link.from_node.select and link.to_node.select)):
  365. continue
  366. elif only_selected and not (link.from_node.select and link.to_node.select):
  367. continue # pass if both links are not selected
  368. links.append( (from_node_name,
  369. from_socket_id,
  370. to_node_name,
  371. to_socket_id,
  372. from_outoput_index,
  373. to_input_index,
  374. from_socket_name,
  375. to_socket_name) ) # it's a tuple
  376. if add_input_node or add_output_node:
  377. all_nodes_bounding_box=[Vector((float("inf"),float("inf"))), Vector((-float("inf"),-float("inf")))]
  378. for n in nodes.values():
  379. if n["location"][0] < all_nodes_bounding_box[0].x:
  380. all_nodes_bounding_box[0].x = n["location"][0]
  381. if n["location"][1] < all_nodes_bounding_box[0].y:
  382. all_nodes_bounding_box[0].y = n["location"][1]
  383. #
  384. if n["location"][0] > all_nodes_bounding_box[1].x:
  385. all_nodes_bounding_box[1].x = n["location"][0]
  386. if n["location"][1] > all_nodes_bounding_box[1].y:
  387. all_nodes_bounding_box[1].y = n["location"][1]
  388. if add_input_node:
  389. in_node["location"] = Vector((all_nodes_bounding_box[0].x-400, all_nodes_bounding_box[0].lerp(all_nodes_bounding_box[1], 0.5).y))
  390. nodes["MANTIS_AUTOGEN_GROUP_INPUT"]=in_node
  391. if add_output_node:
  392. out_node["location"] = Vector((all_nodes_bounding_box[1].x+400, all_nodes_bounding_box[0].lerp(all_nodes_bounding_box[1], 0.5).y))
  393. nodes["MANTIS_AUTOGEN_GROUP_OUTPUT"]=out_node
  394. export_data[tree.name] = (tree_info, tree_in_out, nodes, links,) # f_curves)
  395. import json
  396. if not write_file:
  397. return export_data # gross to have a different type of return value... but I don't care
  398. with open(path, "w") as file:
  399. print(wrapWhite("Writing mantis tree data to: "), wrapGreen(file.name))
  400. file.write( json.dumps(export_data, indent = 4) )
  401. # I'm gonna do this in a totally naive way, because this should already be sorted properly
  402. # for the sake of dependency satisfaction. So the current "tree" should be the "main" tree
  403. tree.filepath = path
  404. def do_import_from_file(filepath, context):
  405. import json
  406. all_trees = [n_tree for n_tree in bpy.data.node_groups if n_tree.bl_idname in ["MantisTree", "SchemaTree"]]
  407. for tree in all_trees:
  408. tree.is_exporting = True
  409. tree.do_live_update = False
  410. def do_cleanup(tree):
  411. tree.is_exporting = False
  412. tree.do_live_update = True
  413. tree.prevent_next_exec = True
  414. with open(filepath, 'r', encoding='utf-8') as f:
  415. data = json.load(f)
  416. do_import(data,context)
  417. for tree in all_trees:
  418. do_cleanup(tree)
  419. tree = bpy.data.node_groups[list(data.keys())[-1]]
  420. try:
  421. context.space_data.node_tree = tree
  422. except AttributeError: # not hovering over the Node Editor
  423. pass
  424. return {'FINISHED'}
  425. # otherwise:
  426. # repeat this because we left the with, this is bad and ugly but I don't care
  427. for tree in all_trees:
  428. do_cleanup(tree)
  429. return {'CANCELLED'}
  430. def do_import(data, context):
  431. trees = []
  432. tree_sock_id_maps = {}
  433. # First: init the interface of the node graph
  434. for tree_name, tree_data in data.items():
  435. tree_info = tree_data[0]
  436. tree_in_out = tree_data[1]
  437. # need to make a new tree; first, try to get it:
  438. tree = bpy.data.node_groups.get(tree_info["name"])
  439. if tree is None:
  440. tree = bpy.data.node_groups.new(tree_info["name"], tree_info["bl_idname"])
  441. tree.nodes.clear(); tree.links.clear(); tree.interface.clear()
  442. # this may be a bad bad thing to do without some kind of warning TODO TODO
  443. tree.is_executing = True
  444. tree.do_live_update = False
  445. trees.append(tree)
  446. tree_sock_id_map = {}
  447. tree_sock_id_maps[tree.name] = tree_sock_id_map
  448. interface_parent_me = {}
  449. # I need to guarantee that the interface items are in the right order.
  450. interface_sockets = [] # I'll just sort them afterwards so I hold them here.
  451. default_position=0 # We'll use this if the position attribute is not set when e.g. making groups.
  452. for s_name, s_props in tree_in_out.items():
  453. if s_props["item_type"] == 'SOCKET':
  454. if s_props["socket_type"] == "LayerMaskSocket":
  455. continue
  456. if (socket_type := s_props["socket_type"]) == "NodeSocketColor":
  457. socket_type = "VectorSocket"
  458. if bpy.app.version != (4,5,0):
  459. sock = tree.interface.new_socket(s_props["name"], in_out=s_props["in_out"], socket_type=socket_type)
  460. else: # blender 4.5.0 LTS, have to workaround a bug!
  461. from .versioning import workaround_4_5_0_interface_update
  462. sock = workaround_4_5_0_interface_update(tree=tree, name=s_props["name"], in_out=s_props["in_out"],
  463. sock_type=socket_type, parent_name=s_props.get("parent", ''))
  464. tree_sock_id_map[s_name] = sock.identifier
  465. if not (socket_position := s_props.get('position')):
  466. socket_position=default_position; default_position+=1
  467. interface_sockets.append( (sock, socket_position) )
  468. # TODO: set whatever properties are needed (default, etc)
  469. if panel := s_props.get("parent"): # this get is just to maintain compatibility with an older form of this script... and it is harmless
  470. interface_parent_me[sock] = (panel, s_props["position"])
  471. else: # it's a panel
  472. panel = tree.interface.new_panel(s_props["name"], description=s_props.get("description"), default_closed=s_props.get("default_closed"))
  473. for socket, (panel, index) in interface_parent_me.items():
  474. tree.interface.move_to_parent(
  475. socket,
  476. tree.interface.items_tree.get(panel),
  477. index,
  478. )
  479. # BUG this was screwing up the order of things
  480. # so I wan tot fix it and re-enable it
  481. if False:
  482. # Go BACK through and set the index/position now that all items exist.
  483. interface_sockets.sort(key=lambda a : a[1])
  484. for (socket, position) in interface_sockets:
  485. tree.interface.move(socket, position)
  486. # Now go and do nodes and links
  487. for tree_name, tree_data in data.items():
  488. print ("Importing sub-graph: %s with %s nodes" % (wrapGreen(tree_name), wrapPurple(len(tree_data[2]))) )
  489. tree_info = tree_data[0]
  490. nodes = tree_data[2]
  491. links = tree_data[3]
  492. parent_me = []
  493. tree = bpy.data.node_groups.get(tree_info["name"])
  494. tree.is_executing = True
  495. tree.do_live_update = False
  496. trees.append(tree)
  497. tree_sock_id_map=tree_sock_id_maps[tree.name]
  498. interface_parent_me = {}
  499. # from mantis.utilities import prRed, prWhite, prOrange, prGreen
  500. for name, propslist in nodes.items():
  501. bl_idname = propslist["bl_idname"]
  502. if bl_idname in NODES_REMOVED:
  503. prWhite(f"INFO: Ignoring import of node {name} of type {bl_idname}; it has been removed.")
  504. continue
  505. n = tree.nodes.new(bl_idname)
  506. if bl_idname in ["DeformerMorphTargetDeform"]:
  507. n.inputs.remove(n.inputs[-1]) # get rid of the wildcard
  508. if n.bl_idname in [ "SchemaArrayInput",
  509. "SchemaArrayInputGet",
  510. "SchemaArrayOutput",
  511. "SchemaConstInput",
  512. "SchemaConstOutput",
  513. "SchemaOutgoingConnection",
  514. "SchemaIncomingConnection",]:
  515. n.update()
  516. if sub_tree := propslist.get("node_tree"):
  517. n.node_tree = bpy.data.node_groups.get(sub_tree)
  518. from .base_definitions import node_group_update
  519. n.is_updating = True
  520. try:
  521. node_group_update(n, force = True)
  522. finally:
  523. n.is_updating=False
  524. sockets_removed = []
  525. for i, (s_id, s_val) in enumerate(propslist["sockets"].items()):
  526. for socket_removed in SOCKETS_REMOVED:
  527. if n.bl_idname == socket_removed[0] and s_id == socket_removed[1]:
  528. prWhite(f"INFO: Ignoring import of socket {s_id}; it has been removed.")
  529. sockets_removed.append(s_val["index"])
  530. sockets_removed.sort()
  531. continue
  532. try:
  533. if s_val["is_output"]: # for some reason it thinks the index is a string?
  534. if n.bl_idname in "MantisSchemaGroup":
  535. n.is_updating = True
  536. try:
  537. socket = n.outputs.new(s_val["bl_idname"], s_val["name"], identifier=s_id)
  538. finally:
  539. n.is_updating=False
  540. else: # IT IS NOT CLEAR but this is what throws the index error below BAD
  541. for socket in n.outputs:
  542. if socket.identifier == s_id:
  543. break
  544. else: # otherwise try to get the index
  545. prRed("Getting Imported Socket by Index. Maybe there will be an error.")
  546. socket = n.outputs[int(s_val["index"])]
  547. else:
  548. for removed_index in sockets_removed:
  549. if s_val["index"] > removed_index:
  550. s_val["index"]-=1
  551. if s_val["index"] >= len(n.inputs):
  552. if n.bl_idname in add_inputs_bl_idnames:
  553. socket = n.inputs.new(s_val["bl_idname"], s_val["name"], identifier=s_id, use_multi_input=s_val["is_multi_input"])
  554. elif n.bl_idname in ["MantisSchemaGroup"]:
  555. n.is_updating = True
  556. try:
  557. socket = n.inputs.new(s_val["bl_idname"], s_val["name"], identifier=s_id, use_multi_input=s_val["is_multi_input"])
  558. finally:
  559. n.is_updating=False
  560. elif n.bl_idname in ["NodeGroupOutput"]:
  561. pass # this is dealt with separately
  562. else:
  563. prWhite("Not found: ", propslist['name'], s_val["name"], s_id)
  564. prRed("Index: ", s_val["index"], "Number of inputs", len(n.inputs))
  565. raise NotImplementedError(wrapRed(f"{n.bl_idname} in {n.id_data.name} needs to be handled in JSON load."))
  566. else:
  567. # IT IS NOT CLEAR but this is what throws the index error below BAD
  568. # first try to get by ID
  569. for socket in n.inputs:
  570. if socket.identifier == s_id:
  571. break
  572. else: # otherwise try to get the index
  573. prRed("Getting Imported Socket by Index. Maybe there will be an error.")
  574. socket = n.inputs[int(s_val["index"])]
  575. except IndexError:
  576. socket = fix_custom_parameter(n, propslist["sockets"][s_id])
  577. if socket is None:
  578. is_output = "output" if {s_val["is_output"]} else "input"
  579. prRed(s_val, type(s_val))
  580. raise RuntimeError(is_output, n.name, s_val["name"], s_id, len(n.inputs))
  581. for s_p, s_v in s_val.items():
  582. if s_p not in ["default_value"]:
  583. if s_p == "search_prop" and n.bl_idname == 'UtilityMetaRig':
  584. socket.node.armature= s_v
  585. socket.search_prop=bpy.data.objects.get(s_v)
  586. if s_p == "search_prop" and n.bl_idname in ['UtilityMatrixFromCurve', 'UtilityMatricesFromCurve']:
  587. socket.search_prop=bpy.data.objects.get(s_v)
  588. elif s_p == "bone" and socket.bl_idname == 'EnumMetaBoneSocket':
  589. socket.bone = s_v
  590. socket.node.pose_bone = s_v
  591. continue # not editable and NOT SAFE
  592. #
  593. if socket.bl_idname in ["BooleanThreeTupleSocket"]:
  594. value = bool(s_v[0]), bool(s_v[1]), bool(s_v[2]),
  595. s_v = value
  596. try:
  597. setattr(socket, s_p , s_v)
  598. except TypeError as e:
  599. prRed("Can't set socket due to type mismatch: ", n.name, socket.name, s_p, s_v)
  600. # raise e
  601. except ValueError as e:
  602. prRed("Can't set socket due to type mismatch: ", n.name, socket.name, s_p, s_v)
  603. # raise e
  604. except AttributeError as e:
  605. prWhite("Tried to write a read-only property, ignoring...")
  606. prWhite(f"{socket.node.name}[{socket.name}].{s_p} is read only, cannot set value to {s_v}")
  607. for p, v in propslist.items():
  608. if p in ["node_tree", "sockets", "warning_propagation", "socket_idname"]:
  609. continue
  610. # will throw AttributeError if read-only
  611. # will throw TypeError if wrong type...
  612. if n.bl_idname == "NodeFrame" and p in ["width, height, location"]:
  613. continue
  614. if version < (4,4,0) and p == 'location_absolute':
  615. continue
  616. if p == "parent" and v is not None:
  617. parent_me.append( (n.name, v) )
  618. v = None # for now) #TODO
  619. try:
  620. setattr(n, p, v)
  621. except Exception as e:
  622. print (p)
  623. raise e
  624. for l in links:
  625. id1 = l[1]
  626. id2 = l[3]
  627. #
  628. name1=l[6]
  629. name2=l[7]
  630. # if the from/to socket or node has been removed, continue
  631. from_node = tree.nodes.get(l[0])
  632. if not from_node:
  633. prWhite(f"INFO: cannot create link {l[0]}:{l[1]} --> {l[2]}:{l[3]}")
  634. continue
  635. if hasattr(from_node, "node_tree"): # now we have to map by name actually
  636. try:
  637. id1 = from_node.outputs[l[4]].identifier
  638. except IndexError:
  639. prRed ("Index incorrect")
  640. id1 = None
  641. elif from_node.bl_idname in ["NodeGroupInput"]:
  642. id1 = tree_sock_id_map.get(l[1])
  643. if id1 is None:
  644. prRed(l[1])
  645. # prOrange (l[1], id1)
  646. elif from_node.bl_idname in ["SchemaArrayInput", "SchemaConstInput", "SchemaIncomingConnection"]:
  647. # try the index instead
  648. id1 = from_node.outputs[l[4]].identifier
  649. for from_sock in from_node.outputs:
  650. if from_sock.identifier == id1: break
  651. else: # we can raise a runtime error here actually
  652. from_sock = None
  653. to_node = tree.nodes[l[2]]
  654. if hasattr(to_node, "node_tree"):
  655. try:
  656. id2 = to_node.inputs[l[5]].identifier
  657. except IndexError:
  658. prRed ("Index incorrect")
  659. id2 = None
  660. elif to_node.bl_idname in ["NodeGroupOutput"]:
  661. id2 = tree_sock_id_map.get(l[3])
  662. elif to_node.bl_idname in ["SchemaArrayOutput", "SchemaConstOutput", "SchemaOutgoingConnection"]:
  663. # try the index instead
  664. id2 = to_node.inputs[l[5]].identifier
  665. for to_sock in to_node.inputs:
  666. if to_sock.identifier == id2: break
  667. else:
  668. to_sock = None
  669. try:
  670. link = tree.links.new(from_sock, to_sock)
  671. except TypeError:
  672. if ((id1 is not None) and ("Layer Mask" in id1)) or ((id2 is not None) and ("Layer Mask" in id2)):
  673. pass
  674. else:
  675. prWhite(f"looking for... {name1}:{id1}, {name2}:{id2}")
  676. prRed (f"Failed: {l[0]}:{l[1]} --> {l[2]}:{l[3]}")
  677. prRed (f" got node: {from_node.name}, {to_node.name}")
  678. prRed (f" got socket: {from_sock}, {to_sock}")
  679. prOrange(to_node.inputs.keys())
  680. if from_sock is None:
  681. prOrange ("Candidates...")
  682. for out in from_node.outputs:
  683. prOrange(" %s, id=%s" % (out.name, out.identifier))
  684. for k, v in tree_sock_id_map.items():
  685. print (wrapOrange(k), wrapPurple(v))
  686. if to_sock is None:
  687. prOrange ("Candidates...")
  688. for inp in to_node.inputs:
  689. prOrange(" %s, id=%s" % (inp.name, inp.identifier))
  690. for k, v in tree_sock_id_map.items():
  691. print (wrapOrange(k), wrapPurple(v))
  692. raise RuntimeError
  693. # if at this point it doesn't work... we need to fix
  694. for name, p in parent_me:
  695. if (n := tree.nodes.get(name)) and (p := tree.nodes.get(p)):
  696. n.parent = p
  697. # otherwise the frame node is missing because it was not included in the data e.g. when grouping nodes.
  698. tree.is_executing = False
  699. tree.do_live_update = True
  700. import bpy
  701. from bpy_extras.io_utils import ImportHelper, ExportHelper
  702. from bpy.props import StringProperty, BoolProperty, EnumProperty
  703. from bpy.types import Operator
  704. # Save As
  705. class MantisExportNodeTreeSaveAs(Operator, ExportHelper):
  706. """Export a Mantis Node Tree by filename."""
  707. bl_idname = "mantis.export_save_as"
  708. bl_label = "Export Mantis Tree as ...(JSON)"
  709. # ExportHelper mix-in class uses this.
  710. filename_ext = ".rig"
  711. filter_glob: StringProperty(
  712. default="*.rig",
  713. options={'HIDDEN'},
  714. maxlen=255, # Max internal buffer length, longer would be clamped.
  715. )
  716. @classmethod
  717. def poll(cls, context):
  718. return hasattr(context.space_data, 'path')
  719. def execute(self, context):
  720. # we need to get the dependent trees from self.tree...
  721. # there is no self.tree
  722. # how do I choose a tree?
  723. base_tree=context.space_data.path[-1].node_tree
  724. from .utilities import all_trees_in_tree
  725. trees = all_trees_in_tree(base_tree)[::-1]
  726. prGreen("Exporting node graph with dependencies...")
  727. for t in trees:
  728. prGreen ("Node graph: \"%s\"" % (t.name))
  729. base_tree.is_exporting = True
  730. export_to_json(trees, self.filepath)
  731. base_tree.is_exporting = False
  732. base_tree.prevent_next_exec = True
  733. return {'FINISHED'}
  734. # Save
  735. class MantisExportNodeTreeSave(Operator):
  736. """Save a Mantis Node Tree to disk."""
  737. bl_idname = "mantis.export_save"
  738. bl_label = "Export Mantis Tree (JSON)"
  739. @classmethod
  740. def poll(cls, context):
  741. return hasattr(context.space_data, 'path')
  742. def execute(self, context):
  743. base_tree=context.space_data.path[-1].node_tree
  744. from .utilities import all_trees_in_tree
  745. trees = all_trees_in_tree(base_tree)[::-1]
  746. prGreen("Exporting node graph with dependencies...")
  747. for t in trees:
  748. prGreen ("Node graph: \"%s\"" % (t.name))
  749. base_tree.is_exporting = True
  750. export_to_json(trees, self.filepath)
  751. base_tree.is_exporting = False
  752. base_tree.prevent_next_exec = True
  753. return {'FINISHED'}
  754. # Save Choose:
  755. class MantisExportNodeTree(Operator):
  756. """Save a Mantis Node Tree to disk."""
  757. bl_idname = "mantis.export_save_choose"
  758. bl_label = "Export Mantis Tree (JSON)"
  759. @classmethod
  760. def poll(cls, context):
  761. return hasattr(context.space_data, 'path')
  762. def execute(self, context):
  763. base_tree=context.space_data.path[-1].node_tree
  764. if base_tree.filepath:
  765. prRed(base_tree.filepath)
  766. return bpy.ops.mantis.export_save()
  767. else:
  768. return bpy.ops.mantis.export_save_as('INVOKE_DEFAULT')
  769. # here is what needs to be done...
  770. # - modify this to work with a sort of parsed-tree instead (sort of)
  771. # - this needs to treat each sub-graph on its own
  772. # - is this a problem? do I need to reconsider how I treat the graph data in mantis?
  773. # - I should learn functional programming / currying
  774. # - then the parsed-tree this builds must be executed as Blender nodes
  775. # - I think... this is not important right now. not yet.
  776. # - KEEP IT SIMPLE, STUPID
  777. class MantisImportNodeTree(Operator, ImportHelper):
  778. """Import a Mantis Node Tree."""
  779. bl_idname = "mantis.import_tree"
  780. bl_label = "Import Mantis Tree (JSON)"
  781. # ImportHelper mixin class uses this
  782. filename_ext = ".rig"
  783. filter_glob : StringProperty(
  784. default="*.rig",
  785. options={'HIDDEN'},
  786. maxlen=255, # Max internal buffer length, longer would be clamped.
  787. )
  788. def execute(self, context):
  789. return do_import_from_file(self.filepath, context)
  790. # this is useful:
  791. # https://blender.stackexchange.com/questions/73286/how-to-call-a-confirmation-dialog-box
  792. # class MantisReloadConfirmMenu(bpy.types.Panel):
  793. # bl_label = "Confirm?"
  794. # bl_idname = "OBJECT_MT_mantis_reload_confirm"
  795. # def draw(self, context):
  796. # layout = self.layout
  797. # layout.operator("mantis.reload_tree")
  798. class MantisReloadNodeTree(Operator):
  799. # """Import a Mantis Node Tree."""
  800. # bl_idname = "mantis.reload_tree"
  801. # bl_label = "Import Mantis Tree"
  802. """Reload Mantis Tree"""
  803. bl_idname = "mantis.reload_tree"
  804. bl_label = "Confirm reload tree?"
  805. bl_options = {'REGISTER', 'INTERNAL'}
  806. @classmethod
  807. def poll(cls, context):
  808. if hasattr(context.space_data, 'path'):
  809. return True
  810. return False
  811. def invoke(self, context, event):
  812. return context.window_manager.invoke_confirm(self, event)
  813. def execute(self, context):
  814. base_tree=context.space_data.path[-1].node_tree
  815. if not base_tree.filepath:
  816. self.report({'ERROR'}, "Tree has not been saved - so it cannot be reloaded.")
  817. return {'CANCELLED'}
  818. self.report({'INFO'}, "reloading tree")
  819. return do_import_from_file(base_tree.filepath, context)
  820. # todo:
  821. # - export metarig and option to import it
  822. # - same with controls
  823. # - it would be nice to have a library of these that can be imported alongside the mantis graph