ops_nodegroup.py 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849
  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 mantis_tree_poll_op(context):
  9. space = context.space_data
  10. if hasattr(space, "node_tree"):
  11. if (space.node_tree):
  12. return (space.tree_type in ["MantisTree", "SchemaTree"])
  13. return False
  14. def any_tree_poll(context):
  15. space = context.space_data
  16. if hasattr(space, "node_tree"):
  17. return True
  18. return False
  19. #########################################################################3
  20. class MantisGroupNodes(Operator):
  21. """Create node-group from selected nodes"""
  22. bl_idname = "mantis.group_nodes"
  23. bl_label = "Group Nodes"
  24. bl_options = {'REGISTER', 'UNDO'}
  25. @classmethod
  26. def poll(cls, context):
  27. return mantis_tree_poll_op(context)
  28. def execute(self, context):
  29. base_tree=context.space_data.path[-1].node_tree
  30. try:
  31. for path_item in context.space_data.path:
  32. path_item.node_tree.is_exporting = True
  33. from .i_o import export_to_json, do_import
  34. from random import random
  35. grp_name = "".join([chr(int(random()*30)+35) for i in range(20)])
  36. trees=[base_tree]
  37. selected_nodes=export_to_json(trees, write_file=False, only_selected=True)
  38. # this snippet of confusing indirection edits the name of the base tree in the JSON data
  39. selected_nodes[base_tree.name][0]["name"]=grp_name
  40. do_import(selected_nodes, context)
  41. affected_links_in = []
  42. affected_links_out = []
  43. for l in base_tree.links:
  44. if l.from_node.select and not l.to_node.select: affected_links_out.append(l)
  45. if not l.from_node.select and l.to_node.select: affected_links_in.append(l)
  46. delete_me = []
  47. all_nodes_bounding_box=[Vector((float("inf"),float("inf"))), Vector((-float("inf"),-float("inf")))]
  48. for n in base_tree.nodes:
  49. if n.select:
  50. node_loc = (0,0,0)
  51. if bpy.app.version <= (4, 4):
  52. node_loc = n.location
  53. parent = n.parent
  54. while (parent): # accumulate parent offset
  55. node_loc += parent.location
  56. parent = parent.parent
  57. else: # there is a new location_absolute property in 4.4
  58. node_loc = n.location_absolute
  59. if node_loc.x < all_nodes_bounding_box[0].x:
  60. all_nodes_bounding_box[0].x = node_loc.x
  61. if node_loc.y < all_nodes_bounding_box[0].y:
  62. all_nodes_bounding_box[0].y = node_loc.y
  63. #
  64. if node_loc.x > all_nodes_bounding_box[1].x:
  65. all_nodes_bounding_box[1].x = node_loc.x
  66. if node_loc.y > all_nodes_bounding_box[1].y:
  67. all_nodes_bounding_box[1].y = node_loc.y
  68. delete_me.append(n)
  69. grp_node = base_tree.nodes.new('MantisNodeGroup')
  70. grp_node.node_tree = bpy.data.node_groups[grp_name]
  71. bb_center = all_nodes_bounding_box[0].lerp(all_nodes_bounding_box[1],0.5)
  72. for n in grp_node.node_tree.nodes:
  73. n.location -= bb_center
  74. 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))
  75. from .base_definitions import node_group_update
  76. grp_node.is_updating=True
  77. try:
  78. node_group_update(grp_node, force=True)
  79. finally:
  80. grp_node.is_updating=False
  81. # for each node in the JSON
  82. for n in selected_nodes[base_tree.name][2].values():
  83. for s in n["sockets"].values(): # for each socket in the node
  84. if source := s.get("source"):
  85. prGreen (s["name"], source[0], source[1])
  86. base_tree_node=base_tree.nodes.get(source[0])
  87. if s["is_output"]:
  88. for output in base_tree_node.outputs:
  89. if output.identifier == source[1]:
  90. break
  91. else:
  92. raise RuntimeError(wrapRed("Socket not found when grouping"))
  93. base_tree.links.new(input=output, output=grp_node.inputs[s["name"]])
  94. else:
  95. for s_input in base_tree_node.inputs:
  96. if s_input.identifier == source[1]:
  97. break
  98. else:
  99. raise RuntimeError(wrapRed("Socket not found when grouping"))
  100. base_tree.links.new(input=grp_node.outputs[s["name"]], output=s_input)
  101. for n in delete_me: base_tree.nodes.remove(n)
  102. base_tree.nodes.active = grp_node
  103. finally: # MAKE SURE to turn it back to not exporting
  104. for path_item in context.space_data.path:
  105. path_item.node_tree.is_exporting = False
  106. grp_node.node_tree.name = "Group_Node.000"
  107. return {'FINISHED'}
  108. class MantisEditGroup(Operator):
  109. """Edit the group referenced by the active node (or exit the current node-group)"""
  110. bl_idname = "mantis.edit_group"
  111. bl_label = "Edit Group"
  112. bl_options = {'REGISTER', 'UNDO'}
  113. @classmethod
  114. def poll(cls, context):
  115. return (
  116. mantis_tree_poll_op(context)
  117. )
  118. def execute(self, context):
  119. space = context.space_data
  120. path = space.path
  121. node = path[len(path)-1].node_tree.nodes.active
  122. base_tree = path[0].node_tree
  123. base_tree.do_live_update = False
  124. base_tree.is_executing = True
  125. try:
  126. if hasattr(node, "node_tree"):
  127. if (node.node_tree):
  128. path.append(node.node_tree, node=node)
  129. path[0].node_tree.display_update(context)
  130. return {"FINISHED"}
  131. elif len(path) > 1:
  132. path.pop()
  133. # get the active node in the current path
  134. active = path[len(path)-1].node_tree.nodes.active
  135. from .base_definitions import node_group_update
  136. active.is_updating = True
  137. try:
  138. node_group_update(active, force = True)
  139. finally:
  140. active.is_updating = False
  141. # call update to force the node group to check if its tree has changed
  142. # now we need to loop through the tree and update all node groups of this type.
  143. from .utilities import get_all_nodes_of_type
  144. for g in get_all_nodes_of_type(base_tree, "MantisNodeGroup"):
  145. if g.node_tree == active.node_tree:
  146. g.is_updating = True
  147. active.is_updating = True
  148. try:
  149. node_group_update(g, force = True)
  150. finally:
  151. g.is_updating = False
  152. active.is_updating = False
  153. base_tree.display_update(context)
  154. base_tree.is_executing = True
  155. # base_tree.is_executing = True # because it seems display_update unsets this.
  156. finally:
  157. base_tree.do_live_update = True
  158. base_tree.is_executing = False
  159. # HACK
  160. base_tree.handler_flip = True # HACK
  161. # HACK
  162. # I have no idea why but the operator finishing causes the exeuction handler to fire
  163. # I have no control over this since it happens after the execution returns...
  164. # so I have to do this ridiculous hack with a Boolean flip bit.
  165. return {"FINISHED"}
  166. class MantisNewNodeTree(Operator):
  167. """Add a new Mantis tree."""
  168. bl_idname = "mantis.new_node_tree"
  169. bl_label = "New Node Group"
  170. bl_options = {'REGISTER', 'UNDO'}
  171. tree_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  172. node_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  173. @classmethod
  174. def poll(cls, context):
  175. return (
  176. mantis_tree_poll_op(context) and \
  177. context.node.bl_idname in ["MantisNodeGroup", "MantisSchemaGroup"]
  178. )
  179. @classmethod
  180. def poll(cls, context):
  181. return True #(hasattr(context, 'active_node') )
  182. def invoke(self, context, event):
  183. self.tree_invoked = context.node.id_data.name
  184. self.node_invoked = context.node.name
  185. return self.execute(context)
  186. def execute(self, context):
  187. node = bpy.data.node_groups[self.tree_invoked].nodes[self.node_invoked]
  188. if node.bl_idname == "MantisSchemaGroup":
  189. if (node.node_tree):
  190. print('a')
  191. return {"CANCELLED"}
  192. else:
  193. from bpy import data
  194. print('b')
  195. node.node_tree = data.node_groups.new(name='Schema Group', type='SchemaTree')
  196. return {'FINISHED'}
  197. elif node.bl_idname == "MantisNodeGroup":
  198. if (node.node_tree):
  199. print('c')
  200. return {"CANCELLED"}
  201. else:
  202. from bpy import data
  203. print('d')
  204. node.node_tree = data.node_groups.new(name='Mantis Group', type='MantisTree')
  205. return {'FINISHED'}
  206. else:
  207. return {"CANCELLED"}
  208. class ExecuteNodeTree(Operator):
  209. """Execute this node tree"""
  210. bl_idname = "mantis.execute_node_tree"
  211. bl_label = "Execute Node Tree"
  212. bl_options = {'REGISTER', 'UNDO'}
  213. @classmethod
  214. def poll(cls, context):
  215. return (mantis_tree_poll_op(context))
  216. def execute(self, context):
  217. from time import time
  218. from .utilities import wrapGreen
  219. tree=context.space_data.path[0].node_tree
  220. import cProfile
  221. from os import environ
  222. start_time = time()
  223. do_profile=False
  224. print (environ.get("DOPROFILE"))
  225. if environ.get("DOPROFILE"):
  226. do_profile=True
  227. pass_error = True
  228. if environ.get("DOERROR"):
  229. pass_error=False
  230. if do_profile:
  231. import pstats, io
  232. from pstats import SortKey
  233. with cProfile.Profile() as pr:
  234. tree.update_tree(context)
  235. tree.execute_tree(context, error_popups = pass_error)
  236. # from the Python docs at https://docs.python.org/3/library/profile.html#module-cProfile
  237. s = io.StringIO()
  238. sortby = SortKey.TIME
  239. # sortby = SortKey.CUMULATIVE
  240. ps = pstats.Stats(pr, stream=s).strip_dirs().sort_stats(sortby)
  241. ps.print_stats(20) # print the top 20
  242. print(s.getvalue())
  243. else:
  244. tree.update_tree(context)
  245. tree.execute_tree(context, error_popups = pass_error)
  246. prGreen("Finished executing tree in %f seconds" % (time() - start_time))
  247. return {"FINISHED"}
  248. class SelectNodesOfType(Operator):
  249. """Selects all nodes of same type as active node."""
  250. bl_idname = "mantis.select_nodes_of_type"
  251. bl_label = "Select Nodes of Same Type as Active"
  252. bl_options = {'REGISTER', 'UNDO'}
  253. @classmethod
  254. def poll(cls, context):
  255. return (any_tree_poll(context))
  256. def execute(self, context):
  257. active_node = context.active_node
  258. tree = active_node.id_data
  259. if not hasattr(active_node, "node_tree"):
  260. for node in tree.nodes:
  261. node.select = (active_node.bl_idname == node.bl_idname)
  262. else:
  263. for node in tree.nodes:
  264. node.select = (active_node.bl_idname == node.bl_idname) and (active_node.node_tree == node.node_tree)
  265. return {"FINISHED"}
  266. def get_parent_tree_interface_enum(operator, context):
  267. ret = []; i = -1
  268. tree = bpy.data.node_groups[operator.tree_invoked]
  269. for sock in tree.interface.items_tree:
  270. if sock.item_type == 'PANEL': continue
  271. if sock.in_out == "OUTPUT": continue
  272. ret.append( (sock.identifier, sock.name, "Socket from Node Group Input", i := i + 1), )
  273. return ret
  274. def get_node_inputs_enum(operator, context):
  275. ret = []; i = -1
  276. n = bpy.data.node_groups[operator.tree_invoked].nodes[operator.node_invoked]
  277. for inp in n.inputs:
  278. ret.append( (inp.identifier, inp.name, "Socket of node to connect to.", i := i + 1), )
  279. return ret
  280. class ConnectNodeToInput(Operator):
  281. """Connects a Node Group Input socket to specified socket of active node and all selected same-type nodes."""
  282. bl_idname = "mantis.connect_nodes_to_input"
  283. bl_label = "Connect Socket to Input for Selected Nodes"
  284. bl_options = {'REGISTER', 'UNDO'}
  285. group_output : bpy.props.EnumProperty(
  286. items=get_parent_tree_interface_enum,
  287. name="Node Group Input Socket",
  288. description="Select which socket from the Node Group Input to connect to this node",)
  289. node_input : bpy.props.EnumProperty(
  290. items=get_node_inputs_enum,
  291. name="Node Input Socket",
  292. description="Select which of this node's sockets to recieve the connection",)
  293. tree_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  294. node_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  295. @classmethod
  296. def poll(cls, context):
  297. return (any_tree_poll(context))
  298. def invoke(self, context, event):
  299. self.tree_invoked = context.active_node.id_data.name
  300. self.node_invoked = context.active_node.name
  301. # we use active_node here ^ because we are comparing the active node to the selection.
  302. wm = context.window_manager
  303. return wm.invoke_props_dialog(self)
  304. def execute(self, context):
  305. t = bpy.data.node_groups[self.tree_invoked]
  306. if hasattr(t, "is_executing"): # for Mantis trees, but this function should just work anywhere.
  307. t.is_executing = True
  308. n = t.nodes[self.node_invoked]
  309. for node in t.nodes:
  310. if n.bl_idname == node.bl_idname and node.select:
  311. # the bl_idname is the same so they both have node_tree
  312. if hasattr(n, "node_tree") and n.node_tree != node.node_tree: continue
  313. # TODO: maybe I should try and find a nearby input node and reuse it
  314. # doing these identifier lookups again and again is slow, whatever. faster than doing it by hand
  315. for connect_to_me in node.inputs:
  316. if connect_to_me.identifier == self.node_input: break
  317. if connect_to_me.is_linked: connect_to_me = None
  318. if connect_to_me: # only make the node if the socket is there and free
  319. inp = t.nodes.new("NodeGroupInput")
  320. connect_me = None
  321. for s in inp.outputs:
  322. if s.identifier != self.group_output: s.hide = True
  323. else: connect_me = s
  324. inp.location = node.location
  325. inp.location.x-=200
  326. t.links.new(input=connect_me, output=connect_to_me)
  327. if hasattr(t, "is_executing"):
  328. t.is_executing = False
  329. return {"FINISHED"}
  330. class QueryNodeSockets(Operator):
  331. """Utility Operator for querying the data in a socket"""
  332. bl_idname = "mantis.query_sockets"
  333. bl_label = "Query Node Sockets"
  334. bl_options = {'REGISTER', 'UNDO'}
  335. @classmethod
  336. def poll(cls, context):
  337. return (mantis_tree_poll_op(context))
  338. def execute(self, context):
  339. active_node = context.active_node
  340. tree = active_node.id_data
  341. for node in tree.nodes:
  342. if not node.select: continue
  343. return {"FINISHED"}
  344. class ForceDisplayUpdate(Operator):
  345. """Utility Operator for querying the data in a socket"""
  346. bl_idname = "mantis.force_display_update"
  347. bl_label = "Force Mantis Display Update"
  348. bl_options = {'REGISTER', 'UNDO'}
  349. @classmethod
  350. def poll(cls, context):
  351. return (mantis_tree_poll_op(context))
  352. def execute(self, context):
  353. base_tree = bpy.context.space_data.path[0].node_tree
  354. base_tree.display_update(context)
  355. return {"FINISHED"}
  356. class CleanUpNodeGraph(bpy.types.Operator):
  357. """Clean Up Node Graph"""
  358. bl_idname = "mantis.nodes_cleanup"
  359. bl_label = "Clean Up Node Graph"
  360. bl_options = {'REGISTER', 'UNDO'}
  361. # num_iterations=bpy.props.IntProperty(default=8)
  362. @classmethod
  363. def poll(cls, context):
  364. return hasattr(context, 'active_node')
  365. def execute(self, context):
  366. base_tree=context.space_data.path[-1].node_tree
  367. from .utilities import SugiyamaGraph
  368. SugiyamaGraph(base_tree, 12)
  369. return {'FINISHED'}
  370. class MantisMuteNode(Operator):
  371. """Mantis Test Operator"""
  372. bl_idname = "mantis.mute_node"
  373. bl_label = "Mute Node"
  374. bl_options = {'REGISTER', 'UNDO'}
  375. @classmethod
  376. def poll(cls, context):
  377. return (mantis_tree_poll_op(context))
  378. def execute(self, context):
  379. path = context.space_data.path
  380. node = path[len(path)-1].node_tree.nodes.active
  381. node.mute = not node.mute
  382. # There should only be one of these
  383. if (enable := node.inputs.get("Enable")):
  384. # annoyingly, 'mute' and 'enable' are opposites
  385. enable.default_value = not node.mute
  386. if (hide := node.inputs.get("Hide")):
  387. hide.default_value = node.mute
  388. return {"FINISHED"}
  389. ePropertyType =(
  390. ('BOOL' , "Boolean", "Boolean", 0),
  391. ('INT' , "Integer", "Integer", 1),
  392. ('FLOAT' , "Float" , "Float" , 2),
  393. ('VECTOR', "Vector" , "Vector" , 3),
  394. ('STRING', "String" , "String" , 4),
  395. #('ENUM' , "Enum" , "Enum" , 5),
  396. )
  397. from .base_definitions import xFormNode
  398. class AddCustomProperty(bpy.types.Operator):
  399. """Add Custom Property to xForm Node"""
  400. bl_idname = "mantis.add_custom_property"
  401. bl_label = "Add Custom Property"
  402. bl_options = {'REGISTER', 'UNDO'}
  403. prop_type : bpy.props.EnumProperty(
  404. items=ePropertyType,
  405. name="New Property Type",
  406. description="Type of data for new Property",
  407. default = 'BOOL',)
  408. prop_name : bpy.props.StringProperty(default='Prop')
  409. min:bpy.props.FloatProperty(default = 0)
  410. max:bpy.props.FloatProperty(default = 1)
  411. soft_min:bpy.props.FloatProperty(default = 0)
  412. soft_max:bpy.props.FloatProperty(default = 1)
  413. description:bpy.props.StringProperty(default = "")
  414. tree_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  415. node_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  416. @classmethod
  417. def poll(cls, context):
  418. return True #( hasattr(context, 'node') )
  419. def invoke(self, context, event):
  420. self.tree_invoked = context.node.id_data.name
  421. self.node_invoked = context.node.name
  422. wm = context.window_manager
  423. return wm.invoke_props_dialog(self)
  424. def execute(self, context):
  425. n = bpy.data.node_groups[self.tree_invoked].nodes[self.node_invoked]
  426. # For whatever reason, context.node doesn't exist anymore
  427. # (probably because I use a window to execute)
  428. # so as a sort of dumb workaround I am saving it to a hidden
  429. # property of the operator... it works but Blender complains.
  430. socktype = ''
  431. if not (self.prop_name):
  432. self.report({'ERROR_INVALID_INPUT'}, "Must name the property.")
  433. return {'CANCELLED'}
  434. if self.prop_type == 'BOOL':
  435. socktype = 'ParameterBoolSocket'
  436. if self.prop_type == 'INT':
  437. socktype = 'ParameterIntSocket'
  438. if self.prop_type == 'FLOAT':
  439. socktype = 'ParameterFloatSocket'
  440. if self.prop_type == 'VECTOR':
  441. socktype = 'ParameterVectorSocket'
  442. if self.prop_type == 'STRING':
  443. socktype = 'ParameterStringSocket'
  444. #if self.prop_type == 'ENUM':
  445. # sock_type = 'ParameterStringSocket'
  446. if (s := n.inputs.get(self.prop_name)):
  447. try:
  448. number = int(self.prop_name[-3:])
  449. # see if it has a number
  450. number+=1
  451. self.prop_name = self.prop_name[:-3] + str(number).zfill(3)
  452. except ValueError:
  453. self.prop_name+='.001'
  454. # WRONG # HACK # TODO # BUG #
  455. new_prop = n.inputs.new( socktype, self.prop_name)
  456. if self.prop_type in ['INT','FLOAT']:
  457. new_prop.min = self.min
  458. new_prop.max = self.max
  459. new_prop.soft_min = self.soft_min
  460. new_prop.soft_max = self.soft_max
  461. new_prop.description = self.description
  462. # now do the output
  463. n.outputs.new( socktype, self.prop_name)
  464. return {'FINISHED'}
  465. def main_get_existing_custom_properties(operator, context):
  466. ret = []; i = -1
  467. n = bpy.data.node_groups[operator.tree_invoked].nodes[operator.node_invoked]
  468. for inp in n.inputs:
  469. if 'Parameter' in inp.bl_idname:
  470. ret.append( (inp.identifier, inp.name, "Custom Property to Modify", i := i + 1), )
  471. return ret
  472. class EditCustomProperty(bpy.types.Operator):
  473. """Edit Custom Property"""
  474. bl_idname = "mantis.edit_custom_property"
  475. bl_label = "Edit Custom Property"
  476. bl_options = {'REGISTER', 'UNDO'}
  477. def get_existing_custom_properties(self, context):
  478. return main_get_existing_custom_properties(self, context)
  479. prop_edit : bpy.props.EnumProperty(
  480. items=get_existing_custom_properties,
  481. name="Property to Edit?",
  482. description="Select which property to edit",)
  483. prop_type : bpy.props.EnumProperty(
  484. items=ePropertyType,
  485. name="New Property Type",
  486. description="Type of data for new Property",
  487. default = 'BOOL',)
  488. prop_name : bpy.props.StringProperty(default='Prop')
  489. min:bpy.props.FloatProperty(default = 0)
  490. max:bpy.props.FloatProperty(default = 1)
  491. soft_min:bpy.props.FloatProperty(default = 0)
  492. soft_max:bpy.props.FloatProperty(default = 1)
  493. description:bpy.props.StringProperty(default = "") # TODO: use getters to fill these automatically
  494. tree_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  495. node_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  496. @classmethod
  497. def poll(cls, context):
  498. return True #( hasattr(context, 'node') )
  499. def invoke(self, context, event):
  500. self.tree_invoked = context.node.id_data.name
  501. self.node_invoked = context.node.name
  502. wm = context.window_manager
  503. return wm.invoke_props_dialog(self)
  504. def execute(self, context):
  505. n = bpy.data.node_groups[self.tree_invoked].nodes[self.node_invoked]
  506. prop = n.inputs.get( self.prop_edit )
  507. if prop:
  508. prop.name = self.prop_name
  509. if (s := n.inputs.get(self.prop_edit)):
  510. if self.prop_type in ['INT','FLOAT']:
  511. prop.min = self.min
  512. prop.max = self.max
  513. prop.soft_min = self.soft_min
  514. prop.soft_max = self.soft_max
  515. prop.description = self.description
  516. return {'FINISHED'}
  517. else:
  518. self.report({'ERROR_INVALID_INPUT'}, "Cannot edit a property that does not exist.")
  519. class RemoveCustomProperty(bpy.types.Operator):
  520. """Remove a Custom Property from an xForm Node"""
  521. bl_idname = "mantis.remove_custom_property"
  522. bl_label = "Remove Custom Property"
  523. bl_options = {'REGISTER', 'UNDO'}
  524. def get_existing_custom_properties(self, context):
  525. return main_get_existing_custom_properties(self, context)
  526. prop_remove : bpy.props.EnumProperty(
  527. items=get_existing_custom_properties,
  528. name="Property to remove?",
  529. description="Select which property to remove",)
  530. tree_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  531. node_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  532. @classmethod
  533. def poll(cls, context):
  534. return True #(hasattr(context, 'active_node') )
  535. def invoke(self, context, event):
  536. self.tree_invoked = context.node.id_data.name
  537. self.node_invoked = context.node.name
  538. t = context.node.id_data
  539. # HACK the props dialog makes this necesary
  540. # because context.node only exists during the event that
  541. # was created by clicking on the node.
  542. t.nodes.active = context.node # HACK
  543. context.node.select = True # HACK
  544. # I need this bc of the callback for the enum property.
  545. # for whatever reason I can't use tree_invoked there
  546. wm = context.window_manager
  547. return wm.invoke_props_dialog(self)
  548. def execute(self, context):
  549. n = bpy.data.node_groups[self.tree_invoked].nodes[self.node_invoked]
  550. # For whatever reason, context.node doesn't exist anymore
  551. # (probably because I use a window to execute)
  552. # so as a sort of dumb workaround I am saving it to a hidden
  553. # property of the operator... it works.
  554. for i, inp in enumerate(n.inputs):
  555. if inp.identifier == self.prop_remove:
  556. break
  557. else:
  558. self.report({'ERROR'}, "Input not found")
  559. return {'CANCELLED'}
  560. # it's possible that the output property's identifier isn't the
  561. # exact same... but I don' care. Shouldn't ever happen. TODO
  562. for j, out in enumerate(n.outputs):
  563. if out.identifier == self.prop_remove:
  564. break
  565. else:
  566. self.report({'ERROR'}, "Output not found")
  567. raise RuntimeError("This should not happen!")
  568. n.inputs.remove ( n.inputs [i] )
  569. n.outputs.remove( n.outputs[j] )
  570. return {'FINISHED'}
  571. # SIMPLE node operators...
  572. # May rewrite these in a more generic way later
  573. class FcurveAddKeyframeInput(bpy.types.Operator):
  574. """Add a keyframe input to the fCurve node"""
  575. bl_idname = "mantis.fcurve_node_add_kf"
  576. bl_label = "Add Keyframe"
  577. bl_options = {'INTERNAL', 'REGISTER', 'UNDO'}
  578. @classmethod
  579. def poll(cls, context):
  580. return (hasattr(context, 'active_node') )
  581. def execute(self, context):
  582. num_keys = len( context.node.inputs)-1
  583. context.node.inputs.new("KeyframeSocket", "Keyframe."+str(num_keys).zfill(3))
  584. return {'FINISHED'}
  585. class FcurveRemoveKeyframeInput(bpy.types.Operator):
  586. """Remove a keyframe input from the fCurve node"""
  587. bl_idname = "mantis.fcurve_node_remove_kf"
  588. bl_label = "Remove Keyframe"
  589. bl_options = {'INTERNAL', 'REGISTER', 'UNDO'}
  590. @classmethod
  591. def poll(cls, context):
  592. return (hasattr(context, 'active_node') )
  593. def execute(self, context):
  594. n = context.node
  595. n.inputs.remove(n.inputs[-1])
  596. return {'FINISHED'}
  597. class DriverAddDriverVariableInput(bpy.types.Operator):
  598. """Add a Driver Variable input to the Driver node"""
  599. bl_idname = "mantis.driver_node_add_variable"
  600. bl_label = "Add Driver Variable"
  601. bl_options = {'INTERNAL', 'REGISTER', 'UNDO'}
  602. @classmethod
  603. def poll(cls, context):
  604. return (hasattr(context, 'active_node') )
  605. def execute(self, context): # unicode for 'a'
  606. i = len (context.node.inputs) - 2 + 96
  607. context.node.inputs.new("DriverVariableSocket", chr(i))
  608. return {'FINISHED'}
  609. class DriverRemoveDriverVariableInput(bpy.types.Operator):
  610. """Remove a DriverVariable input from the active Driver node"""
  611. bl_idname = "mantis.driver_node_remove_variable"
  612. bl_label = "Remove Driver Variable"
  613. bl_options = {'INTERNAL', 'REGISTER', 'UNDO'}
  614. @classmethod
  615. def poll(cls, context):
  616. return (hasattr(context, 'active_node') )
  617. def execute(self, context):
  618. n = context.node
  619. n.inputs.remove(n.inputs[-1])
  620. return {'FINISHED'}
  621. class LinkArmatureAddTargetInput(bpy.types.Operator):
  622. """Add a Driver Variable input to the Driver node"""
  623. bl_idname = "mantis.link_armature_node_add_target"
  624. bl_label = "Add Target"
  625. bl_options = {'INTERNAL', 'REGISTER', 'UNDO'}
  626. @classmethod
  627. def poll(cls, context):
  628. return hasattr(context, 'node')
  629. def execute(self, context): # unicode for 'a'
  630. num_targets = len( list(context.node.inputs)[6:])//2
  631. context.node.inputs.new("xFormSocket", "Target."+str(num_targets).zfill(3))
  632. context.node.inputs.new("FloatFactorSocket", "Weight."+str(num_targets).zfill(3))
  633. return {'FINISHED'}
  634. class LinkArmatureRemoveTargetInput(bpy.types.Operator):
  635. """Remove a DriverVariable input from the active Driver node"""
  636. bl_idname = "mantis.link_armature_node_remove_target"
  637. bl_label = "Remove Target"
  638. bl_options = {'INTERNAL', 'REGISTER', 'UNDO'}
  639. @classmethod
  640. def poll(cls, context):
  641. return hasattr(context, 'node')
  642. def execute(self, context):
  643. n = context.node
  644. n.inputs.remove(n.inputs[-1]); n.inputs.remove(n.inputs[-1])
  645. return {'FINISHED'}
  646. def get_socket_enum(operator, context):
  647. valid_types = []; i = -1
  648. from .socket_definitions import TellClasses, MantisSocket
  649. for cls in TellClasses():
  650. if cls.is_valid_interface_type:
  651. valid_types.append( (cls.bl_idname, cls.bl_label, "Socket Type", i := i + 1), )
  652. return valid_types
  653. class B4_4_0_Workaround_NodeTree_Interface_Update(Operator):
  654. """Selects all nodes of same type as active node."""
  655. bl_idname = "mantis.node_tree_interface_update_4_4_0_workaround"
  656. bl_label = "Add Socket to Node Tree"
  657. bl_options = {'REGISTER', 'UNDO'}
  658. socket_name : bpy.props.StringProperty()
  659. output : bpy.props.BoolProperty()
  660. socket_type : bpy.props.EnumProperty(
  661. name="Socket Type",
  662. description="Socket Type",
  663. items=get_socket_enum,
  664. default=0,)
  665. tree_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  666. @classmethod
  667. def poll(cls, context):
  668. return (any_tree_poll(context))
  669. def invoke(self, context, event):
  670. self.tree_invoked = context.active_node.id_data.name
  671. # we use active_node here ^ because we are comparing the active node to the selection.
  672. wm = context.window_manager
  673. return wm.invoke_props_dialog(self)
  674. def execute(self, context):
  675. tree = bpy.data.node_groups[self.tree_invoked]
  676. in_out = 'OUTPUT' if self.output else 'INPUT'
  677. tree.interface.new_socket(self.socket_name, in_out=in_out, socket_type=self.socket_type)
  678. # try to prevent the next execution
  679. # because updating the interface triggers a depsgraph update.
  680. # this doesn't actually work though...TODO
  681. if tree.bl_idname == "MantisTree":
  682. tree.prevent_next_exec=True
  683. return {"FINISHED"}
  684. # this has to be down here for some reason. what a pain
  685. classes = [
  686. MantisGroupNodes,
  687. MantisEditGroup,
  688. MantisNewNodeTree,
  689. ExecuteNodeTree,
  690. # CreateMetaGroup,
  691. QueryNodeSockets,
  692. ForceDisplayUpdate,
  693. CleanUpNodeGraph,
  694. MantisMuteNode,
  695. SelectNodesOfType,
  696. ConnectNodeToInput,
  697. # xForm
  698. AddCustomProperty,
  699. EditCustomProperty,
  700. RemoveCustomProperty,
  701. # EditFCurveNode,
  702. FcurveAddKeyframeInput,
  703. FcurveRemoveKeyframeInput,
  704. # Driver
  705. DriverAddDriverVariableInput,
  706. DriverRemoveDriverVariableInput,
  707. # Armature Link Node
  708. LinkArmatureAddTargetInput,
  709. LinkArmatureRemoveTargetInput,
  710. ]
  711. if (bpy.app.version >= (4, 4, 0)):
  712. classes.append(B4_4_0_Workaround_NodeTree_Interface_Update)
  713. def TellClasses():
  714. return classes