ops_nodegroup.py 39 KB

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