Source code for fastplotlib.graphics.features._vectors

import numpy as np
import pylinalg as la

from ._base import (
    GraphicFeature,
    GraphicFeatureEvent,
    block_reentrance,
)


# it doesn't make sense to modify just a portion of a vector field, I can't think of a use case.
# so we only allow setting the entire buffer, but allow getting portions of it
[docs] class VectorPositions(GraphicFeature): event_info_spec = [ { "dict key": "value", "type": "np.ndarray", "description": "new vector positions", }, ] def __init__( self, positions: np.ndarray, isolated_buffer: bool = True, property_name: str = "positions", ): """ Manages vector field positions by managing the translation elements of the mesh instance transform matrix buffer """ positions = np.asarray(positions, dtype=np.float32) if positions.ndim != 2: raise ValueError( f"vector field positions must be of shape [n, 2] or [n, 3]" ) if positions.shape[1] == 2: positions = np.column_stack( [ positions[:, 0], positions[:, 1], np.zeros(positions.shape[0], dtype=np.float32), ] ) elif positions.shape[1] == 3: pass else: raise ValueError( f"vector field positions must be of shape [n, 2] or [n, 3]" ) self._positions = positions super().__init__(property_name=property_name) @property def value(self) -> np.ndarray: return self._positions def __getitem__(self, item): return self.value[item] def __setitem__(self, key, value): raise NotImplementedError( "cannot set individual slices of vector positions, must set all positions" ) @block_reentrance def set_value(self, graphic, value: np.ndarray): if value.shape[0] != self._positions.shape[0]: raise ValueError( f"number of vector positions in passed array != number of vectors in graphic: " f"{value.shape[0]} != {self._positions.shape[0]}" ) if value.shape[1] == 2: # assume 2d self._positions[:, :-1] = value else: self._positions[:] = value for i in range(self._positions.shape[0]): # only need to update the translation vector graphic.world_object.instance_buffer.data["matrix"][i][3, 0:3] = ( self._positions[i] ) graphic.world_object.instance_buffer.update_full() event = GraphicFeatureEvent(type="positions", info={"value": value}) self._call_event_handlers(event)
[docs] class VectorDirections(GraphicFeature): event_info_spec = [ { "dict key": "value", "type": "np.ndarray", "description": "new vector directions", }, ] # vector is always pointing in [0, 0, 1] when mesh is initialized init_direction = np.array([0, 0, 1]) init_direction.flags.writeable = False def __init__( self, directions: np.ndarray, isolated_buffer: bool = True, property_name: str = "directions", ): """Manages vector field positions by managing the mesh instance buffer's full transform matrix""" directions = np.asarray(directions, dtype=np.float32) if directions.ndim != 2: raise ValueError( f"vector field directions must be of shape [n, 2] or [n, 3]" ) if directions.shape[1] == 2: directions = np.column_stack( [ directions[:, 0], directions[:, 1], np.zeros(directions.shape[0], dtype=np.float32), ] ) elif directions.shape[1] == 3: pass else: raise ValueError( f"vector field directions must be of shape [n, 2] or [n, 3]" ) self._directions = directions super().__init__(property_name=property_name) @property def value(self) -> np.ndarray: return self._directions def __getitem__(self, item): return self.value[item] def __setitem__(self, key, value): raise NotImplementedError( "cannot set individual slices of vector directions, must set all directions" ) @block_reentrance def set_value(self, graphic, value: np.ndarray): if value.shape[0] != self._directions.shape[0]: raise ValueError( f"number of vector directions in passed array != number of vectors in graphic: " f"{value.shape[0]} != {self._directions.shape[0]}" ) if value.shape[1] == 2: # assume 2d self._directions[:, :-1] = value else: self._directions[:] = value # vector determines the size of the vector magnitudes = np.linalg.norm(self._directions, axis=1, ord=2) for i in range(self._directions.shape[0]): # get quaternion to rotate vector to new direction rotation = la.quat_from_vecs(self.init_direction, self._directions[i]) # get the new transform transform = la.mat_compose(graphic.positions[i], rotation, magnitudes[i]) # set the buffer graphic.world_object.instance_buffer.data["matrix"][i] = transform.T graphic.world_object.instance_buffer.update_full() event = GraphicFeatureEvent(type="directions", info={"value": value}) self._call_event_handlers(event)