base_definitions.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  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. useTarget : BoolProperty(default=False)
  132. @classmethod
  133. def poll(cls, ntree):
  134. return (ntree.bl_idname in ['MantisTree', 'SchemaTree'])
  135. class xFormNode(MantisNode):
  136. @classmethod
  137. def poll(cls, ntree):
  138. return (ntree.bl_idname in ['MantisTree', 'SchemaTree'])
  139. class DeformerNode(MantisNode):
  140. @classmethod
  141. def poll(cls, ntree):
  142. return (ntree.bl_idname in ['MantisTree', 'SchemaTree'])
  143. def poll_node_tree(self, object):
  144. if isinstance(object, MantisTree):
  145. return True
  146. return False
  147. # TODO: try to check identifiers instead of name.
  148. def node_group_update(node, force = False):
  149. if not force:
  150. if (node.id_data.do_live_update == False) or (node.id_data.is_executing == True):
  151. return
  152. # note: if (node.id_data.is_exporting == True) I need to be able to update so I can make links.
  153. toggle_update = node.id_data.do_live_update
  154. node.id_data.do_live_update = False
  155. identifiers_in={socket.identifier:socket for socket in node.inputs}
  156. identifiers_out={socket.identifier:socket for socket in node.outputs}
  157. if node.node_tree is None:
  158. node.inputs.clear(); node.outputs.clear()
  159. node.id_data.do_live_update = toggle_update
  160. return
  161. found_in, found_out = [], []
  162. update_input, update_output = False, False
  163. for item in node.node_tree.interface.items_tree:
  164. if item.item_type != "SOCKET": continue
  165. if item.in_out == 'OUTPUT':
  166. if s:= identifiers_out.get(item.identifier): # if the requested output doesn't exist, update
  167. found_out.append(item.identifier)
  168. if update_output: continue
  169. if s.bl_idname != item.socket_type: update_output = True; continue
  170. else: update_output = True; continue
  171. else:
  172. if s:= identifiers_in.get(item.identifier): # if the requested input doesn't exist, update
  173. found_in.append(item.identifier)
  174. if update_input: continue # done here
  175. if s.bl_idname != item.socket_type: update_input = True; continue
  176. else: update_input = True; continue
  177. # Schema has an extra input for Length and for Extend.
  178. if node.bl_idname == 'MantisSchemaGroup':
  179. found_in.extend(['Schema Length', ''])
  180. # if we have too many elements, just get rid of the ones we don't need
  181. if len(node.inputs) > len(found_in):#
  182. for inp in node.inputs:
  183. if inp.identifier in found_in: continue
  184. node.inputs.remove(inp)
  185. if len(node.outputs) > len(found_out):
  186. for out in node.outputs:
  187. if out.identifier in found_out: continue
  188. node.outputs.remove(out)
  189. #
  190. if len(node.inputs) > 0 and (inp := node.inputs[-1]).bl_idname == 'WildcardSocket' and inp.is_linked:
  191. update_input = True
  192. if len(node.outputs) > 0 and (out := node.outputs[-1]).bl_idname == 'WildcardSocket' and out.is_linked:
  193. update_output = True
  194. #
  195. if not (update_input or update_output):
  196. node.id_data.do_live_update = toggle_update
  197. return
  198. if update_input or update_output:
  199. socket_map_in, socket_map_out = get_socket_maps(node)
  200. if update_input :
  201. if node.bl_idname == 'MantisSchemaGroup':
  202. schema_length=0
  203. if sl := node.inputs.get("Schema Length"):
  204. schema_length = sl.default_value
  205. # sometimes this isn't available yet # TODO not happy about this solution
  206. node.inputs.clear()
  207. if node.bl_idname == 'MantisSchemaGroup':
  208. node.inputs.new("IntSocket", "Schema Length", identifier='Schema Length')
  209. node.inputs['Schema Length'].default_value = schema_length
  210. if update_output: node.outputs.clear()
  211. for item in node.node_tree.interface.items_tree:
  212. if item.item_type != "SOCKET": continue
  213. if (item.in_out == 'INPUT' and update_input):
  214. relink_socket_map(node, node.inputs, socket_map_in, item)
  215. if (item.in_out == 'OUTPUT' and update_output):
  216. relink_socket_map(node, node.outputs, socket_map_out, item)
  217. # at this point there is no wildcard socket
  218. if '__extend__' in socket_map_in.keys():
  219. do_relink(node, None, socket_map_in, in_out='INPUT', parent_name='Constant' )
  220. node.id_data.do_live_update = toggle_update
  221. def node_tree_prop_update(self, context):
  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. if self.bl_idname in ['MantisSchemaGroup'] and self.node_tree is not None:
  228. if len(self.inputs) == 0:
  229. self.inputs.new("IntSocket", "Schema Length", identifier='Schema Length')
  230. if self.inputs[-1].bl_idname != "WildcardSocket":
  231. self.inputs.new("WildcardSocket", "", identifier="__extend__")
  232. from bpy.types import NodeCustomGroup
  233. class MantisNodeGroup(Node, MantisNode):
  234. bl_idname = "MantisNodeGroup"
  235. bl_label = "Node Group"
  236. node_tree:PointerProperty(type=NodeTree, poll=poll_node_tree, update=node_tree_prop_update,)
  237. is_updating:BoolProperty(default=False)
  238. def update(self):
  239. live_update = self.id_data.do_live_update
  240. if self.is_updating: # update() can be called from update() and that leads to an infinite loop.
  241. return # so we check if an update is currently running.
  242. try:
  243. self.is_updating = True
  244. node_group_update(self)
  245. self.is_updating = False
  246. finally: # we need to reset this regardless of whether or not the operation succeeds!
  247. self.is_updating = False
  248. self.id_data.do_live_update = live_update # ensure this remains the same
  249. def draw_buttons(self, context, layout):
  250. row = layout.row(align=True)
  251. row.prop(self, "node_tree", text="")
  252. row.operator("mantis.edit_group", text="", icon='NODETREE', emboss=True)
  253. class GraphError(Exception):
  254. pass
  255. def get_signature_from_edited_tree(node, context):
  256. sig_path=[None,]
  257. for item in context.space_data.path[:-1]:
  258. sig_path.append(item.node_tree.nodes.active.name)
  259. return tuple(sig_path+[node.name])
  260. def poll_node_tree_schema(self, object):
  261. if isinstance(object, SchemaTree):
  262. return True
  263. return False
  264. # TODO tiny UI problem - inserting new links into the tree will not place them in the right place.
  265. class SchemaGroup(Node, MantisNode):
  266. bl_idname = "MantisSchemaGroup"
  267. bl_label = "Node Schema"
  268. node_tree:PointerProperty(type=NodeTree, poll=poll_node_tree_schema, update=node_tree_prop_update,)
  269. is_updating:BoolProperty(default=False)
  270. def draw_buttons(self, context, layout):
  271. row = layout.row(align=True)
  272. row.prop(self, "node_tree", text="")
  273. row.operator("mantis.edit_group", text="", icon='NODETREE', emboss=True)
  274. def update(self):
  275. live_update = self.id_data.do_live_update
  276. if self.is_updating: # update() can be called from update() and that leads to an infinite loop.
  277. return # so we check if an update is currently running.
  278. self.is_updating = True
  279. try:
  280. node_group_update(self)
  281. # reset things if necessary:
  282. if self.node_tree:
  283. if len(self.inputs) == 0:
  284. self.inputs.new("IntSocket", "Schema Length", identifier='Schema Length')
  285. if self.inputs[-1].bl_idname != "WildcardSocket":
  286. self.inputs.new("WildcardSocket", "", identifier="__extend__")
  287. finally: # we need to reset this regardless of whether or not the operation succeeds!
  288. self.is_updating = False
  289. self.id_data.do_live_update = live_update # ensure this remains the same
  290. NODES_REMOVED=["xFormRootNode"]
  291. # Node bl_idname, # Socket Name
  292. SOCKETS_REMOVED=[("UtilityDriverVariable", "Transform Channel"),
  293. ("xFormRootNode","World Out"),
  294. ("UtilitySwitch","xForm"),
  295. ("LinkDrivenParameter", "Enable")]
  296. # Node Class #Prior bl_idname # prior name # new bl_idname # new name, # Multi
  297. SOCKETS_RENAMED=[ ("LinkDrivenParameter", "DriverSocket", "Driver", "FloatSocket", "Value", False)]
  298. # NODE CLASS NAME IN_OUT SOCKET TYPE SOCKET NAME INDEX MULTI DEFAULT
  299. SOCKETS_ADDED=[("DeformerMorphTargetDeform", 'INPUT', 'BooleanSocket', "Use Shape Key", 1, False, False),
  300. ("DeformerMorphTargetDeform", 'INPUT', 'BooleanSocket', "Use Offset", 2, False, True),
  301. ("UtilityFCurve", 'INPUT', "eFCrvExtrapolationMode", "Extrapolation Mode", 0, False, 'CONSTANT')]
  302. # replace names with bl_idnames for reading the tree and solving schemas.
  303. replace_types = ["NodeGroupInput", "NodeGroupOutput", "SchemaIncomingConnection",
  304. "SchemaArrayInput", "SchemaConstInput", "SchemaConstOutput", "SchemaIndex",
  305. "SchemaOutgoingConnection", "SchemaConstantOutput", "SchemaArrayOutput",
  306. "SchemaArrayInputGet",]
  307. # anything that gets properties added in the graph... this is a clumsy approach but I need to watch for this
  308. # in schema generation and this is the easiest way to do it for now.
  309. custom_props_types = ["LinkArmature", "UtilityKeyframe", "UtilityFCurve", "UtilityDriver", "xFormBone"]
  310. # filters for determining if a link is a hierarchy link or a non-hierarchy (cyclic) link.
  311. from_name_filter = ["Driver",]
  312. to_name_filter = [
  313. "Custom Object xForm Override",
  314. "Custom Object",
  315. "Deform Bones",
  316. ]