node.py 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. # ##### BEGIN GPL LICENSE BLOCK #####
  2. #
  3. # This program is free software; you can redistribute it and/or
  4. # modify it under the terms of the GNU General Public License
  5. # as published by the Free Software Foundation; either version 2
  6. # of the License, or (at your option) any later version.
  7. #
  8. # This program is distributed in the hope that it will be useful,
  9. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. # GNU General Public License for more details.
  12. #
  13. # You should have received a copy of the GNU General Public License
  14. # along with this program; if not, write to the Free Software Foundation,
  15. # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  16. #
  17. # ##### END GPL LICENSE BLOCK #####
  18. # <pep8-80 compliant>
  19. import bpy
  20. import nodeitems_utils
  21. from bpy.types import (
  22. Operator,
  23. PropertyGroup,
  24. )
  25. from bpy.props import (
  26. BoolProperty,
  27. CollectionProperty,
  28. EnumProperty,
  29. IntProperty,
  30. StringProperty,
  31. )
  32. class NodeSetting(PropertyGroup):
  33. value: StringProperty(
  34. name="Value",
  35. description="Python expression to be evaluated "
  36. "as the initial node setting",
  37. default="",
  38. )
  39. # Base class for node 'Add' operators
  40. class NodeAddOperator:
  41. type: StringProperty(
  42. name="Node Type",
  43. description="Node type",
  44. )
  45. use_transform: BoolProperty(
  46. name="Use Transform",
  47. description="Start transform operator after inserting the node",
  48. default=False,
  49. )
  50. settings: CollectionProperty(
  51. name="Settings",
  52. description="Settings to be applied on the newly created node",
  53. type=NodeSetting,
  54. options={'SKIP_SAVE'},
  55. )
  56. @staticmethod
  57. def store_mouse_cursor(context, event):
  58. space = context.space_data
  59. tree = space.edit_tree
  60. # convert mouse position to the View2D for later node placement
  61. if context.region.type == 'WINDOW':
  62. # convert mouse position to the View2D for later node placement
  63. space.cursor_location_from_region(
  64. event.mouse_region_x, event.mouse_region_y)
  65. else:
  66. space.cursor_location = tree.view_center
  67. # XXX explicit node_type argument is usually not necessary,
  68. # but required to make search operator work:
  69. # add_search has to override the 'type' property
  70. # since it's hardcoded in bpy_operator_wrap.c ...
  71. def create_node(self, context, node_type=None):
  72. space = context.space_data
  73. tree = space.edit_tree
  74. if node_type is None:
  75. node_type = self.type
  76. # select only the new node
  77. for n in tree.nodes:
  78. n.select = False
  79. node = tree.nodes.new(type=node_type)
  80. for setting in self.settings:
  81. # XXX catch exceptions here?
  82. value = eval(setting.value)
  83. try:
  84. setattr(node, setting.name, value)
  85. except AttributeError as e:
  86. self.report(
  87. {'ERROR_INVALID_INPUT'},
  88. "Node has no attribute " + setting.name)
  89. print(str(e))
  90. # Continue despite invalid attribute
  91. node.select = True
  92. tree.nodes.active = node
  93. node.location = space.cursor_location
  94. return node
  95. @classmethod
  96. def poll(cls, context):
  97. space = context.space_data
  98. # needs active node editor and a tree to add nodes to
  99. return ((space.type == 'NODE_EDITOR') and
  100. space.edit_tree and not space.edit_tree.library)
  101. # Default execute simply adds a node
  102. def execute(self, context):
  103. if self.properties.is_property_set("type"):
  104. self.create_node(context)
  105. return {'FINISHED'}
  106. else:
  107. return {'CANCELLED'}
  108. # Default invoke stores the mouse position to place the node correctly
  109. # and optionally invokes the transform operator
  110. def invoke(self, context, event):
  111. self.store_mouse_cursor(context, event)
  112. result = self.execute(context)
  113. if self.use_transform and ('FINISHED' in result):
  114. # removes the node again if transform is canceled
  115. bpy.ops.node.translate_attach_remove_on_cancel('INVOKE_DEFAULT')
  116. return result
  117. # Simple basic operator for adding a node
  118. class NODE_OT_add_node(NodeAddOperator, Operator):
  119. '''Add a node to the active tree'''
  120. bl_idname = "node.add_node"
  121. bl_label = "Add Node"
  122. bl_options = {'REGISTER', 'UNDO'}
  123. # Add a node and link it to an existing socket
  124. class NODE_OT_add_and_link_node(NodeAddOperator, Operator):
  125. '''Add a node to the active tree and link to an existing socket'''
  126. bl_idname = "node.add_and_link_node"
  127. bl_label = "Add and Link Node"
  128. bl_options = {'REGISTER', 'UNDO'}
  129. link_socket_index: IntProperty(
  130. name="Link Socket Index",
  131. description="Index of the socket to link",
  132. )
  133. def execute(self, context):
  134. space = context.space_data
  135. ntree = space.edit_tree
  136. node = self.create_node(context)
  137. if not node:
  138. return {'CANCELLED'}
  139. to_socket = getattr(context, "link_to_socket", None)
  140. if to_socket:
  141. ntree.links.new(node.outputs[self.link_socket_index], to_socket)
  142. from_socket = getattr(context, "link_from_socket", None)
  143. if from_socket:
  144. ntree.links.new(from_socket, node.inputs[self.link_socket_index])
  145. return {'FINISHED'}
  146. class NODE_OT_add_search(NodeAddOperator, Operator):
  147. '''Add a node to the active tree'''
  148. bl_idname = "node.add_search"
  149. bl_label = "Search and Add Node"
  150. bl_options = {'REGISTER', 'UNDO'}
  151. bl_property = "node_item"
  152. _enum_item_hack = []
  153. # Create an enum list from node items
  154. def node_enum_items(self, context):
  155. enum_items = NODE_OT_add_search._enum_item_hack
  156. enum_items.clear()
  157. for index, item in enumerate(nodeitems_utils.node_items_iter(context)):
  158. if isinstance(item, nodeitems_utils.NodeItem):
  159. enum_items.append(
  160. (str(index),
  161. item.label,
  162. "",
  163. index,
  164. ))
  165. return enum_items
  166. # Look up the item based on index
  167. def find_node_item(self, context):
  168. node_item = int(self.node_item)
  169. for index, item in enumerate(nodeitems_utils.node_items_iter(context)):
  170. if index == node_item:
  171. return item
  172. return None
  173. node_item: EnumProperty(
  174. name="Node Type",
  175. description="Node type",
  176. items=node_enum_items,
  177. )
  178. def execute(self, context):
  179. item = self.find_node_item(context)
  180. # no need to keep
  181. self._enum_item_hack.clear()
  182. if item:
  183. # apply settings from the node item
  184. for setting in item.settings.items():
  185. ops = self.settings.add()
  186. ops.name = setting[0]
  187. ops.value = setting[1]
  188. self.create_node(context, item.nodetype)
  189. if self.use_transform:
  190. bpy.ops.node.translate_attach_remove_on_cancel(
  191. 'INVOKE_DEFAULT')
  192. return {'FINISHED'}
  193. else:
  194. return {'CANCELLED'}
  195. def invoke(self, context, event):
  196. self.store_mouse_cursor(context, event)
  197. # Delayed execution in the search popup
  198. context.window_manager.invoke_search_popup(self)
  199. return {'CANCELLED'}
  200. class NODE_OT_collapse_hide_unused_toggle(Operator):
  201. '''Toggle collapsed nodes and hide unused sockets'''
  202. bl_idname = "node.collapse_hide_unused_toggle"
  203. bl_label = "Collapse and Hide Unused Sockets"
  204. bl_options = {'REGISTER', 'UNDO'}
  205. @classmethod
  206. def poll(cls, context):
  207. space = context.space_data
  208. # needs active node editor and a tree
  209. return ((space.type == 'NODE_EDITOR') and
  210. (space.edit_tree and not space.edit_tree.library))
  211. def execute(self, context):
  212. space = context.space_data
  213. tree = space.edit_tree
  214. for node in tree.nodes:
  215. if node.select:
  216. hide = (not node.hide)
  217. node.hide = hide
  218. # Note: connected sockets are ignored internally
  219. for socket in node.inputs:
  220. socket.hide = hide
  221. for socket in node.outputs:
  222. socket.hide = hide
  223. return {'FINISHED'}
  224. class NODE_OT_tree_path_parent(Operator):
  225. '''Go to parent node tree'''
  226. bl_idname = "node.tree_path_parent"
  227. bl_label = "Parent Node Tree"
  228. bl_options = {'REGISTER', 'UNDO'}
  229. @classmethod
  230. def poll(cls, context):
  231. space = context.space_data
  232. # needs active node editor and a tree
  233. return (space.type == 'NODE_EDITOR' and len(space.path) > 1)
  234. def execute(self, context):
  235. space = context.space_data
  236. space.path.pop()
  237. return {'FINISHED'}
  238. classes = (
  239. NodeSetting,
  240. NODE_OT_add_and_link_node,
  241. NODE_OT_add_node,
  242. NODE_OT_add_search,
  243. NODE_OT_collapse_hide_unused_toggle,
  244. NODE_OT_tree_path_parent,
  245. )