i_o.py 39 KB

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