i_o.py 64 KB

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