i_o.py 47 KB

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