Source code for UnitCellReplicator
import bpy
from typing import Iterable
from bpy.types import Object
from mathutils import Vector
[docs]
def parent_atoms_and_bonds_to_empty_object(objects_in_scene: Iterable[Object]) -> Object:
"""
Expects Blender objects representing atoms, bonds, or other scene geometry.
Cameras and lights should be filtered out by the caller.
"""
# --- Create the Empty object ---
empty = bpy.data.objects.new(name="UnitCell_Root", object_data=None)
empty.empty_display_type = 'PLAIN_AXES'
empty.empty_display_size = 1.0
# Link the Empty to the active collection
bpy.context.collection.objects.link(empty)
# --- Parent objects ---
for obj in objects_in_scene:
if obj is empty:
continue # safety guard
obj.parent = empty
return empty
[docs]
def replicate_and_translate_cell(cell: Object, direction: Vector) -> Object:
"""
Duplicates a unit-cell root Empty and all its children,
translates the duplicate by `direction`, and returns the new root.
"""
# NOTE: Assumes a flat hierarchy (no nested children).
# If nested objects are introduced later, this must become recursive.
if cell.type != 'EMPTY':
raise TypeError("replicate_and_translate_cell expects an Empty object as root")
# --- Duplicate the root Empty ---
new_cell = cell.copy()
new_cell.data = None # empties have no data, but explicit is good practice
bpy.context.collection.objects.link(new_cell)
# --- Duplicate and re-parent children ---
for child in cell.children:
new_child = child.copy()
new_child.data = child.data.copy() if child.data else None
bpy.context.collection.objects.link(new_child)
new_child.parent = new_cell
# Preserve transforms relative to parent
new_child.matrix_parent_inverse = new_cell.matrix_world.inverted()
# --- Translate the duplicated cell ---
new_cell.location += direction
return new_cell
[docs]
def delete_unit_cell_roots() -> int:
"""
Removes all Empty objects created as unit-cell replication roots
(named 'UnitCell_Root' and Blender-suffixed copies thereof).
Should be called after flatten_scene_hierarchy, once parenting is no longer needed.
:return: (int) Number of Empty objects removed.
"""
roots = [
obj for obj in bpy.context.scene.objects
if obj.type == 'EMPTY' and obj.name.split('.')[0] == 'UnitCell_Root'
]
for obj in roots:
bpy.data.objects.remove(obj, do_unlink=True)
print(f"delete_unit_cell_roots: removed {len(roots)} empty root(s)")
return len(roots)
[docs]
def flatten_scene_hierarchy() -> None:
"""
Clears all parent relationships in the scene while preserving
world-space transforms, leaving a flat hierarchy of mesh objects.
"""
bpy.ops.object.select_all(action='DESELECT')
meshes = [obj for obj in bpy.context.scene.objects if obj.type == 'MESH']
for obj in meshes:
obj.select_set(True)
if not meshes:
return
bpy.context.view_layer.objects.active = meshes[0]
bpy.ops.object.parent_clear(type='CLEAR_KEEP_TRANSFORM')
bpy.ops.object.select_all(action='DESELECT')
def _expand_along_axis(roots, frontier, n, direction):
"""
Expands a list of unit-cell roots by n-1 additional layers along one
lattice direction. Each iteration replicates only the most recently
placed layer (the frontier), so cells are never duplicated regardless
of n.
:param roots: (list) Accumulator of all root Empties; mutated in place.
:param frontier: (list) The layer of roots to replicate from first.
:param n: (int) Total number of cells along this axis (including the seed).
:param direction:(Vector) Cartesian translation vector for one lattice step.
:return: (list) The final frontier after all layers are placed.
"""
for _ in range(1, n):
batch = [replicate_and_translate_cell(r, direction) for r in frontier]
roots += batch
frontier = batch
return frontier