i_o.py 58 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288
  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, Matrix
  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. # Debugging values.
  15. print_read_only_warning = False
  16. print_link_failure = False
  17. # ignore these because they are either unrelated python stuff or useless or borked
  18. prop_ignore = [ "__dict__", "__doc__", "__module__", "__weakref__",# "name",
  19. "bl_height_default", "bl_height_max", "bl_height_min",
  20. "bl_icon", "bl_rna", "bl_static_type", "bl_description",
  21. "bl_width_default", "bl_width_max", "bl_width_min",
  22. "__annotations__", "original", "rna_type", "view_center",
  23. "links", "nodes", "internal_links", "inputs", "outputs",
  24. "__slots__", "dimensions", "type", "interface",
  25. "library_weak_reference", "parsed_tree", "node_tree_updater",
  26. "asset_data", "preview", # blender asset stuff
  27. "object_reference", # this one is here to hold on to widgets when appending
  28. "color_tag" , # added in blender 4.4, not used by Mantis, readonly.
  29. # more blender properties...
  30. "bl_use_group_interface", "default_group_node_width", "id_type",
  31. # blender runtime stuff
  32. "animation_data", "description", "grease_pencil", "is_editable",
  33. "is_embedded_data", "is_evaluated", "is_library_indirect", "is_missing",
  34. "is_runtime_data", "library", "name_full", "override_library",
  35. "session_uid", "tag", "use_extra_user", "use_fake_user", "users",
  36. # some Mantis stuff I don't need to save
  37. "do_live_update", "is_executing", "is_exporting", "hash", "filepath",
  38. "prevent_next_exec", "execution_id", "num_links", "tree_valid",
  39. "interface_helper",
  40. # node stuff
  41. "mantis_node_class_name", "color", "height", "initialized", "select",
  42. "show_options", "show_preview", "show_texture", "use_custom_color",
  43. "warning_propagation",
  44. # these are in Bone
  45. "socket_count", "display_bb_settings", "display_def_settings",
  46. "display_ik_settings", "display_vp_settings",
  47. ]
  48. # don't ignore: "bl_idname", "bl_label",
  49. # ignore the name, it's the dict - key for the node props
  50. # no that's stupid don't ignore the name good grief
  51. # I am doing this because these are interactions with other addons that cause problems and probably don't exist for any given user
  52. prop_ignore.extend(['keymesh'])
  53. # trees
  54. prop_ignore_tree = prop_ignore.copy()
  55. prop_ignore_tree.extend(["bl_label", "name"])
  56. prop_ignore_interface = prop_ignore.copy()
  57. # Geometry Nodes stuff that Mantis doesn't use
  58. prop_ignore_interface.extend( [ "attribute_domain",
  59. "default_attribute_name",
  60. "default_input",
  61. "force_non_field",
  62. "hide_in_modifier",
  63. "hide_value",
  64. # no idea what this is, also don't care
  65. "is_inspect_output",
  66. "is_panel_toggle",
  67. "layer_selection_field",
  68. "structure_type", ] )
  69. from bpy.app import version
  70. if version >= (4,5,0):
  71. SOCKETS_REMOVED.append( ("LinkSplineIK", "Use Original Scale"))
  72. add_inputs_bl_idnames = [
  73. "UtilityDriver", "UtilityFCurve", "DeformerMorphTargetDeform",
  74. "LinkArmature",
  75. "xFormBoneNode"
  76. # for custom properties, right?
  77. # For a long time this wasn't in here and I guess there weren't problems
  78. # I really don't know if adding it here is right...
  79. ]
  80. # this works but it is really ugly and probably quite inneficient
  81. # TODO: make hotkeys for export and import and reload from file
  82. # we need to give the tree a filepath attribute and update it on saving
  83. # then we need to use the filepath attribute to load from
  84. # finally we need to use a few operators to choose whether to open a menu or not
  85. # and we need a message to display on save/load so that the user knows it is happening
  86. # TODO:
  87. # Additionally export MetaRig and Curve and other referenced data
  88. # Meshes can be exported as .obj and imported via GN
  89. def TellClasses():
  90. return [ MantisExportNodeTreeSaveAs,
  91. MantisExportNodeTreeSave,
  92. MantisExportNodeTree,
  93. MantisImportNodeTree,
  94. MantisReloadNodeTree]
  95. # https://stackoverflow.com/questions/42033142/is-there-an-easy-way-to-check-if-an-object-is-json-serializable-in-python - thanks!
  96. def is_jsonable(x):
  97. import json
  98. try:
  99. json.dumps(x)
  100. return True
  101. except (TypeError, OverflowError):
  102. return False
  103. # https://stackoverflow.com/questions/295135/turn-a-stritree-into-a-valid-filename - thank you user "Sophie Gage"
  104. def remove_special_characters(name):
  105. import re; return re.sub('[^\w_.)( -]', '', name)# re = regular expressions
  106. def fix_custom_parameter(n, property_definition, ):
  107. if n.bl_idname in ['xFormNullNode', 'xFormBoneNode', 'xFormArmatureNode', 'xFormGeometryObjectNode',]:
  108. prop_name = property_definition["name"]
  109. prop_type = property_definition["bl_idname"]
  110. if prop_type in ['ParameterBoolSocket', 'ParameterIntSocket', 'ParameterFloatSocket', 'ParameterVectorSocket' ]:
  111. # is it good to make both of them?
  112. input = n.inputs.new( prop_type, prop_name)
  113. output = n.outputs.new( prop_type, prop_name)
  114. if property_definition["is_output"] == True:
  115. return output
  116. return input
  117. elif n.bl_idname in ['LinkArmature']:
  118. prop_name = property_definition["name"]
  119. prop_type = property_definition["bl_idname"]
  120. input = n.inputs.new( prop_type, prop_name)
  121. return input
  122. return None
  123. # def scan_tree_for_objects(base_tree, current_tree):
  124. # # goal: find all referenced armature and curve objects
  125. # # return [set(armatures), set(curves)]
  126. # armatures = set()
  127. # curves = set()
  128. # for node in base_tree.parsed_tree.values():
  129. # from .utilities import get_node_prototype
  130. # if node.ui_signature is None:
  131. # continue
  132. # ui_node = get_node_prototype(node.ui_signature, node.base_tree)
  133. # if ui_node is None or ui_node.id_data != current_tree:
  134. # continue
  135. # if hasattr(node, "bGetObject"):
  136. # ob = node.bGetObject()
  137. # print(node, ob)
  138. # if ob is None:
  139. # continue
  140. # if not hasattr(node, "type"):
  141. # continue
  142. # if ob.type == 'ARMATURE':
  143. # armatures.add(ob)
  144. # if ob.type == 'CURVE':
  145. # curves.add(ob)
  146. # return (armatures, curves)
  147. # Currently this isn't very robust and doesn't seek backwards
  148. # to see if a dependency is created by node connections.
  149. # TODO it remains to be seen if that is even a desirable behaviour.
  150. def scan_tree_for_objects(base_tree, current_tree):
  151. from bpy import data
  152. armatures, curves = set(), set()
  153. for node in current_tree.nodes:
  154. match node.bl_idname:
  155. case "UtilityMetaRig":
  156. if node.inputs[0].is_linked:
  157. continue
  158. if (armature := node.inputs[0].search_prop) is not None:
  159. armatures.add(armature)
  160. case "InputExistingGeometryObjectNode":
  161. if node.inputs["Name"].is_linked:
  162. continue
  163. ob_name = node.inputs["Name"].default_value
  164. if ob := data.objects.get(ob_name):
  165. if ob.type == "ARMATURE":
  166. armatures.add(ob)
  167. elif ob.type == "CURVE":
  168. curves.add(ob)
  169. case "xFormArmatureNode":
  170. if node.inputs["Name"].is_linked:
  171. continue
  172. armature_name = node.inputs["Name"].default_value
  173. armature = data.objects.get(armature_name)
  174. if armature:
  175. armatures.add(armature)
  176. case "xFormGeometryObjectNode":
  177. if node.inputs["Name"].is_linked:
  178. continue
  179. ob_name = node.inputs["Name"].default_value
  180. if ob := data.objects.get(ob_name):
  181. if ob.type == "ARMATURE":
  182. armatures.add(ob)
  183. elif ob.type == "CURVE":
  184. curves.add(ob)
  185. for input in node.inputs:
  186. if input.bl_idname in ["EnumCurveSocket"]:
  187. if input.search_prop is not None:
  188. curves.add(input.search_prop)
  189. # NOW check the parsed_tree and see if it is possible to find any other
  190. # objects referred to/provided by the tree
  191. return (curves, armatures )
  192. from dataclasses import dataclass, field, asdict
  193. # some basic classes to define curve point types
  194. @dataclass
  195. class crv_pnt_data():
  196. co : tuple[float, float, float] = field(default=(0,0,0,))
  197. handle_left : tuple[float, float, float] = field(default=(0,0,0,))
  198. handle_right : tuple[float, float, float] = field(default=(0,0,0,))
  199. handle_left_type : str = field(default="ALIGNED")
  200. handle_right_type : str = field(default="ALIGNED")
  201. radius : float = field(default=0.0)
  202. tilt : float = field(default=0.0)
  203. w : float = field(default=0.0)
  204. @dataclass
  205. class spline_data():
  206. type : str = field(default='POLY')
  207. points : list[dict] = field(default_factory=[])
  208. order_u : int = field(default=4)
  209. radius_interpolation : str = field(default="LINEAR")
  210. tilt_interpolation : str = field(default="LINEAR")
  211. resolution_u : int = field(default=12)
  212. use_bezier_u : bool = field(default=False)
  213. use_cyclic_u : bool = field(default=False)
  214. use_endpoint_u : bool = field(default=False)
  215. index : int = field(default=0)
  216. object_name : str = field(default='Curve')
  217. def get_curve_for_pack(object):
  218. splines = []
  219. for i, spline in enumerate(object.data.splines):
  220. points = []
  221. if spline.type == 'BEZIER':
  222. for point in spline.bezier_points:
  223. export_pnt = crv_pnt_data(
  224. co = tuple(point.co),
  225. radius = point.radius,
  226. tilt = point.tilt,
  227. handle_left = tuple(point.handle_left),
  228. handle_right = tuple(point.handle_right),
  229. handle_left_type = point.handle_left_type,
  230. handle_right_type = point.handle_right_type,
  231. )
  232. points.append(asdict(export_pnt))
  233. else:
  234. for point in spline.points:
  235. export_pnt = crv_pnt_data(
  236. co = point.co[:3], # exclude the w value
  237. radius = point.radius,
  238. tilt = point.tilt,
  239. w = point.co[3],
  240. )
  241. points.append(asdict(export_pnt))
  242. export_spl = spline_data(
  243. type = spline.type,
  244. points = points,
  245. order_u = spline.order_u,
  246. radius_interpolation = spline.radius_interpolation,
  247. tilt_interpolation = spline.tilt_interpolation,
  248. resolution_u = spline.resolution_u,
  249. use_bezier_u = spline.use_bezier_u,
  250. use_cyclic_u = spline.use_cyclic_u,
  251. use_endpoint_u = spline.use_endpoint_u,
  252. index = i,
  253. object_name = object.name,)
  254. splines.append(asdict(export_spl))
  255. return splines
  256. def matrix_as_tuple(matrix):
  257. return ( matrix[0][0], matrix[0][1], matrix[0][2], matrix[0][3],
  258. matrix[1][0], matrix[1][1], matrix[1][2], matrix[1][3],
  259. matrix[2][0], matrix[2][1], matrix[2][2], matrix[2][3],
  260. matrix[3][0], matrix[3][1], matrix[3][2], matrix[3][3], )
  261. @dataclass
  262. class metabone_data:
  263. object_name : str = field(default='')
  264. name : str = field(default=''),
  265. type : str = field(default='BONE'),
  266. matrix : tuple[float] = field(default=()),
  267. parent : str = field(default=''),
  268. length : float = field(default=-1.0),
  269. children : list[str] = field(default_factory=[]),
  270. # keep it really simple for now. I'll add BBone and envelope later on
  271. # when I make them accessible from the meta-rig
  272. def get_armature_for_pack(object):
  273. metarig_data = {}
  274. armature_children = []
  275. for bone in object.data.bones:
  276. parent_name = ''
  277. if bone.parent is None:
  278. armature_children.append(bone.name)
  279. else:
  280. parent_name=bone.parent.name
  281. children=[]
  282. for c in bone.children:
  283. children.append(c.name)
  284. bone_data = metabone_data( object_name = object.name,
  285. name=bone.name, type='BONE',
  286. matrix=matrix_as_tuple(bone.matrix_local),
  287. parent=parent_name, length = bone.length, children = children,
  288. )
  289. metarig_data[bone.name]=asdict(bone_data)
  290. armature_data = metabone_data( object_name = object.name,
  291. name=object.name, type='ARMATURE',
  292. matrix=matrix_as_tuple(object.matrix_world),
  293. parent="", # NOTE that this is not always a fair assumption!
  294. length = -1.0, children = armature_children,)
  295. metarig_data[object.name] = asdict(armature_data)
  296. metarig_data["MANTIS_RESERVED"] = asdict(armature_data) # just in case a bone is named the same as the armature
  297. return metarig_data
  298. def get_socket_data(socket, ignore_if_default=False):
  299. # TODO: don't get stuff in the socket templates
  300. # PROBLEM: I don't have easy access to this from the ui class (or mantis class)
  301. socket_data = {}
  302. socket_data["name"] = socket.name
  303. socket_data["bl_idname"] = socket.bl_idname
  304. socket_data["is_output"] = socket.is_output
  305. socket_data["is_multi_input"] = socket.is_multi_input
  306. # here is where we'll handle a socket_data'socket special data
  307. if socket.bl_idname == "EnumMetaBoneSocket":
  308. socket_data["bone"] = socket.bone
  309. if socket.bl_idname in ["EnumMetaBoneSocket", "EnumMetaRigSocket", "EnumCurveSocket"]:
  310. if sp := socket.get("search_prop"): # may be None
  311. socket_data["search_prop"] = sp.name # this is an object.
  312. #
  313. if hasattr(socket, "default_value"):
  314. value = socket.default_value
  315. else:
  316. value = None
  317. return socket_data # we don't need to store any more.
  318. if not is_jsonable(value): # FIRST try and make a tuple out of it because JSON doesn't like mutables
  319. value = tuple(value)
  320. if not is_jsonable(value): # now see if it worked and crash out if it didn't
  321. raise RuntimeError(f"Error serializing data in {socket.node.name}::{socket.name} for value of type {type(value)}")
  322. socket_data["default_value"] = value
  323. # TODO TODO implement "ignore if default" feature here
  324. # at this point we can get the custom parameter ui hints if we want
  325. if not socket.is_output:
  326. # try and get this data
  327. if value := getattr(socket,'min', None):
  328. socket_data["min"] = value
  329. if value := getattr(socket,'max', None):
  330. socket_data["max"] = value
  331. if value := getattr(socket,'soft_min', None):
  332. socket_data["soft_min"] = value
  333. if value := getattr(socket,'soft_max', None):
  334. socket_data["soft_max"] = value
  335. if value := getattr(socket,'description', None):
  336. socket_data["description"] = value
  337. return socket_data
  338. #
  339. def get_node_data(ui_node):
  340. # if this is a node-group, force it to update its interface, because it may be messed up.
  341. # can remove this HACK when I have stronger guarentees about node-group's keeping the interface
  342. from .base_definitions import node_group_update
  343. if hasattr(ui_node, "node_tree"):
  344. ui_node.is_updating = True
  345. try: # HERE BE DANGER
  346. node_group_update(ui_node, force=True)
  347. finally: # ensure this line is run even if there is an error
  348. ui_node.is_updating = False
  349. node_props, inputs, outputs = {}, {}, {}
  350. for propname in dir(ui_node):
  351. value = getattr(ui_node, propname)
  352. if propname in ['fake_fcurve_ob']:
  353. value=value.name
  354. if (propname in prop_ignore) or ( callable(value) ):
  355. continue
  356. if value.__class__.__name__ in ["Vector", "Color"]:
  357. value = tuple(value)
  358. if isinstance(value, bpy.types.NodeTree):
  359. value = value.name
  360. if isinstance(value, bpy.types.bpy_prop_array):
  361. value = tuple(value)
  362. if propname == "parent" and value:
  363. value = value.name
  364. if not is_jsonable(value):
  365. raise RuntimeError(f"Could not export... {ui_node.name}, {propname}, {type(value)}")
  366. if value is None:
  367. continue
  368. node_props[propname] = value
  369. # so we have to accumulate the parent location because the location is not absolute
  370. if propname == "location" and ui_node.parent is not None:
  371. location_acc = Vector((0,0))
  372. parent = ui_node.parent
  373. while (parent):
  374. location_acc += parent.location
  375. parent = parent.parent
  376. location_acc += getattr(ui_node, propname)
  377. node_props[propname] = tuple(location_acc)
  378. # this works!
  379. if ui_node.bl_idname in ['RerouteNode']:
  380. return node_props # we don't need to get the socket information.
  381. for i, ui_socket in enumerate(ui_node.inputs):
  382. socket = get_socket_data(ui_socket)
  383. socket["index"]=i
  384. inputs[ui_socket.identifier] = socket
  385. for i, ui_socket in enumerate(ui_node.outputs):
  386. socket = get_socket_data(ui_socket)
  387. socket["index"]=i
  388. outputs[ui_socket.identifier] = socket
  389. node_props["inputs"] = inputs
  390. node_props["outputs"] = outputs
  391. return node_props
  392. def get_tree_data(tree):
  393. tree_info = {}
  394. for propname in dir(tree):
  395. # if getattr(tree, propname):
  396. # pass
  397. if (propname in prop_ignore_tree) or ( callable(getattr(tree, propname)) ):
  398. continue
  399. v = getattr(tree, propname)
  400. if isinstance(getattr(tree, propname), bpy.types.bpy_prop_array):
  401. v = tuple(getattr(tree, propname))
  402. if not is_jsonable( v ):
  403. raise RuntimeError(f"Not JSON-able: {propname}, type: {type(v)}")
  404. tree_info[propname] = v
  405. tree_info["name"]=tree.name
  406. return tree_info
  407. def get_interface_data(tree, tree_in_out):
  408. for sock in tree.interface.items_tree:
  409. sock_data={}
  410. if sock.item_type == 'PANEL':
  411. sock_data["name"] = sock.name
  412. sock_data["item_type"] = sock.item_type
  413. sock_data["description"] = sock.description
  414. sock_data["default_closed"] = sock.default_closed
  415. tree_in_out[sock.name] = sock_data
  416. # if it is a socket....
  417. else:
  418. # we need to get the socket class from the bl_idname
  419. bl_socket_idname = sock.bl_socket_idname
  420. # try and import it
  421. from . import socket_definitions
  422. # WANT an attribute error if this fails.
  423. socket_class = getattr(socket_definitions, bl_socket_idname)
  424. sock_parent = None
  425. if sock.parent:
  426. sock_parent = sock.parent.name
  427. for propname in dir(sock):
  428. if propname in prop_ignore_interface:
  429. continue
  430. if (propname == "parent"):
  431. sock_data[propname] = sock_parent
  432. continue
  433. v = getattr(sock, propname)
  434. if (propname in prop_ignore) or ( callable(v) ):
  435. continue
  436. if isinstance(getattr(sock, propname), bpy.types.bpy_prop_array):
  437. v = tuple(getattr(sock, propname))
  438. if not is_jsonable( v ):
  439. raise RuntimeError(f"{propname}, {type(v)}")
  440. sock_data[propname] = v
  441. # this is a property. pain.
  442. sock_data["socket_type"] = socket_class.interface_type.fget(socket_class)
  443. tree_in_out[sock.identifier] = sock_data
  444. def export_to_json(trees, base_tree=None, path="", write_file=True, only_selected=False, ):
  445. export_data = {}
  446. for tree in trees:
  447. current_tree_is_base_tree = False
  448. if tree is trees[-1]:
  449. current_tree_is_base_tree = True
  450. tree_info, tree_in_out = {}, {}
  451. tree_info = get_tree_data(tree)
  452. curves, metarig_data = {}, {}
  453. embed_metarigs=True
  454. if base_tree and embed_metarigs:
  455. curves_in_tree, metarigs_in_tree = scan_tree_for_objects(base_tree, tree)
  456. for crv in curves_in_tree:
  457. curves[crv.name] = get_curve_for_pack(crv)
  458. for mr in metarigs_in_tree:
  459. metarig_data[mr.name] = get_armature_for_pack(mr)
  460. # if only_selected:
  461. # # all in/out links, relative to the selection, should be marked and used to initialize tree properties
  462. if not only_selected: # we'll handle this later with the links
  463. for sock in tree.interface.items_tree:
  464. get_interface_data(tree, tree_in_out) # it concerns me that this one modifies
  465. # the collection instead of getting the data and returning it. TODO refactor this
  466. nodes = {}
  467. for node in tree.nodes:
  468. if only_selected and node.select == False:
  469. continue
  470. nodes[node.name] = get_node_data(node)
  471. links = []
  472. in_sockets, out_sockets = {}, {}
  473. unique_sockets_from, unique_sockets_to = {}, {}
  474. in_node = {"name":"MANTIS_AUTOGEN_GROUP_INPUT", "bl_idname":"NodeGroupInput", "inputs":in_sockets}
  475. out_node = {"name":"MANTIS_AUTOGEN_GROUP_OUTPUT", "bl_idname":"NodeGroupOutput", "outputs":out_sockets}
  476. add_input_node, add_output_node = False, False
  477. for link in tree.links:
  478. from_node_name, from_socket_id = link.from_node.name, link.from_socket.identifier
  479. to_node_name, to_socket_id = link.to_node.name, link.to_socket.identifier
  480. from_socket_name, to_socket_name = link.from_socket.name, link.to_socket.name
  481. # get the indices of the sockets to be absolutely sure
  482. for from_outoput_index, outp in enumerate(link.from_node.outputs):
  483. # for some reason, 'is' does not return True no matter what...
  484. # so we are gonn compare the memory address directly, this is stupid
  485. if (outp.as_pointer() == link.from_socket.as_pointer()): break
  486. else:
  487. problem=link.from_node.name + "::" + link.from_socket.name
  488. raise RuntimeError(wrapRed(f"Error saving index of socket: {problem}"))
  489. for to_input_index, inp in enumerate(link.to_node.inputs):
  490. if (inp.as_pointer() == link.to_socket.as_pointer()): break
  491. else:
  492. problem = link.to_node.name + "::" + link.to_socket.name
  493. raise RuntimeError(wrapRed(f"Error saving index of socket: {problem}"))
  494. if current_tree_is_base_tree:
  495. if (only_selected and link.from_node.select) and (not link.to_node.select):
  496. # handle an output in the tree
  497. add_output_node=True
  498. if not (sock_name := unique_sockets_to.get(link.from_socket.node.name+link.from_socket.identifier)):
  499. sock_name = link.to_socket.name; name_stub = sock_name
  500. used_names = list(tree_in_out.keys()); i=0
  501. while sock_name in used_names:
  502. sock_name=name_stub+'.'+str(i).zfill(3); i+=1
  503. unique_sockets_to[link.from_socket.node.name+link.from_socket.identifier]=sock_name
  504. out_sock = out_sockets.get(sock_name)
  505. if not out_sock:
  506. out_sock = {}; out_sockets[sock_name] = out_sock
  507. out_sock["index"]=len(out_sockets) # zero indexed, so zero length makes zero the first index and so on, this works
  508. # what in the bad word is happening here?
  509. # why?
  510. # why no de-duplication?
  511. # what was I thinking?
  512. # TODO REFACTOR THIS SOON
  513. out_sock["name"] = sock_name
  514. out_sock["identifier"] = sock_name
  515. out_sock["bl_idname"] = link.to_socket.bl_idname
  516. out_sock["is_output"] = False
  517. out_sock["source"]=[link.to_socket.node.name,link.to_socket.identifier]
  518. 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
  519. sock_data={}
  520. sock_data["name"] = sock_name
  521. sock_data["item_type"] = "SOCKET"
  522. sock_data["default_closed"] = False
  523. # record the actual bl_idname and the proper interface type.
  524. sock_data["socket_type"] = link.from_socket.interface_type
  525. sock_data["bl_socket_idname"] = link.from_socket.bl_idname
  526. sock_data["identifier"] = sock_name
  527. sock_data["in_out"]="OUTPUT"
  528. sock_data["index"]=out_sock["index"]
  529. tree_in_out[sock_name] = sock_data
  530. to_node_name=out_node["name"]
  531. to_socket_id=out_sock["identifier"]
  532. to_input_index=out_sock["index"]
  533. to_socket_name=out_sock["name"]
  534. elif (only_selected and (not link.from_node.select)) and link.to_node.select:
  535. add_input_node=True
  536. # we need to get a unique name for this
  537. # use the Tree IN/Out because we are dealing with Group in/out
  538. if not (sock_name := unique_sockets_from.get(link.from_socket.node.name+link.from_socket.identifier)):
  539. sock_name = link.from_socket.name; name_stub = sock_name
  540. used_names = list(tree_in_out.keys()); i=0
  541. while sock_name in used_names:
  542. sock_name=name_stub+'.'+str(i).zfill(3); i+=1
  543. unique_sockets_from[link.from_socket.node.name+link.from_socket.identifier]=sock_name
  544. in_sock = in_sockets.get(sock_name)
  545. if not in_sock:
  546. in_sock = {}; in_sockets[sock_name] = in_sock
  547. in_sock["index"]=len(in_sockets) # zero indexed, so zero length makes zero the first index and so on, this works
  548. #
  549. in_sock["name"] = sock_name
  550. in_sock["identifier"] = sock_name
  551. in_sock["bl_idname"] = link.from_socket.bl_idname
  552. in_sock["is_output"] = True
  553. 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
  554. in_sock["source"] = [link.from_socket.node.name,link.from_socket.identifier]
  555. sock_data={}
  556. sock_data["name"] = sock_name
  557. sock_data["item_type"] = "SOCKET"
  558. sock_data["default_closed"] = False
  559. # record the actual bl_idname and the proper interface type.
  560. sock_data["socket_type"] = link.from_socket.interface_type
  561. sock_data["bl_socket_idname"] = link.from_socket.bl_idname
  562. sock_data["identifier"] = sock_name
  563. sock_data["in_out"]="INPUT"
  564. sock_data["index"]=in_sock["index"]
  565. tree_in_out[sock_name] = sock_data
  566. from_node_name=in_node.get("name")
  567. from_socket_id=in_sock["identifier"]
  568. from_outoput_index=in_sock["index"]
  569. from_socket_name=in_node.get("name")
  570. # parentheses matter here...
  571. elif (only_selected and not (link.from_node.select and link.to_node.select)):
  572. continue
  573. elif only_selected and not (link.from_node.select and link.to_node.select):
  574. continue # pass if both links are not selected
  575. links.append( (from_node_name,
  576. from_socket_id,
  577. to_node_name,
  578. to_socket_id,
  579. from_outoput_index,
  580. to_input_index,
  581. from_socket_name,
  582. to_socket_name) ) # it's a tuple
  583. if add_input_node or add_output_node:
  584. all_nodes_bounding_box=[Vector((float("inf"),float("inf"))), Vector((-float("inf"),-float("inf")))]
  585. for n in nodes.values():
  586. if n["location"][0] < all_nodes_bounding_box[0].x:
  587. all_nodes_bounding_box[0].x = n["location"][0]
  588. if n["location"][1] < all_nodes_bounding_box[0].y:
  589. all_nodes_bounding_box[0].y = n["location"][1]
  590. #
  591. if n["location"][0] > all_nodes_bounding_box[1].x:
  592. all_nodes_bounding_box[1].x = n["location"][0]
  593. if n["location"][1] > all_nodes_bounding_box[1].y:
  594. all_nodes_bounding_box[1].y = n["location"][1]
  595. if add_input_node:
  596. 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))
  597. nodes["MANTIS_AUTOGEN_GROUP_INPUT"]=in_node
  598. if add_output_node:
  599. 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))
  600. nodes["MANTIS_AUTOGEN_GROUP_OUTPUT"]=out_node
  601. export_data[tree.name] = (tree_info, tree_in_out, nodes, links, curves, metarig_data,) # f_curves)
  602. return export_data
  603. def write_json_data(data, path):
  604. import json
  605. with open(path, "w") as file:
  606. print(wrapWhite("Writing mantis tree data to: "), wrapGreen(file.name))
  607. file.write( json.dumps(data, indent = 4) )
  608. def get_link_sockets(link, tree, tree_socket_id_map):
  609. from_node_name = link[0]
  610. from_socket_id = link[1]
  611. to_node_name = link[2]
  612. to_socket_id = link[3]
  613. from_output_index = link[4]
  614. to_input_index = link[5]
  615. from_socket_name = link[6]
  616. to_socket_name = link[7]
  617. # TODO: make this a loop and swap out the in/out stuff
  618. # this is OK but I want to avoid code-duplication, which this almost is.
  619. from_node = tree.nodes.get(from_node_name)
  620. # first try and get by name. we'll use this if the ID and the name do not match.
  621. # from_sock = from_node.outputs.get(from_socket_name)
  622. id1 = from_socket_id
  623. if hasattr(from_node, "node_tree") or \
  624. from_node.bl_idname in ["SchemaArrayInput",
  625. "SchemaArrayInputGet",
  626. "SchemaArrayInputAll",
  627. "SchemaConstInput",
  628. "SchemaIncomingConnection", ]: # now we have to map by something else
  629. try:
  630. id1 = from_node.outputs[from_socket_name].identifier
  631. except KeyError: # we'll try index if nothing else works
  632. try:
  633. id1 = from_node.outputs[from_output_index].identifier
  634. except IndexError as e:
  635. prRed("failed to create link: "
  636. f"{from_node_name}:{from_socket_id} --> {to_node_name}:{to_socket_id}")
  637. return (None, None)
  638. elif from_node.bl_idname in ["NodeGroupInput"]:
  639. id1 = tree_socket_id_map.get(from_socket_id)
  640. for from_sock in from_node.outputs:
  641. if from_sock.identifier == id1: break
  642. else:
  643. from_sock = None
  644. id2 = to_socket_id
  645. to_node = tree.nodes[to_node_name]
  646. if hasattr(to_node, "node_tree") or \
  647. to_node.bl_idname in ["SchemaArrayOutput",
  648. "SchemaConstOutput",
  649. "SchemaOutgoingConnection", ]: # now we have to map by something else
  650. try:
  651. id2 = to_node.inputs[to_socket_name].identifier
  652. except KeyError: # we'll try index if nothing else works
  653. try: # nesting try/except is ugly but it is right...
  654. id2 = to_node.inputs[to_input_index].identifier
  655. except IndexError as e:
  656. prRed("failed to create link: "
  657. f"{from_node_name}:{from_socket_id} --> {to_node_name}:{to_socket_id}")
  658. return (None, None)
  659. elif to_node.bl_idname in ["NodeGroupOutput"]:
  660. id2 = tree_socket_id_map.get(to_socket_id)
  661. for to_sock in to_node.inputs:
  662. if to_sock.identifier == id2: break
  663. else:
  664. to_sock = None
  665. return from_sock, to_sock
  666. def setup_sockets(node, propslist, in_out="inputs"):
  667. sockets_removed = []
  668. for i, (s_id, s_val) in enumerate(propslist[in_out].items()):
  669. if node.bl_idname in ['NodeReroute']:
  670. break # Reroute Nodes do not have anything I can set or modify.
  671. for socket_removed in SOCKETS_REMOVED:
  672. if node.bl_idname == socket_removed[0] and s_id == socket_removed[1]:
  673. prWhite(f"INFO: Ignoring import of socket {s_id}; it has been removed.")
  674. sockets_removed.append(s_val["index"])
  675. sockets_removed.sort()
  676. continue
  677. if s_val["is_output"]:
  678. if node.bl_idname in "MantisSchemaGroup":
  679. node.is_updating = True
  680. try:
  681. socket = node.outputs.new(s_val["bl_idname"], s_val["name"], identifier=s_id)
  682. finally:
  683. node.is_updating=False
  684. elif s_val["index"] >= len(node.outputs):
  685. if node.bl_idname in add_inputs_bl_idnames:
  686. socket = node.outputs.new(s_val["bl_idname"], s_val["name"], identifier=s_id, )
  687. else: # first try to get by ID AND name. ID's switch around a bit so we need both to match.
  688. for socket in node.outputs:
  689. if socket.identifier == s_id and socket.name == s_val['name']:
  690. break
  691. # this often fails for group outputs and such
  692. # because the socket ID may not be the same when it is re-generated
  693. else: # otherwise try to get the index
  694. # IT IS NOT CLEAR but this is what throws the index error below BAD
  695. # try to get by name
  696. socket = node.outputs.get(s_val['name'])
  697. if not socket:
  698. try:
  699. socket = node.outputs[int(s_val["index"])]
  700. except IndexError as e:
  701. print (node.id_data.name)
  702. print (propslist['name'])
  703. print (s_id, s_val['name'], s_val['index'])
  704. raise e
  705. if socket.name != s_val["name"]:
  706. right_name = s_val['name']
  707. prRed( "There has been an error getting a socket while importing data."
  708. f"found name: {socket.name}; should have found: {right_name}.")
  709. else:
  710. for removed_index in sockets_removed:
  711. if s_val["index"] > removed_index:
  712. s_val["index"]-=1
  713. if s_val["index"] >= len(node.inputs):
  714. if node.bl_idname in add_inputs_bl_idnames:
  715. socket = node.inputs.new(s_val["bl_idname"], s_val["name"], identifier=s_id, use_multi_input=s_val["is_multi_input"])
  716. elif node.bl_idname in ["MantisSchemaGroup"]:
  717. node.is_updating = True
  718. try:
  719. socket = node.inputs.new(s_val["bl_idname"], s_val["name"], identifier=s_id, use_multi_input=s_val["is_multi_input"])
  720. finally:
  721. node.is_updating=False
  722. elif node.bl_idname in ["NodeGroupOutput"]:
  723. pass # this is dealt with separately
  724. else:
  725. prWhite("Not found: ", propslist['name'], s_val["name"], s_id)
  726. prRed("Index: ", s_val["index"], "Number of inputs", len(node.inputs))
  727. for thing1, thing2 in zip(propslist[in_out].keys(), getattr(node, in_out).keys()):
  728. print (thing1, thing2)
  729. raise NotImplementedError(wrapRed(f"{node.bl_idname} in {node.id_data.name} needs to be handled in JSON load."))
  730. else: # first try to get by ID AND name. ID's switch around a bit so we need both to match.
  731. for socket in node.inputs:
  732. if socket.identifier == s_id and socket.name == s_val['name']:
  733. break
  734. # failing to find the socket by ID is less common for inputs than outputs.
  735. # it usually isn't a problem.
  736. else: # otherwise try to get the index
  737. # IT IS NOT CLEAR but this is what throws the index error below BAD
  738. socket = node.inputs.get(s_val["name"])
  739. if not socket:
  740. socket = node.inputs[int(s_val["index"])]
  741. # finally we need to check that the name matches.
  742. if socket.name != s_val["name"]:
  743. right_name = s_val['name']
  744. prRed( "There has been an error getting a socket while importing data."
  745. f"found name: {socket.name}; should have found: {right_name}.")
  746. # set the value
  747. for s_p, s_v in s_val.items():
  748. if s_p not in ["default_value"]:
  749. if s_p == "search_prop" and node.bl_idname == 'UtilityMetaRig':
  750. socket.node.armature= s_v
  751. socket.search_prop=bpy.data.objects.get(s_v)
  752. if s_p == "search_prop" and node.bl_idname in ['UtilityMatrixFromCurve', 'UtilityMatricesFromCurve']:
  753. socket.search_prop=bpy.data.objects.get(s_v)
  754. elif s_p == "bone" and socket.bl_idname == 'EnumMetaBoneSocket':
  755. socket.bone = s_v
  756. socket.node.pose_bone = s_v
  757. continue # not editable and NOT SAFE
  758. #
  759. if socket.bl_idname in ["BooleanThreeTupleSocket"]:
  760. value = bool(s_v[0]), bool(s_v[1]), bool(s_v[2]),
  761. s_v = value
  762. try:
  763. setattr(socket, s_p , s_v)
  764. except TypeError as e:
  765. prRed("Can't set socket due to type mismatch: ", node.name, socket.name, s_p, s_v)
  766. # raise e
  767. except ValueError as e:
  768. prRed("Can't set socket due to type mismatch: ", node.name, socket.name, s_p, s_v)
  769. # raise e
  770. except AttributeError as e:
  771. if print_read_only_warning == True:
  772. prWhite("Tried to write a read-only property, ignoring...")
  773. prWhite(f"{socket.node.name}[{socket.name}].{s_p} is read only, cannot set value to {s_v}")
  774. def do_import_from_file(filepath, context):
  775. import json
  776. all_trees = [n_tree for n_tree in bpy.data.node_groups if n_tree.bl_idname in ["MantisTree", "SchemaTree"]]
  777. for tree in all_trees:
  778. tree.is_exporting = True
  779. tree.do_live_update = False
  780. def do_cleanup(tree):
  781. tree.is_exporting = False
  782. tree.do_live_update = True
  783. tree.prevent_next_exec = True
  784. with open(filepath, 'r', encoding='utf-8') as f:
  785. data = json.load(f)
  786. do_import(data,context, search_multi_files=True, filepath=filepath)
  787. for tree in all_trees:
  788. do_cleanup(tree)
  789. tree = bpy.data.node_groups[list(data.keys())[-1]]
  790. try:
  791. context.space_data.node_tree = tree
  792. except AttributeError: # not hovering over the Node Editor
  793. pass
  794. return {'FINISHED'}
  795. # otherwise:
  796. # repeat this because we left the with, this is bad and ugly but I don't care
  797. for tree in all_trees:
  798. do_cleanup(tree)
  799. return {'CANCELLED'}
  800. # TODO figure out the right way to dedupe this stuff (see above)
  801. # I need this function for recursing through multi-file components
  802. # but I am using the with statement in the above function...
  803. # it should be easy to refactor but I don't know 100% for sure
  804. # the behaviour will be identical or if that matters.
  805. def get_graph_data_from_json(filepath) -> dict:
  806. import json
  807. with open(filepath, 'r', encoding='utf-8') as f:
  808. data = json.load(f)
  809. return data
  810. def do_import(data, context, search_multi_files=False, filepath=''):
  811. trees = []
  812. tree_sock_id_maps = {}
  813. # First: init the interface of the node graph
  814. for tree_name, tree_data in data.items():
  815. tree_info = tree_data[0]
  816. tree_in_out = tree_data[1]
  817. # TODO: IMPORT THIS DATA HERE!!!
  818. try:
  819. curves = tree_data[4]
  820. armatures = tree_data[5]
  821. except IndexError: # shouldn't happen but maybe someone has an old file
  822. curves = {}
  823. armatures = {}
  824. for curve_name, curve_data in curves.items():
  825. from .utilities import import_curve_data_to_object, import_metarig_data
  826. import_curve_data_to_object(curve_name, curve_data)
  827. for armature_name, armature_data in armatures.items():
  828. import_metarig_data(armature_data)
  829. # need to make a new tree; first, try to get it:
  830. tree = bpy.data.node_groups.get(tree_info["name"])
  831. if tree is None:
  832. tree = bpy.data.node_groups.new(tree_info["name"], tree_info["bl_idname"])
  833. tree.nodes.clear(); tree.links.clear(); tree.interface.clear()
  834. # this may be a bad bad thing to do without some kind of warning TODO TODO
  835. tree.is_executing = True
  836. tree.do_live_update = False
  837. trees.append(tree)
  838. tree_sock_id_map = {}
  839. tree_sock_id_maps[tree.name] = tree_sock_id_map
  840. interface_parent_me = {}
  841. # I need to guarantee that the interface items are in the right order.
  842. interface_sockets = [] # I'll just sort them afterwards so I hold them here.
  843. default_position=0 # We'll use this if the position attribute is not set when e.g. making groups.
  844. for s_name, s_props in tree_in_out.items():
  845. if s_props["item_type"] == 'SOCKET':
  846. if s_props["socket_type"] == "LayerMaskSocket":
  847. continue
  848. if (socket_type := s_props["socket_type"]) == "NodeSocketColor":
  849. socket_type = "VectorSocket"
  850. if bpy.app.version != (4,5,0):
  851. sock = tree.interface.new_socket(s_props["name"], in_out=s_props["in_out"], socket_type=socket_type)
  852. else: # blender 4.5.0 LTS, have to workaround a bug!
  853. from .versioning import workaround_4_5_0_interface_update
  854. sock = workaround_4_5_0_interface_update(tree=tree, name=s_props["name"], in_out=s_props["in_out"],
  855. sock_type=socket_type, parent_name=s_props.get("parent", ''))
  856. tree_sock_id_map[s_name] = sock.identifier
  857. if not (socket_position := s_props.get('position')):
  858. socket_position=default_position; default_position+=1
  859. interface_sockets.append( (sock, socket_position) )
  860. # TODO: set whatever properties are needed (default, etc)
  861. if panel := s_props.get("parent"): # this get is just to maintain compatibility with an older form of this script... and it is harmless
  862. interface_parent_me[sock] = (panel, s_props["position"])
  863. else: # it's a panel
  864. panel = tree.interface.new_panel(s_props["name"], description=s_props.get("description"), default_closed=s_props.get("default_closed"))
  865. for socket, (panel, index) in interface_parent_me.items():
  866. tree.interface.move_to_parent(
  867. socket,
  868. tree.interface.items_tree.get(panel),
  869. index,
  870. )
  871. # BUG this was screwing up the order of things
  872. # so I want to fix it and re-enable it
  873. if True:
  874. # Go BACK through and set the index/position now that all items exist.
  875. interface_sockets.sort(key=lambda a : a[1])
  876. for (socket, position) in interface_sockets:
  877. tree.interface.move(socket, position)
  878. # Now go and do nodes and links
  879. for tree_name, tree_data in data.items():
  880. print ("Importing sub-graph: %s with %s nodes" % (wrapGreen(tree_name), wrapPurple(len(tree_data[2]))) )
  881. tree_info = tree_data[0]
  882. nodes = tree_data[2]
  883. links = tree_data[3]
  884. parent_me = []
  885. tree = bpy.data.node_groups.get(tree_info["name"])
  886. tree.is_executing = True
  887. tree.do_live_update = False
  888. trees.append(tree)
  889. tree_sock_id_map=tree_sock_id_maps[tree.name]
  890. interface_parent_me = {}
  891. # from mantis.utilities import prRed, prWhite, prOrange, prGreen
  892. for name, propslist in nodes.items():
  893. bl_idname = propslist["bl_idname"]
  894. if bl_idname in NODES_REMOVED:
  895. prWhite(f"INFO: Ignoring import of node {name} of type {bl_idname}; it has been removed.")
  896. continue
  897. n = tree.nodes.new(bl_idname)
  898. if bl_idname in ["DeformerMorphTargetDeform"]:
  899. n.inputs.remove(n.inputs[-1]) # get rid of the wildcard
  900. if n.bl_idname in [ "SchemaArrayInput",
  901. "SchemaArrayInputGet",
  902. "SchemaArrayInputAll",
  903. "SchemaArrayOutput",
  904. "SchemaConstInput",
  905. "SchemaConstOutput",
  906. "SchemaOutgoingConnection",
  907. "SchemaIncomingConnection",]:
  908. n.update()
  909. if sub_tree := propslist.get("node_tree"):
  910. # now that I am doing multi-file exports, this is tricky
  911. # we need to see if the tree exists and if not, recurse
  912. # and import that tree before continuing.
  913. grp_tree = bpy.data.node_groups.get(sub_tree)
  914. if grp_tree is None: # for multi-file component this is intentional
  915. if search_multi_files: # we'll get the filename and recurse
  916. from bpy.path import native_pathsep, clean_name
  917. from os import path as os_path
  918. native_filepath = native_pathsep(filepath)
  919. directory = os_path.split(native_filepath)[0]
  920. subtree_filepath = os_path.join(directory, clean_name(sub_tree)+'.rig')
  921. subtree_data = get_graph_data_from_json(subtree_filepath)
  922. do_import(subtree_data, context,
  923. search_multi_files=True,
  924. filepath=subtree_filepath)
  925. #now get the grp_tree lol
  926. grp_tree = bpy.data.node_groups[sub_tree]
  927. else: # otherwise it is an error
  928. raise RuntimeError(f"Tree {sub_tree} not available to import.")
  929. n.node_tree = grp_tree
  930. from .base_definitions import node_group_update
  931. n.is_updating = True
  932. try:
  933. node_group_update(n, force = True)
  934. finally:
  935. n.is_updating=False
  936. # set up sockets
  937. setup_sockets(n, propslist, in_out="inputs")
  938. setup_sockets(n, propslist, in_out="outputs")
  939. for p, v in propslist.items():
  940. if p in ["node_tree",
  941. "sockets",
  942. "inputs",
  943. "outputs",
  944. "warning_propagation",
  945. "socket_idname"]:
  946. continue
  947. # will throw AttributeError if read-only
  948. # will throw TypeError if wrong type...
  949. if n.bl_idname == "NodeFrame" and p in ["width, height, location"]:
  950. continue
  951. if version < (4,4,0) and p == 'location_absolute':
  952. continue
  953. if p == "parent" and v is not None:
  954. parent_me.append( (n.name, v) )
  955. v = None # for now) #TODO
  956. try:
  957. setattr(n, p, v)
  958. except Exception as e:
  959. prRed (p)
  960. raise e
  961. for l in links:
  962. from_socket_name = l[6]
  963. to_socket_name = l[7]
  964. name1=l[0]
  965. name2=l[2]
  966. from_sock, to_sock = get_link_sockets(l, tree, tree_sock_id_map)
  967. try:
  968. link = tree.links.new(from_sock, to_sock)
  969. except TypeError:
  970. prPurple (from_sock)
  971. prOrange (to_sock)
  972. if print_link_failure:
  973. from_node_name = link[0]; from_socket_id = link[1]
  974. to_node_name = link[2]; to_socket_id = link[3]
  975. prWhite(f"looking for... {from_node_name}:{from_socket_id}, {to_node_name}:{to_socket_id}")
  976. prRed (f"Failed: {l[0]}:{l[1]} --> {l[2]}:{l[3]}")
  977. prRed (f" got node: {from_node_name}, {to_node_name}")
  978. prRed (f" got socket: {from_sock}, {to_sock}")
  979. raise RuntimeError
  980. else:
  981. prRed(f"Failed to add link in {tree.name}: {name1}:{from_socket_name}, {name2}:{to_socket_name}")
  982. # if at this point it doesn't work... we need to fix
  983. for name, p in parent_me:
  984. if (n := tree.nodes.get(name)) and (p := tree.nodes.get(p)):
  985. n.parent = p
  986. # otherwise the frame node is missing because it was not included in the data e.g. when grouping nodes.
  987. tree.is_executing = False
  988. tree.do_live_update = True
  989. def export_multi_file(trees : list, filepath : str, base_name :str) -> None:
  990. for t in trees:
  991. # this should name them the name of the tree...
  992. from bpy.path import native_pathsep, clean_name
  993. from os import path as os_path
  994. from os import mkdir
  995. native_filepath = native_pathsep(filepath)
  996. directory = os_path.split(native_filepath)[0]
  997. export_data = export_to_json([t], os_path.join(directory,
  998. clean_name(t.name)+'.rig'))
  999. write_json_data(export_data, os_path.join(directory,
  1000. clean_name(t.name)+'.rig'))
  1001. import bpy
  1002. from bpy_extras.io_utils import ImportHelper, ExportHelper
  1003. from bpy.props import StringProperty, BoolProperty, EnumProperty
  1004. from bpy.types import Operator
  1005. # Save As
  1006. class MantisExportNodeTreeSaveAs(Operator, ExportHelper):
  1007. """Export a Mantis Node Tree by filename."""
  1008. bl_idname = "mantis.export_save_as"
  1009. bl_label = "Export Mantis Tree as ...(.rig)"
  1010. # ExportHelper mix-in class uses this.
  1011. filename_ext = ".rig"
  1012. filter_glob: StringProperty(
  1013. default="*.rig",
  1014. options={'HIDDEN'},
  1015. maxlen=255, # Max internal buffer length, longer would be clamped.
  1016. )
  1017. export_trees_together : BoolProperty(
  1018. default=False,
  1019. name="Pack All Sub-Trees",
  1020. description="Pack all Sub-trees into one file?")
  1021. @classmethod
  1022. def poll(cls, context):
  1023. return hasattr(context.space_data, 'path')
  1024. def execute(self, context):
  1025. # we need to get the dependent trees from self.tree...
  1026. # there is no self.tree
  1027. # how do I choose a tree?
  1028. base_tree=context.space_data.path[-1].node_tree
  1029. from .utilities import all_trees_in_tree
  1030. trees = all_trees_in_tree(base_tree)[::-1]
  1031. prGreen("Exporting node graph with dependencies...")
  1032. for t in trees:
  1033. prGreen ("Node graph: \"%s\"" % (t.name))
  1034. base_tree.is_exporting = True
  1035. if self.export_trees_together:
  1036. export_data = export_to_json(trees, base_tree, self.filepath)
  1037. write_json_data(export_data, self.filepath)
  1038. else:
  1039. export_multi_file(trees, self.filepath, base_tree.name)
  1040. base_tree.is_exporting = False
  1041. base_tree.prevent_next_exec = True
  1042. # set the properties on the base tree for re-exporting with alt-s
  1043. base_tree.filepath = self.filepath
  1044. base_tree.export_all_subtrees_together = self.export_trees_together
  1045. return {'FINISHED'}
  1046. # Save
  1047. class MantisExportNodeTreeSave(Operator):
  1048. """Save a Mantis Node Tree to disk."""
  1049. bl_idname = "mantis.export_save"
  1050. bl_label = "Export Mantis Tree (.rig)"
  1051. @classmethod
  1052. def poll(cls, context):
  1053. return hasattr(context.space_data, 'path')
  1054. def execute(self, context):
  1055. base_tree=context.space_data.path[-1].node_tree
  1056. filepath = base_tree.filepath
  1057. from .utilities import all_trees_in_tree
  1058. trees = all_trees_in_tree(base_tree)[::-1]
  1059. prGreen("Exporting node graph with dependencies...")
  1060. for t in trees:
  1061. prGreen ("Node graph: \"%s\"" % (t.name))
  1062. base_tree.is_exporting = True
  1063. if base_tree.export_all_subtrees_together:
  1064. export_data = export_to_json(trees, filepath)
  1065. write_json_data(export_data, filepath)
  1066. else:
  1067. export_multi_file(trees, filepath, base_tree.name)
  1068. base_tree.is_exporting = False
  1069. base_tree.prevent_next_exec = True
  1070. return {'FINISHED'}
  1071. # Save Choose:
  1072. class MantisExportNodeTree(Operator):
  1073. """Save a Mantis Node Tree to disk."""
  1074. bl_idname = "mantis.export_save_choose"
  1075. bl_label = "Export Mantis Tree (.rig)"
  1076. @classmethod
  1077. def poll(cls, context):
  1078. return hasattr(context.space_data, 'path')
  1079. def execute(self, context):
  1080. base_tree=context.space_data.path[-1].node_tree
  1081. if base_tree.filepath:
  1082. prRed(base_tree.filepath)
  1083. return bpy.ops.mantis.export_save()
  1084. else:
  1085. return bpy.ops.mantis.export_save_as('INVOKE_DEFAULT')
  1086. # here is what needs to be done...
  1087. # - modify this to work with a sort of parsed-tree instead (sort of)
  1088. # - this needs to treat each sub-graph on its own
  1089. # - is this a problem? do I need to reconsider how I treat the graph data in mantis?
  1090. # - I should learn functional programming / currying
  1091. # - then the parsed-tree this builds must be executed as Blender nodes
  1092. # - I think... this is not important right now. not yet.
  1093. # - KEEP IT SIMPLE, STUPID
  1094. class MantisImportNodeTree(Operator, ImportHelper):
  1095. """Import a Mantis Node Tree."""
  1096. bl_idname = "mantis.import_tree"
  1097. bl_label = "Import Mantis Tree (.rig)"
  1098. # ImportHelper mixin class uses this
  1099. filename_ext = ".rig"
  1100. filter_glob : StringProperty(
  1101. default="*.rig",
  1102. options={'HIDDEN'},
  1103. maxlen=255, # Max internal buffer length, longer would be clamped.
  1104. )
  1105. def execute(self, context):
  1106. return do_import_from_file(self.filepath, context)
  1107. # this is useful:
  1108. # https://blender.stackexchange.com/questions/73286/how-to-call-a-confirmation-dialog-box
  1109. # class MantisReloadConfirmMenu(bpy.types.Panel):
  1110. # bl_label = "Confirm?"
  1111. # bl_idname = "OBJECT_MT_mantis_reload_confirm"
  1112. # def draw(self, context):
  1113. # layout = self.layout
  1114. # layout.operator("mantis.reload_tree")
  1115. class MantisReloadNodeTree(Operator):
  1116. # """Import a Mantis Node Tree."""
  1117. # bl_idname = "mantis.reload_tree"
  1118. # bl_label = "Import Mantis Tree"
  1119. """Reload Mantis Tree"""
  1120. bl_idname = "mantis.reload_tree"
  1121. bl_label = "Confirm reload tree?"
  1122. bl_options = {'REGISTER', 'INTERNAL'}
  1123. @classmethod
  1124. def poll(cls, context):
  1125. if hasattr(context.space_data, 'path'):
  1126. return True
  1127. return False
  1128. def invoke(self, context, event):
  1129. return context.window_manager.invoke_confirm(self, event)
  1130. def execute(self, context):
  1131. base_tree=context.space_data.path[-1].node_tree
  1132. if not base_tree.filepath:
  1133. self.report({'ERROR'}, "Tree has not been saved - so it cannot be reloaded.")
  1134. return {'CANCELLED'}
  1135. self.report({'INFO'}, "reloading tree")
  1136. return do_import_from_file(base_tree.filepath, context)
  1137. # todo:
  1138. # - export metarig and option to import it
  1139. # - same with controls
  1140. # - it would be nice to have a library of these that can be imported alongside the mantis graph