Add-ons Geometri Molekul untuk Blender Minimal Versi 4.3.0

Jumat, 20 Juni 2025 edit

Berikut ini hasil belajar membuat add-ons untuk Blender 3D. Add-ons ini untuk membuat berbagai bentuk molekul umum yang digunakan untuk pengajaran kimia. Bentuk yang tersedia: oktahedral dan turunannya, bipiramida segitiga dan turunannya, tetrahedral dan turunannya, trigonal dan turunannya, dan linier.

Untuk menambahkan add-ons ini sila unduh dari sini file phyton atau salin langsung dari kotak kode di bawah, perhatikan letak file hasil unduhan. Kemudian buka Aplikasi Blender (dianjurkan versi 4.3.0 atau lebih tinggi) > klik menu Edit > Preferences > Add-ons, klik tanda di bawah tanda x > Install from disk... Arahkan ke folder hasil unduhan.

Untuk mengaksesnya pada Object Mode (View3D) pilih Add > Mesh > Molecular Geometry. Secara default yang muncul adalah geometri tetrahedral. Atur parameter yang diinginkan di panel operator. Pilih jenis geometri molekul yang ingin dibuat. Selanjutnya lakukan ubahan seperlunya.


import bpy
import math
from mathutils import Vector
import uuid

bl_info = {
    "name": "Molecular Geometry Builder",
    "author": "Urip.info",
    "version": (1, 14),
    "blender": (4, 3, 0),
    "location": "View3D > Add > Mesh > Molecular Geometry",
    "description": "Creates molecular geometry models with atoms and bonds",
    "warning": "Requires Blender 4.3 or higher. Experimental, may have rendering issues in Eevee. Ensure 'Show Geometry Guide' is disabled for clean models.",
    "doc_url": "https://www.urip.info/2025/06/add-ons-geometri-molekul-untuk-blender.html",
    "category": "Add Mesh",
}

