base_definitions.py 16 KB

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