i_o.py 45 KB

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