Source code for goo.division

from typing import Callable

import bpy
import bmesh
from mathutils import Vector
import numpy as np

from goo.cell import Cell
from goo.utils import *
from goo.handler import Handler
from goo.molecule import DiffusionSystem

from typing_extensions import override


[docs] class DivisionLogic: """Base class for defining division logic for cells."""
[docs] def make_divide(self, mother: Cell) -> tuple[Cell, Cell]: """Divide a mother cell into two daughter cells. Args: mother: The mother cell to divide. Returns: A tuple containing the two daughter cells that will result from the division. Note: The meshes of the daughter should not be updated at this stage. `flush()` must be called to update all cells at once so that they do not interfere with each other. """ pass
[docs] def flush(self): """Finish performing all stored divisions.""" pass
[docs] class BisectDivisionLogic(DivisionLogic): """Division logic that uses low-level `bmesh` operations, i.e. `bmesh.ops.bisect_plane` to divide a cell along its major axis. Attributes: margin (float): Distance of margin between divided cells. """ def __init__(self, margin=0.025): self.margin = margin self.to_flush = []
[docs] @override def make_divide(self, mother): com = mother.COM(local_coords=True) axis = mother.major_axis().axis(local_coords=True) base_name = mother.name m_mb = self._bisect(mother.obj_eval, com, axis, True, self.margin) d_mb = self._bisect(mother.obj_eval, com, axis, False, self.margin) daughter = mother.copy() mother.name = base_name + ".0" daughter.name = base_name + ".1" self.to_flush.append((m_mb, mother)) self.to_flush.append((d_mb, daughter)) return mother, daughter
def _bisect( self, obj_eval: bpy.types.Object, com: Vector, axis: Axis, inner: bool, margin: float, ): """Bisect a mesh along a plane defined by center of mass and axis. Args: obj_eval: The evaluated object. com: The center of mass of the mesh. axis: The major axis of the mesh. inner: Whether to clear inner or outer part of the bisection. margin: The margin used for bisection. Returns: The `bmesh` object containing the resulting bisection. """ bm = bmesh.new() bm.from_mesh(obj_eval.to_mesh()) # bisect with plane geom = bm.verts[:] + bm.edges[:] + bm.faces[:] plane_co = com + axis * margin / 2 if inner else com - axis * margin / 2 result = bmesh.ops.bisect_plane( bm, geom=geom, plane_co=plane_co, plane_no=axis, clear_inner=inner, clear_outer=not inner, ) # fill in bisected face edges = [e for e in result["geom_cut"] if isinstance(e, bmesh.types.BMEdge)] bmesh.ops.edgeloop_fill(bm, edges=edges) return bm
[docs] @override def flush(self): for bm, cell in self.to_flush: bm.to_mesh(cell.obj.data) bm.free() cell.remesh() cell.recenter() self.to_flush.clear()
[docs] class BooleanDivisionLogic(DivisionLogic): """Division logic that creates a plane of division and applies the Boolean modifier to create a division.""" # TODO: Update to work with physics. def __init__(self): pass
[docs] @override def make_divide(self, mother: Cell): plane = self._create_division_plane( mother.name, mother.major_axis(), mother.COM() ) obj = mother.obj # cut mother cell by division plane bpy.context.view_layer.objects.active = obj bool_mod = obj.modifiers.new(name="Boolean", type="BOOLEAN") bool_mod.operand_type = "OBJECT" bool_mod.object = plane bool_mod.operation = "DIFFERENCE" bool_mod.solver = "EXACT" bpy.ops.object.modifier_apply(modifier=bool_mod.name) # separate two daughter cells bpy.ops.object.mode_set(mode="EDIT") bpy.ops.mesh.separate(type="LOOSE") bpy.ops.object.mode_set(mode="OBJECT") daughter = Cell(bpy.context.selected_objects[0]) daughter.obj.select_set(False) daughter.name = mother.name + ".1" mother.name = mother.name + ".0" # remesh daughter cells mother.remesh() daughter.remesh() # clean up bpy.data.meshes.remove(plane.data, do_unlink=True) return mother, daughter
[docs] @override def flush(self): pass
def _create_division_plane(self, name, major_axis, com, collection=None): """ Creates a plane orthogonal to the long axis vector and passing through the cell's center of mass. """ # Define new plane plane = create_mesh( f"{name}_division_plane", loc=com, mesh="plane", size=major_axis.length() + 1, rotation=major_axis.axis().to_track_quat("Z", "Y"), ) bpy.context.scene.collection.objects.link(plane) # Add thickness to plane solid_mod = plane.modifiers.new(name="Solidify", type="SOLIDIFY") solid_mod.offset = 0 solid_mod.thickness = 0.025 plane.hide_set(True) return plane
[docs] class DivisionHandler(Handler): """Handler for managing cell division processes. This handler is responsible for managing the division of cells based on the provided division logic. It determines which cells are eligible for division and performs the division process. Attributes: division_logic (DivisionLogic): The division logic used to execute cell division. """ def __init__(self, division_logic, mu, sigma): self.division_logic = division_logic() self.mu = mu self.sigma = sigma
[docs] @override def setup(self, get_cells: Callable[[], list[Cell]], get_diffsystems: Callable[[], list[DiffusionSystem]], dt: float): super(DivisionHandler, self).setup(get_cells, get_diffsystems, dt) for cell in self.get_cells(): cell["divided"] = False self._cells_to_update = []
[docs] def can_divide(self, cell: Cell) -> bool: """Check if a cell is eligible for division. This method must be implemented by all subclasses. Args: cell: The cell to check. Returns: True if the cell can divide, False otherwise. """ raise NotImplementedError("Subclasses must implement can_divide() method.")
[docs] def update_on_divide(self, cell: Cell): """Perform updates after a cell has divided. This method can be overridden by subclasses to perform additional updates (e.g. set a property) after a cell has divided. Args: cell: The cell that has divided. """ pass
[docs] @override def run(self, scene, depsgraph): for cell in self._cells_to_update: cell.enable_physics() cell.cloth_mod.point_cache.frame_start = scene.frame_current cell["divided"] = False self._cells_to_update.clear() for cell in self.get_cells(): if self.can_divide(cell): mother, daughter = cell.divide(self.division_logic) self.update_on_divide(mother) self.update_on_divide(daughter) if mother.physics_enabled: self._cells_to_update.extend([mother, daughter]) for cell in self._cells_to_update: cell.disable_physics() cell["divided"] = True self.division_logic.flush()
[docs] class TimeDivisionHandler(DivisionHandler): """Division handler that determines eligibility based on time from last divsion. Attributes: division_logic (DivisionLogic): see base class. mu (float): Time interval between cell divisions. var (float): Variance in the time interval. """ def __init__(self, division_logic, mu=20, sigma=0): super(TimeDivisionHandler, self).__init__(division_logic, mu, sigma)
[docs] @override def setup(self, get_cells, dt): super(TimeDivisionHandler, self).setup(get_cells, dt) for cell in self.get_cells(): cell["last_division_time"] = 0
[docs] @override def can_divide(self, cell: Cell): time = bpy.context.scene.frame_current * self.dt if "last_division_time" not in cell: cell["last_division_time"] = time return False # implement variance too div_time = int(np.random.normal(self.mu, self.sigma)) return time - cell["last_division_time"] >= div_time
[docs] @override def update_on_divide(self, cell: Cell): time = bpy.context.scene.frame_current * self.dt cell["last_division_time"] = time
[docs] class SizeDivisionHandler(DivisionHandler): """Division handler that determines eligibility based on size of cell. Attributes: division_logic (DivisionLogic): see base class. threshold (float): minimum size of cell able to divide. """ def __init__(self, division_logic, mu=30, sigma=0): super(SizeDivisionHandler, self).__init__(division_logic, mu, sigma)
[docs] @override def can_divide(self, cell: Cell): div_volume = np.random.normal(self.mu, self.sigma) return cell.volume() >= div_volume