base_definitions.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  1. #Mantis Nodes Base
  2. import bpy
  3. from bpy.props import BoolProperty, StringProperty, EnumProperty, CollectionProperty, IntProperty, PointerProperty, BoolVectorProperty
  4. from . import ops_nodegroup
  5. from bpy.types import NodeTree, Node, PropertyGroup, Operator, UIList, Panel
  6. from .utilities import (prRed, prGreen, prPurple, prWhite,
  7. prOrange,
  8. wrapRed, wrapGreen, wrapPurple, wrapWhite,
  9. wrapOrange,)
  10. from .utilities import get_socket_maps, relink_socket_map, do_relink
  11. def TellClasses():
  12. #Why use a function to do this? Because I don't need every class to register.
  13. return [ MantisTree,
  14. SchemaTree,
  15. MantisNodeGroup,
  16. SchemaGroup,
  17. ]
  18. def error_popup_draw(self, context):
  19. self.layout.label(text="Error executing tree. See Console.")
  20. class MantisTree(NodeTree):
  21. '''A custom node tree type that will show up in the editor type list'''
  22. bl_idname = 'MantisTree'
  23. bl_label = "Rigging Nodes"
  24. bl_icon = 'OUTLINER_OB_ARMATURE'
  25. tree_valid:BoolProperty(default=False)
  26. do_live_update:BoolProperty(default=True) # use this to disable updates for e.g. scripts
  27. num_links:IntProperty(default=-1)
  28. filepath:StringProperty(default="", subtype='FILE_PATH')
  29. is_executing:BoolProperty(default=False)
  30. is_exporting:BoolProperty(default=False)
  31. execution_id:StringProperty(default='')
  32. parsed_tree={}
  33. if bpy.app.version >= (3, 2): # in 3.1 this can lead to a crash
  34. @classmethod
  35. def valid_socket_type(cls, socket_type: str):
  36. # https://docs.blender.org/api/master/bpy.types.NodeTree.html#bpy.types.NodeTree.valid_socket_type
  37. from .socket_definitions import Tell_bl_idnames
  38. return socket_type in Tell_bl_idnames()
  39. # thank you, Sverchok
  40. def update_tree(self, context = None):
  41. if self.is_exporting:
  42. return
  43. # return
  44. self.is_executing = True
  45. from . import readtree
  46. prGreen("Validating Tree: %s" % self.name)
  47. try:
  48. self.parsed_tree = readtree.parse_tree(self)
  49. if context:
  50. self.display_update(context)
  51. self.is_executing = False
  52. self.tree_valid = True
  53. except GraphError as e:
  54. prRed("Failed to update node tree due to error.")
  55. self.tree_valid = False
  56. self.is_executing = False
  57. raise e
  58. finally:
  59. self.is_executing = False
  60. def display_update(self, context):
  61. if self.is_exporting:
  62. return
  63. current_tree = bpy.context.space_data.path[-1].node_tree
  64. for node in current_tree.nodes:
  65. if hasattr(node, "display_update"):
  66. try:
  67. node.display_update(self.parsed_tree, context)
  68. except Exception as e:
  69. print("Node \"%s\" failed to update display with error: %s" %(wrapGreen(node.name), wrapRed(e)))
  70. # raise e
  71. # TODO: deal with invalid links properly.
  72. # - Non-hierarchy links should be ignored in the circle-check and so the links should be marked valid in such a circle
  73. # - hierarchy-links should be marked invalid and prevent the tree from executing.
  74. def execute_tree(self,context):
  75. if self.is_exporting:
  76. return
  77. # return
  78. prGreen("Executing Tree: %s" % self.name)
  79. self.is_executing = True
  80. from . import readtree
  81. try:
  82. readtree.execute_tree(self.parsed_tree, self, context)
  83. except RecursionError as e:
  84. prRed("Recursion error while parsing tree.")
  85. finally:
  86. self.is_executing = False
  87. class SchemaTree(NodeTree):
  88. '''A node tree representing a schema to generate a Mantis tree'''
  89. bl_idname = 'SchemaTree'
  90. bl_label = "Rigging Nodes Schema"
  91. bl_icon = 'RIGID_BODY_CONSTRAINT'
  92. do_live_update:BoolProperty(default=False) # this is only needed for consistency, it is never used.
  93. if bpy.app.version >= (3, 2): # in 3.1 this can lead to a crash
  94. @classmethod
  95. def valid_socket_type(cls, socket_type: str):
  96. # https://docs.blender.org/api/master/bpy.types.NodeTree.html#bpy.types.NodeTree.valid_socket_type
  97. from .socket_definitions import Tell_bl_idnames
  98. return socket_type in Tell_bl_idnames()
  99. # thank you, Sverchok
  100. class MantisNode:
  101. @classmethod
  102. def poll(cls, ntree):
  103. return (ntree.bl_idname in ['MantisTree', 'SchemaTree'])
  104. def insert_link(self, link):
  105. context = bpy.context
  106. if context.space_data:
  107. node_tree = context.space_data.path[0].node_tree
  108. from . import readtree
  109. if node_tree.do_live_update:
  110. node_tree.update_tree(context)
  111. if (link.to_socket.is_linked == False):
  112. node_tree.num_links+=1
  113. elif (link.to_socket.is_multi_input):
  114. node_tree.num_links+=1
  115. class SchemaNode:
  116. @classmethod
  117. def poll(cls, ntree):
  118. return (ntree.bl_idname in ['SchemaTree'])
  119. class LinkNode(MantisNode):
  120. useTarget : BoolProperty(default=False)
  121. @classmethod
  122. def poll(cls, ntree):
  123. return (ntree.bl_idname in ['MantisTree', 'SchemaTree'])
  124. class xFormNode(MantisNode):
  125. @classmethod
  126. def poll(cls, ntree):
  127. return (ntree.bl_idname in ['MantisTree', 'SchemaTree'])
  128. class DeformerNode(MantisNode):
  129. @classmethod
  130. def poll(cls, ntree):
  131. return (ntree.bl_idname in ['MantisTree', 'SchemaTree'])
  132. def poll_node_tree(self, object):
  133. if isinstance(object, MantisTree):
  134. return True
  135. return False
  136. # TODO: try to check identifiers instead of name.
  137. def node_group_update(node):
  138. if (node.id_data.do_live_update == False) or (node.id_data.is_executing == True):
  139. return
  140. # note: if (node.id_data.is_exporting == True) I need to be able to update so I can make links.
  141. toggle_update = node.id_data.do_live_update
  142. node.id_data.do_live_update = False
  143. identifiers_in={socket.identifier:socket for socket in node.inputs}
  144. identifiers_out={socket.identifier:socket for socket in node.outputs}
  145. if node.node_tree is None:
  146. node.inputs.clear(); node.outputs.clear()
  147. node.id_data.do_live_update = toggle_update
  148. return
  149. found_in, found_out = [], []
  150. update_input, update_output = False, False
  151. for item in node.node_tree.interface.items_tree:
  152. if item.item_type != "SOCKET": continue
  153. if item.in_out == 'OUTPUT':
  154. if s:= identifiers_out.get(item.identifier): # if the requested output doesn't exist, update
  155. found_out.append(item.identifier)
  156. if update_output: continue
  157. if s.bl_idname != item.socket_type: update_output = True; continue
  158. else: update_output = True; continue
  159. else:
  160. if s:= identifiers_in.get(item.identifier): # if the requested input doesn't exist, update
  161. found_in.append(item.identifier)
  162. if update_input: continue # done here
  163. if s.bl_idname != item.socket_type: update_input = True; continue
  164. else: update_input = True; continue
  165. # Schema has an extra input for Length and for Extend.
  166. if node.bl_idname == 'MantisSchemaGroup':
  167. found_in.extend(['Schema Length', ''])
  168. # if we have too many elements, just get rid of the ones we don't need
  169. if len(node.inputs) > len(found_in):#
  170. for inp in node.inputs:
  171. if inp.identifier in found_in: continue
  172. node.inputs.remove(inp)
  173. if len(node.outputs) > len(found_out):
  174. for out in node.outputs:
  175. if out.identifier in found_out: continue
  176. node.outputs.remove(out)
  177. #
  178. if len(node.inputs) > 0 and (inp := node.inputs[-1]).bl_idname == 'WildcardSocket' and inp.is_linked:
  179. update_input = True
  180. if len(node.outputs) > 0 and (out := node.outputs[-1]).bl_idname == 'WildcardSocket' and out.is_linked:
  181. update_output = True
  182. #
  183. if not (update_input or update_output):
  184. node.id_data.do_live_update = toggle_update
  185. return
  186. if update_input or update_output:
  187. socket_map_in, socket_map_out = get_socket_maps(node)
  188. if update_input :
  189. if node.bl_idname == 'MantisSchemaGroup':
  190. schema_length=0
  191. if sl := node.inputs.get("Schema Length"):
  192. schema_length = sl.default_value
  193. # sometimes this isn't available yet # TODO not happy about this solution
  194. node.inputs.clear()
  195. if node.bl_idname == 'MantisSchemaGroup':
  196. node.inputs.new("IntSocket", "Schema Length", identifier='Schema Length')
  197. node.inputs['Schema Length'].default_value = schema_length
  198. if update_output: node.outputs.clear()
  199. for item in node.node_tree.interface.items_tree:
  200. if item.item_type != "SOCKET": continue
  201. if (item.in_out == 'INPUT' and update_input):
  202. relink_socket_map(node, node.inputs, socket_map_in, item)
  203. if (item.in_out == 'OUTPUT' and update_output):
  204. relink_socket_map(node, node.outputs, socket_map_out, item)
  205. # at this point there is no wildcard socket
  206. if '__extend__' in socket_map_in.keys():
  207. do_relink(node, None, socket_map_in, in_out='INPUT', parent_name='Constant' )
  208. node.id_data.do_live_update = toggle_update
  209. def node_tree_prop_update(self, context):
  210. if self.is_updating: # update() can be called from update() and that leads to an infinite loop.
  211. return # so we check if an update is currently running.
  212. self.is_updating = True
  213. node_group_update(self)
  214. self.is_updating = False
  215. if self.bl_idname in ['MantisSchemaGroup'] and self.node_tree is not None:
  216. if len(self.inputs) == 0:
  217. self.inputs.new("IntSocket", "Schema Length", identifier='Schema Length')
  218. if self.inputs[-1].bl_idname != "WildcardSocket":
  219. self.inputs.new("WildcardSocket", "", identifier="__extend__")
  220. from bpy.types import NodeCustomGroup
  221. class MantisNodeGroup(Node, MantisNode):
  222. bl_idname = "MantisNodeGroup"
  223. bl_label = "Node Group"
  224. node_tree:PointerProperty(type=NodeTree, poll=poll_node_tree, update=node_tree_prop_update,)
  225. is_updating:BoolProperty(default=False)
  226. def update(self):
  227. if self.is_updating: # update() can be called from update() and that leads to an infinite loop.
  228. return # so we check if an update is currently running.
  229. self.is_updating = True
  230. node_group_update(self)
  231. self.is_updating = False
  232. def draw_buttons(self, context, layout):
  233. row = layout.row(align=True)
  234. row.prop(self, "node_tree", text="")
  235. row.operator("mantis.edit_group", text="", icon='NODETREE', emboss=True)
  236. class GraphError(Exception):
  237. pass
  238. def get_signature_from_edited_tree(node, context):
  239. sig_path=[None,]
  240. for item in context.space_data.path[:-1]:
  241. sig_path.append(item.node_tree.nodes.active.name)
  242. return tuple(sig_path+[node.name])
  243. def poll_node_tree_schema(self, object):
  244. if isinstance(object, SchemaTree):
  245. return True
  246. return False
  247. # TODO tiny UI problem - inserting new links into the tree will not place them in the right place.
  248. class SchemaGroup(Node, MantisNode):
  249. bl_idname = "MantisSchemaGroup"
  250. bl_label = "Node Schema"
  251. node_tree:PointerProperty(type=NodeTree, poll=poll_node_tree_schema, update=node_tree_prop_update,)
  252. is_updating:BoolProperty(default=False)
  253. def draw_buttons(self, context, layout):
  254. row = layout.row(align=True)
  255. row.prop(self, "node_tree", text="")
  256. row.operator("mantis.edit_group", text="", icon='NODETREE', emboss=True)
  257. def update(self):
  258. if self.is_updating: # update() can be called from update() and that leads to an infinite loop.
  259. return # so we check if an update is currently running.
  260. self.is_updating = True
  261. node_group_update(self)
  262. # reset things if necessary:
  263. if self.node_tree:
  264. if len(self.inputs) == 0:
  265. self.inputs.new("IntSocket", "Schema Length", identifier='Schema Length')
  266. if self.inputs[-1].bl_idname != "WildcardSocket":
  267. self.inputs.new("WildcardSocket", "", identifier="__extend__")
  268. self.is_updating = False
  269. # replace names with bl_idnames for reading the tree and solving schemas.
  270. replace_types = ["NodeGroupInput", "NodeGroupOutput", "SchemaIncomingConnection",
  271. "SchemaArrayInput", "SchemaConstInput", "SchemaConstOutput", "SchemaIndex",
  272. "SchemaOutgoingConnection", "SchemaConstantOutput", "SchemaArrayOutput",
  273. "SchemaArrayInputGet",]
  274. # anything that gets properties added in the graph... this is a clumsy approach but I need to watch for this
  275. # in schema generation and this is the easiest way to do it for now.
  276. custom_props_types = ["LinkArmature", "UtilityKeyframe", "UtilityFCurve", "UtilityDriver", "xFormBone"]
  277. # filters for determining if a link is a hierarchy link or a non-hierarchy (cyclic) link.
  278. from_name_filter = ["Driver",]
  279. to_name_filter = [
  280. "Custom Object xForm Override",
  281. "Custom Object",
  282. "Deform Bones",
  283. ]