Source code for BondOverwriter

import re
import bpy

[docs] def parse_connection_string(connect_list_string): """Splits the input string into a list of cleaned connection entries.""" return [conn.strip() for conn in connect_list_string.split(';') if conn.strip()]
[docs] def build_atom_identity_map(coords): """ Builds a map of atom indices to their element symbols from a list of raw coordinates. :param coords: (list) List of [element, x, y, z] entries. :return: (dict) Dictionary mapping index strings to element symbols, e.g., {'01': 'C', '02': 'O'} """ identity_map = {} for i, entry in enumerate(coords, start=1): symbol = entry[0] index = f"{i:02d}" # Two-digit index identity_map[index] = symbol return identity_map
[docs] def build_connection_lookup(connect_with_symbols): """Creates a dictionary of existing connections using unordered atom pairs as keys.""" return {frozenset((a, b)): (a, b, bond) for a, b, bond in connect_with_symbols}
[docs] def parse_connection_entry(conn): """ Parses a connection entry like 'Fe07-Cl25' into atom1, bond, atom2. Assumes bond is a single character. """ for i, char in enumerate(conn): if char in "-=#_%": atom1 = conn[:i] bond = conn[i] atom2 = conn[i+1:] return atom1, bond, atom2 raise ValueError(f"Invalid connection format: {conn}")
[docs] def validate_atom_identity(atom_label, identity_map): """Validates that the atom exists and matches the expected symbol.""" symbol = ''.join(filter(str.isalpha, atom_label)) index = ''.join(filter(str.isdigit, atom_label)) if index not in identity_map: raise ValueError(f"Atom index {index} not found in molecule.") if identity_map[index] != symbol: raise ValueError(f"Atom identity mismatch: expected {identity_map[index]}, got {symbol}.")
[docs] def overwrite_connectivity(connect_list_string, connect_with_symbols, coords): """ Updates or appends bond connections based on a user-defined string. Validates that atom indices and identities match the current molecule. :param connect_list_string: (str) e.g., "Fe07-Cl25; C03=C07" :param connect_with_symbols: (list) Existing connectivity list :return: (list) Updated connectivity list """ if not connect_list_string.strip(): return connect_with_symbols new_connections = parse_connection_string(connect_list_string) if not new_connections: return connect_with_symbols atom_identity_map = build_atom_identity_map(coords) existing_connections = build_connection_lookup(connect_with_symbols) for conn in new_connections: atom1, bond, atom2 = parse_connection_entry(conn) validate_atom_identity(atom1, atom_identity_map) validate_atom_identity(atom2, atom_identity_map) key = frozenset((atom1, atom2)) existing_connections[key] = (atom1, atom2, bond) print("optional step: overwriting conectivity list") return list(existing_connections.values())
[docs] def delete_forbidden_bonds_from_scene(custom_bond_thresholds): """ Reads custom threshold rules with bond order 0, then deletes every bond object from the scene whose endpoint element types match a forbidden pair. :param custom_bond_thresholds: (list) Dicts from get_custom_thresholds(), each with 'atom_pair' and 'bond_order'. :return: (int) Number of bond objects removed from the scene. """ BOND_NAME_CHARS = {'_', '-', '=', '#', '%'} def bare_element(atom_label): base = atom_label.split('.')[0] match = re.match(r"([A-Za-z]+)", base) return match.group(1) if match else None forbidden_pairs = { frozenset(rule["atom_pair"]) for rule in (custom_bond_thresholds or []) if rule.get("bond_order") == 0 } if not forbidden_pairs: return 0 count = 0 for obj in list(bpy.context.scene.objects): if obj.type != 'MESH': continue bond_char = next((c for c in BOND_NAME_CHARS if c in obj.name), None) if bond_char is None: continue parts = obj.name.split(bond_char, 1) if len(parts) != 2: continue elem1 = bare_element(parts[0]) elem2 = bare_element(parts[1]) if elem1 and elem2 and frozenset((elem1, elem2)) in forbidden_pairs: bpy.data.objects.remove(obj, do_unlink=True) count += 1 print(f"delete_forbidden_bonds_from_scene: removed {count} bond object(s)") return count
[docs] def get_forbidden_type_pairs(custom_bond_thresholds): """ Extracts element type pairs designated as bond order 0 from the custom threshold rules. :param custom_bond_thresholds: (list) Rules from get_custom_thresholds(), each a dict with 'atom_pair' and 'bond_order'. :return: (set) Frozensets of element symbol pairs, e.g. {frozenset({'V', 'V'})}. """ return { frozenset(rule["atom_pair"]) for rule in (custom_bond_thresholds or []) if rule.get("bond_order") == 0 }
[docs] def remove_forbidden_bonds_from_connectivity(connect_with_symbols, forbidden_type_pairs): """ Filters connect_with_symbols, removing any entry whose atom type pair is in forbidden_type_pairs. :param connect_with_symbols: (list) Connectivity list of (atomA, atomB, bond_char) tuples. :param forbidden_type_pairs: (set) Output of get_forbidden_type_pairs. :return: (list) Filtered connectivity list. """ filtered = [] for atomA, atomB, bond_char in connect_with_symbols: elem1 = re.match(r"([A-Za-z]+)", atomA.split('.')[0]).group(1) elem2 = re.match(r"([A-Za-z]+)", atomB.split('.')[0]).group(1) if frozenset((elem1, elem2)) not in forbidden_type_pairs: filtered.append((atomA, atomB, bond_char)) return filtered