Source code for Primitives

import bpy

from bpy import context
import math
from math import *
import mathutils

[docs] def InstantiateElementsFromDictionary(pos_dict, element_data, materials_dict, van_der_waals = False): """ instantiates spheres of different radii & materials at the allocated Vector3 positions. :param pos_dict: Dictionary<string, Vector3> all the symbols & labels of elements and their Vector3 positions :param element_data: Dictionary<string, Atom_Data(class)> available data for the present elements :param materials_dict: Dictionary<string, bpy.Material> materials that can be accessed with present elements' symbols """ for key in pos_dict: e_symbol = ''.join(i for i in key.split('.')[0] if not i.isdigit()) #remove numbers from name if e_symbol in element_data: if van_der_waals == False: r = element_data.get(e_symbol).get_radius() / 2 else: r = element_data.get(e_symbol).get_vanDerWaals() x = pos_dict[key].x y = pos_dict[key].y z = pos_dict[key].z bpy.ops.mesh.primitive_uv_sphere_add(enter_editmode=False, location=(x, y, z), radius=r) ModifyNamesAndMaterials(key, e_symbol, materials_dict) bpy.ops.object.shade_smooth() print("6: Instantiating element: ", key) else: print("AddElement(): invalid element name", key)
[docs] def InstantiateIonsFromDictionary(pos_dict, ion_data, materials_dict): """ Instantiates spheres for ions at the allocated Vector3 positions. :param pos_dict: (dict) All the symbols and labels of ions and their Vector3 positions. :param ion_data: (dict) Available data for the present ions. :param materials_dict: (dict) Materials that can be accessed with present ions' symbols. :return: None """ for key in pos_dict: i_symbol = ''.join(i for i in key.split('.')[0] if not i.isdigit()) #remove numbers from name if i_symbol in ion_data: r = ion_data[i_symbol].radius /2 x = pos_dict[key].x y = pos_dict[key].y z = pos_dict[key].z bpy.ops.mesh.primitive_uv_sphere_add(enter_editmode=False, location=(x, y, z), radius=r) ModifyNamesAndMaterials(key, i_symbol, materials_dict) bpy.ops.object.shade_smooth() print("6: Instantiating ion: ", key) else: print("AddElement(): invalid element name", key)
[docs] def ModifyNamesAndMaterials(obj_name, e_symbol, materials_dict): """ names of the active object and appends to it the required material. :param obj_name: (str) The name of the sphere to be instantiated. :param e_symbol: (str) Atom symbol, taken from name, used to access materials. :param materials_dict: (dict) Materials that can be accessed with present elements' symbols. :return: None """ bpy.context.active_object.name = obj_name bpy.context.active_object.data.name = obj_name mat = materials_dict.get(e_symbol) try: bpy.context.active_object.data.materials.append(mat) except: print("6: Material not found @Primitives.ModifyNamesAndMaterials")
[docs] def InstantiateBondsFromConnectivity(pos_dict, mat_dict, connect_list, unit_cell="0"): """ Instantiates bonds based on connectivity information. :param pos_dict: (dict) Atomic symbols and their positions. :param mat_dict: (dict) Atomic symbols and their materials. :param connect_list: (list) List of connections between atoms. :param unit_cell: (str) Unit cell identifier. :return: None """ #helper functions to be stored in a dictionary, which wich will depend on the bond_type def handle_single_bond(atom1, atom2): bond_label = atom1 + '-' + atom2 bond_label2 = atom2 + '-' + atom1 CreateFragmentedBonds(pos_dict, mat_dict, atom1, atom2, '-') SelectTwoMeshesAndJoin(bond_label, bond_label2) def handle_double_bond(atom1, atom2): bond_label = atom1 + '=' + atom2 bond_label2 = atom2 + '=' + atom1 CreateFragmentedBonds(pos_dict, mat_dict, atom1, atom2, '=') CreateFragmentedBonds(pos_dict, mat_dict, atom1, atom2, '=') MoveObjectOnLocalAxis(bond_label, (0.0, 0.1, 0.0)) MoveObjectOnLocalAxis(bond_label2, (0.0, 0.1, 0.0)) MoveObjectOnLocalAxis(bond_label + ".001", (0.0, -0.1, 0.0)) MoveObjectOnLocalAxis(bond_label2 + ".001", (0.0, -0.1, 0.0)) SelectTwoMeshesAndJoin(bond_label, bond_label2) SelectTwoMeshesAndJoin(bond_label + ".001", bond_label2 + ".001") SelectTwoMeshesAndJoin(bond_label, bond_label + ".001") def handle_triple_bond(atom1, atom2): bond_label = atom1 + '#' + atom2 bond_label2 = atom2 + '#' + atom1 CreateFragmentedBonds(pos_dict, mat_dict, atom1, atom2, '#') CreateFragmentedBonds(pos_dict, mat_dict, atom1, atom2, '#') CreateFragmentedBonds(pos_dict, mat_dict, atom1, atom2, '#') MoveObjectOnLocalAxis(bond_label + ".001", (0.0, 0.15, 0.0)) MoveObjectOnLocalAxis(bond_label + ".002", (0.0, -0.15, 0.0)) MoveObjectOnLocalAxis(bond_label2 + ".001", (0.0, 0.15, 0.0)) MoveObjectOnLocalAxis(bond_label2 + ".002", (0.0, -0.15, 0.0)) SelectTwoMeshesAndJoin(bond_label, bond_label2) SelectTwoMeshesAndJoin(bond_label + ".001", bond_label2 + ".001") SelectTwoMeshesAndJoin(bond_label + ".002", bond_label2 + ".002") SelectTwoMeshesAndJoin(bond_label + ".001", bond_label + ".002") SelectTwoMeshesAndJoin(bond_label + ".001", bond_label2) def handle_hydrogen_bond(atom1, atom2): if unit_cell == "0": CreateAndJoinTrantientBond(pos_dict, mat_dict, atom1, atom2, '_', 0.2, 0.06, h_bonding=True) else: CreateFragmentedBonds(pos_dict, mat_dict, atom1, atom2, '_', unit_cell) def handle_resonance_bond(atom1, atom2): bond_label = atom1 + '%' + atom2 bond_label2 = atom2 + '%' + atom1 CreateFragmentedBonds(pos_dict, mat_dict, atom1, atom2, '%') MoveObjectOnLocalAxis(bond_label, (0.0, 0.1, 0.0)) MoveObjectOnLocalAxis(bond_label2, (0.0, 0.1, 0.0)) SelectTwoMeshesAndJoin(bond_label, bond_label2) CreateAndJoinTrantientBond(pos_dict, mat_dict, atom1, atom2, '%', 0.18, 0.08) #function dictionary, the keys will be the bond_types for each entry in connect_list bond_actions = { '_': handle_hydrogen_bond, '-': handle_single_bond, '=': handle_double_bond, '#': handle_triple_bond, 'res1': handle_resonance_bond, '%': handle_resonance_bond #function stored twice because xyz and com files manage aromatic bonds differently } for item in connect_list: atom1 = item[0] atom2 = item[1] bond_type = item[2] print("6: Instantiating bond: ", atom1 + bond_type + atom2) action = bond_actions.get(bond_type) if action: action(atom1, atom2) else: print("Error on bond type! @Primitives.InstantiateBondsFromConnectivity")
[docs] def CreateAndJoinTrantientBond(pos_dict, mat_dict, key1, key2, bond_type, dash_len, bond_radius, h_bonding=False): """ Creates and joins transient bonds between two atoms. :param pos_dict: (dict) Atomic symbols and their positions. :param mat_dict: (dict) Atomic symbols and their materials. :param key1: (str) Symbol and number for the first atom. :param key2: (str) Symbol and number for the second atom. :param bond_type: (str) Type of bond. :param dash_len: (float) Length of each dash in the bond. :param bond_radius: (float) Radius of the bond. :param h_bonding: (bool) Whether the bond is a hydrogen bond. :return: None """ scene = bpy.context.scene #temporary names for the bonds instantiated name1 = key1 + bond_type + key2 name2 = key2 + bond_type + key1 #spawn positions for elements, their distance and how many dashes the bond would have origin = pos_dict.get(key1) end = pos_dict.get(key2) vector = end - origin distance = vector.magnitude normal_vector = vector / distance dash_nmbr = int(distance/dash_len) #number of times dash_len fits in distance. ref_nmbr = int(dash_nmbr/2) #middle point in the number of dashes #elements taken from names by removing numerical part type1 = ''.join(i for i in key1.split('.')[0] if not i.isdigit()) type2 = ''.join(i for i in key2.split('.')[0] if not i.isdigit()) bpy.ops.object.mode_set(mode="OBJECT") #ensure program is in object mode #instantiating dashes between spawn points and mid point for i in range(dash_nmbr): if i != 0 and i % 2 == 0: #will instantiate dashes only in half of the spaces mid_point = (normal_vector * dash_len * i) + origin bpy.ops.mesh.primitive_cylinder_add(radius=bond_radius, depth=dash_len, enter_editmode=False, location=mid_point) try: phi = math.atan2(vector.y, vector.x) except ValueError: phi = math.pi / 2 try: theta = math.acos(vector.z/distance) except ValueError: theta = 0 bpy.context.object.rotation_euler[1] = theta #dash orientation management bpy.context.object.rotation_euler[2] = phi if h_bonding == False: if i <= ref_nmbr: #dashes closest to atom 1 ModifyNamesAndMaterials(name1, type1, mat_dict) else: #dashes closest to atom 2 ModifyNamesAndMaterials(name1, type2, mat_dict) else: ModifyNamesAndMaterials(name1, "Xx", mat_dict) #For trantient or hydrogen bonding #getting all the objects with name that starts with name1 name1_objs = [o for o in scene.objects if o.name.startswith(name1)] print(f"Objects to join: {[o.name for o in name1_objs]}") JoinMeshesFromObjectList(name1_objs) print(f"Joined meshes for: {name1}")
[docs] def CreateFragmentedBonds(pos_dict, mat_dict, atom1, atom2, bond_type, unit_cell="0"): """ Instantiates bonds from atoms to the middle-point and joins them. :param pos_dict: (dict) Atomic symbols and their positions. :param mat_dict: (dict) Atomic symbols and their materials. :param atom1: (str) Symbol and number for the first atom. :param atom2: (str) Symbol and number for the second atom. :param bond_type: (str) Type of bond (single, double, or triple). :param unit_cell: (str) Unit cell identifier. :return: None """ #temporary names for the bonds instantiated name1 = atom1 + bond_type + atom2 name2 = atom2 + bond_type + atom1 #spawn positions for elements and their middle point v1 = pos_dict.get(atom1) v3 = pos_dict.get(atom2) v2 = (v1+v3)/2 #elements taken from names by removing numerical part element1 = ''.join(i for i in atom1.split('.')[0] if not i.isdigit()) element2 = ''.join(i for i in atom2.split('.')[0] if not i.isdigit()) #instantiating bonds and assigning materials and names if unit_cell == "0": InstantiateBondBetweenTwoPoints(v1, v2) ModifyNamesAndMaterials(name1, element1, mat_dict) InstantiateBondBetweenTwoPoints(v2, v3) ModifyNamesAndMaterials(name2, element2, mat_dict) else: InstantiateBondBetweenTwoPoints(v1, v2, 0.03) ModifyNamesAndMaterials(name1, "Xx", mat_dict) InstantiateBondBetweenTwoPoints(v2, v3, 0.03) ModifyNamesAndMaterials(name2, "Xx", mat_dict)
[docs] def MoveObjectOnLocalAxis(obj_name, value): """ Moves an object along its local axis. :param obj_name: (str) Name of the object to move. :param value: (tuple) Vector by which to move the object. :return: None """ obj = bpy.data.objects[obj_name] distz = mathutils.Vector(value) rotationMAT = obj.rotation_euler.to_matrix() rotationMAT.invert() zVector = distz @ rotationMAT # project the vector to the world using the rotation matrix obj.location += zVector
[docs] def InstantiateBondBetweenTwoPoints(p1, p2, r=0.06): #p1 and p2 are the origin and end points """ Instantiates a bond between two points. :param p1: (Mathutils.Vector) Origin point. :param p2: (Mathutils.Vector) End point. :param r: (float) Radius of the bond. :return: None """ v = p2 - p1 #vector between the two points d = v.magnitude m_p = (p1+p2)/2 #midpoint between p1 and p2 bpy.ops.mesh.primitive_cylinder_add(radius=r, depth=d, enter_editmode=False, location=m_p) try: phi = math.atan2(v.y, v.x) #returns a bug if phi is 90 degrees, as tan(90) is not defined except ValueError: phi = math.pi/2 #to handle the 90 degrees exception try: theta = math.acos(v.z/d) #returns a bug if theta is 0 degrees except ValueError: theta = 0 bpy.context.object.rotation_euler[1] = theta bpy.context.object.rotation_euler[2] = phi
[docs] def SelectTwoMeshesAndJoin(name1, name2): """ Selects two mesh objects and joins them. :param name1: (str) Name of the first mesh object. :param name2: (str) Name of the second mesh object. :return: None """ scene = bpy.context.scene obs = [obj for obj in scene.objects if obj.type == 'MESH' and obj.name in {name1, name2}] if len(obs) != 2: print(f"Error: Could not find two mesh objects named {name1} and {name2}.") return bpy.ops.object.mode_set(mode='OBJECT') # Ensure we are in Object Mode for obj in obs: obj.select_set(True) bpy.context.view_layer.objects.active = obs[0] try: bpy.ops.object.join() except RuntimeError as e: print(f"Error joining objects {name1} and {name2}: {e}")
#finally: # for obj in obs: # obj.select_set(False) # Deselect all objects after joining
[docs] def JoinMeshesFromObjectList(obj_list): """ Joins a list of mesh objects. :param obj_list: (list) List of mesh objects to join. :return: None """ bpy.ops.object.mode_set(mode='OBJECT') # Ensure we're in Object Mode before joining bpy.ops.object.select_all(action='DESELECT') # Deselect all objects first for obj in obj_list: # Select and activate the objects in the list obj.select_set(True) bpy.context.view_layer.objects.active = obj_list[0] original_name = obj_list[0].name bpy.ops.object.join() # Join the selected objects joined_object = bpy.context.active_object joined_object.name = original_name
#print(f"Joined {len(obj_list)} objects: {[obj.name for obj in obj_list]}")