base_definitions.py 14 KB

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