Source code for goo.force

from typing import Optional
from typing_extensions import override

import bpy
from goo.utils import *


[docs] class Force(BlenderObject): """A force. Forces are represented in Blender by force field objects. They interact with cells to influence their motion. Args: obj: Blender object to be used as a representation for the force. Attributes: type (str): Type of force. """ def __init__(self, obj: bpy.types.Object, type="FORCE"): super(Force, self).__init__(obj) self.type = type # Instantiate force field object if not self.obj.field: with bpy.context.temp_override(active_object=self.obj, object=self.obj): bpy.ops.object.forcefield_toggle() self.enable()
[docs] def enable(self): """Enables the force.""" self.obj.field.type = self.type
[docs] def disable(self): """Disables the force.""" if self.obj.field: self.obj.field.type = "NONE"
[docs] def enabled(self) -> bool: """Checks if the force field is enabled.""" return self.obj.field and self.obj.field.type == self.type
@property def strength(self) -> int: """Strength of the force field.""" return self.obj.field.strength @strength.setter def strength(self, strength: int): self.obj.field.strength = strength @property def falloff(self) -> float: """Falloff power of the force. Strength at distance :math:`r` is given by :math:`\\text{strength} / r ^ \\text{falloff}`.""" return self.obj.field.falloff @falloff.setter def falloff(self, falloff): self.obj.field.falloff_power = falloff @property def min_dist(self) -> float: """Minimum distance an object must be from a force to be affected.""" if self.obj.field.use_min_distance: return self.obj.field.min_dist return None @min_dist.setter def min_dist(self, min_dist: Optional[float]): if min_dist is None: self.obj.field.use_min_distance = False else: self.obj.field.use_min_distance = False self.obj.field.distance_min = min_dist @property def max_dist(self) -> float: """Maximum distance an object can be from a force to be affected.""" if self.obj.field.use_max_distance: return self.obj.field.max_dist return None @max_dist.setter def max_dist(self, max_dist: Optional[float]): if max_dist is None: self.obj.field.use_max_distance = False else: self.obj.field.use_max_distance = False self.obj.field.distance_max = max_dist @property def shape(self) -> int: """Shape of the force field.""" return self.obj.field.shape @shape.setter def shape(self, shape: int): self.obj.field.shape = shape @property def impulse_clamp(self) -> int: """Impulse clamp of the force field.""" return self.obj.modifiers["Cloth"].collision_settings.impulse_clamp @impulse_clamp.setter def impulse_clamp(self, impulse_clamp: int): self.obj.modifiers["Cloth"].collision_settings.impulse_clamp = impulse_clamp self.obj.modifiers["Cloth"].collision_settings.self_impulse_clamp = impulse_clamp
[docs] class AdhesionForce(Force): """An adhesion force.""" def __init__(self, obj): super(AdhesionForce, self).__init__(obj, "FORCE") @override @property def strength(self) -> int: return -self.obj.field.strength @strength.setter def strength(self, strength: int): self.obj.field.strength = -strength
[docs] class MotionForce(Force): """A motion force.""" def __init__(self, obj: bpy.types.Object): super(MotionForce, self).__init__(obj, "FORCE") obj.field.shape = "PLANE" obj.field.apply_to_rotation = False @override @property def strength(self): return -self.obj.field.strength @strength.setter def strength(self, strength): self.obj.field.strength = -strength
[docs] def set_loc(self, new_loc: Vector, target_loc: Vector): """Set location of a motion force, towards which a cell will move. Args: new_loc: Location of the motion force. target_loc: Location of the cell upon which the motion acts. """ dir = target_loc - new_loc self.loc = new_loc print("self.loc", self.loc) self.obj.rotation_euler = dir.to_track_quat("Z", "X").to_euler()
# TODO: remove because not used
[docs] def create_force( name: str, loc: tuple, strength: int, type: str = "FORCE", falloff: float = 0, min_dist: float = None, max_dist: float = None, shape: str = "POINTS", # POINTS ) -> Force: """Creates a new force field. Args: name: The name of the force field object. loc: The location of the force field object. strength: The strength of the force field. type: The type of the force field. falloff: The falloff power of the force field. min_dist: The minimum distance for the force field. max_dist: The maximum distance for the force field. shape: The shape of the force field. Defaults to "SURFACE". Returns: Force: The created force field object. """ obj = bpy.data.objects.new(name, None) obj.location = loc force = Force(obj, type) force.strength = strength force.falloff = falloff force.min_dist = min_dist force.max_dist = max_dist force.shape = shape ForceCollection.global_forces().add_force(force) return force
[docs] def create_adhesion( strength: int, obj: Optional[bpy.types.Object] = None, name: str = None, loc: tuple = (0, 0, 0), shape: str = "SURFACE", ) -> AdhesionForce: """Creates a new adhesion force. Adhesion forces can either be created from cells, in which they are homotypic forces, Or they can be created de novo, in which they are heterotypic adhesion forces meant to allow two different cell types to interact with each other. Args: strength: Strength of the adhesion force. obj: Cell to use as origin of the adhesion force. If None, a new object is created. name: Name of the adhesion force. loc: Initial location of the adhesion force. shape: Shape of the adhesion force. """ if obj is None: obj = bpy.data.objects.new(name, None) obj.location = loc adhesion_force = AdhesionForce(obj) adhesion_force.strength = strength adhesion_force.shape = shape adhesion_force.min_dist = 0.6 adhesion_force.max_dist = 1.4 adhesion_force.falloff = 1 return adhesion_force
[docs] def create_motion(name: str, loc: tuple, strength: int) -> MotionForce: """Creates a new motion force. Args: name: Name of the motion force. loc: Initial location of the motion force. strength: Strength of the motion force. """ obj = bpy.data.objects.new(name, None) obj.location = loc force = MotionForce(obj) force.strength = strength return force
[docs] class ForceCollection: """A class representing a collection of forces. A force collection is represented by Blender collections of Blender forces. """ _global_forces = None def __init__(self, name: str): self._col = bpy.data.collections.new(name) self._forces = [] @property def name(self) -> str: """Name of the collection of forces.""" return self._col.name @property def collection(self) -> bpy.types.Collection: """The underlying Blender collection.""" return self._col
[docs] def add_force(self, force: Force): """Add a Force to the collection.""" self._col.objects.link(force.obj) self._forces.append(force)
[docs] def remove_force(self, force: Force): """Remove a Force from the collection.""" self._col.objects.unlink(force.obj) self._forces.remove(force)
@property def forces(self): """List of forces in this collection.""" return self._forces
[docs] @staticmethod def global_forces() -> "ForceCollection": """Collection of forces that affects all cells. Returns: The collection containing global forces. """ if ForceCollection._global_forces is None: ForceCollection._global_forces = ForceCollection("globals") bpy.context.scene.collection.children.link( ForceCollection._global_forces.collection ) return ForceCollection._global_forces
[docs] class Boundary(BlenderObject): """A boundary for cells."""
[docs] def setup_physics(self): """Set up physics for the boundary.""" BoundaryCollisionConstructor().construct(self.obj)
[docs] def create_boundary(loc: tuple, size: float, mesh: str = "icosphere"): """Create a boundary. Args: loc: Center of the boundary. size: Radius of the boundary. mesh: Shape fo the boundary. """ obj = create_mesh("Boundary", loc, mesh=mesh, size=size) bpy.context.scene.collection.objects.link(obj) boundary = Boundary(obj) boundary.setup_physics() boundary.hide() return boundary