i_o.py 39 KB

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