base_definitions.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  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. # this prevents the node group from executing on the next depsgraph update
  35. # because I don't always have control over when the dg update happens.
  36. prevent_next_exec:BoolProperty(default=False)
  37. parsed_tree={}
  38. if bpy.app.version >= (3, 2): # in 3.1 this can lead to a crash
  39. @classmethod
  40. def valid_socket_type(cls, socket_type: str):
  41. # https://docs.blender.org/api/master/bpy.types.NodeTree.html#bpy.types.NodeTree.valid_socket_type
  42. from .socket_definitions import Tell_bl_idnames
  43. return socket_type in Tell_bl_idnames()
  44. # thank you, Sverchok
  45. def update_tree(self, context = None):
  46. if self.is_exporting:
  47. return
  48. # return
  49. self.is_executing = True
  50. from . import readtree
  51. prGreen("Validating Tree: %s" % self.name)
  52. try:
  53. self.parsed_tree = readtree.parse_tree(self)
  54. if context:
  55. self.display_update(context)
  56. self.is_executing = False
  57. self.tree_valid = True
  58. except GraphError as e:
  59. prRed("Failed to update node tree due to error.")
  60. self.tree_valid = False
  61. self.is_executing = False
  62. raise e
  63. finally:
  64. self.is_executing = False
  65. def display_update(self, context):
  66. if self.is_exporting:
  67. return
  68. self.is_executing = True
  69. current_tree = bpy.context.space_data.path[-1].node_tree
  70. for node in current_tree.nodes:
  71. if hasattr(node, "display_update"):
  72. try:
  73. node.display_update(self.parsed_tree, context)
  74. except Exception as e:
  75. print("Node \"%s\" failed to update display with error: %s" %(wrapGreen(node.name), wrapRed(e)))
  76. self.is_executing = False
  77. # TODO: deal with invalid links properly.
  78. # - Non-hierarchy links should be ignored in the circle-check and so the links should be marked valid in such a circle
  79. # - hierarchy-links should be marked invalid and prevent the tree from executing.
  80. def execute_tree(self,context, error_popups = False):
  81. self.prevent_next_exec = False
  82. if self.is_exporting:
  83. return
  84. # return
  85. prGreen("Executing Tree: %s" % self.name)
  86. self.is_executing = True
  87. from . import readtree
  88. try:
  89. readtree.execute_tree(self.parsed_tree, self, context, error_popups)
  90. except RecursionError as e:
  91. prRed("Recursion error while parsing tree.")
  92. finally:
  93. self.is_executing = False
  94. class SchemaTree(NodeTree):
  95. '''A node tree representing a schema to generate a Mantis tree'''
  96. bl_idname = 'SchemaTree'
  97. bl_label = "Rigging Nodes Schema"
  98. bl_icon = 'RIGID_BODY_CONSTRAINT'
  99. # these are only needed for consistent interface, but should not be used
  100. do_live_update:BoolProperty(default=True) # default to true so that updates work
  101. is_executing:BoolProperty(default=False)
  102. is_exporting:BoolProperty(default=False)
  103. mantis_version:IntVectorProperty(default=[0,9,2])
  104. if bpy.app.version >= (3, 2): # in 3.1 this can lead to a crash
  105. @classmethod
  106. def valid_socket_type(cls, socket_type: str):
  107. # https://docs.blender.org/api/master/bpy.types.NodeTree.html#bpy.types.NodeTree.valid_socket_type
  108. from .socket_definitions import Tell_bl_idnames
  109. return socket_type in Tell_bl_idnames()
  110. # thank you, Sverchok
  111. class MantisNode:
  112. @classmethod
  113. def poll(cls, ntree):
  114. return (ntree.bl_idname in ['MantisTree', 'SchemaTree'])
  115. def insert_link(self, link):
  116. context = bpy.context
  117. if context.space_data:
  118. node_tree = context.space_data.path[0].node_tree
  119. from . import readtree
  120. if node_tree.do_live_update:
  121. node_tree.update_tree(context)
  122. if (link.to_socket.is_linked == False):
  123. node_tree.num_links+=1
  124. elif (link.to_socket.is_multi_input):
  125. node_tree.num_links+=1
  126. class SchemaNode:
  127. @classmethod
  128. def poll(cls, ntree):
  129. return (ntree.bl_idname in ['SchemaTree'])
  130. class LinkNode(MantisNode):
  131. @classmethod
  132. def poll(cls, ntree):
  133. return (ntree.bl_idname in ['MantisTree', 'SchemaTree'])
  134. class xFormNode(MantisNode):
  135. @classmethod
  136. def poll(cls, ntree):
  137. return (ntree.bl_idname in ['MantisTree', 'SchemaTree'])
  138. class DeformerNode(MantisNode):
  139. @classmethod
  140. def poll(cls, ntree):
  141. return (ntree.bl_idname in ['MantisTree', 'SchemaTree'])
  142. def poll_node_tree(self, object):
  143. if isinstance(object, MantisTree):
  144. return True
  145. return False
  146. # TODO: try to check identifiers instead of name.
  147. def node_group_update(node, force = False):
  148. if not force:
  149. if (node.id_data.do_live_update == False) or (node.id_data.is_executing == True):
  150. return
  151. # note: if (node.id_data.is_exporting == True) I need to be able to update so I can make links.
  152. toggle_update = node.id_data.do_live_update
  153. node.id_data.do_live_update = False
  154. identifiers_in={socket.identifier:socket for socket in node.inputs}
  155. identifiers_out={socket.identifier:socket for socket in node.outputs}
  156. if node.node_tree is None:
  157. node.inputs.clear(); node.outputs.clear()
  158. node.id_data.do_live_update = toggle_update
  159. return
  160. found_in, found_out = [], []
  161. update_input, update_output = False, False
  162. for item in node.node_tree.interface.items_tree:
  163. if item.item_type != "SOCKET": continue
  164. if item.in_out == 'OUTPUT':
  165. if s:= identifiers_out.get(item.identifier): # if the requested output doesn't exist, update
  166. found_out.append(item.identifier)
  167. if update_output: continue
  168. if s.bl_idname != item.socket_type: update_output = True; continue
  169. else: update_output = True; continue
  170. else:
  171. if s:= identifiers_in.get(item.identifier): # if the requested input doesn't exist, update
  172. found_in.append(item.identifier)
  173. if update_input: continue # done here
  174. if s.bl_idname != item.socket_type: update_input = True; continue
  175. else: update_input = True; continue
  176. # Schema has an extra input for Length and for Extend.
  177. if node.bl_idname == 'MantisSchemaGroup':
  178. found_in.extend(['Schema Length', ''])
  179. # if we have too many elements, just get rid of the ones we don't need
  180. if len(node.inputs) > len(found_in):#
  181. for inp in node.inputs:
  182. if inp.identifier in found_in: continue
  183. node.inputs.remove(inp)
  184. if len(node.outputs) > len(found_out):
  185. for out in node.outputs:
  186. if out.identifier in found_out: continue
  187. node.outputs.remove(out)
  188. #
  189. if len(node.inputs) > 0 and (inp := node.inputs[-1]).bl_idname == 'WildcardSocket' and inp.is_linked:
  190. update_input = True
  191. if len(node.outputs) > 0 and (out := node.outputs[-1]).bl_idname == 'WildcardSocket' and out.is_linked:
  192. update_output = True
  193. #
  194. if not (update_input or update_output):
  195. node.id_data.do_live_update = toggle_update
  196. return
  197. if update_input or update_output:
  198. socket_map_in, socket_map_out = get_socket_maps(node)
  199. if update_input :
  200. if node.bl_idname == 'MantisSchemaGroup':
  201. schema_length=0
  202. if sl := node.inputs.get("Schema Length"):
  203. schema_length = sl.default_value
  204. # sometimes this isn't available yet # TODO not happy about this solution
  205. node.inputs.clear()
  206. if node.bl_idname == 'MantisSchemaGroup':
  207. node.inputs.new("IntSocket", "Schema Length", identifier='Schema Length')
  208. node.inputs['Schema Length'].default_value = schema_length
  209. if update_output: node.outputs.clear()
  210. for item in node.node_tree.interface.items_tree:
  211. if item.item_type != "SOCKET": continue
  212. if (item.in_out == 'INPUT' and update_input):
  213. relink_socket_map(node, node.inputs, socket_map_in, item)
  214. if (item.in_out == 'OUTPUT' and update_output):
  215. relink_socket_map(node, node.outputs, socket_map_out, item)
  216. # at this point there is no wildcard socket
  217. if '__extend__' in socket_map_in.keys():
  218. do_relink(node, None, socket_map_in, in_out='INPUT', parent_name='Constant' )
  219. node.id_data.do_live_update = toggle_update
  220. def node_tree_prop_update(self, context):
  221. if self.is_updating: # update() can be called from update() and that leads to an infinite loop.
  222. return # so we check if an update is currently running.
  223. self.is_updating = True
  224. node_group_update(self)
  225. self.is_updating = False
  226. if self.bl_idname in ['MantisSchemaGroup'] and self.node_tree is not None:
  227. if len(self.inputs) == 0:
  228. self.inputs.new("IntSocket", "Schema Length", identifier='Schema Length')
  229. if self.inputs[-1].bl_idname != "WildcardSocket":
  230. self.inputs.new("WildcardSocket", "", identifier="__extend__")
  231. from bpy.types import NodeCustomGroup
  232. class MantisNodeGroup(Node, MantisNode):
  233. bl_idname = "MantisNodeGroup"
  234. bl_label = "Node Group"
  235. node_tree:PointerProperty(type=NodeTree, poll=poll_node_tree, update=node_tree_prop_update,)
  236. is_updating:BoolProperty(default=False)
  237. def update(self):
  238. live_update = self.id_data.do_live_update
  239. if self.is_updating: # update() can be called from update() and that leads to an infinite loop.
  240. return # so we check if an update is currently running.
  241. try:
  242. self.is_updating = True
  243. node_group_update(self)
  244. self.is_updating = False
  245. finally: # we need to reset this regardless of whether or not the operation succeeds!
  246. self.is_updating = False
  247. self.id_data.do_live_update = live_update # ensure this remains the same
  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. class GraphError(Exception):
  253. pass
  254. def get_signature_from_edited_tree(node, context):
  255. sig_path=[None,]
  256. for item in context.space_data.path[:-1]:
  257. sig_path.append(item.node_tree.nodes.active.name)
  258. return tuple(sig_path+[node.name])
  259. def poll_node_tree_schema(self, object):
  260. if isinstance(object, SchemaTree):
  261. return True
  262. return False
  263. # TODO tiny UI problem - inserting new links into the tree will not place them in the right place.
  264. class SchemaGroup(Node, MantisNode):
  265. bl_idname = "MantisSchemaGroup"
  266. bl_label = "Node Schema"
  267. node_tree:PointerProperty(type=NodeTree, poll=poll_node_tree_schema, update=node_tree_prop_update,)
  268. is_updating:BoolProperty(default=False)
  269. def draw_buttons(self, context, layout):
  270. row = layout.row(align=True)
  271. row.prop(self, "node_tree", text="")
  272. row.operator("mantis.edit_group", text="", icon='NODETREE', emboss=True)
  273. def update(self):
  274. live_update = self.id_data.do_live_update
  275. if self.is_updating: # update() can be called from update() and that leads to an infinite loop.
  276. return # so we check if an update is currently running.
  277. self.is_updating = True
  278. try:
  279. node_group_update(self)
  280. # reset things if necessary:
  281. if self.node_tree:
  282. if len(self.inputs) == 0:
  283. self.inputs.new("IntSocket", "Schema Length", identifier='Schema Length')
  284. if self.inputs[-1].bl_idname != "WildcardSocket":
  285. self.inputs.new("WildcardSocket", "", identifier="__extend__")
  286. finally: # we need to reset this regardless of whether or not the operation succeeds!
  287. self.is_updating = False
  288. self.id_data.do_live_update = live_update # ensure this remains the same
  289. NODES_REMOVED=["xFormRootNode"]
  290. # Node bl_idname, # Socket Name
  291. SOCKETS_REMOVED=[("UtilityDriverVariable", "Transform Channel"),
  292. ("xFormRootNode","World Out"),
  293. ("UtilitySwitch","xForm"),
  294. ("LinkDrivenParameter", "Enable")]
  295. # Node Class #Prior bl_idname # prior name # new bl_idname # new name, # Multi
  296. SOCKETS_RENAMED=[ ("LinkDrivenParameter", "DriverSocket", "Driver", "FloatSocket", "Value", False)]
  297. # NODE CLASS NAME IN_OUT SOCKET TYPE SOCKET NAME INDEX MULTI DEFAULT
  298. SOCKETS_ADDED=[("DeformerMorphTargetDeform", 'INPUT', 'BooleanSocket', "Use Shape Key", 1, False, False),
  299. ("DeformerMorphTargetDeform", 'INPUT', 'BooleanSocket', "Use Offset", 2, False, True),
  300. ("UtilityFCurve", 'INPUT', "eFCrvExtrapolationMode", "Extrapolation Mode", 0, False, 'CONSTANT')]
  301. # replace names with bl_idnames for reading the tree and solving schemas.
  302. replace_types = ["NodeGroupInput", "NodeGroupOutput", "SchemaIncomingConnection",
  303. "SchemaArrayInput", "SchemaConstInput", "SchemaConstOutput", "SchemaIndex",
  304. "SchemaOutgoingConnection", "SchemaConstantOutput", "SchemaArrayOutput",
  305. "SchemaArrayInputGet",]
  306. # anything that gets properties added in the graph... this is a clumsy approach but I need to watch for this
  307. # in schema generation and this is the easiest way to do it for now.
  308. custom_props_types = ["LinkArmature", "UtilityKeyframe", "UtilityFCurve", "UtilityDriver", "xFormBone"]
  309. # filters for determining if a link is a hierarchy link or a non-hierarchy (cyclic) link.
  310. from_name_filter = ["Driver",]
  311. to_name_filter = [
  312. "Custom Object xForm Override",
  313. "Custom Object",
  314. "Deform Bones",
  315. ]