base_definitions.py 14 KB

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