base_definitions.py 13 KB

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