Source code for goo.division

from collections.abc import Callable

import bmesh
import bpy
import numpy as np

from mathutils import Vector
from typing_extensions import override

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


[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. to_flush (list[tuple[bmesh.types.BMesh, Cell]]): List of bmesh objects and cells to flush. """ def __init__(self, margin=0.025): self.margin = margin self.to_flush = []
[docs] @override def make_divide(self, mother: Cell) -> tuple[Cell, Cell]: 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, ) -> bmesh.types.BMesh: """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): """Flush the bmesh objects and cells from the division logic.""" 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. Boolean division is depreciated and will be removed in the future. The reason behind this is that it is not possible to apply the Boolean modifier to a cell with physics enabled; therefore leads to instability and simulation failure. """ def __init__(self): pass
[docs] @override def make_divide(self, mother: Cell) -> tuple[Cell, 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) # Copy mother's pressure to daughter if mother.cloth_mod and daughter.cloth_mod: daughter.pressure = mother.pressure 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: str, major_axis: Axis, com: Vector) -> bpy.types.Object: """ 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. mu (float): Mean metric for determining cell division. sigma (float): Standard deviation of the metric for determining cell division. """ def __init__(self, division_logic: DivisionLogic, mu: float, sigma: float): self.division_logic: DivisionLogic = division_logic() self.mu: float = mu self.sigma: float = sigma
[docs] @override def setup( self, get_cells: Callable[[], list[Cell]], get_diffsystems: Callable[[], list[DiffusionSystem]], dt: float, ) -> None: super().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: DivisionLogic, mu: float = 20, sigma: float = 0): super().__init__(division_logic, mu, sigma)
[docs] @override def setup(self, get_cells: Callable[[], list[Cell]], get_diffsystem: Callable[[], DiffusionSystem], dt: float) -> None: super().setup(get_cells, get_diffsystem, dt) for cell in self.get_cells(): cell["last_division_time"] = 0
[docs] @override def can_divide(self, cell: Cell) -> bool: 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) -> None: 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. one_division_only (bool): if True, cells will only divide once and never again. """ def __init__(self, division_logic: DivisionLogic, mu: float = 50, sigma: float = 0, one_division_only: bool = False): super().__init__(division_logic, mu, sigma) self.one_division_only = one_division_only
[docs] @override def setup(self, get_cells: Callable[[], list[Cell]], get_diffsystem: Callable[[], DiffusionSystem], dt: float) -> None: super().setup(get_cells, get_diffsystem, dt) # Initialize has_divided property for all cells for cell in self.get_cells(): if "has_divided" not in cell: cell["has_divided"] = False
[docs] @override def can_divide(self, cell: Cell) -> bool: # If one_division_only is True and cell has already divided, prevent further division if self.one_division_only and "has_divided" in cell and cell["has_divided"]: return False div_volume = np.random.normal(self.mu, self.sigma) return cell.volume() >= div_volume
[docs] @override def update_on_divide(self, cell: Cell) -> None: time = bpy.context.scene.frame_current * self.dt cell["last_division_time"] = time cell["has_divided"] = True