Skip to content

Commit d014764

Browse files
authored
Merge pull request #3 from BradyAJohnston/MartinBaGar/main
A bit of cleanup and using self.*_named_attribute
2 parents 63f0c66 + 865b4ab commit d014764

File tree

3 files changed

+116
-122
lines changed

3 files changed

+116
-122
lines changed

molecularnodes/addon.py

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -62,16 +62,11 @@ def register():
6262

6363
bpy.types.Scene.MNSession = session.MNSession() # type: ignore
6464
bpy.types.Object.uuid = props.uuid_property # type: ignore
65-
bpy.types.Object.mn = PointerProperty(
66-
type=props.MolecularNodesObjectProperties) # type: ignore
67-
bpy.types.Scene.mn = PointerProperty(
68-
type=props.MolecularNodesSceneProperties) # type: ignore
65+
bpy.types.Object.mn = PointerProperty(type=props.MolecularNodesObjectProperties) # type: ignore
66+
bpy.types.Scene.mn = PointerProperty(type=props.MolecularNodesSceneProperties) # type: ignore
6967
bpy.types.Object.mn_trajectory_selections = CollectionProperty( # type: ignore
7068
type=props.TrajectorySelectionItem # type: ignore
7169
)
72-
bpy.types.Scene.interaction_visualiser = PointerProperty(
73-
type=entities.interaction.interaction.InteractionVisualiserProperties
74-
)
7570

7671

7772
def unregister():
@@ -91,6 +86,5 @@ def unregister():
9186
frame_change_pre.remove(update_entities)
9287
del bpy.types.Scene.MNSession # type: ignore
9388
del bpy.types.Scene.mn # type: ignore
94-
del bpy.types.Scene.interaction_visualiser # type: ignore
9589
del bpy.types.Object.mn # type: ignore
9690
del bpy.types.Object.mn_trajectory_selections # type: ignore
Lines changed: 109 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import bpy
22
import databpy
3+
from databpy import Domains, AttributeTypes
34
import numpy as np
4-
from bpy.types import Operator, PropertyGroup
5+
from bpy.types import Operator
56
from bpy.props import StringProperty
67
from mathutils import Vector
78
import 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-
268275
CLASSES = [
269-
InteractionVisualiserProperties,
270-
OBJECT_OT_interaction_visualiser,
276+
MN_OT_interaction_visualiser,
271277
]

molecularnodes/ui/panel.py

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,7 @@ def change_style_node_menu(self, context):
4545
layout.label(text="Molecular Nodes", icon="MOD_PARTICLES")
4646

4747
row = layout.row()
48-
op = row.operator_menu_enum(
49-
"mn.node_swap", "node_items", text="Change Node")
48+
op = row.operator_menu_enum("mn.node_swap", "node_items", text="Change Node")
5049
op.node_description = "The topology nodes"
5150

5251
layout.separator()
@@ -161,19 +160,14 @@ def panel_md_properties(layout, context):
161160
box.label(text="Invalid Selection", icon="ERROR")
162161
box.label(text=item.message)
163162
box.alert = True
164-
op = box.operator(
165-
"wm.url_open", text="Selection Langauge Docs", icon="URL")
163+
op = box.operator("wm.url_open", text="Selection Langauge Docs", icon="URL")
166164
op.url = (
167165
"https://docs.mdanalysis.org/stable/documentation_pages/selections.html"
168166
)
169167

170-
layout.label(text="Interactions", icon="OPTIONS")
171-
scene = context.scene
172-
interaction_props = scene.interaction_visualiser
173-
174-
box = layout.box()
175-
box.prop(interaction_props, "json_file")
176-
box.operator("object.interaction_visualiser")
168+
layout.label(text="Annotations", icon="OPTIONS")
169+
row = layout.row()
170+
row.operator("mn.interaction_visualiser")
177171

178172

179173
def panel_object(layout, context):

0 commit comments

Comments
 (0)