class MOL_OT_add_molecular_geometry(bpy.types.Operator):
    """Create molecular geometry models"""
    bl_idname = "mesh.add_molecular_geometry"
    bl_label = "Molecular Geometry"
    bl_options = {'REGISTER', 'UNDO'}

    geometry_type: bpy.props.EnumProperty(
        name="Geometry Type",
        items=[
            ('OCTAHEDRAL', "Octahedral", "Octahedral geometry (AX6)"),
            ('SQUARE_PYRAMIDAL', "Square Pyramidal", "Square pyramidal geometry (AX5E)"),
            ('SQUARE_PLANAR', "Square Planar", "Square planar geometry (AX4E2)"),
            ('TRIGONAL_BIPYRAMIDAL', "Trigonal Bipyramidal", "Trigonal bipyramidal geometry (AX5)"),
            ('SEE_SAW', "See-Saw", "See-saw geometry (AX4E)"),
            ('T_SHAPED', "T-Shaped", "T-shaped geometry (AX3E2)"),
            ('TETRAHEDRAL', "Tetrahedral", "Tetrahedral geometry (AX4)"),
            ('TRIGONAL_PYRAMIDAL', "Trigonal Pyramidal", "Trigonal pyramidal geometry (AX3E)"),
            ('TRIGONAL_PLANAR', "Trigonal Planar", "Trigonal planar geometry (AX3)"),
            ('BENT_TRIGONAL', "Bent Trigonal", "Bent geometry from trigonal planar (AX2E)"),
            ('LINEAR', "Linear", "Linear geometry (AX2)"),
            ('BENT_TETRAHEDRAL', "Bent (Tetrahedral)", "Bent geometry from tetrahedral (AX2E2)"),
        ],
        default='TETRAHEDRAL',
    )
    
    central_atom_radius: bpy.props.FloatProperty(
        name="Central Atom Radius",
        default=0.5,
        min=0.1,
        max=2.0,
        step=10,
        precision=2,
    )
    
    ligand_atom_radius: bpy.props.FloatProperty(
        name="Ligand Atom Radius",
        default=0.3,
        min=0.1,
        max=2.0,
        step=5,
        precision=2,
    )
    
    bond_radius: bpy.props.FloatProperty(
        name="Bond Radius",
        default=0.1,
        min=0.01,
        max=0.5,
        step=1,
        precision=2,
    )
    
    bond_length: bpy.props.FloatProperty(
        name="Bond Length",
        default=1.875,
        min=0.5,
        max=5.0,
        step=10,
        precision=2,
    )
    
    show_geometry_guide: bpy.props.BoolProperty(
        name="Show Geometry Guide",
        default=False,
        description="Show faint guide lines for the molecular geometry"
    )

    def create_material(self, name, color):
        """Create a material for Cycles renderer"""
        try:
            mat = bpy.data.materials.new(name=name)
            mat.use_nodes = True
            nodes = mat.node_tree.nodes
            links = mat.node_tree.links
            
            nodes.clear()
            principled = nodes.new('ShaderNodeBsdfPrincipled')
            principled.inputs['Base Color'].default_value = color
            output = nodes.new('ShaderNodeOutputMaterial')
            links.new(principled.outputs['BSDF'], output.inputs['Surface'])
            
            return mat
        except Exception as e:
            self.report({'ERROR'}, f"Failed to create material {name}: {str(e)}")
            return None

    def execute(self, context):
        try:
            # Create a new collection for the molecule
            mol_collection = bpy.data.collections.new(f"Molecule_{str(uuid.uuid4())[:8]}")
            context.scene.collection.children.link(mol_collection)
            
            # Set the new collection as active
            context.view_layer.active_layer_collection = context.view_layer.layer_collection.children[mol_collection.name]
            
            # Deselect all objects
            bpy.ops.object.select_all(action='DESELECT')
            
            # Create central atom
            bpy.ops.mesh.primitive_uv_sphere_add(
                radius=self.central_atom_radius,
                location=(0, 0, 0),
                segments=32,
                ring_count=16
            )
            central_atom = context.active_object
            central_atom.name = "Central_Atom"
            
            # Set up materials
            mat_central = self.create_material("Central_Atom_Material", (0.8, 0.2, 0.2, 1))
            mat_ligand = self.create_material("Ligand_Atom_Material", (0.2, 0.2, 0.8, 1))
            mat_bond = self.create_material("Bond_Material", (0.7, 0.7, 0.7, 1))
            mat_guide = self.create_material("Guide_Material", (0.9, 0.9, 0.9, 0.2))
            
            if not all([mat_central, mat_ligand, mat_bond, mat_guide]):
                raise Exception("Failed to create one or more materials")
            
            central_atom.data.materials.append(mat_central)
            
            # Create ligands and bonds based on geometry type
            positions = []
            guide_positions = []
            
            if self.geometry_type == 'OCTAHEDRAL':
                positions = [
                    (0, 0, self.bond_length),
                    (0, 0, -self.bond_length),
                    (self.bond_length, 0, 0),
                    (-self.bond_length, 0, 0),
                    (0, self.bond_length, 0),
                    (0, -self.bond_length, 0),
                ]
                guide_positions = positions if self.show_geometry_guide else []
                
            elif self.geometry_type == 'SQUARE_PYRAMIDAL':
                # Axial ligand at ~84.8° to equatorial ligands due to lone pair repulsion (e.g., BrF5)
                angle = math.radians(84.8)
                x = self.bond_length * math.sin(angle)
                z = self.bond_length * math.cos(angle)
                positions = [
                    (0, 0, self.bond_length),  # Axial
                    (x, 0, z),  # Equatorial 1
                    (-x, 0, z),  # Equatorial 2
                    (0, x, z),  # Equatorial 3
                    (0, -x, z),  # Equatorial 4
                ]
                guide_positions = positions if self.show_geometry_guide else []
                
            elif self.geometry_type == 'SQUARE_PLANAR':
                positions = [
                    (self.bond_length, 0, 0),
                    (-self.bond_length, 0, 0),
                    (0, self.bond_length, 0),
                    (0, -self.bond_length, 0),
                ]
                guide_positions = positions if self.show_geometry_guide else []
                
            elif self.geometry_type == 'TRIGONAL_BIPYRAMIDAL':
                positions = [
                    (0, 0, self.bond_length),  # Axial top
                    (0, 0, -self.bond_length),  # Axial bottom
                    (self.bond_length, 0, 0),  # Equatorial
                    (-self.bond_length*0.5, self.bond_length*math.sqrt(3)/2, 0),  # Equatorial
                    (-self.bond_length*0.5, -self.bond_length*math.sqrt(3)/2, 0),  # Equatorial
                ]
                guide_positions = positions if self.show_geometry_guide else []
                
            elif self.geometry_type == 'SEE_SAW':
                # Axial ligands at ~173.1° (tilted due to equatorial lone pair repulsion, e.g., SF4)
                axial_angle = math.radians(3.45)  # Tilt from Z-axis to achieve 180° - 2*3.45° = 173.1°
                # Equatorial ligands at ~101.7° (VSEPR-adjusted from 120° due to lone pair repulsion)
                equatorial_angle = math.radians(101.7)
                positions = [
                    (self.bond_length * math.sin(axial_angle), 0, self.bond_length * math.cos(axial_angle)),  # Axial top
                    (-self.bond_length * math.sin(axial_angle), 0, -self.bond_length * math.cos(axial_angle)),  # Axial bottom
                    (self.bond_length, 0, 0),  # Equatorial 1
                    (-self.bond_length * math.cos(equatorial_angle), self.bond_length * math.sin(equatorial_angle), 0),  # Equatorial 2
                ]
                guide_positions = positions if self.show_geometry_guide else []
                
            elif self.geometry_type == 'T_SHAPED':
                # Axial ligands at ~171° due to two equatorial lone pairs (e.g., BrF3)
                axial_angle = math.radians(4.5)  # Tilt from Z-axis to achieve 180° - 2*4.5° = 171°
                positions = [
                    (self.bond_length * math.sin(axial_angle), 0, self.bond_length * math.cos(axial_angle)),  # Axial top
                    (-self.bond_length * math.sin(axial_angle), 0, -self.bond_length * math.cos(axial_angle)),  # Axial bottom
                    (self.bond_length, 0, 0),  # Equatorial
                ]
                guide_positions = positions if self.show_geometry_guide else []
                
            elif self.geometry_type == 'TETRAHEDRAL':
                # Tetrahedral vertices, normalized and scaled
                scale = self.bond_length / math.sqrt(3)
                positions = [
                    (scale, scale, scale),
                    (scale, -scale, -scale),
                    (-scale, scale, -scale),
                    (-scale, -scale, scale),
                ]
                guide_positions = positions if self.show_geometry_guide else []
                
            elif self.geometry_type == 'TRIGONAL_PYRAMIDAL':
                # Use tetrahedral positions, remove one ligand
                scale = self.bond_length / math.sqrt(3)
                positions = [
                    (scale, scale, scale),
                    (scale, -scale, -scale),
                    (-scale, scale, -scale),
                ]
                guide_positions = positions if self.show_geometry_guide else []
                
            elif self.geometry_type == 'TRIGONAL_PLANAR':
                positions = [
                    (self.bond_length, 0, 0),
                    (-self.bond_length*0.5, self.bond_length*math.sqrt(3)/2, 0),
                    (-self.bond_length*0.5, -self.bond_length*math.sqrt(3)/2, 0),
                ]
                guide_positions = positions if self.show_geometry_guide else []
                
            elif self.geometry_type == 'BENT_TRIGONAL':
                # Two ligands at ~118° due to lone pair repulsion (e.g., SO2)
                angle = math.radians(118 / 2)  # Half-angle for symmetry
                positions = [
                    (self.bond_length * math.cos(angle), self.bond_length * math.sin(angle), 0),
                    (self.bond_length * math.cos(-angle), self.bond_length * math.sin(-angle), 0),
                ]
                guide_positions = positions if self.show_geometry_guide else []
                
            elif self.geometry_type == 'LINEAR':
                positions = [
                    (0, 0, self.bond_length),
                    (0, 0, -self.bond_length),
                ]
                guide_positions = positions if self.show_geometry_guide else []
                
            elif self.geometry_type == 'BENT_TETRAHEDRAL':
                # Two ligands at 104.5°, equal bond lengths
                angle = math.radians(104.5)
                scale = self.bond_length
                positions = [
                    (0, 0, scale),  # First ligand along Z-axis
                    (scale * math.sin(angle), 0, scale * math.cos(angle)),  # Second ligand
                ]
                guide_positions = positions if self.show_geometry_guide else []

            # Create ligands and bonds
            for i, pos in enumerate(positions):
                # Verify bond length
                bond_length = Vector(pos).length
                if abs(bond_length - self.bond_length) > 0.001:
                    self.report({'WARNING'}, f"Ligand {i+1} bond length {bond_length:.3f} deviates from {self.bond_length}")
                
                bpy.ops.mesh.primitive_uv_sphere_add(
                    radius=self.ligand_atom_radius,
                    location=pos,
                    segments=32,
                    ring_count=16
                )
                ligand = context.active_object
                ligand.name = f"Ligand_{i+1}"
                ligand.data.materials.append(mat_ligand)
                
                self.create_bond(
                    context,
                    mol_collection,
                    (0, 0, 0),
                    pos,
                    self.bond_radius,
                    mat_bond
                )
            
            if self.show_geometry_guide:
                self.create_geometry_guide(context, mol_collection, mat_guide, guide_positions)
            
            context.view_layer.objects.active = central_atom
            central_atom.select_set(True)
            
            return {'FINISHED'}
        
        except Exception as e:
            self.report({'ERROR'}, f"Failed to execute operator: {str(e)}")
            return {'CANCELLED'}

    def create_bond(self, context, collection, start, end, radius, material):
        """Create a bond (cylinder) between two points"""
        try:
            start_vec = Vector(start)
            end_vec = Vector(end)
            center = (start_vec + end_vec) / 2
            length = (end_vec - start_vec).length
            
            bpy.ops.mesh.primitive_cylinder_add(
                radius=radius,
                depth=length,
                location=center,
                vertices=16
            )
            bond = context.active_object
            
            direction = end_vec - start_vec
            rot_quat = direction.to_track_quat('Z', 'Y')
            bond.rotation_euler = rot_quat.to_euler()
            
            bond.name = f"Bond_{str(uuid.uuid4())[:8]}"
            bond.data.materials.append(material)
            
        except Exception as e:
            self.report({'ERROR'}, f"Failed to create bond: {str(e)}")
            raise

    def create_geometry_guide(self, context, collection, material, positions):
        """Create faint guide lines showing the molecular geometry"""
        try:
            scale = self.bond_length * 1.2
            
            for i, pos in enumerate(positions):
                dir_vec = Vector(pos).normalized()
                end_pos = dir_vec * scale
                
                self.create_bond(
                    context,
                    collection,
                    (0, 0, 0),
                    end_pos,
                    self.bond_radius * 0.3,
                    material
                )
                
                bpy.ops.mesh.primitive_uv_sphere_add(
                    radius=self.bond_radius * 0.5,
                    location=end_pos,
                    segments=16,
                    ring_count=8
                )
                guide_sphere = context.active_object
                guide_sphere.name = f"Guide_Sphere_{i+1}"
                guide_sphere.data.materials.append(material)
                
        except Exception as e:
            self.report({'ERROR'}, f"Failed to create geometry guide: {str(e)}")
            raise

def menu_func(self, context):
    self.layout.operator(
        MOL_OT_add_molecular_geometry.bl_idname,
        text="Molecular Geometry",
        icon='MESH_CUBE'
    )

def register():
    try:
        bpy.utils.register_class(MOL_OT_add_molecular_geometry)
        bpy.types.VIEW3D_MT_mesh_add.append(menu_func)
    except Exception as e:
        print(f"Failed to register addon: {str(e)}")

def unregister():
    try:
        bpy.utils.unregister_class(MOL_OT_add_molecular_geometry)
        bpy.types.VIEW3D_MT_mesh_add.remove(menu_func)
    except Exception as e:
        print(f"Failed to unregister addon: {str(e)}")

if __name__ == "__main__":
    register()

Gunakan secara bijak, jangan disebarkan untuk komersialisasi.

Bagikan di

Tidak ada komentar:

Posting Komentar

 
Copyright © 2015-2025 Urip dot Info | Disain Template oleh Herdiansyah Dimodivikasi Urip.Info