i_o.py 39 KB

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