i_o.py 40 KB

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