####################################################################################
#
# STEPS - STochastic Engine for Pathway Simulation
# Copyright (C) 2007-2026 Okinawa Institute of Science and Technology, Japan.
# Copyright (C) 2003-2006 University of Antwerp, Belgium.
#
# See the file AUTHORS for details.
# This file is part of STEPS.
#
# STEPS is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3,
# as published by the Free Software Foundation.
#
# STEPS is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
#################################################################################
###
try:
import bpy
except ImportError:
pass
from . import objects
class State:
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._state = {}
self._interpRatio = 0
self._stateCache = {}
self._users = []
def _addSubscriber(self, user):
self._users.append(user)
def _getState(self, tind):
"""Return a state dictionary for a given STEPS time step index"""
raise NotImplementedError()
def _stateInterpolation(self, state1, state2, ratio):
"""Return a state dictionary that is an interpolation between state1 and state2
ratio == 0.5 should return a state that is midway between state1 and state2"""
raise NotImplementedError()
def _updateDisplay(self, scene, depg, state):
raise NotImplementedError()
def updateDisplay(self, scene, depg):
"""Update the way the state is displayed
This will be called after the Blender dependency graph is updated.
Things like setting the position of particles should be done here.
"""
self._updateDisplay(scene, depg, self._state)
def _getCachedState(self, stind):
if stind not in self._stateCache:
self._stateCache[stind] = self._getState(stind)
return self._stateCache[stind]
def updateState(self, scene, depg, blenderLoader):
"""Trigger queries and update the state
Display modifications can also be done here but only if they do not involve calls
to an object's `evaluated_get` method.
Display modifications that would involve dependency graph updates should be done here.
"""
blcurr = scene.frame_current
ststart = blenderLoader._Blender2STEPSTime(blcurr)
blstart = blenderLoader._STEPS2BlenderTime(ststart)
stend = ststart + 1
blend = blenderLoader._STEPS2BlenderTime(stend)
ratio = (blcurr - blstart) / (blend - blstart)
self._interpRatio = blenderLoader.timeInterpFunc(ratio)
state1 = self._getCachedState(ststart)
if blcurr > blstart:
state2 = self._getCachedState(stend)
self._state = self._stateInterpolation(state1, state2, self._interpRatio)
else:
self._state = state1
for user in self._users:
user._signalUpdatedState(self, scene, depg)
@property
def state(self):
return self._state
class StateUser:
stateClasses = []
def _handleSubscriptions(self, parent):
for cls in self.stateClasses:
for obj in parent._getAllChildren(cls):
obj._addSubscriber(self)
def _signalUpdatedState(self, caller, scene, depg):
pass
def updateBlenderObjects(self, scene, depg):
pass
[docs]
class StateDependentObject(objects.BlenderObject, StateUser):
"""Base class for objects whose material can change as a funciton of its state
This class can be used in place of `BlenderObject` to customize the material of the object
as a function of the state of the STEPS object.
There are two possible ways to achieve this:
- Use a small number of materials that are already declared in the Blender file;
- Programatically customize the material of the object.
The first way is done by setting the ``setMaterialFunc`` parameter to a function with the following
signature::
def myCustomMatSetFunc(scene, depg, state) -> str
The first two parameters are passed from the ``bpy.app.handlers.frame_change_pre`` callback function
(see https://docs.blender.org/api/current/bpy.app.handlers.html). The third parameter contains the
state of the object (see table in :py:class:`StateDependentMesh`). The function should return a
string that is the name of a material in the Blender file.
The second way is done by setting the ``updateMaterialFunc`` parameter to a function with the
following signature::
def myCustomMatUpdateFunc(scene, depg, material, state) -> None
The first two parameters are identical. The third parameter is the :py:class:`bpy.types.Material`
object that is attributed to the object and should be modified by the function. The last parameter
is the state of the object (see tables in :py:class:`StateDependentMesh`).
"""
updateMaterialFunc = None
setMaterialFunc = None
def __init__(self, **kwargs):
super().__init__(**kwargs)
if self.updateMaterialFunc is not None and self.setMaterialFunc is not None:
raise ValueError(f'Cannot supply both "updateMaterialFunc" and "setMaterialFunc" simultaneously')
self._currState = {}
def updateBlenderObjects(self, scene, depg):
if self.updateMaterialFunc is not None:
self.updateMaterialFunc(scene, depg, self.material.blenderObj, self._currState)
elif self.setMaterialFunc is not None:
matName = self.setMaterialFunc(scene, depg, self._currState)
self.blenderObj.material_slots[0].material = bpy.data.materials[matName]
self._currState = {}
[docs]
class StateDependentSeparateObjects(objects.SeparateObjects, StateUser):
"""Base class for state-dependent objects like Vesicles or Rafts
This class can be used in place of ``SeparateObjects`` to customize the material of each object
as a function of the state of the STEPS object (its position, the species on its surface, etc.).
There are two possible ways to achieve this:
- Use a small number of materials that are already declared in the Blender file;
- Programatically customize the material of each object.
The first way is done by setting the ``setMaterialFunc`` parameter to a function with the following
signature::
def myCustomMatSetFunc(scene, depg, state) -> str
The first two parameters are passed from the ``bpy.app.handlers.frame_change_pre`` callback function
(see https://docs.blender.org/api/current/bpy.app.handlers.html). The third parameter contains the
state of the object (see tables in :py:class:`StateDependentVesicle` or
:py:class:`StateDependentRaft`). The function should return a string that is the name of a
material in the Blender file.
The second way is done by setting the ``updateMaterialFunc`` parameter to a function with the
following signature::
def myCustomMatUpdateFunc(scene, depg, material, state) -> None
The first two parameters are identical. The third parameter is the :py:class:`bpy.types.Material`
object that is attributed to the object and should be modified by the function. The last parameter
is the state of the object (see tables in :py:class:`StateDependentVesicle` or
:py:class:`StateDependentRaft`).
"""
updateMaterialFunc = None
setMaterialFunc = None
def __init__(self, **kwargs):
super().__init__(**kwargs)
if self.updateMaterialFunc is not None and self.setMaterialFunc is not None:
raise ValueError(f'Cannot supply both "updateMaterialFunc" and "setMaterialFunc" simultaneously')
self._stateDict = {}
if self.updateMaterialFunc is not None:
self._allMaterials = {}
mat = self.obj.material.blenderObj
for idx, obj in self._objects.items():
# Create as many materials as there are vesicles
matName = f'{mat.name}_{idx}'
if matName not in bpy.data.materials:
self._allMaterials[idx] = mat.copy()
self._allMaterials[idx].name = matName
else:
self._allMaterials[idx] = bpy.data.materials[matName]
obj.blenderObj.material_slots[0].link = 'OBJECT'
obj.blenderObj.material_slots[0].material = self._allMaterials[idx]
if self.setMaterialFunc is not None:
for idx, obj in self._objects.items():
obj.blenderObj.material_slots[0].link = 'OBJECT'
def updateBlenderObjects(self, scene, depg):
if self.updateMaterialFunc is not None:
for idx, dct in self._stateDict.items():
self.updateMaterialFunc(scene, depg, self._allMaterials[idx], dct)
elif self.setMaterialFunc is not None:
for idx, dct in self._stateDict.items():
matName = self.setMaterialFunc(scene, depg, dct)
self._objects[idx].blenderObj.material_slots[0].material = bpy.data.materials[matName]
self._stateDict = {}