ops_nodegroup.py 32 KB

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