ops_nodegroup.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611
  1. import bpy
  2. from bpy.types import Operator
  3. from mathutils import Vector
  4. from .utilities import (prRed, prGreen, prPurple, prWhite,
  5. prOrange,
  6. wrapRed, wrapGreen, wrapPurple, wrapWhite,
  7. wrapOrange,)
  8. def TellClasses():
  9. return [
  10. MantisGroupNodes,
  11. MantisEditGroup,
  12. ExecuteNodeTree,
  13. # CreateMetaGroup,
  14. QueryNodeSockets,
  15. ForceDisplayUpdate,
  16. CleanUpNodeGraph,
  17. MantisMuteNode,
  18. # xForm
  19. AddCustomProperty,
  20. EditCustomProperty,
  21. RemoveCustomProperty,
  22. # EditFCurveNode,
  23. FcurveAddKeyframeInput,
  24. FcurveRemoveKeyframeInput,
  25. # Driver
  26. DriverAddDriverVariableInput,
  27. DriverRemoveDriverVariableInput,
  28. # Armature Link Node
  29. LinkArmatureAddTargetInput,
  30. LinkArmatureRemoveTargetInput,]
  31. # ExportNodeTreeToJSON,]
  32. def mantis_tree_poll_op(context):
  33. space = context.space_data
  34. if hasattr(space, "node_tree"):
  35. if (space.node_tree):
  36. return (space.tree_type in ["MantisTree", "SchemaTree"])
  37. return False
  38. #########################################################################3
  39. class MantisGroupNodes(Operator):
  40. """Create node-group from selected nodes"""
  41. bl_idname = "mantis.group_nodes"
  42. bl_label = "Group Nodes"
  43. bl_options = {'REGISTER', 'UNDO'}
  44. @classmethod
  45. def poll(cls, context):
  46. return mantis_tree_poll_op(context)
  47. def execute(self, context):
  48. base_tree=context.space_data.path[-1].node_tree
  49. for path_item in context.space_data.path:
  50. path_item.node_tree.is_exporting = True
  51. from .i_o import export_to_json, do_import
  52. from random import random
  53. grp_name = "".join([chr(int(random()*30)+35) for i in range(20)])
  54. trees=[base_tree]
  55. selected_nodes=export_to_json(trees, write_file=False, only_selected=True)
  56. # this snippet of confusing indirection edits the name of the base tree in the JSON data
  57. selected_nodes[base_tree.name][0]["name"]=grp_name
  58. do_import(selected_nodes, context)
  59. affected_links_in = []
  60. affected_links_out = []
  61. for l in base_tree.links:
  62. if l.from_node.select and not l.to_node.select: affected_links_out.append(l)
  63. if not l.from_node.select and l.to_node.select: affected_links_in.append(l)
  64. delete_me = []
  65. all_nodes_bounding_box=[Vector((float("inf"),float("inf"))), Vector((-float("inf"),-float("inf")))]
  66. for n in base_tree.nodes:
  67. if n.select:
  68. node_loc = (0,0,0)
  69. if bpy.app.version <= (4, 4):
  70. node_loc = n.location
  71. parent = n.parent
  72. while (parent): # accumulate parent offset
  73. node_loc += parent.location
  74. parent = parent.parent
  75. else: # there is a new location_absolute property in 4.4
  76. node_loc = n.location_absolute
  77. if node_loc.x < all_nodes_bounding_box[0].x:
  78. all_nodes_bounding_box[0].x = node_loc.x
  79. if node_loc.y < all_nodes_bounding_box[0].y:
  80. all_nodes_bounding_box[0].y = node_loc.y
  81. #
  82. if node_loc.x > all_nodes_bounding_box[1].x:
  83. all_nodes_bounding_box[1].x = node_loc.x
  84. if node_loc.y > all_nodes_bounding_box[1].y:
  85. all_nodes_bounding_box[1].y = node_loc.y
  86. delete_me.append(n)
  87. grp_node = base_tree.nodes.new('MantisNodeGroup')
  88. grp_node.node_tree = bpy.data.node_groups[grp_name]
  89. bb_center = all_nodes_bounding_box[0].lerp(all_nodes_bounding_box[1],0.5)
  90. for n in grp_node.node_tree.nodes:
  91. n.location -= bb_center
  92. grp_node.location = Vector((all_nodes_bounding_box[0].x+200, all_nodes_bounding_box[0].lerp(all_nodes_bounding_box[1], 0.5).y))
  93. from .base_definitions import node_group_update
  94. node_group_update(grp_node, force=True)
  95. # for each node in the JSON
  96. for n in selected_nodes[base_tree.name][2].values():
  97. for s in n["sockets"].values(): # for each socket in the node
  98. if source := s.get("source"):
  99. prGreen (s["name"], source[0], source[1])
  100. base_tree_node=base_tree.nodes.get(source[0])
  101. if s["is_output"]:
  102. for output in base_tree_node.outputs:
  103. if output.identifier == source[1]:
  104. break
  105. else:
  106. raise RuntimeError(wrapRed("Socket not found when grouping"))
  107. base_tree.links.new(input=output, output=grp_node.inputs[s["name"]])
  108. else:
  109. for s_input in base_tree_node.inputs:
  110. if s_input.identifier == source[1]:
  111. break
  112. else:
  113. raise RuntimeError(wrapRed("Socket not found when grouping"))
  114. base_tree.links.new(input=grp_node.outputs[s["name"]], output=s_input)
  115. for n in delete_me: base_tree.nodes.remove(n)
  116. base_tree.nodes.active = grp_node
  117. for path_item in context.space_data.path:
  118. path_item.node_tree.is_exporting = False
  119. grp_node.node_tree.name = "Group_Node.000"
  120. return {'FINISHED'}
  121. class MantisEditGroup(Operator):
  122. """Edit the group referenced by the active node (or exit the current node-group)"""
  123. bl_idname = "mantis.edit_group"
  124. bl_label = "Edit Group"
  125. bl_options = {'REGISTER', 'UNDO'}
  126. @classmethod
  127. def poll(cls, context):
  128. return (
  129. mantis_tree_poll_op(context)
  130. )
  131. def execute(self, context):
  132. space = context.space_data
  133. path = space.path
  134. node = path[len(path)-1].node_tree.nodes.active
  135. if hasattr(node, "node_tree"):
  136. if (node.node_tree):
  137. path.append(node.node_tree, node=node)
  138. path[0].node_tree.display_update(context)
  139. return {"FINISHED"}
  140. elif len(path) > 1:
  141. path.pop()
  142. # get the active node in the current path
  143. active = path[len(path)-1].node_tree.nodes.active
  144. from .base_definitions import node_group_update
  145. active.is_updating = True
  146. node_group_update(active, force = True)
  147. active.is_updating = False
  148. base_tree = path[0].node_tree
  149. base_tree.do_live_update = False
  150. # call update to force the node group to check if its tree has changed
  151. # now we need to loop through the tree and update all node groups of this type.
  152. from .utilities import get_all_nodes_of_type
  153. for g in get_all_nodes_of_type(base_tree, "MantisNodeGroup"):
  154. if g.node_tree == active.node_tree:
  155. g.is_updating = False
  156. node_group_update(g, force = True)
  157. g.is_updating = True
  158. path[0].node_tree.display_update(context)
  159. base_tree.do_live_update = True
  160. return {"FINISHED"}
  161. return {"CANCELLED"}
  162. class ExecuteNodeTree(Operator):
  163. """Execute this node tree"""
  164. bl_idname = "mantis.execute_node_tree"
  165. bl_label = "Execute Node Tree"
  166. bl_options = {'REGISTER', 'UNDO'}
  167. @classmethod
  168. def poll(cls, context):
  169. return (mantis_tree_poll_op(context))
  170. def execute(self, context):
  171. from time import time
  172. from .utilities import wrapGreen
  173. tree=context.space_data.path[0].node_tree
  174. import cProfile
  175. from os import environ
  176. start_time = time()
  177. do_profile=False
  178. print (environ.get("DOPROFILE"))
  179. if environ.get("DOPROFILE"):
  180. do_profile=True
  181. if do_profile:
  182. import pstats, io
  183. from pstats import SortKey
  184. with cProfile.Profile() as pr:
  185. tree.update_tree(context)
  186. tree.execute_tree(context)
  187. # from the Python docs at https://docs.python.org/3/library/profile.html#module-cProfile
  188. s = io.StringIO()
  189. sortby = SortKey.TIME
  190. # sortby = SortKey.CUMULATIVE
  191. ps = pstats.Stats(pr, stream=s).strip_dirs().sort_stats(sortby)
  192. ps.print_stats(20) # print the top 20
  193. print(s.getvalue())
  194. else:
  195. tree.update_tree(context)
  196. tree.execute_tree(context)
  197. prGreen("Finished executing tree in %f seconds" % (time() - start_time))
  198. return {"FINISHED"}
  199. class QueryNodeSockets(Operator):
  200. """Utility Operator for querying the data in a socket"""
  201. bl_idname = "mantis.query_sockets"
  202. bl_label = "Query Node Sockets"
  203. bl_options = {'REGISTER', 'UNDO'}
  204. @classmethod
  205. def poll(cls, context):
  206. return (mantis_tree_poll_op(context))
  207. def execute(self, context):
  208. node = context.active_node
  209. print ("Node type: ", node.bl_idname)
  210. # This is useful. Todo: reimplement this eventually.
  211. return {"FINISHED"}
  212. class ForceDisplayUpdate(Operator):
  213. """Utility Operator for querying the data in a socket"""
  214. bl_idname = "mantis.force_display_update"
  215. bl_label = "Force Mantis Display Update"
  216. bl_options = {'REGISTER', 'UNDO'}
  217. @classmethod
  218. def poll(cls, context):
  219. return (mantis_tree_poll_op(context))
  220. def execute(self, context):
  221. base_tree = bpy.context.space_data.path[0].node_tree
  222. base_tree.display_update(context)
  223. return {"FINISHED"}
  224. class CleanUpNodeGraph(bpy.types.Operator):
  225. """Clean Up Node Graph"""
  226. bl_idname = "mantis.nodes_cleanup"
  227. bl_label = "Clean Up Node Graph"
  228. bl_options = {'REGISTER', 'UNDO'}
  229. # num_iterations=bpy.props.IntProperty(default=8)
  230. @classmethod
  231. def poll(cls, context):
  232. return hasattr(context, 'active_node')
  233. def execute(self, context):
  234. base_tree=context.space_data.path[-1].node_tree
  235. from .utilities import SugiyamaGraph
  236. SugiyamaGraph(base_tree, 12)
  237. return {'FINISHED'}
  238. class MantisMuteNode(Operator):
  239. """Mantis Test Operator"""
  240. bl_idname = "mantis.mute_node"
  241. bl_label = "Mute Node"
  242. bl_options = {'REGISTER', 'UNDO'}
  243. @classmethod
  244. def poll(cls, context):
  245. return (mantis_tree_poll_op(context))
  246. def execute(self, context):
  247. path = context.space_data.path
  248. node = path[len(path)-1].node_tree.nodes.active
  249. node.mute = not node.mute
  250. # There should only be one of these
  251. if (enable := node.inputs.get("Enable")):
  252. # annoyingly, 'mute' and 'enable' are opposites
  253. enable.default_value = not node.mute
  254. if (hide := node.inputs.get("Hide")):
  255. hide.default_value = node.mute
  256. return {"FINISHED"}
  257. ePropertyType =(
  258. ('BOOL' , "Boolean", "Boolean", 0),
  259. ('INT' , "Integer", "Integer", 1),
  260. ('FLOAT' , "Float" , "Float" , 2),
  261. ('VECTOR', "Vector" , "Vector" , 3),
  262. ('STRING', "String" , "String" , 4),
  263. #('ENUM' , "Enum" , "Enum" , 5),
  264. )
  265. from .base_definitions import xFormNode
  266. class AddCustomProperty(bpy.types.Operator):
  267. """Add Custom Property to xForm Node"""
  268. bl_idname = "mantis.add_custom_property"
  269. bl_label = "Add Custom Property"
  270. bl_options = {'REGISTER', 'UNDO'}
  271. prop_type : bpy.props.EnumProperty(
  272. items=ePropertyType,
  273. name="New Property Type",
  274. description="Type of data for new Property",
  275. default = 'BOOL',)
  276. prop_name : bpy.props.StringProperty(default='Prop')
  277. min:bpy.props.FloatProperty(default = 0)
  278. max:bpy.props.FloatProperty(default = 1)
  279. soft_min:bpy.props.FloatProperty(default = 0)
  280. soft_max:bpy.props.FloatProperty(default = 1)
  281. description:bpy.props.StringProperty(default = "")
  282. tree_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  283. node_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  284. @classmethod
  285. def poll(cls, context):
  286. return True #( hasattr(context, 'node') )
  287. def invoke(self, context, event):
  288. self.tree_invoked = context.node.id_data.name
  289. self.node_invoked = context.node.name
  290. wm = context.window_manager
  291. return wm.invoke_props_dialog(self)
  292. def execute(self, context):
  293. n = bpy.data.node_groups[self.tree_invoked].nodes[self.node_invoked]
  294. # For whatever reason, context.node doesn't exist anymore
  295. # (probably because I use a window to execute)
  296. # so as a sort of dumb workaround I am saving it to a hidden
  297. # property of the operator... it works but Blender complains.
  298. socktype = ''
  299. if not (self.prop_name):
  300. self.report({'ERROR_INVALID_INPUT'}, "Must name the property.")
  301. return {'CANCELLED'}
  302. if self.prop_type == 'BOOL':
  303. socktype = 'ParameterBoolSocket'
  304. if self.prop_type == 'INT':
  305. socktype = 'ParameterIntSocket'
  306. if self.prop_type == 'FLOAT':
  307. socktype = 'ParameterFloatSocket'
  308. if self.prop_type == 'VECTOR':
  309. socktype = 'ParameterVectorSocket'
  310. if self.prop_type == 'STRING':
  311. socktype = 'ParameterStringSocket'
  312. #if self.prop_type == 'ENUM':
  313. # sock_type = 'ParameterStringSocket'
  314. if (s := n.inputs.get(self.prop_name)):
  315. try:
  316. number = int(self.prop_name[-3:])
  317. # see if it has a number
  318. number+=1
  319. self.prop_name = self.prop_name[:-3] + str(number).zfill(3)
  320. except ValueError:
  321. self.prop_name+='.001'
  322. # WRONG # HACK # TODO # BUG #
  323. new_prop = n.inputs.new( socktype, self.prop_name)
  324. if self.prop_type in ['INT','FLOAT']:
  325. new_prop.min = self.min
  326. new_prop.max = self.max
  327. new_prop.soft_min = self.soft_min
  328. new_prop.soft_max = self.soft_max
  329. new_prop.description = self.description
  330. # now do the output
  331. n.outputs.new( socktype, self.prop_name)
  332. return {'FINISHED'}
  333. def main_get_existing_custom_properties(operator, context):
  334. ret = []; i = -1
  335. n = bpy.data.node_groups[operator.tree_invoked].nodes[operator.node_invoked]
  336. for inp in n.inputs:
  337. if 'Parameter' in inp.bl_idname:
  338. ret.append( (inp.identifier, inp.name, "Custom Property to Modify", i := i + 1), )
  339. return ret
  340. class EditCustomProperty(bpy.types.Operator):
  341. """Edit Custom Property"""
  342. bl_idname = "mantis.edit_custom_property"
  343. bl_label = "Edit Custom Property"
  344. bl_options = {'REGISTER', 'UNDO'}
  345. def get_existing_custom_properties(self, context):
  346. return main_get_existing_custom_properties(self, context)
  347. prop_edit : bpy.props.EnumProperty(
  348. items=get_existing_custom_properties,
  349. name="Property to Edit?",
  350. description="Select which property to edit",)
  351. prop_type : bpy.props.EnumProperty(
  352. items=ePropertyType,
  353. name="New Property Type",
  354. description="Type of data for new Property",
  355. default = 'BOOL',)
  356. prop_name : bpy.props.StringProperty(default='Prop')
  357. min:bpy.props.FloatProperty(default = 0)
  358. max:bpy.props.FloatProperty(default = 1)
  359. soft_min:bpy.props.FloatProperty(default = 0)
  360. soft_max:bpy.props.FloatProperty(default = 1)
  361. description:bpy.props.StringProperty(default = "") # TODO: use getters to fill these automatically
  362. tree_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  363. node_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  364. @classmethod
  365. def poll(cls, context):
  366. return True #( hasattr(context, 'node') )
  367. def invoke(self, context, event):
  368. self.tree_invoked = context.node.id_data.name
  369. self.node_invoked = context.node.name
  370. wm = context.window_manager
  371. return wm.invoke_props_dialog(self)
  372. def execute(self, context):
  373. n = bpy.data.node_groups[self.tree_invoked].nodes[self.node_invoked]
  374. prop = n.inputs.get( self.prop_edit )
  375. if prop:
  376. prop.name = self.prop_name
  377. if (s := n.inputs.get(self.prop_edit)):
  378. if self.prop_type in ['INT','FLOAT']:
  379. prop.min = self.min
  380. prop.max = self.max
  381. prop.soft_min = self.soft_min
  382. prop.soft_max = self.soft_max
  383. prop.description = self.description
  384. return {'FINISHED'}
  385. else:
  386. self.report({'ERROR_INVALID_INPUT'}, "Cannot edit a property that does not exist.")
  387. class RemoveCustomProperty(bpy.types.Operator):
  388. """Remove a Custom Property from an xForm Node"""
  389. bl_idname = "mantis.remove_custom_property"
  390. bl_label = "Remove Custom Property"
  391. bl_options = {'REGISTER', 'UNDO'}
  392. def get_existing_custom_properties(self, context):
  393. return main_get_existing_custom_properties(self, context)
  394. prop_remove : bpy.props.EnumProperty(
  395. items=get_existing_custom_properties,
  396. name="Property to remove?",
  397. description="Select which property to remove",)
  398. tree_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  399. node_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  400. @classmethod
  401. def poll(cls, context):
  402. return True #(hasattr(context, 'active_node') )
  403. def invoke(self, context, event):
  404. self.tree_invoked = context.node.id_data.name
  405. self.node_invoked = context.node.name
  406. t = context.node.id_data
  407. # HACK the props dialog makes this necesary
  408. # because context.node only exists during the event that
  409. # was created by clicking on the node.
  410. t.nodes.active = context.node # HACK
  411. context.node.select = True # HACK
  412. # I need this bc of the callback for the enum property.
  413. # for whatever reason I can't use tree_invoked there
  414. wm = context.window_manager
  415. return wm.invoke_props_dialog(self)
  416. def execute(self, context):
  417. n = bpy.data.node_groups[self.tree_invoked].nodes[self.node_invoked]
  418. # For whatever reason, context.node doesn't exist anymore
  419. # (probably because I use a window to execute)
  420. # so as a sort of dumb workaround I am saving it to a hidden
  421. # property of the operator... it works.
  422. for i, inp in enumerate(n.inputs):
  423. if inp.identifier == self.prop_remove:
  424. break
  425. else:
  426. self.report({'ERROR'}, "Input not found")
  427. return {'CANCELLED'}
  428. # it's possible that the output property's identifier isn't the
  429. # exact same... but I don' care. Shouldn't ever happen. TODO
  430. for j, out in enumerate(n.outputs):
  431. if out.identifier == self.prop_remove:
  432. break
  433. else:
  434. self.report({'ERROR'}, "Output not found")
  435. raise RuntimeError("This should not happen!")
  436. n.inputs.remove ( n.inputs [i] )
  437. n.outputs.remove( n.outputs[j] )
  438. return {'FINISHED'}
  439. # SIMPLE node operators...
  440. # May rewrite these in a more generic way later
  441. class FcurveAddKeyframeInput(bpy.types.Operator):
  442. """Add a keyframe input to the fCurve node"""
  443. bl_idname = "mantis.fcurve_node_add_kf"
  444. bl_label = "Add Keyframe"
  445. bl_options = {'INTERNAL', 'REGISTER', 'UNDO'}
  446. @classmethod
  447. def poll(cls, context):
  448. return (hasattr(context, 'active_node') )
  449. def execute(self, context):
  450. num_keys = len( context.node.inputs)
  451. context.node.inputs.new("KeyframeSocket", "Keyframe."+str(num_keys).zfill(3))
  452. return {'FINISHED'}
  453. class FcurveRemoveKeyframeInput(bpy.types.Operator):
  454. """Remove a keyframe input from the fCurve node"""
  455. bl_idname = "mantis.fcurve_node_remove_kf"
  456. bl_label = "Remove Keyframe"
  457. bl_options = {'INTERNAL', 'REGISTER', 'UNDO'}
  458. @classmethod
  459. def poll(cls, context):
  460. return (hasattr(context, 'active_node') )
  461. def execute(self, context):
  462. n = context.node
  463. n.inputs.remove(n.inputs[-1])
  464. return {'FINISHED'}
  465. class DriverAddDriverVariableInput(bpy.types.Operator):
  466. """Add a Driver Variable input to the Driver node"""
  467. bl_idname = "mantis.driver_node_add_variable"
  468. bl_label = "Add Driver Variable"
  469. bl_options = {'INTERNAL', 'REGISTER', 'UNDO'}
  470. @classmethod
  471. def poll(cls, context):
  472. return (hasattr(context, 'active_node') )
  473. def execute(self, context): # unicode for 'a'
  474. i = len (context.node.inputs) - 2 + 96
  475. context.node.inputs.new("DriverVariableSocket", chr(i))
  476. return {'FINISHED'}
  477. class DriverRemoveDriverVariableInput(bpy.types.Operator):
  478. """Remove a DriverVariable input from the active Driver node"""
  479. bl_idname = "mantis.driver_node_remove_variable"
  480. bl_label = "Remove Driver Variable"
  481. bl_options = {'INTERNAL', 'REGISTER', 'UNDO'}
  482. @classmethod
  483. def poll(cls, context):
  484. return (hasattr(context, 'active_node') )
  485. def execute(self, context):
  486. n = context.node
  487. n.inputs.remove(n.inputs[-1])
  488. return {'FINISHED'}
  489. class LinkArmatureAddTargetInput(bpy.types.Operator):
  490. """Add a Driver Variable input to the Driver node"""
  491. bl_idname = "mantis.link_armature_node_add_target"
  492. bl_label = "Add Target"
  493. bl_options = {'INTERNAL', 'REGISTER', 'UNDO'}
  494. @classmethod
  495. def poll(cls, context):
  496. return hasattr(context, 'node')
  497. def execute(self, context): # unicode for 'a'
  498. num_targets = len( list(context.node.inputs)[6:])//2
  499. context.node.inputs.new("xFormSocket", "Target."+str(num_targets).zfill(3))
  500. context.node.inputs.new("FloatFactorSocket", "Weight."+str(num_targets).zfill(3))
  501. return {'FINISHED'}
  502. class LinkArmatureRemoveTargetInput(bpy.types.Operator):
  503. """Remove a DriverVariable input from the active Driver node"""
  504. bl_idname = "mantis.link_armature_node_remove_target"
  505. bl_label = "Remove Target"
  506. bl_options = {'INTERNAL', 'REGISTER', 'UNDO'}
  507. @classmethod
  508. def poll(cls, context):
  509. return hasattr(context, 'node')
  510. def execute(self, context):
  511. n = context.node
  512. n.inputs.remove(n.inputs[-1]); n.inputs.remove(n.inputs[-1])
  513. return {'FINISHED'}