i_o.py 38 KB

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