ops_nodegroup.py 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993
  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 InvalidateNodeTree(Operator):
  209. """Invalidates this node tree, forcing it to read all data again."""
  210. bl_idname = "mantis.invalidate_node_tree"
  211. bl_label = "Clear Node Tree Cache"
  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. print("Clearing Active Node Tree cache")
  218. tree=context.space_data.path[0].node_tree
  219. tree.execution_id=''; tree.hash=''
  220. return {"FINISHED"}
  221. class ExecuteNodeTree(Operator):
  222. """Execute this node tree"""
  223. bl_idname = "mantis.execute_node_tree"
  224. bl_label = "Execute Node Tree"
  225. bl_options = {'REGISTER', 'UNDO'}
  226. @classmethod
  227. def poll(cls, context):
  228. return (mantis_tree_poll_op(context))
  229. def execute(self, context):
  230. from time import time
  231. from .utilities import wrapGreen
  232. tree=context.space_data.path[0].node_tree
  233. import cProfile
  234. from os import environ
  235. start_time = time()
  236. do_profile=False
  237. if environ.get("DOPROFILE"):
  238. do_profile=True
  239. pass_error = True
  240. if environ.get("DOERROR"):
  241. pass_error=False
  242. if do_profile:
  243. import pstats, io
  244. from pstats import SortKey
  245. with cProfile.Profile() as pr:
  246. tree.update_tree(context, error_popups = pass_error)
  247. tree.execute_tree(context, error_popups = pass_error)
  248. # from the Python docs at https://docs.python.org/3/library/profile.html#module-cProfile
  249. s = io.StringIO()
  250. sortby = SortKey.TIME
  251. # sortby = SortKey.CUMULATIVE
  252. ps = pstats.Stats(pr, stream=s).strip_dirs().sort_stats(sortby)
  253. ps.print_stats(20) # print the top 20
  254. print(s.getvalue())
  255. else:
  256. tree.update_tree(context, error_popups = pass_error)
  257. tree.execute_tree(context, error_popups = pass_error)
  258. prGreen("Finished executing tree in %f seconds" % (time() - start_time))
  259. return {"FINISHED"}
  260. class SelectNodesOfType(Operator):
  261. """Selects all nodes of same type as active node."""
  262. bl_idname = "mantis.select_nodes_of_type"
  263. bl_label = "Select Nodes of Same Type as Active"
  264. bl_options = {'REGISTER', 'UNDO'}
  265. @classmethod
  266. def poll(cls, context):
  267. return (any_tree_poll(context))
  268. def execute(self, context):
  269. active_node = context.active_node
  270. tree = active_node.id_data
  271. if not hasattr(active_node, "node_tree"):
  272. for node in tree.nodes:
  273. node.select = (active_node.bl_idname == node.bl_idname)
  274. else:
  275. for node in tree.nodes:
  276. node.select = (active_node.bl_idname == node.bl_idname) and (active_node.node_tree == node.node_tree)
  277. return {"FINISHED"}
  278. def get_parent_tree_interface_enum(operator, context):
  279. ret = []; i = -1
  280. tree = bpy.data.node_groups[operator.tree_invoked]
  281. for sock in tree.interface.items_tree:
  282. if sock.item_type == 'PANEL': continue
  283. if sock.in_out == "OUTPUT": continue
  284. ret.append( (sock.identifier, sock.name, "Socket from Node Group Input", i := i + 1), )
  285. return ret
  286. def get_node_inputs_enum(operator, context):
  287. ret = []; i = -1
  288. n = bpy.data.node_groups[operator.tree_invoked].nodes[operator.node_invoked]
  289. for inp in n.inputs:
  290. ret.append( (inp.identifier, inp.name, "Socket of node to connect to.", i := i + 1), )
  291. return ret
  292. class ConnectNodeToInput(Operator):
  293. """Connects a Node Group Input socket to specified socket of active node and all selected same-type nodes."""
  294. bl_idname = "mantis.connect_nodes_to_input"
  295. bl_label = "Connect Socket to Input for Selected Nodes"
  296. bl_options = {'REGISTER', 'UNDO'}
  297. group_output : bpy.props.EnumProperty(
  298. items=get_parent_tree_interface_enum,
  299. name="Node Group Input Socket",
  300. description="Select which socket from the Node Group Input to connect to this node",)
  301. node_input : bpy.props.EnumProperty(
  302. items=get_node_inputs_enum,
  303. name="Node Input Socket",
  304. description="Select which of this node's sockets to recieve the connection",)
  305. tree_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  306. node_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  307. @classmethod
  308. def poll(cls, context):
  309. return (any_tree_poll(context))
  310. def invoke(self, context, event):
  311. self.tree_invoked = context.active_node.id_data.name
  312. self.node_invoked = context.active_node.name
  313. # we use active_node here ^ because we are comparing the active node to the selection.
  314. wm = context.window_manager
  315. return wm.invoke_props_dialog(self)
  316. def execute(self, context):
  317. t = bpy.data.node_groups[self.tree_invoked]
  318. if hasattr(t, "is_executing"): # for Mantis trees, but this function should just work anywhere.
  319. t.is_executing = True
  320. n = t.nodes[self.node_invoked]
  321. for node in t.nodes:
  322. if n.bl_idname == node.bl_idname and node.select:
  323. # the bl_idname is the same so they both have node_tree
  324. if hasattr(n, "node_tree") and n.node_tree != node.node_tree: continue
  325. # TODO: maybe I should try and find a nearby input node and reuse it
  326. # doing these identifier lookups again and again is slow, whatever. faster than doing it by hand
  327. for connect_to_me in node.inputs:
  328. if connect_to_me.identifier == self.node_input: break
  329. if connect_to_me.is_linked: connect_to_me = None
  330. if connect_to_me: # only make the node if the socket is there and free
  331. inp = t.nodes.new("NodeGroupInput")
  332. connect_me = None
  333. for s in inp.outputs:
  334. if s.identifier != self.group_output: s.hide = True
  335. else: connect_me = s
  336. inp.location = node.location
  337. inp.location.x-=200
  338. t.links.new(input=connect_me, output=connect_to_me)
  339. if hasattr(t, "is_executing"):
  340. t.is_executing = False
  341. return {"FINISHED"}
  342. class QueryNodeSockets(Operator):
  343. """Utility Operator for querying the data in a socket"""
  344. bl_idname = "mantis.query_sockets"
  345. bl_label = "Query Node Sockets"
  346. bl_options = {'REGISTER', 'UNDO'}
  347. @classmethod
  348. def poll(cls, context):
  349. return (mantis_tree_poll_op(context))
  350. def execute(self, context):
  351. active_node = context.active_node
  352. tree = active_node.id_data
  353. for node in tree.nodes:
  354. if not node.select: continue
  355. return {"FINISHED"}
  356. class ForceDisplayUpdate(Operator):
  357. """Utility Operator for querying the data in a socket"""
  358. bl_idname = "mantis.force_display_update"
  359. bl_label = "Force Mantis Display Update"
  360. bl_options = {'REGISTER', 'UNDO'}
  361. @classmethod
  362. def poll(cls, context):
  363. return (mantis_tree_poll_op(context))
  364. def execute(self, context):
  365. base_tree = bpy.context.space_data.path[0].node_tree
  366. base_tree.display_update(context)
  367. return {"FINISHED"}
  368. class CleanUpNodeGraph(bpy.types.Operator):
  369. """Clean Up Node Graph"""
  370. bl_idname = "mantis.nodes_cleanup"
  371. bl_label = "Clean Up Node Graph"
  372. bl_options = {'REGISTER', 'UNDO'}
  373. # num_iterations=bpy.props.IntProperty(default=8)
  374. @classmethod
  375. def poll(cls, context):
  376. return hasattr(context, 'active_node')
  377. def execute(self, context):
  378. base_tree=context.space_data.path[-1].node_tree
  379. from .utilities import SugiyamaGraph
  380. SugiyamaGraph(base_tree, 12)
  381. return {'FINISHED'}
  382. class MantisMuteNode(Operator):
  383. """Mantis Test Operator"""
  384. bl_idname = "mantis.mute_node"
  385. bl_label = "Mute Node"
  386. bl_options = {'REGISTER', 'UNDO'}
  387. @classmethod
  388. def poll(cls, context):
  389. return (mantis_tree_poll_op(context))
  390. def execute(self, context):
  391. path = context.space_data.path
  392. node = path[len(path)-1].node_tree.nodes.active
  393. node.mute = not node.mute
  394. # There should only be one of these
  395. if (enable := node.inputs.get("Enable")):
  396. # annoyingly, 'mute' and 'enable' are opposites
  397. enable.default_value = not node.mute
  398. # this one is for Deformers
  399. elif (enable := node.inputs.get("Enable in Viewport")):
  400. # annoyingly, 'mute' and 'enable' are opposites
  401. enable.default_value = not node.mute
  402. elif (hide := node.inputs.get("Hide")):
  403. hide.default_value = node.mute
  404. elif (hide := node.inputs.get("Hide in Viewport")):
  405. hide.default_value = node.mute
  406. return {"FINISHED"}
  407. ePropertyType =(
  408. ('BOOL' , "Boolean", "Boolean", 0),
  409. ('INT' , "Integer", "Integer", 1),
  410. ('FLOAT' , "Float" , "Float" , 2),
  411. ('VECTOR', "Vector" , "Vector" , 3),
  412. ('STRING', "String" , "String" , 4),
  413. #('ENUM' , "Enum" , "Enum" , 5),
  414. )
  415. from .base_definitions import xFormNode
  416. class AddCustomProperty(bpy.types.Operator):
  417. """Add Custom Property to xForm Node"""
  418. bl_idname = "mantis.add_custom_property"
  419. bl_label = "Add Custom Property"
  420. bl_options = {'REGISTER', 'UNDO'}
  421. prop_type : bpy.props.EnumProperty(
  422. items=ePropertyType,
  423. name="New Property Type",
  424. description="Type of data for new Property",
  425. default = 'BOOL',)
  426. prop_name : bpy.props.StringProperty(default='Prop')
  427. min:bpy.props.FloatProperty(default = 0)
  428. max:bpy.props.FloatProperty(default = 1)
  429. soft_min:bpy.props.FloatProperty(default = 0)
  430. soft_max:bpy.props.FloatProperty(default = 1)
  431. description:bpy.props.StringProperty(default = "")
  432. tree_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  433. node_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  434. @classmethod
  435. def poll(cls, context):
  436. return True #( hasattr(context, 'node') )
  437. def invoke(self, context, event):
  438. self.tree_invoked = context.node.id_data.name
  439. self.node_invoked = context.node.name
  440. wm = context.window_manager
  441. return wm.invoke_props_dialog(self)
  442. def execute(self, context):
  443. n = bpy.data.node_groups[self.tree_invoked].nodes[self.node_invoked]
  444. # For whatever reason, context.node doesn't exist anymore
  445. # (probably because I use a window to execute)
  446. # so as a sort of dumb workaround I am saving it to a hidden
  447. # property of the operator... it works but Blender complains.
  448. socktype = ''
  449. if not (self.prop_name):
  450. self.report({'ERROR_INVALID_INPUT'}, "Must name the property.")
  451. return {'CANCELLED'}
  452. if self.prop_type == 'BOOL':
  453. socktype = 'ParameterBoolSocket'
  454. if self.prop_type == 'INT':
  455. socktype = 'ParameterIntSocket'
  456. if self.prop_type == 'FLOAT':
  457. socktype = 'ParameterFloatSocket'
  458. if self.prop_type == 'VECTOR':
  459. socktype = 'ParameterVectorSocket'
  460. if self.prop_type == 'STRING':
  461. socktype = 'ParameterStringSocket'
  462. #if self.prop_type == 'ENUM':
  463. # sock_type = 'ParameterStringSocket'
  464. if (s := n.inputs.get(self.prop_name)):
  465. try:
  466. number = int(self.prop_name[-3:])
  467. # see if it has a number
  468. number+=1
  469. self.prop_name = self.prop_name[:-3] + str(number).zfill(3)
  470. except ValueError:
  471. self.prop_name+='.001'
  472. # WRONG # HACK # TODO # BUG #
  473. new_prop = n.inputs.new( socktype, self.prop_name)
  474. if self.prop_type in ['INT','FLOAT']:
  475. new_prop.min = self.min
  476. new_prop.max = self.max
  477. new_prop.soft_min = self.soft_min
  478. new_prop.soft_max = self.soft_max
  479. new_prop.description = self.description
  480. # now do the output
  481. n.outputs.new( socktype, self.prop_name)
  482. return {'FINISHED'}
  483. def main_get_existing_custom_properties(operator, context):
  484. ret = []; i = -1
  485. n = bpy.data.node_groups[operator.tree_invoked].nodes[operator.node_invoked]
  486. for inp in n.inputs:
  487. if 'Parameter' in inp.bl_idname:
  488. ret.append( (inp.identifier, inp.name, "Custom Property to Modify", i := i + 1), )
  489. return ret
  490. class EditCustomProperty(bpy.types.Operator):
  491. """Edit Custom Property"""
  492. bl_idname = "mantis.edit_custom_property"
  493. bl_label = "Edit Custom Property"
  494. bl_options = {'REGISTER', 'UNDO'}
  495. def get_existing_custom_properties(self, context):
  496. return main_get_existing_custom_properties(self, context)
  497. prop_edit : bpy.props.EnumProperty(
  498. items=get_existing_custom_properties,
  499. name="Property to Edit?",
  500. description="Select which property to edit",)
  501. prop_type : bpy.props.EnumProperty(
  502. items=ePropertyType,
  503. name="New Property Type",
  504. description="Type of data for new Property",
  505. default = 'BOOL',)
  506. prop_name : bpy.props.StringProperty(default='Prop')
  507. min:bpy.props.FloatProperty(default = 0)
  508. max:bpy.props.FloatProperty(default = 1)
  509. soft_min:bpy.props.FloatProperty(default = 0)
  510. soft_max:bpy.props.FloatProperty(default = 1)
  511. description:bpy.props.StringProperty(default = "") # TODO: use getters to fill these automatically
  512. tree_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  513. node_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  514. @classmethod
  515. def poll(cls, context):
  516. return True #( hasattr(context, 'node') )
  517. def invoke(self, context, event):
  518. self.tree_invoked = context.node.id_data.name
  519. self.node_invoked = context.node.name
  520. wm = context.window_manager
  521. return wm.invoke_props_dialog(self)
  522. def execute(self, context):
  523. n = bpy.data.node_groups[self.tree_invoked].nodes[self.node_invoked]
  524. prop = n.inputs.get( self.prop_edit )
  525. if prop:
  526. prop.name = self.prop_name
  527. if (s := n.inputs.get(self.prop_edit)):
  528. if self.prop_type in ['INT','FLOAT']:
  529. prop.min = self.min
  530. prop.max = self.max
  531. prop.soft_min = self.soft_min
  532. prop.soft_max = self.soft_max
  533. prop.description = self.description
  534. return {'FINISHED'}
  535. else:
  536. self.report({'ERROR_INVALID_INPUT'}, "Cannot edit a property that does not exist.")
  537. class RemoveCustomProperty(bpy.types.Operator):
  538. """Remove a Custom Property from an xForm Node"""
  539. bl_idname = "mantis.remove_custom_property"
  540. bl_label = "Remove Custom Property"
  541. bl_options = {'REGISTER', 'UNDO'}
  542. def get_existing_custom_properties(self, context):
  543. return main_get_existing_custom_properties(self, context)
  544. prop_remove : bpy.props.EnumProperty(
  545. items=get_existing_custom_properties,
  546. name="Property to remove?",
  547. description="Select which property to remove",)
  548. tree_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  549. node_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  550. @classmethod
  551. def poll(cls, context):
  552. return True #(hasattr(context, 'active_node') )
  553. def invoke(self, context, event):
  554. self.tree_invoked = context.node.id_data.name
  555. self.node_invoked = context.node.name
  556. t = context.node.id_data
  557. # HACK the props dialog makes this necesary
  558. # because context.node only exists during the event that
  559. # was created by clicking on the node.
  560. t.nodes.active = context.node # HACK
  561. context.node.select = True # HACK
  562. # I need this bc of the callback for the enum property.
  563. # for whatever reason I can't use tree_invoked there
  564. wm = context.window_manager
  565. return wm.invoke_props_dialog(self)
  566. def execute(self, context):
  567. n = bpy.data.node_groups[self.tree_invoked].nodes[self.node_invoked]
  568. # For whatever reason, context.node doesn't exist anymore
  569. # (probably because I use a window to execute)
  570. # so as a sort of dumb workaround I am saving it to a hidden
  571. # property of the operator... it works.
  572. for i, inp in enumerate(n.inputs):
  573. if inp.identifier == self.prop_remove:
  574. break
  575. else:
  576. self.report({'ERROR'}, "Input not found")
  577. return {'CANCELLED'}
  578. # it's possible that the output property's identifier isn't the
  579. # exact same... but I don' care. Shouldn't ever happen. TODO
  580. for j, out in enumerate(n.outputs):
  581. if out.identifier == self.prop_remove:
  582. break
  583. else:
  584. self.report({'ERROR'}, "Output not found")
  585. raise RuntimeError("This should not happen!")
  586. n.inputs.remove ( n.inputs [i] )
  587. n.outputs.remove( n.outputs[j] )
  588. return {'FINISHED'}
  589. # SIMPLE node operators...
  590. # May rewrite these in a more generic way later
  591. class FcurveAddKeyframeInput(bpy.types.Operator):
  592. """Add a keyframe input to the fCurve node"""
  593. bl_idname = "mantis.fcurve_node_add_kf"
  594. bl_label = "Add Keyframe"
  595. bl_options = {'INTERNAL', 'REGISTER', 'UNDO'}
  596. @classmethod
  597. def poll(cls, context):
  598. return (hasattr(context, 'active_node') )
  599. def execute(self, context):
  600. num_keys = len( context.node.inputs)-1
  601. context.node.inputs.new("KeyframeSocket", "Keyframe."+str(num_keys).zfill(3))
  602. return {'FINISHED'}
  603. class FcurveRemoveKeyframeInput(bpy.types.Operator):
  604. """Remove a keyframe input from the fCurve node"""
  605. bl_idname = "mantis.fcurve_node_remove_kf"
  606. bl_label = "Remove Keyframe"
  607. bl_options = {'INTERNAL', 'REGISTER', 'UNDO'}
  608. @classmethod
  609. def poll(cls, context):
  610. return (hasattr(context, 'active_node') )
  611. def execute(self, context):
  612. n = context.node
  613. n.inputs.remove(n.inputs[-1])
  614. return {'FINISHED'}
  615. class DriverAddDriverVariableInput(bpy.types.Operator):
  616. """Add a Driver Variable input to the Driver node"""
  617. bl_idname = "mantis.driver_node_add_variable"
  618. bl_label = "Add Driver Variable"
  619. bl_options = {'INTERNAL', 'REGISTER', 'UNDO'}
  620. @classmethod
  621. def poll(cls, context):
  622. return (hasattr(context, 'active_node') )
  623. def execute(self, context): # unicode for 'a'
  624. i = len (context.node.inputs) - 2 + 96
  625. context.node.inputs.new("DriverVariableSocket", chr(i))
  626. return {'FINISHED'}
  627. class DriverRemoveDriverVariableInput(bpy.types.Operator):
  628. """Remove a DriverVariable input from the active Driver node"""
  629. bl_idname = "mantis.driver_node_remove_variable"
  630. bl_label = "Remove Driver Variable"
  631. bl_options = {'INTERNAL', 'REGISTER', 'UNDO'}
  632. @classmethod
  633. def poll(cls, context):
  634. return (hasattr(context, 'active_node') )
  635. def execute(self, context):
  636. n = context.node
  637. n.inputs.remove(n.inputs[-1])
  638. return {'FINISHED'}
  639. class LinkArmatureAddTargetInput(bpy.types.Operator):
  640. """Add a Driver Variable input to the Driver node"""
  641. bl_idname = "mantis.link_armature_node_add_target"
  642. bl_label = "Add Target"
  643. bl_options = {'INTERNAL', 'REGISTER', 'UNDO'}
  644. @classmethod
  645. def poll(cls, context):
  646. return hasattr(context, 'node')
  647. def execute(self, context): # unicode for 'a'
  648. num_targets = len( list(context.node.inputs)[6:])//2
  649. context.node.inputs.new("xFormSocket", "Target."+str(num_targets).zfill(3))
  650. context.node.inputs.new("FloatFactorSocket", "Weight."+str(num_targets).zfill(3))
  651. return {'FINISHED'}
  652. class LinkArmatureRemoveTargetInput(bpy.types.Operator):
  653. """Remove a DriverVariable input from the active Driver node"""
  654. bl_idname = "mantis.link_armature_node_remove_target"
  655. bl_label = "Remove Target"
  656. bl_options = {'INTERNAL', 'REGISTER', 'UNDO'}
  657. @classmethod
  658. def poll(cls, context):
  659. return hasattr(context, 'node')
  660. def execute(self, context):
  661. n = context.node
  662. n.inputs.remove(n.inputs[-1]); n.inputs.remove(n.inputs[-1])
  663. return {'FINISHED'}
  664. class CollectionAddNewOutput(bpy.types.Operator):
  665. """Add a new Collection output to the Driver node"""
  666. bl_idname = "mantis.collection_add_new"
  667. bl_label = "+ Child"
  668. bl_options = {'REGISTER', 'UNDO'}
  669. collection_name : bpy.props.StringProperty(default='Collection')
  670. tree_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  671. node_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  672. socket_invoked : bpy.props.StringProperty(options ={'HIDDEN'}) # set by caller
  673. @classmethod
  674. def poll(cls, context):
  675. return True #(hasattr(context, 'active_node') )
  676. # DUPLICATED CODE HERE, DUMMY
  677. def invoke(self, context, event):
  678. self.tree_invoked = context.node.id_data.name
  679. self.node_invoked = context.node.name
  680. t = context.node.id_data
  681. t.nodes.active = context.node
  682. context.node.select = True
  683. wm = context.window_manager
  684. return wm.invoke_props_dialog(self)
  685. def execute(self, context):
  686. if not self.collection_name:
  687. return {'CANCELLED'}
  688. n = bpy.data.node_groups[self.tree_invoked].nodes[self.node_invoked]
  689. # we need to know which socket it is called from...
  690. s = None
  691. for socket in n.outputs:
  692. if socket.identifier == self.socket_invoked:
  693. s=socket; break
  694. parent_path = ''
  695. if s is not None and s.collection_path:
  696. parent_path = s.collection_path + '>'
  697. outer_dict = n.read_declarations_from_json()
  698. current_data = outer_dict
  699. if parent_path:
  700. for name_elem in parent_path.split('>'):
  701. if name_elem == '': continue # HACK around being a bad programmer
  702. current_data = current_data[name_elem]
  703. current_data[self.collection_name] = {}
  704. n.push_declarations_to_json(outer_dict)
  705. n.update_interface()
  706. return {'FINISHED'}
  707. # TODO: should this prune the children, too?
  708. class CollectionRemoveOutput(bpy.types.Operator):
  709. """Remove a Collection output to the Driver node"""
  710. bl_idname = "mantis.collection_remove"
  711. bl_label = "X"
  712. bl_options = {'REGISTER', 'UNDO'}
  713. tree_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  714. node_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  715. socket_invoked : bpy.props.StringProperty(options ={'HIDDEN'}) # set by caller
  716. @classmethod
  717. def poll(cls, context):
  718. return True #(hasattr(context, 'active_node') )
  719. # DUPLICATED CODE HERE, DUMMY
  720. def invoke(self, context, event):
  721. self.tree_invoked = context.node.id_data.name
  722. self.node_invoked = context.node.name
  723. t = context.node.id_data
  724. t.nodes.active = context.node
  725. context.node.select = True
  726. return self.execute(context)
  727. def execute(self, context):
  728. n = bpy.data.node_groups[self.tree_invoked].nodes[self.node_invoked]
  729. s = None
  730. for socket in n.outputs:
  731. if socket.identifier == self.socket_invoked:
  732. s=socket; break
  733. if not s:
  734. return {'CANCELLED'}
  735. parent_path = ''
  736. if s is not None and s.collection_path:
  737. parent_path = s.collection_path + '>'
  738. outer_dict = n.read_declarations_from_json()
  739. current_data = outer_dict
  740. print(parent_path)
  741. if parent_path:
  742. for name_elem in parent_path.split('>')[:-2]: # just skip the last one
  743. print(name_elem)
  744. if name_elem == '': continue # HACK around being a bad programmer
  745. current_data = current_data[name_elem]
  746. del current_data[s.name]
  747. n.push_declarations_to_json(outer_dict)
  748. n.update_interface()
  749. return {'FINISHED'}
  750. def get_socket_enum(operator, context):
  751. valid_types = []; i = -1
  752. from .socket_definitions import TellClasses, MantisSocket
  753. for cls in TellClasses():
  754. if cls.is_valid_interface_type:
  755. valid_types.append( (cls.bl_idname, cls.bl_label, "Socket Type", i := i + 1), )
  756. return valid_types
  757. class B4_4_0_Workaround_NodeTree_Interface_Update(Operator):
  758. """Selects all nodes of same type as active node."""
  759. bl_idname = "mantis.node_tree_interface_update_4_4_0_workaround"
  760. bl_label = "Add Socket to Node Tree"
  761. bl_options = {'REGISTER', 'UNDO'}
  762. socket_name : bpy.props.StringProperty()
  763. output : bpy.props.BoolProperty()
  764. socket_type : bpy.props.EnumProperty(
  765. name="Socket Type",
  766. description="Socket Type",
  767. items=get_socket_enum,
  768. default=0,)
  769. tree_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  770. @classmethod
  771. def poll(cls, context):
  772. return (any_tree_poll(context))
  773. def invoke(self, context, event):
  774. self.tree_invoked = context.active_node.id_data.name
  775. # we use active_node here ^ because we are comparing the active node to the selection.
  776. wm = context.window_manager
  777. return wm.invoke_props_dialog(self)
  778. def execute(self, context):
  779. tree = bpy.data.node_groups[self.tree_invoked]
  780. in_out = 'OUTPUT' if self.output else 'INPUT'
  781. tree.interface.new_socket(self.socket_name, in_out=in_out, socket_type=self.socket_type)
  782. # try to prevent the next execution
  783. # because updating the interface triggers a depsgraph update.
  784. # this doesn't actually work though...TODO
  785. if tree.bl_idname == "MantisTree":
  786. tree.prevent_next_exec=True
  787. return {"FINISHED"}
  788. class ConvertBezierCurveToNURBS(Operator):
  789. """Converts all bezier splines of curve to NURBS."""
  790. bl_idname = "mantis.convert_bezcrv_to_nurbs"
  791. bl_label = "Convert Bezier Curve to NURBS"
  792. bl_options = {'REGISTER', 'UNDO'}
  793. @classmethod
  794. def poll(cls, context):
  795. return (context.active_object is not None) and (context.active_object.type=='CURVE')
  796. def execute(self, context):
  797. from .utilities import nurbs_copy_bez_spline
  798. curve = context.active_object
  799. bez=[]
  800. for spl in curve.data.splines:
  801. if spl.type=='BEZIER':
  802. bez.append(spl)
  803. for bez_spline in bez:
  804. new_spline=nurbs_copy_bez_spline(curve, bez_spline)
  805. curve.data.splines.remove(bez_spline)
  806. return {"FINISHED"}
  807. # this has to be down here for some reason. what a pain
  808. classes = [
  809. MantisGroupNodes,
  810. MantisEditGroup,
  811. MantisNewNodeTree,
  812. InvalidateNodeTree,
  813. ExecuteNodeTree,
  814. # CreateMetaGroup,
  815. QueryNodeSockets,
  816. ForceDisplayUpdate,
  817. CleanUpNodeGraph,
  818. MantisMuteNode,
  819. SelectNodesOfType,
  820. ConnectNodeToInput,
  821. # xForm
  822. AddCustomProperty,
  823. EditCustomProperty,
  824. RemoveCustomProperty,
  825. # EditFCurveNode,
  826. FcurveAddKeyframeInput,
  827. FcurveRemoveKeyframeInput,
  828. # Driver
  829. DriverAddDriverVariableInput,
  830. DriverRemoveDriverVariableInput,
  831. # Armature Link Node
  832. LinkArmatureAddTargetInput,
  833. LinkArmatureRemoveTargetInput,
  834. # managing collections
  835. CollectionAddNewOutput,
  836. CollectionRemoveOutput,
  837. # rigging utilities
  838. ConvertBezierCurveToNURBS,
  839. ]
  840. if (bpy.app.version >= (4, 4, 0)):
  841. classes.append(B4_4_0_Workaround_NodeTree_Interface_Update)
  842. def TellClasses():
  843. return classes