base_definitions.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  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. if (node.id_data.do_live_update == False) or (node.id_data.is_executing == True):
  137. return
  138. # note: if (node.id_data.is_exporting == True) I need to be able to update so I can make links.
  139. toggle_update = node.id_data.do_live_update
  140. node.id_data.do_live_update = False
  141. identifiers_in={socket.identifier:socket for socket in node.inputs}
  142. identifiers_out={socket.identifier:socket for socket in node.outputs}
  143. if node.node_tree is None:
  144. node.inputs.clear(); node.outputs.clear()
  145. node.id_data.do_live_update = toggle_update
  146. return
  147. found_in, found_out = [], []
  148. update_input, update_output = False, False
  149. for item in node.node_tree.interface.items_tree:
  150. if item.item_type != "SOCKET": continue
  151. if item.in_out == 'OUTPUT':
  152. if s:= identifiers_out.get(item.identifier): # if the requested output doesn't exist, update
  153. found_out.append(item.identifier)
  154. if update_output: continue
  155. if s.bl_idname != item.socket_type: update_output = True; continue
  156. else: update_output = True; continue
  157. else:
  158. if s:= identifiers_in.get(item.identifier): # if the requested input doesn't exist, update
  159. found_in.append(item.identifier)
  160. if update_input: continue # done here
  161. if s.bl_idname != item.socket_type: update_input = True; continue
  162. else: update_input = True; continue
  163. # Schema has an extra input for Length and for Extend.
  164. if node.bl_idname == 'MantisSchemaGroup':
  165. found_in.extend(['Schema Length', ''])
  166. # if we have too many elements, just get rid of the ones we don't need
  167. if len(node.inputs) > len(found_in):#
  168. for inp in node.inputs:
  169. if inp.identifier in found_in: continue
  170. node.inputs.remove(inp)
  171. if len(node.outputs) > len(found_out):
  172. for out in node.outputs:
  173. if out.identifier in found_out: continue
  174. node.outputs.remove(out)
  175. #
  176. if len(node.inputs) > 0 and (inp := node.inputs[-1]).bl_idname == 'WildcardSocket' and inp.is_linked:
  177. update_input = True
  178. if len(node.outputs) > 0 and (out := node.outputs[-1]).bl_idname == 'WildcardSocket' and out.is_linked:
  179. update_output = True
  180. #
  181. if not (update_input or update_output):
  182. node.id_data.do_live_update = toggle_update
  183. return
  184. if update_input or update_output:
  185. socket_map_in, socket_map_out = get_socket_maps(node)
  186. if update_input :
  187. if node.bl_idname == 'MantisSchemaGroup':
  188. schema_length=0
  189. if sl := node.inputs.get("Schema Length"):
  190. schema_length = sl.default_value
  191. # sometimes this isn't available yet # TODO not happy about this solution
  192. node.inputs.clear()
  193. if node.bl_idname == 'MantisSchemaGroup':
  194. node.inputs.new("IntSocket", "Schema Length", identifier='Schema Length')
  195. node.inputs['Schema Length'].default_value = schema_length
  196. if update_output: node.outputs.clear()
  197. for item in node.node_tree.interface.items_tree:
  198. if item.item_type != "SOCKET": continue
  199. if (item.in_out == 'INPUT' and update_input):
  200. relink_socket_map(node, node.inputs, socket_map_in, item)
  201. if (item.in_out == 'OUTPUT' and update_output):
  202. relink_socket_map(node, node.outputs, socket_map_out, item)
  203. # at this point there is no wildcard socket
  204. if '__extend__' in socket_map_in.keys():
  205. do_relink(node, None, socket_map_in, in_out='INPUT', parent_name='Constant' )
  206. node.id_data.do_live_update = toggle_update
  207. def node_tree_prop_update(self, context):
  208. if self.is_updating: # update() can be called from update() and that leads to an infinite loop.
  209. return # so we check if an update is currently running.
  210. self.is_updating = True
  211. node_group_update(self)
  212. self.is_updating = False
  213. if self.bl_idname in ['MantisSchemaGroup'] and self.node_tree is not None:
  214. if len(self.inputs) == 0:
  215. self.inputs.new("IntSocket", "Schema Length", identifier='Schema Length')
  216. if self.inputs[-1].bl_idname != "WildcardSocket":
  217. self.inputs.new("WildcardSocket", "", identifier="__extend__")
  218. from bpy.types import NodeCustomGroup
  219. class MantisNodeGroup(Node, MantisNode):
  220. bl_idname = "MantisNodeGroup"
  221. bl_label = "Node Group"
  222. node_tree:PointerProperty(type=NodeTree, poll=poll_node_tree, update=node_tree_prop_update,)
  223. is_updating:BoolProperty(default=False)
  224. def update(self):
  225. if self.is_updating: # update() can be called from update() and that leads to an infinite loop.
  226. return # so we check if an update is currently running.
  227. self.is_updating = True
  228. node_group_update(self)
  229. self.is_updating = False
  230. def draw_buttons(self, context, layout):
  231. row = layout.row(align=True)
  232. row.prop(self, "node_tree", text="")
  233. row.operator("mantis.edit_group", text="", icon='NODETREE', emboss=True)
  234. class GraphError(Exception):
  235. pass
  236. def get_signature_from_edited_tree(node, context):
  237. sig_path=[None,]
  238. for item in context.space_data.path[:-1]:
  239. sig_path.append(item.node_tree.nodes.active.name)
  240. return tuple(sig_path+[node.name])
  241. def poll_node_tree_schema(self, object):
  242. if isinstance(object, SchemaTree):
  243. return True
  244. return False
  245. # TODO tiny UI problem - inserting new links into the tree will not place them in the right place.
  246. class SchemaGroup(Node, MantisNode):
  247. bl_idname = "MantisSchemaGroup"
  248. bl_label = "Node Schema"
  249. node_tree:PointerProperty(type=NodeTree, poll=poll_node_tree_schema, update=node_tree_prop_update,)
  250. is_updating:BoolProperty(default=False)
  251. def draw_buttons(self, context, layout):
  252. row = layout.row(align=True)
  253. row.prop(self, "node_tree", text="")
  254. row.operator("mantis.edit_group", text="", icon='NODETREE', emboss=True)
  255. def update(self):
  256. if self.is_updating: # update() can be called from update() and that leads to an infinite loop.
  257. return # so we check if an update is currently running.
  258. self.is_updating = True
  259. node_group_update(self)
  260. # reset things if necessary:
  261. if self.node_tree:
  262. if len(self.inputs) == 0:
  263. self.inputs.new("IntSocket", "Schema Length", identifier='Schema Length')
  264. if self.inputs[-1].bl_idname != "WildcardSocket":
  265. self.inputs.new("WildcardSocket", "", identifier="__extend__")
  266. self.is_updating = False
  267. # replace names with bl_idnames for reading the tree and solving schemas.
  268. replace_types = ["NodeGroupInput", "NodeGroupOutput", "SchemaIncomingConnection",
  269. "SchemaArrayInput", "SchemaConstInput", "SchemaConstOutput", "SchemaIndex",
  270. "SchemaOutgoingConnection", "SchemaConstantOutput", "SchemaArrayOutput",
  271. "SchemaArrayInputGet",]
  272. # anything that gets properties added in the graph... this is a clumsy approach but I need to watch for this
  273. # in schema generation and this is the easiest way to do it for now.
  274. custom_props_types = ["LinkArmature", "UtilityKeyframe", "UtilityFCurve", "UtilityDriver", "xFormBone"]
  275. # filters for determining if a link is a hierarchy link or a non-hierarchy (cyclic) link.
  276. from_name_filter = ["Driver",]
  277. to_name_filter = [
  278. "Custom Object xForm Override",
  279. "Custom Object",
  280. "Deform Bones",
  281. ]