i_o.py 39 KB

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