11import bpy
22import databpy
3+ from databpy import Domains , AttributeTypes
34import numpy as np
4- from bpy .types import Operator , PropertyGroup
5+ from bpy .types import Operator
56from bpy .props import StringProperty
67from mathutils import Vector
78import json
@@ -34,46 +35,47 @@ def __init__(self):
3435 self .vertices = np .array ([])
3536 self .edges = np .array ([])
3637 self .EDGE_COLORS = {
37- 1 : [1.0 , 0.0 , 0.0 ], # Red
38- 2 : [0.0 , 1.0 , 0.0 ], # Green
39- 3 : [0.0 , 0.0 , 1.0 ], # Blue
40- 4 : [1.0 , 0.0 , 1.0 ], # Pink
41- ' default' : [1.0 , 1.0 , 1.0 ] # White
38+ 1 : [1.0 , 0.0 , 0.0 , 1.0 ], # Red
39+ 2 : [0.0 , 1.0 , 0.0 , 1.0 ], # Green
40+ 3 : [0.0 , 0.0 , 1.0 , 1.0 ], # Blue
41+ 4 : [1.0 , 0.0 , 1.0 , 1.0 ], # Pink
42+ " default" : [1.0 , 1.0 , 1.0 ], # White
4243 }
4344
4445 # Define interaction types as a class constant
45- self .INTERACTION_TYPES = ["CationPi" ,
46- "Hydrogen" , "PiStacking" , "Salt_Bridge" ]
46+ self .INTERACTION_TYPES = ["CationPi" , "Hydrogen" , "PiStacking" , "Salt_Bridge" ]
4747
4848 def setup_geometry_nodes (self ) -> None :
4949 # Add a Geometry Nodes modifier
50- geo_modifier = self .object .modifiers .new (
51- name = "GeometryNodes" , type = 'NODES' )
50+ geo_modifier = self .object .modifiers .new (name = "GeometryNodes" , type = "NODES" )
5251
5352 # Create a new node tree for Geometry Nodes
5453 node_tree = bpy .data .node_groups .new (
55- name = "CurveConversion" , type = 'GeometryNodeTree' )
54+ name = "CurveConversion" , type = "GeometryNodeTree"
55+ )
5656 geo_modifier .node_group = node_tree
5757
5858 # Set up the inputs and outputs for the node group
5959 node_tree .interface .new_socket (
60- name = "Mesh" , in_out = 'INPUT' , socket_type = 'NodeSocketGeometry' )
60+ name = "Mesh" , in_out = "INPUT" , socket_type = "NodeSocketGeometry"
61+ )
6162 node_tree .interface .new_socket (
62- name = "Curve" , in_out = 'OUTPUT' , socket_type = 'NodeSocketGeometry' )
63+ name = "Curve" , in_out = "OUTPUT" , socket_type = "NodeSocketGeometry"
64+ )
6365
6466 # Add nodes to the node tree
6567 nodes = node_tree .nodes
66- group_input = nodes .new (type = ' NodeGroupInput' )
67- group_output = nodes .new (type = ' NodeGroupOutput' )
68- mesh_to_curve = nodes .new (type = ' GeometryNodeMeshToCurve' )
69- curve_circle = nodes .new (type = ' GeometryNodeCurvePrimitiveCircle' )
70- curve_to_mesh = nodes .new (type = ' GeometryNodeCurveToMesh' )
71- set_material = nodes .new (type = ' GeometryNodeSetMaterial' )
68+ group_input = nodes .new (type = " NodeGroupInput" )
69+ group_output = nodes .new (type = " NodeGroupOutput" )
70+ mesh_to_curve = nodes .new (type = " GeometryNodeMeshToCurve" )
71+ curve_circle = nodes .new (type = " GeometryNodeCurvePrimitiveCircle" )
72+ curve_to_mesh = nodes .new (type = " GeometryNodeCurveToMesh" )
73+ set_material = nodes .new (type = " GeometryNodeSetMaterial" )
7274
7375 # Set the default properties for the nodes
7476 curve_circle .inputs ["Radius" ].default_value = 0.0025
7577 curve_circle .inputs ["Resolution" ].default_value = 16
76- set_material .inputs [2 ].default_value = bpy .data .materials ["MN Default" ]
78+ set_material .inputs ["Material" ].default_value = bpy .data .materials ["MN Default" ]
7779
7880 # Set node positions
7981 group_input .location = (0 , 0 )
@@ -84,31 +86,31 @@ def setup_geometry_nodes(self) -> None:
8486 group_output .location = (800 , 0 )
8587
8688 # Set up node connections
89+ node_tree .links .new (group_input .outputs ["Mesh" ], mesh_to_curve .inputs ["Mesh" ])
8790 node_tree .links .new (
88- group_input .outputs ["Mesh " ],
89- mesh_to_curve . inputs [ "Mesh" ] )
91+ mesh_to_curve .outputs ["Curve " ], curve_to_mesh . inputs [ "Curve" ]
92+ )
9093 node_tree .links .new (
91- mesh_to_curve .outputs ["Curve" ],
92- curve_to_mesh . inputs [ "Curve" ] )
94+ curve_circle .outputs ["Curve" ], curve_to_mesh . inputs [ "Profile Curve" ]
95+ )
9396 node_tree .links .new (
94- curve_circle .outputs ["Curve " ],
95- curve_to_mesh . inputs [ "Profile Curve" ] )
97+ curve_to_mesh .outputs ["Mesh " ], set_material . inputs [ "Geometry" ]
98+ )
9699 node_tree .links .new (
97- curve_to_mesh .outputs ["Mesh" ],
98- set_material .inputs ["Geometry" ])
99- node_tree .links .new (
100- set_material .outputs ["Geometry" ],
101- group_output .inputs ["Curve" ])
100+ set_material .outputs ["Geometry" ], group_output .inputs ["Curve" ]
101+ )
102102
103- def _collect_interaction_geometry (self , frame : int = 0 ) -> tuple [np .ndarray , np .ndarray , list ]:
103+ def _collect_interaction_geometry (
104+ self , frame : int = 0
105+ ) -> tuple [np .ndarray , np .ndarray , np .ndarray ]:
104106 vertices = []
105107 edge_types = []
106108 frame_str = str (frame )
107109
108110 # Get the trajectory object
109111 blender_object = bpy .data .objects .get (self .trajectory_name )
110112 if not blender_object :
111- return np .array ([]), np .array ([]), []
113+ return np .array ([]), np .array ([]), np . array ([])
112114
113115 # Collect vertices for all interactions
114116 for interaction_type , frames in self .frame_dict .items ():
@@ -117,19 +119,16 @@ def _collect_interaction_geometry(self, frame: int = 0) -> tuple[np.ndarray, np.
117119 continue
118120
119121 for a , b in frame_data :
120- interaction_type_index = self .INTERACTION_TYPES .index (
121- interaction_type )
122+ interaction_type_index = self .INTERACTION_TYPES .index (interaction_type )
122123
123124 if interaction_type == "PiStacking" :
124125 # For PiStacking, a and b are tuples of indices
125126 ligand_indices = a
126127 protein_indices = b
127128
128129 # Calculate centroids for ligand and protein rings
129- pos_a = self .calculate_centroid (
130- blender_object , ligand_indices )
131- pos_b = self .calculate_centroid (
132- blender_object , protein_indices )
130+ pos_a = self .calculate_centroid (blender_object , ligand_indices )
131+ pos_b = self .calculate_centroid (blender_object , protein_indices )
133132
134133 # Convert Vector to numpy array if necessary
135134 if isinstance (pos_a , Vector ):
@@ -141,63 +140,75 @@ def _collect_interaction_geometry(self, frame: int = 0) -> tuple[np.ndarray, np.
141140 a_idx , b_idx = int (a ), int (b )
142141
143142 # Get positions from named attributes
144- pos_a = databpy .named_attribute (
145- blender_object , 'position' )[a_idx ]
146- pos_b = databpy .named_attribute (
147- blender_object , 'position' )[b_idx ]
143+ pos_a = databpy .named_attribute (blender_object , "position" )[a_idx ]
144+ pos_b = databpy .named_attribute (blender_object , "position" )[b_idx ]
148145
149146 vertices .extend ([pos_a , pos_b ])
150147 edge_types .append (interaction_type_index )
151148
152149 # Convert to numpy arrays
153150 vertices_array = np .array (vertices )
154151 if len (vertices_array ) > 0 :
155- edges_array = np .array ([(i , i + 1 )
156- for i in range (0 , len (vertices_array ), 2 )])
152+ edges_array = np .array (
153+ [(i , i + 1 ) for i in range (0 , len (vertices_array ), 2 )]
154+ )
157155 else :
158156 edges_array = np .array ([])
159157
160- return vertices_array , edges_array , edge_types
158+ return vertices_array , edges_array , np . array ( edge_types )
161159
162- def set_color (self , edge_types ) :
160+ def compute_colors (self , edge_types : np . ndarray ) -> np . ndarray :
163161 # Convert edge types to RGB colors
164- return np .array ([self .EDGE_COLORS .get (et , [1.0 , 1.0 , 1.0 ])
165- for et in edge_types ], dtype = np .float32 )
162+ return np .array (
163+ [self .EDGE_COLORS .get (et , [1.0 , 1.0 , 1.0 , 1.0 ]) for et in edge_types ],
164+ dtype = np .float32 ,
165+ )
166166
167- def create_object (self , style : str = "vdw" , name : str = None ) -> bpy .types .Object :
167+ def create_object (self , style : str = "vdw" ) -> bpy .types .Object :
168168 vertices , edges , edge_types = self ._collect_interaction_geometry (0 )
169- if name is None :
170- name = f"{ self .trajectory_name } _interactions"
171-
172- bob = databpy .create_bob (
173- vertices = vertices , edges = edges , name = name , collection = bpy .data .collections ["MolecularNodes" ])
174- self .object = bob .object # Store the actual Blender object
175- bob = databpy .BlenderObject (self .object )
176169
177- edge_colors = self .set_color (edge_types )
178- bob .store_named_attribute (
179- edge_colors , "Color" , atype = "FLOAT_VECTOR" , domain = "EDGE" )
180- bob .store_named_attribute (
181- np .array (edge_types ), "interaction_type" , atype = "INT" , domain = "EDGE" )
170+ self .object = databpy .create_object (
171+ vertices = vertices ,
172+ edges = edges ,
173+ name = f"{ self .trajectory_name } _interactions" ,
174+ collection = bpy .data .collections ["MolecularNodes" ],
175+ )
176+
177+ self .store_named_attribute (
178+ self .compute_colors (edge_types ),
179+ "Color" ,
180+ atype = AttributeTypes .FLOAT_COLOR ,
181+ domain = Domains .EDGE ,
182+ )
183+ self .store_named_attribute (
184+ edge_types ,
185+ "interaction_type" ,
186+ atype = AttributeTypes .INT ,
187+ domain = Domains .EDGE ,
188+ )
182189
183190 return self .object
184191
185192 def set_frame (self , frame : int ) -> None :
186193 vertices , edges , edge_types = self ._collect_interaction_geometry (frame )
187194
188- bob = databpy .BlenderObject (self .object )
189- bob .new_from_pydata (vertices , edges )
190-
191- edge_colors = self .set_color (edge_types )
192- bob .store_named_attribute (
193- edge_colors , "Color" , atype = "FLOAT_VECTOR" , domain = "EDGE" )
194- bob .store_named_attribute (
195- np .array (edge_types ), "interaction_type" , atype = "INT" , domain = "EDGE" )
195+ self .new_from_pydata (vertices , edges )
196+ self .store_named_attribute (
197+ self .compute_colors (edge_types ),
198+ "Color" ,
199+ atype = AttributeTypes .FLOAT_COLOR ,
200+ domain = Domains .EDGE ,
201+ )
202+ self .store_named_attribute (
203+ edge_types ,
204+ "interaction_type" ,
205+ atype = AttributeTypes .INT ,
206+ domain = Domains .EDGE ,
207+ )
196208
197209 @staticmethod
198210 def calculate_centroid (trajectory_object , indices ):
199- vertices = [
200- trajectory_object .data .vertices [int (idx )].co for idx in indices ]
211+ vertices = [trajectory_object .data .vertices [int (idx )].co for idx in indices ]
201212 return sum (vertices , Vector ((0 , 0 , 0 ))) / len (vertices )
202213
203214 def setup_from_json (self , json_file , object_name ):
@@ -218,54 +229,49 @@ def setup_from_json(self, json_file, object_name):
218229 for interaction in interactions :
219230 if interaction_type == "PiStacking" :
220231 self .frame_dict [interaction_type ][frame_key ].add (
221- (tuple (interaction ["Ligand" ]),
222- tuple (interaction ["Protein" ]))
232+ (
233+ tuple (interaction ["Ligand" ]),
234+ tuple (interaction ["Protein" ]),
235+ )
223236 )
224237 else :
225238 self .frame_dict [interaction_type ][frame_key ].add (
226- (float (interaction ["Ligand" ]),
227- float (interaction ["Protein" ]))
239+ (
240+ float (interaction ["Ligand" ]),
241+ float (interaction ["Protein" ]),
242+ )
228243 )
229244
230245
231- class OBJECT_OT_interaction_visualiser (Operator ):
232- bl_idname = "object.interaction_visualiser"
233- bl_label = "Visualise Interactions"
246+ class MN_OT_interaction_visualiser (Operator ):
247+ bl_idname = "mn.interaction_visualiser"
248+ bl_label = "Add Interactions"
249+ bl_description = "Loads a .json file with per-frame interaction information as an annotation object"
234250 bl_options = {"REGISTER" , "UNDO" }
235251
236- def execute (self , context ):
237- scene = context .scene
238- blender_object = context .active_object
239- object_name = blender_object .name
240- interaction_props = scene .interaction_visualiser
241- json_file = interaction_props .json_file
252+ filepath : StringProperty (
253+ name = "JSON File" ,
254+ description = "Path to the JSON file containing the interaction data" ,
255+ default = "" ,
256+ subtype = "FILE_PATH" ,
257+ ) # type: ignore
258+
259+ def invoke (self , context , event ):
260+ context .window_manager .fileselect_add (self )
261+ return {"RUNNING_MODAL" }
242262
263+ def execute (self , context ):
243264 # Create and setup interaction
265+ traj_name = context .active_object .name
244266 interaction = Interaction ()
245- interaction .setup_from_json (json_file , object_name )
267+ interaction .setup_from_json (self . filepath , traj_name )
246268 interaction .create_object ()
247269 interaction .setup_geometry_nodes ()
248270
249271 self .report ({"INFO" }, "Interaction visualization setup complete" )
250272 return {"FINISHED" }
251273
252274
253- class InteractionVisualiserProperties (PropertyGroup ):
254- json_file : StringProperty (
255- name = "JSON File" ,
256- description = "Path to the JSON file containing data" ,
257- default = "" ,
258- maxlen = 1024 ,
259- subtype = "FILE_PATH" ,
260- )
261- object_name : StringProperty (
262- name = "Blender Object Name" ,
263- description = "Name of the Blender object representing the molecule" ,
264- default = "" ,
265- )
266-
267-
268275CLASSES = [
269- InteractionVisualiserProperties ,
270- OBJECT_OT_interaction_visualiser ,
276+ MN_OT_interaction_visualiser ,
271277]
0 commit comments