ops_nodegroup.py 41 KB

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